#5 從零開始制作在線 代碼編輯器

上一篇
#4 從零開始制作在線 代碼編輯器

刪除 與 BackSpace 與 Delete


BackSpace

為了方便. 這里所說的刪除的只考慮以下兩種鍵的最簡單的刪除行為:

  1. BackSpace 往左邊刪除一個字符
  2. Delete 往右邊刪除一個字符

有選區(qū)狀態(tài)時所做的刪除或者替換. 這里不考慮哦.

因為做了簡化. 這里的流程就會比較簡單. 要說明的是:
當(dāng)光標位于行首時. 再使用BackSpace 的時候. 要刪除當(dāng)前行.
把這個方法加在 harusame-line.js

code

@path serval/script/harusame-line.js
// 只有部分代碼

/**
 * 刪除指定行
 */
self.deleteLine = function (v_line_number) {
    var $line_container = document.querySelector('.line-container') // 同樣. 這里暫時這么寫...
    var $line = self.getLineContentByLogicalY(v_line_number).parentNode.parentNode
    $line_container.removeChild($line)
}
@path serval/script/harusame-serval.js
// 只有部分代碼
  
/**
 * KEY: BackSpace  
 * 0. 阻止默認行為
 * 1. 如果光標在首行首列, 什么都不干
 * 2. 如果光標在該行第0個位置
 *      2.1. 得到光標后的內(nèi)容(left_content), 刪除當(dāng)前行
 *      2.2. 修正行號 -> 暫時是這樣
 *      2.3. 光標上移一行, 且放置到該行最后一列
 *      2.4. 將上一行的殘留下來的內(nèi)容(left_content) 追加到該行末尾
 * 3. 其他情況下
 *      3.1. 光標往左移動一列
 *      3.2. 刪除一個字符
 */
Serval.prototype.keydownHandler.'8': function (event) {
    event.preventDefault() /* 0 */
    var self = this
    self.allocTask(function (v_cursor) {
        var $line = v_cursor.line.$line_content
        var textContent = $line.textContent
        var logicalX = v_cursor.logicalX
        var left_content = textContent.substring(logicalX, textContent.length)

        var logicalY = v_cursor.logicalY
        if (logicalY === 0) {
            if (logicalX === 0) {
                return /* 2 */
            }
        } else {
            if (logicalX === 0) { /* 2 */
                Line.deleteLine(v_cursor.logicalY) /* 2.1 */
                Line.fixLineNumber(v_cursor.logicalY) /* 2.2 */

                v_cursor.logicalY -= 1 /* 2.3 */
                v_cursor.logicalX = v_cursor.line.$line_content.textContent /* 2.3 */

                var $line = v_cursor.line.$line_content
                var textContent = $line.textContent
                $line.textContent = textContent + left_content /* 2.4 */

                return
            }
        }

        v_cursor.logicalX -= 1 /* 3.1 */
        $line.textContent = textContent.substring(0, logicalX - 1) + left_content /* 3.2 */
    })
},

效果是這樣... 很普通. 見 圖5-1

圖5-1.gif

Delete

Delete 的原理同 BackSpace. 但是還是有一些差異. 在做的時候. 能感受到之前代碼的不合理(蠢).

Delete 中. 當(dāng)光標位于 最后一行 最后一列 時. 需要先得到一共有幾行. 才能做比較. 可是問題在于訪問 Line.max_line_number 會觸發(fā)它的 getter. 并且修改了數(shù)據(jù). 見 圖5-2. 這是一個看似簡單但是會充滿麻煩的行為. 畢竟每次訪問. 他都在變. 導(dǎo)致在做 最后一行 最后一列 的判斷時. 只要按下 一次以上的 Delete. 這個判斷就會失效.

圖5-2.png

先把問題補了吧...

  1. ++ 改成 刪掉 (這里暫時這么改... 事實上沒有必要控制 getter setter 了)
  1. 每次調(diào)用 Line.generateLine 的時候. 最大行數(shù)加一.
  1. 同樣地. 每次調(diào)用Line.deleteLine的時候. 最大行數(shù)減一.

code

@path serval/script/harusame-serval.js
// 部分代碼

/**
 * KEY: Delete
 * 0. 阻止默認行為
 * 1. 如果光標在最后一行最后一列, 什么都不干
 * 2. 如果光標在該行最后一個位置
 *      2.1. 得到下一行的光標后的內(nèi)容(left_content, 另外 也肯定是該行全部的內(nèi)容)
 *      2.2. 刪除下一行
 *      2.3. 修正行號 -> 暫時是這樣
 *      2.4. 將 left_content 內(nèi)容 追加到當(dāng)前行末尾
 * 3. 其他情況下
 *      3.1. 向右刪除一個字符
 */
Serval.prototype.keydownHandler.  '46': function (event) {
    event.preventDefault() /* 0 */
    var self = this
    self.allocTask(function (v_cursor) {
        var $line = v_cursor.line.$line_content
        var textContent = $line.textContent
        var logicalX = v_cursor.logicalX
        var left_content = textContent.substring(logicalX + 1, textContent.length)

        var logicalY = v_cursor.logicalY
        var max = Line.max_line_number - 1
        console.log('max', max)
        if (logicalY === max) {
            if (logicalX === textContent.length) {
                return /* 1 */
            }
        } else {
            if (logicalX === textContent.length) {

                var left_content = Line.getLineContentByLogicalY(logicalY + 1).textContent /* 2.1 */
                Line.deleteLine(logicalY + 1) /* 2.2 */
                Line.fixLineNumber(v_cursor.logicalY) /* 2.3 */

                $line.textContent = v_cursor.line.$line_content.textContent + left_content /* 2.4 */
                return
            }
        }
        $line.textContent = textContent.substring(0, logicalX) + left_content /* 3.1 */
    })
},  

看看效果的說. 很普通. 見圖 5-3. 順便又試了下 BackSpace

圖5-3.gif

選區(qū) 與 selection


在進行復(fù)制等操作前. 需要讓計算姬知道哪些對象需要操作. 選區(qū)就是這樣一個東西. 感覺也沒什么好說的.

編寫選區(qū)的整體思路

鑒于常識與操作習(xí)慣. 這里規(guī)定一個光標只能有一個選區(qū) 并且 選擇的內(nèi)容必須是連續(xù)的.

對于一個選區(qū)... 只要

  1. 擁有起點與終點. 由于區(qū)域已經(jīng)規(guī)定必須是連續(xù)的. 那么
  2. 他所包含的區(qū)域就可以計算出來.
  3. 選區(qū)中的內(nèi)容才可以獲得.
  4. 選區(qū)也才可以繪制出來.

但是有個問題!

可能很早之前說了.? 終點總是光標的當(dāng)前位置. 所以只要記下選區(qū)的起點就行了.

然而所說的起點并不是 鼠標按下時候(onmousedown)的那個點.! 這里為了方便記錄. 把鼠標按下時候的那個點叫做 基準點 ..

因為選區(qū)可能是從 基準點開始往左/左上角拉 或者 往右/右下角拉. 圖5-4 這樣.

圖5-4.gif

需要做一個判斷來確定選區(qū)真正的 起點 和 終點. 這樣

  1. 繪制選區(qū)才會比較方便的找準點...
  2. 截取內(nèi)容時. 才會正確?.(下面會說這個問號是為什么)

獲得選區(qū)內(nèi)容

觀察已有的編輯器功能. 可以了解到選區(qū)的創(chuàng)建方式:

  1. 按下 鼠標的時候確定一個選區(qū)基準點
  2. 拖動 鼠標來選取內(nèi)容. (也就是根據(jù)選區(qū)基準點 與 光標當(dāng)前位置 計算選區(qū)起點與終點. 之后根據(jù)起點與終點繪制視圖).
  3. 放開 鼠標后確定選取的內(nèi)容. (實際上步驟同2)

這里也剛好對應(yīng)三個常用的鼠標事件:

  1. 按下 - onmousedown
  2. 拖動 - onmousemove
  3. 放開 - onmouseup

具體功能邏輯的話... 其實原理很簡單.. 只是由于設(shè)計上的問題. 導(dǎo)致代碼暫時有點臃腫. 就直接通過代碼來顯示了.

繪制選區(qū)視圖

最后是關(guān)于選區(qū)的視圖...

根據(jù)選區(qū)必須是連續(xù)的特性以及盒子模型一般是矩形的特性... 可以將選區(qū)分為三種類型 以方便地適應(yīng)所有情況. 見 圖5-5.

圖5-5.png
  1. 只有一行的: 用一個帶背景顏色的<div class="selection-part" /> 并控制他的偏移量與寬度 來顯示
  2. 只有二行的: 那就用二個<div class="selection-part" />
  3. 多于三行的: 無論多少行都可以用三個來表示<div class="selection-part" />

記得最初的時候創(chuàng)建了selected-container.

就把 <div class="selection-part" /> 放進這層中. 另外 selected-container 這名字好有問題... 不如改成 selection. 再加上這幾個 container 有明確的覆蓋關(guān)系... 最終會改成 selection-layer 這種形式 可能比較好一點... 這里暫時不會改

code - 獲得選區(qū)內(nèi)容

首先來綁定事件... 因為邏輯比較清晰啦... 這里是就直接先調(diào)用還不存在的函數(shù)... 然后再去寫函數(shù)內(nèi)部的具體邏輯..(從界面進入到邏輯)

@path serval/script/harusame-serval.js
// 目標代碼

Serval.prototype._bindMouseEvent: function () {
    var self = this

    var isMouseDown = false

    /**
     * addEventListener 是指自己寫的方法,見最下面
     * 當(dāng) mousedown 時,就對光標位置進行計算
     * 1. 取消鼠標默認的行為,否則 2 不會生效
     * 2. 讓編輯器總是能夠接受鍵盤事件
     * 3. 定位鼠標
     * 4. 設(shè)置選區(qū)基準點
     * 5. 記憶鼠標已經(jīng)點擊還未彈起, 用來避免鼠標沒有點擊就一直更新選區(qū)
     */
    addEventListener(self.$serval_container, 'mousedown', function (event) {
        event.preventDefault() /* 1 */

        self.$inputer.focus() /* 2 */

        self.allocTask(function (v_cursor) {
            v_cursor.psysicalY = event.layerY /* 3 */
            v_cursor.psysicalX = event.layerX /* 3 */

            v_cursor.setSelectionBase() /* 4 */
        })

        isMouseDown = true /* 5 */
    })

    /**
     * 這里先不管觸發(fā)的頻次是否頻繁什么的...
     * 1. 當(dāng) mousemove 時 且 鼠標 按下 時,更新光標位置
     * 2. 更新選區(qū)起點 與 終點 與 視圖
     */
    addEventListener(self.$serval_container, 'mousemove', function (event) {
        event.preventDefault()
        if (isMouseDown) { 
            self.allocTask(function (v_cursor) {
                v_cursor.psysicalY = event.layerY /* 1 */
                v_cursor.psysicalX = event.layerX /* 1 */

                v_cursor.updateSelection() /* 2 */
            })
        }
    })

    /**
     * 1. 標記鼠標已經(jīng)彈起
     * 2. 更新光標位置
     * 3. 更新選區(qū)起點 與 終點 與 視圖
     * 4. 當(dāng)存在選區(qū)的時候
     *      4.1. 獲得選區(qū)內(nèi)容, ---> 這里先測試下是否能獲取到選區(qū)內(nèi)容 <---
     */
    addEventListener(self.$serval_container, 'mouseup', function (event) {
        event.preventDefault()

        isMouseDown = false /* 1 */

        self.allocTask(function (v_cursor) {
            v_cursor.psysicalY = event.layerY /* 2 */
            v_cursor.psysicalX = event.layerX /* 2 */

            v_cursor.updateSelection() /* 3 */

            if (v_cursor.isSelectionExist()) { /* 4 */
                console.log(v_cursor.getSelectionContent()) /* 4.1 */
            }
        })
    })
},
@path serval/script/harusame-cursor.js

/**
 * 設(shè)定選區(qū)基準點
 */
Cursor.prototype.setSelectionBase: function () {
    this.mousedown_point = {
        logicalY: this.logicalY,
        logicalX: this.logicalX,
        psysicalY: this.psysicalY,
        psysicalX: this.psysicalX
    }
},

/**
 * 更新選區(qū)
 */
Cursor.prototype.updateSelection: function () {
    this.findSelection()
    // this.updateSelectionView() 用來更新選區(qū)視圖
},

/**
 * 找到選區(qū)的 起點 與 終點
 * @ 這個函數(shù)名字不怎么合適.. 而且代碼很丑..
 * @ 如果把坐標單獨做成一個類 就會好看很多(大概
 * @ 甚至可以比如 point_end = this.mousedown_point
 * @ 但是就現(xiàn)在來說的話干脆就讓他更臃腫 反而好看點.(大概 
 * @ _(:3」∠)_
 */
Cursor.prototype.findSelection: function () {
    var point_start = {}
    var point_end = {}

    if (this.logicalY < this.mousedown_point.logicalY) {
        point_start.logicalY = this.logicalY
        point_start.logicalX = this.logicalX
        point_start.psysicalY = this.psysicalY
        point_start.psysicalX = this.psysicalX

        point_end.logicalY = this.mousedown_point.logicalY
        point_end.logicalX = this.mousedown_point.logicalX
        point_end.psysicalY = this.mousedown_point.psysicalY
        point_end.psysicalX = this.mousedown_point.psysicalX
    } else if (this.logicalY === this.mousedown_point.logicalY) {
        if (this.logicalX < this.mousedown_point.logicalX) {
            point_start.logicalY = this.logicalY
            point_start.logicalX = this.logicalX
            point_start.psysicalY = this.psysicalY
            point_start.psysicalX = this.psysicalX

            point_end.logicalY = this.mousedown_point.logicalY
            point_end.logicalX = this.mousedown_point.logicalX
            point_end.psysicalY = this.mousedown_point.psysicalY
            point_end.psysicalX = this.mousedown_point.psysicalX
        } else {
            point_start.logicalY = this.mousedown_point.logicalY
            point_start.logicalX = this.mousedown_point.logicalX
            point_start.psysicalY = this.mousedown_point.psysicalY
            point_start.psysicalX = this.mousedown_point.psysicalX

            point_end.logicalY = this.logicalY
            point_end.logicalX = this.logicalX
            point_end.psysicalY = this.psysicalY
            point_end.psysicalX = this.psysicalX
        }
    } else {
        point_start.logicalY = this.mousedown_point.logicalY
        point_start.logicalX = this.mousedown_point.logicalX
        point_start.psysicalY = this.mousedown_point.psysicalY
        point_start.psysicalX = this.mousedown_point.psysicalX

        point_end.logicalY = this.logicalY
        point_end.logicalX = this.logicalX
        point_end.psysicalY = this.psysicalY
        point_end.psysicalX = this.psysicalX
    }

    this.selection_start = point_start
    this.selection_end = point_end
},

/**
 * 判斷是否有選區(qū)
 */
Cursor.prototype.isSelectionExist: function () {
    if (this.logicalY === this.mousedown_point.logicalY && this.logicalX === this.mousedown_point  .logicalX) {
        return false
    }
    return true
},

/**
 * 獲得選區(qū)內(nèi)容
 * 1. 如果選區(qū)只有一行
 *      1.1. 截取 起點 與 終點 的內(nèi)容,且不需要換行
 * 2. 如果選區(qū)只有二行
 *      2.1. 截取 起點 到 起點行末尾 的內(nèi)容
 *      2.2. 截取 終點行開始 到 終點 的內(nèi)容,且不需要換行
 * 3. 如果選區(qū)大于二行
 *      3.1. 截取 起點 到 起點行末尾 的內(nèi)容
 *      3.2. 遍歷除了 起點行 與 終點行 的其他行
 *          3.2.1. 截取該行的整段內(nèi)容
 *      3.3. 截取 終點行開始 到 終點 的內(nèi)容,且不需要換行
 */
getSelectionContent: function () {
    var point_start = this.selection_start
    var point_end = this.selection_end

    var result = ''
    var count = point_end.logicalY - point_start.logicalY
    var start_line_text = Line.getLineContentByLogicalY(point_start.logicalY).textContent

    /* 1 */
    if (count === 0) {
        console.log('--> 選區(qū)類型 : 一行 <--')
        result += start_line_text.substring(point_start.logicalX, point_end.logicalX) /* 1.1 */

    /* 2 */
    } else if (count === 1) {
        console.log('--> 選區(qū)類型 : 二行 <--')
        result += start_line_text.substring(point_start.logicalX, start_line_text.length) + '\n' /* 2.1 */

        var end_line_text = Line.getLineContentByLogicalY(point_end.logicalY).textContent
        result += end_line_text.substring(0, point_end.logicalX) /* 2.2 */

    /* 3 */
    } else {
        console.log('--> 選區(qū)類型 : 多行 <--')
        result += start_line_text.substring(point_start.logicalX, start_line_text.length) + '\n' /* 3.1 */

        /* 3.2 */
        for (var i = point_start.logicalY + 1; i < point_end.logicalY; i++) {
            result += Line.getLineContentByLogicalY(i).textContent + '\n' /* 3.2.1 */
        }

        var end_line_text = Line.getLineContentByLogicalY(point_end.logicalY).textContent
        result += end_line_text.substring(0, point_end.logicalX) /* 3.3 */
    }

    return result
},

就是這樣.先來調(diào)試一下.保證選區(qū)數(shù)據(jù)是返回正確的再來做視圖哦... 來看看效果... 見圖5-6.

圖5-6.gif

... 嗯嗯.. 內(nèi)容能用各種姿勢獲取到.~

這里額外說一下... 好早之前的版本中忘記做了單行選區(qū)的 選區(qū)起點 終點的判斷
會產(chǎn)生比如這樣的事情 textContent.substring(6, 0). 但這并沒有報錯...

見挺靠譜的文檔 MDN 其中說到了

If indexStart is greater than indexEnd, then the effect of substring() is as if the two arguments were swapped; for example, str.substring(1, 0) == str.substring(0, 1).

感覺有點神奇... 會做這樣的處理...

code - 獲得選區(qū)內(nèi)容

確保選區(qū)的數(shù)據(jù)獲得是正確的之后. 來嘗試做選區(qū)的視圖部分...

簡化的選區(qū)視圖
編輯器中的選區(qū)視圖

之前也說過.. 這里的選區(qū)最多只會劃分為三段... 這是為了防止操作太多的DOM起見...(偷懶.
當(dāng)然像一般的編輯器那樣. 每一行單獨一段高亮的選區(qū)也不是不行啦... 只是還沒做就感覺會卡(hen)卡(ma)的(fan).

CSS

為了能快地看到成型后的效果. 先以最快速度把<div class="selection-part" /> 塞進 <div class="selected-container /> 中. 再來做 js 的部分.

  1. 因為選區(qū)的樣式已經(jīng)很直觀了... 這里就先寫的 css.
.selection-content {
    position: absolute;
    top: 0;
    left: 0; 

    right: 0; 
    /* 
     * 這里用 right: 0 讓寬度鋪滿一行
     * 不用 width: 100% 是因為在個人習(xí)慣調(diào)試的時候盡量不麻煩其他元素節(jié)點的樣式
     * 并且此時任意一個父類也還沒有設(shè)置 overflow: hidden; 就換了個方法_(:3」∠)...
     */
    
    height: 20px;
    
    background-color: rgba(120, 120, 120, .5); // 隨便挑一個常用的灰色做測試
}
  1. 再放進 <div class="selected-container /> 中. 嗯嗯..放這里
@path serval/script/harusame-template.js

Template.editor = function () {
    // ...

    /*
     * before:
     * var $selected_container = SatoriDom.compile(e('div', {'class': 'selected-container'})
     */
    var $selected_container = SatoriDom.compile(e('div', {'class': 'selected-container'}, [
        e('div', {'class': 'selection-part'}),
        e('div', {'class': 'selection-part'}),
        e('div', {'class': 'selection-part'})
    ]))

    // ...
}

  1. 嗯嗯... 這就是效果. 見圖5-7
圖5-7.png
  1. 這里先模擬實際效果. 再把模擬的過程轉(zhuǎn)化為用 js 來控制:
    選區(qū)的控制是通過改變 top left rightheight 來實現(xiàn)的.
    見圖5-5 中 (這里復(fù)制過來了..
圖5-5.png

先規(guī)定一下:

  • 選區(qū)中的最上面這行. 比如 1 3 7 行. 之后記為 $selection_top

  • 選區(qū)中的中間的部分. 比如 8-9 行. 之后記為 $selection_middle

  • 選區(qū)中的最下面這行. 比如 4 10 行. 之后記為 $selection_bottom

模擬過程比如像這樣. 見圖5-8:

圖5-8.gif

可以看到由于樣式方面的原因.(可能算是問題).. 計算選區(qū)視圖大小的時候要額外算上行號所在的空間的寬度. 是 50px.

繪制視圖的整個流程就是:

當(dāng) mousemove 或者 mouseup 的時候. 比對當(dāng)前光標的位置 與 選區(qū)起點 是否出了偏差 (計算 logicalY)... 如果有就更新選區(qū)視圖

  1. 計算并存儲兩點間的 Y 上的差
    var diffY = point_end.logicalY - point_start.logicalY

2.1. 如果 diffY === 0. 更新
$selection_top 的 DOM 的 top left

2.2. 如果 diffY === 1. 更新
$selection_toptop left &&
$selection_bottomtop right

2.3. 如果 diffY > 1. 更新
$selection_toptop left &&
$selection_middletop height &&
$selection_bottomtop right

JS

現(xiàn)在測試都基本沒問題了. 如果測試內(nèi)容對實際要做的東西會有干擾. 就考慮刪掉哦.. 有以下這個:

還原為

然后在 template 中加入這個

@path serval/script/harusame-template.js

/**
 * 選區(qū)片段
 */
selectionPart: function () {
    return SatoriDom.compile(
        e('div', {'class': 'selection-part'})
    )
}

接下來把之前想要做的流程轉(zhuǎn)換為代碼..

@path serval/script/harusame-cursor.js

/**
 * 1. 光標本身的元素節(jié)點
 * 2. 之前所說的基準點可以做一個初始化.
 */
var Cursor = function (config) {
    // ...

    this.mousedown_point = {}   /* 2 */

    this.$selection_top = Template.selectionPart()
    this.$selection_middle = Template.selectionPart()
    this.$selection_bottom = Template.selectionPart()

    // ...
}

/**
 * 更新選區(qū)的 值 與 視圖
 */
Cursor.prototype.updateSelection: function () {
    this.findSelection()
    this.updateSelectionView()
},

/**
 * 更新選區(qū)視圖
 */
Cursor.prototype.updateSelectionView: function () {
    var point_start = this.selection_start
    var point_end = this.selection_end

    var diffY = point_end.logicalY - point_start.logicalY

    switch (diffY) {

        case 0:
            this.$selection_top.style.cssText =
                'top:' + point_start.psysicalY + 'px;' +
                'right:' + (750 - point_end.psysicalX) + 'px;' +
                'left:' + (50 + point_start.psysicalX) + 'px;' +
                'display:' + 'block;'

            this.$selection_middle.style.display = 'none'

            this.$selection_bottom.style.display = 'none'

            break

        case 1:
            this.$selection_top.style.cssText =
                'top:' + point_start.psysicalY + 'px;' +
                'right:' + 0 + 'px;' +
                'left:' + (50 + point_start.psysicalX) + 'px;' +
                'display: block;'

            this.$selection_middle.style.display = 'none'

            this.$selection_bottom.style.cssText =
                'top:' + point_end.psysicalY + 'px;' +
                'right:' + (750 - point_end.psysicalX) + 'px;' +
                'left:' + 50 + 'px;' +
                'display: block;'

            break

        default:
            this.$selection_top.style.cssText =
                'top:' + point_start.psysicalY + 'px;' +
                'right:' + 0 + 'px;' +
                'left:' + (50 + point_start.psysicalX) + 'px;' +
                'display: block;'

            this.$selection_middle.style.cssText =
                'top:' + (point_start.psysicalY + Line.line_height) + 'px;' +
                'left:' + 50 + 'px;' +
                'height:' + (point_end.psysicalY - point_start.psysicalY - Line.line_height) + 'px;' +
                'display: block;'

            this.$selection_bottom.style.cssText =
                'top:' + point_end.psysicalY + 'px;' +
                'right:' + (750 - point_end.psysicalX) + 'px;' +
                'left:' + 50 + 'px;' +
                'display: block;'

            break
    }
},

來看看有沒有問題... 見 圖5-9.

圖5-9.gif

嗯... 選區(qū)應(yīng)該沒有什么問題.

說起來示例gif 里的內(nèi)容都是無意義的數(shù)字之類的...因為復(fù)制粘貼什么的還沒有做..就暫時用這些代替了..

接下來可能是 復(fù)制 剪切 粘貼 Home End ↑ ↓ ← → ...
感覺內(nèi)容好多... 其實感覺依舊好水_(:3」∠)...

在做完這些最基礎(chǔ)的功能之后... 重新調(diào)整與優(yōu)化代碼.. 之后再做多個光標.. 代碼高亮&&智能提示 之類的東西


CHANGELOG

2017年8月10日14:14:00
F 在 getSelectionContent 中 修復(fù)了多余的 \n


上一篇
#4 從零開始制作在線 代碼編輯器

下一篇
#6 從零開始制作在線 代碼編輯器

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容