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

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

目錄結(jié)構(gòu)和說明


目錄初始結(jié)構(gòu)

創(chuàng)建一些目錄以及文件燎含,如圖1-1 所示。這里只是隨手創(chuàng)的,也可以自定目錄結(jié)構(gòu)。

1-1 目錄結(jié)構(gòu)

目錄說明

  • serval/script/harusame-dom.js 提供了創(chuàng)建 DOM 的工具皆看,比createElement(tagName) 那種要稍微方便一點,創(chuàng)建方式借鑒了虛擬DOM背零。使用方式就在這里說明了腰吟,之后也不細(xì)說。
// 創(chuàng)建元素節(jié)點
var $node = SatoriDom.compile(e('div', {'class': 'demo', 'id': 'demo'}, '這里是文本節(jié)點: parent', [
    e('div', {'class': 'child'}, [
        e('p', '這里是文本節(jié)點: child x child')
    ])
]))

console.log($node.tagName) // DIV

// 其他可能用到的方式
function template_submit (data) {
    var $event_node = SatoriDom.compile(e('input', {'class': 'i-have-event', 'type': 'submit'}))

    $event_node.addEventListener('click', function (event) {
        console.log('You click me:' + data.content)
    })

    return SatoriDom.compile(e('form', {'class': 'this-is-a-form'}, [
        $event_node
    ]))
}

  • serval/script/harusame-template.js 存放了一些模板徙瓶,把與創(chuàng)建DOM 相關(guān)的代碼抽離出來毛雇,避免其在邏輯代碼中占用很多篇幅。

  • serval/script/harusame-cursor.js 光標(biāo)會被單獨抽象成一個類侦镇。

  • serval/script/harusame-serval.js 綁定整個編輯器的事件灵疮,以及邏輯的處理,最重要的部分壳繁。

  • serval/style/normalize.scssnormalize.scss 官網(wǎng)震捣,這里改了后綴只是方便后綴格式相同....

  • serval/style/harusame-code.scss 是目錄 serval/style/code/ 下所有需要高亮的語言的樣式的 入口,這樣便于以后做其他語言的樣式擴展闹炉。這里先有能力解決 js 的語法高亮再想著其他語言吧蒿赢。

  • serval/style/harusame-serval.scss 描述了編輯器樣式。

先直接描繪出成型后的編輯器


對著已有的優(yōu)秀的編輯器觀察剩胁,把看到的東西抽象成幾部分诉植,再根據(jù)這些編寫成 DOMs 祥国。
這里對著 Sublime Text 3 截了一張圖 1-2:

1-2 Sublime Text 3

挺小也挺簡單的一張圖昵观,但是包含了想做的編輯器的大部分所需 DOMs,或者稱他為組件舌稀,這里說下明確當(dāng)前要做的部分:

  • 行號 ( 6 ~ 15)
  • 光標(biāo) ( 第七行最后的白色豎線)啊犬,與多重光標(biāo)(7, 9, 15)
  • 選中行提示(比如第七行)
  • 選中內(nèi)容提示(比如第九行,第 13 ~ 15 行)
    這里插入一點壁查,以下內(nèi)容會是顯而易見的觉至,但是也是編輯器組成中最最重要的,盡量把這些理所當(dāng)然的也把它梳理出來睡腿,畢竟他們也需要 DOM 來顯示
  • 行的內(nèi)容(比如第七行的 state = {
  • 編輯器樣式(比如這里深灰色语御,行號的灰色,非關(guān)鍵詞的白色)
    以下是一些肉眼看不到的
  • 行能夠接受鍵盤輸入
  • 點擊一個位置席怪,光標(biāo)會自動偏移到該行中应闯,離點擊字符最近的位置。
  • 行號無法選中

至于一些其他的細(xì)節(jié):代碼高亮挂捻,成對的括號提示等碉纺,這里會放在以后再做/說。

于是根據(jù)這些梳理好的內(nèi)容,轉(zhuǎn)換成 DOMs:

  • 編輯器容器 設(shè)置編輯器的背景顏色等骨田,也是存放 其他容器的容器(父節(jié)點)
  • 光標(biāo)容器 存放 所有光標(biāo) 的元素節(jié)點
  • 行容器 存放 一行 的元素節(jié)點耿导,包括它的行號,以及行的內(nèi)容
  • 選擇容器 存放比如 當(dāng)前選擇行态贤,選擇內(nèi)容 的背景高亮的元素節(jié)點
  • 鍵盤輸入事件接收器 正如之前所說(好像說了)舱呻,一個 div 本身是不能接受鍵盤事件的,需要一個接受鍵盤事件的容器悠汽,再將事件的處理邏輯與相應(yīng)的元素節(jié)點綁定狮荔。

以上就是需要的 DOMs 了,包含關(guān)系也基本沒啥問題了:

文件路徑 serval/index

<div id="input-container" class="input-container">
    <!-- 創(chuàng)建一個 serval 專屬的區(qū)域介粘,且總是鋪滿 編輯器的容器 -->
    <div class="serval theme-harusame">
        <!-- 包裹區(qū)域殖氏,總是由 行 的高度所撐開 -->
        <div class="serval-container">
            <!-- 所有 行 的 容器 -->
            <div class="line-container">
                <div class="line">
                        <div class="line-number-wrap">
                            <span class="line-number">1</span>
                        </div>
                       <!--  在pre中,添加使用等寬字體的 buff姻采,使用等寬字體是為了之后雅采,便于文字寬度的計算 -->
                        <pre class="code-wrap">
                            <!-- 因為一般放的是代碼,codeMirror 是用的 span慨亲,試試 code -->
                            <code class="code-content">const PI = 3.1415</code>
                        </pre>
                </div>
                <div class="line">
                        <div class="line-number-wrap">
                            <span class="line-number">2</span>
                        </div>
                        <pre class="code-wrap">
                            <code class="code-content">console.info('PI', PI)</code>
                        </pre>
                </div>
                <div class="line">
                        <div class="line-number-wrap">
                            <span class="line-number">3</span>
                        </div>
                        <pre class="code-wrap">
                            <code class="code-content"></code>
                        </pre>
                </div>
            </div>
            <!--  接受鍵盤事件 的容器 的容器 -->
            <div class="inputer-container">
                <textarea class="inputer"> 
                <!-- 這里使用一個 看不見的(并不是隱藏哦) textarea 來接受鍵盤事件 -->
            </div>
            <!--  所有 光標(biāo) 的容器 -->
            <div class="cursor-container">
                <i class="fake-cursor blink"></i>
            </div>
            <!-- 選擇行 提示 -->
            <div class="selected-container">
                <div class="selected-line"></div>
            </div>
        </div>
    </div>
</div>

文件路徑 serval/style/harusame-serval.css
這里就直接給了編譯后的
還是挺暴力的婚瓜,所有都直接嵌套,防止命名沖突

.serval {
  position: relative;
  height: 100%;
}
.serval .serval-container {
  height: 100%;
}
.serval .serval-container .line-container {
  margin-left: 50px;
  font-size: 0;
}
.serval .serval-container .line-container .line .line-number-wrap {
  position: absolute;
  left: 0;
  text-align: right;
}
.serval .serval-container .line-container .line .line-number-wrap .line-number {
  display: inline-block;
  padding-right: 15px;
  box-sizing: border-box;
  width: 50px;
  height: 20px;
  line-height: 20px;
  font-size: 12px;
}
.serval .serval-container .line-container .line .code-wrap .code-content {
  display: block;
  height: 20px;
  line-height: 20px;
  font-size: 12px;
}
.serval .serval-container .cursor-container {
  position: absolute;
  top: 0;
  margin-left: 50px;
}
.serval .serval-container .cursor-container .fake-cursor {
  display: block;
  position: absolute;
  left: 0;
  top: 0;
  width: 0;
  height: 18px;
  margin-top: 1px;
}
.serval .serval-container .selected-container {
  position: absolute;
  top: 0;
  width: 100%;
}
.serval .serval-container .selected-container .selected-line {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 20px;
}
.serval .serval-container .inputer-container {
  position: absolute;
  top: 0;
  margin-left: 50px;
}
.serval .serval-container .inputer-container .inputer {
  position: absolute;
  left: 0;
  top: 0;
  border: 0;
  resize: none;
  width: 0;
  height: 20px;
  line-height: 20px;
  opacity: 0;
}

.theme-harusame {
  background-color: rgba(0, 0, 0, 0.8);
}
.theme-harusame .serval-container .line-container {
  font-family: Consolas;
}
.theme-harusame .serval-container .line-container .line .line-number-wrap .line-number {
  color: white;
  cursor: default;
}
.theme-harusame .serval-container .line-container .line .code-wrap {
  cursor: text;
}
.theme-harusame .serval-container .line-container .line .code-wrap .code-content {
  font-family: Consolas;
  color: white;
}
.theme-harusame .serval-container .cursor-container .fake-cursor {
  border-right: 1px solid rgba(255, 255, 255, 0.9);
}
.theme-harusame .serval-container .selected-container .selected-line {
  background-color: rgba(255, 255, 255, 0.15);
}

效果圖刑棵,見圖1-3 巴刻,嗯嗯,不錯的感覺蛉签。

1-3 效果圖

HTML 方面應(yīng)該沒有什么問題胡陪,就把它們轉(zhuǎn)換成為模板,寫入 template.js中碍舍,并且刪掉 index.html 中剛才寫的 HTML標(biāo)簽柠座。

文件路徑 serval/script/harusame-template.js

;
(function () {
    Template = {
        /**
         * 編輯器
         */
        editor: function () {
            var $line_container = SatoriDom.compile(e('div', {'class': 'line-container'}))
            var $inputer_container = SatoriDom.compile(e('div', {'class': 'inputer-container'}))
            var $cursor_container = SatoriDom.compile(e('div', {'class': 'cursor-container'}))
            var $selected_container = SatoriDom.compile(e('div', {'class': 'selected-container'}))

            var $serval_container = SatoriDom.compile(e('div', {'class': 'serval-container'}, [
                $inputer_container,
                $selected_container,
                $cursor_container,
                $line_container
            ]))

            var $fragment = SatoriDom.compile(
                e('div', {'class': 'serval theme-harusame'}, [
                    $serval_container
                ])
            )

            return {
                $editor: $fragment,
                nodes: {
                    $serval_container: $serval_container,
                    $line_container: $line_container,
                    $inputer_container: $inputer_container,
                    $cursor_container: $cursor_container,
                    $selected_container: $selected_container
                }
            }
        },

        /**
         * 行
         * @param line_number {string} 行號
         * @param initial_content {string} 該行初始內(nèi)容
         */
        line: function (params) {
            console.info('params', params)
            return SatoriDom.compile(
                e('div', {'class': 'line'}, [
                    e('div', {'class': 'line-number-wrap'}, [
                        e('span', {'class': 'line-number'}, params.line_number)
                    ]),
                    e('div', {'class': 'code-wrap'}, [
                        e('code', {'class': 'code-content'}, params.initial_content || '')
                    ])
                ])
            )
        },

        /**
         * 當(dāng)前選擇行
         */
        selectedLine: function () {
            return SatoriDom.compile(
                e('div', {'class': 'selected-line'})
            )
        },

        /**
         * 光標(biāo)
         */
        cursor: function () {
            return SatoriDom.compile(
                e('i', {'class': 'fake-cursor blink'})
            )
        },

        /**
         * 鍵盤事件接收器
         */
        inputer: function () {
            return SatoriDom.compile(
                e('textarea', {'class': 'inputer'})
            )
        }
    }

    window.Template = Template
})()
文件路徑 serval/script/cursor.js

;
(function () {
    var Cursor = function () {
        this.$ref = null

        this._generateCursor()
    }

    Cursor.prototype = {
        constructor: Cursor,

        /**
         * 創(chuàng)建一個游標(biāo)對象
         */
        _generateCursor: function () {
            this.$ref = SatoriDom.compile(
                e('i', {'class': 'fake-cursor'})
            )
        }
    }

    window.Cursor = Cursor
})()
文件路徑 serval/script/serval.js

;
(function () {
    /**
     * 1. 存放所有的存在游標(biāo)
     */
    var Serval = function (config) {
        /* 1 */
        this.cursor_list = []

        this._generateEditor(config)
        this._generateInputer()
        this._generateCursor()
        this._generateSelectedLine()

        this._generateLine() // 測試用!F稹妈经!
    }

    Serval.prototype = {
        constructor: Serval,

        /**
         * 生成編輯器的主要 DOM結(jié)構(gòu)
         */
        _generateEditor: function (config) {
            var temp = Template.editor()
            var nodes = temp.nodes

            this.$serval_container = nodes.$serval_container
            this.$line_container = nodes.$line_container
            this.$inputer_container = nodes.$inputer_container
            this.$cursor_container = nodes.$cursor_container
            this.$selected_container = nodes.$selected_container

            config['editor-container'].appendChild(temp.$editor)
        },

        /**
         * 生成鍵盤事件接收器,并渲染
         */
        _generateInputer: function () {
            var $inputer = Template.inputer()

            this.$inputer = $inputer
            this.$inputer_container.appendChild($inputer)
        },

        /**
         * 生成一個光標(biāo)捧书,并渲染
         * 1. 創(chuàng)建 cursor 實例
         * 2. 持久化該 cursor 實例
         * 3. 得到該 cursor 實例的元素節(jié)點吹泡,渲染到 光標(biāo)容器 中
         */
        _generateCursor: function () {
            var cursor = new Cursor() /* 1 */

            this.cursor_list.push(cursor) /* 1 */

            this.$cursor_container.appendChild(cursor.$ref) /* 3 */
        },

        /**
         * 生成一行,并渲染
         */
        _generateLine: function () {
            var $line = Template.line({line_number: '1', initial_content: '初始化內(nèi)容'})

            this.$line_container.appendChild($line)
        },

        /**
         * 生成當(dāng)前選擇行的背景顏色提示经瓷,并渲染
         */
        _generateSelectedLine: function () {
            var $selected_line = Template.selectedLine()

            this.$selected_line = $selected_line
            this.$selected_container.appendChild($selected_line)
        }
    }

    window.Serval = Serval
})()

效果圖爆哑,見圖1-4

1-4 效果圖

嗯嗯,好像沒什么毛病...
隨手框選了一下了嚎,發(fā)現(xiàn)泪漂,這幾個字沒法選中廊营,有點不符合預(yù)期,在之后做選取內(nèi)容時萝勤,肯定會坑露筒,所以立馬填了。
額...因為在當(dāng)前層疊上下文中敌卓,選擇行<.selected_container> 的層疊樣式最大(他在最下面) 并且 他有position: absolute 的屬性慎式,導(dǎo)致覆蓋了<.line_container>,所以

  1. template.js 中調(diào)整一下位置趟径,令他們的覆蓋情況更合邏輯瘪吏。
  2. <.line_container> 添加一個 position: relative 屬性
  3. 另外2的副作用就是,由于 <.line_container>具備了定位屬性蜗巧,行的 left: 0掌眠,得手工設(shè)置為left: -50px了,不然布局會亂哦

做這些理論上就可以了幕屹,當(dāng)然如果懶得改的話蓝丙,設(shè)置z-indexpointer-events之類的也可以望拖,這里就不記了:

文件路徑 serval/script/harusame-template.js
改成這樣 ↓

var $fragment = SatoriDom.compile(
    e('div', {'class': 'serval theme-harusame'}, [
        e('div', {'class': 'serval-container'}, [
            $inputer_container,
            $selected_container,
            $cursor_container,
            $line_container
        ])
    ])
)
文件路徑 serval/style/harusame-serval.scss

.line-container {
    // 添加一條
    position: relative;
}

.line-number-wrap {
    // 添加一條
    left: -50px;
    // 這里的 50px 取決于 .line-number 的 width 屬性
}

光標(biāo)


先說說光標(biāo)的邏輯渺尘。
可以在 #0 節(jié)的例子中了解到

// ...
... substring(0, logicalX) ...
// ...

編輯器實際上通過 字符的位置 來添加,刪除文本/代碼说敏,這里所說的字符的位置鸥跟,我自作主張把它稱為 邏輯位置,對應(yīng)到 Cursor實例 中盔沫,給他命名為 logical医咨,相應(yīng)地,也有 物理位置(視圖位置)迅诬,對應(yīng)到 Cursor實例 的DOM 中腋逆,給他命名為 psysical

logicalY 決定了光標(biāo)在編輯器中的 行號
psysicalY 決定了光標(biāo) DOM 的 top
logicalX 決定了光標(biāo)在編輯器中的 列號侈贷,或者說 第 logicalX 個字符
psysicalX 決定了光標(biāo) DOM 的 left

除了這些,光標(biāo)還需要一個保存選區(qū)的屬性等脂,這里命名為selection俏蛮,

selection 是一個對象,它包含一個起始點坐標(biāo)start上遥,與一個終點坐標(biāo)end

舉個栗子搏屑,對于圖 1-5:

1-5

為了計數(shù)方便,刪除了部分縮進(jìn)粉楚,以及這里的縮進(jìn)都是 4個空格辣恋。
有以下光標(biāo):

  • 第三行的光標(biāo) cursor_1
  • 第四行的光標(biāo) cursor_2
  • 第五行的光標(biāo) cursor_3
  • 第六行的光標(biāo) cursor_4

它們分別有以下屬性:
(如果看懂了文字說明亮垫,以下代碼就隨便看看)

cursor_1 = {
    logicalY: 2, /* 從零開始計算 */
    psysicalY: 40, /* 約定一行的高度為 20px */

    logicalX: 2, /* 從零開始計算 */
    psysicalX: 14, 
    /* 字符'l',字符'e'的寬度伟骨,通過標(biāo)尺類工具可以得到一個字符(英文)的寬度約等于 7px
     * 手工寫死的字符寬度面對不同的情況或者說樣式上變化饮潦,會比較難維護(hù)
     * 接下來馬上會說怎么使用瀏覽器的計算,雖然也不怎么優(yōu)雅......_(:3」∠)...
     * /

    selection: null
}

cursor_2 = {
    logicalY: 3, 
    psysicalY: 60, 

    logicalX: 0, 
    psysicalX: 0, 

    selection: null
}

cursor_3 = {
    logicalY: 4, 
    psysicalY: 80, 

    logicalX: 5, 
    psysicalX: 35, // 5 * 7

    selection = {
        start: {
            logicalY: 4,
            psysicalY: 80,
            logicalX: 0,
            psysicalX: 0
        },

       /* end: {}
        * 寫到這里的時候發(fā)現(xiàn)携狭,end 的位置總是與當(dāng)前位置相同继蜡,于是決定不要這個冗余的部分,
        * 雖然與 start 相對應(yīng)的 end 缺失了逛腿,可能會 '引起不適'稀并,
        * 但是暫時覺得沒必要多出一份冗余數(shù)據(jù)
        * 所以這里改變以下 selection的寫法 ↓,把 selection 刪了单默,只留下 selection_start
        */
    }

    selection_start = {
        logicalY: 4,
        psysicalY: 80,
        logicalX: 0,
        psysicalX: 0
    }
}

cursor_4 = {
    logicalY: 5, 
    psysicalY: 100,
    logicalX: 4,
    /*
     * 剛好四個空格碘举,觀察仔細(xì)會發(fā)現(xiàn),圖1-5 的距離不會是 4格搁廓,
     * 這里因為偷懶用了 tab進(jìn)行縮進(jìn)殴俱,導(dǎo)致如此。
     * 不要在意細(xì)節(jié)..._(:3」∠)...啊枚抵,不對线欲,編程還是得很注意細(xì)節(jié)的
     */

    psysicalX: 28, /* 空格也約為 7px */

    selection_start = {
        logicalY: 5,
        psysicalY: 100,
        logicalX: 4,
        psysicalX: 28
    }
}

如果看完了甚至沒看上面的代碼,都可以 顯然 得到:

psysicalY = line_height * logicalY
logicalY = psysicalY / line_height
$current_line = document.querySelector(SIGN_LINE + logicalY)

psysicalX = single_byte_length * 7 + double_byte_length * 12
logicalX = single_byte_length + double_byte_length

  • 這里的 line_height汽摹,不是指 css 中的 line-height 哦李丰,是指 該行的高度

  • 這里的SIGN_LINE,是一個自己約定的名字逼泣,這里傾向于使用id趴泌,而不是class,個人覺得理由如下:

    1.每行都是唯一的
    2.性能稍微高一點
    3.可以利用錨點方便地跳轉(zhuǎn)到該行位置

比如 github 中的代碼編輯器(也可以看 codeMirror

github中的代碼編輯器

  • single_byte_length 這個變量名字譯為單字節(jié)字符的長度(數(shù)量)拉庶,同理double_byte_length 變量名字譯為雙字節(jié)字符的長度(數(shù)量)嗜憔。
    (這個名字我不清楚對不對,之后再查閱下文獻(xiàn)氏仗。

為什么這樣分呢吉捶?
因為在計算光標(biāo)的psysicalX時,需要計算字符的 寬度皆尔,所以根據(jù)寬度來分呐舔。
font-size: 12px 的前提下,
單字節(jié)字符的寬度慷蠕,比如數(shù)字珊拼,英文字符,半角標(biāo)點符號
a b c 1 2 3 . , /
等等 都約為 7px

雙字節(jié)字符的寬度流炕,比如漢字澎现,日語仅胞,全角標(biāo)點符號
你 好 啊 こ は お ア 。剑辫,干旧、
等等 都約為 12px

說明就到這里。
之所以把這些類似公式一樣的東西列出來揭斧,就是想說它們之間總是一一對應(yīng)莱革,改變其中一個讹开,另外一個也相應(yīng)地改變。這個情況闹击,是不是有點類似于雙向綁定的概念呢。提到雙向綁定成艘,在如今赏半,前端大佬們有個比較好的處理方法就是使用Object.defineProperty哦断箫。

接下來終于可以開始寫代碼。

計算光標(biāo)位置

首先 先去serval/script/harusame-serval.js 中提供事件

文件位置 serval/script/harusame-serval.js
為了能顯示更多的有效內(nèi)容秋冰,先把寫過的代碼在這里刪掉了仲义。

;
(function () {
    var Serval = function (config) {
        this._bindMouseEvent()
    }

    Serval.prototype = {
        /**
         * 綁定各種鼠標(biāo)事件
         */
        _bindMouseEvent: function () {
            var self = this

            /**
             * addEventListener 是指自己寫的方法剑勾,見最下面
             * mousedown 時,就對光標(biāo)位置進(jìn)行計算
             */
            addEventListener(self.$serval_container, 'mousedown', function (event) {
                console.log(event) // 先看看 event 中有什么好用的屬性暂刘。
            })
        },
    }

    /**
     * 可能會對 addEventListener 進(jìn)行一些兼容處理
     * 實際上并沒有處理捂刺,不過也好,至少留一條后路...
     */
    function addEventListener (v_el, v_type, v_callback) {
        v_el.addEventListener(v_type, v_callback)
    }

    window.Serval = Serval
})()

先看看 event 中有什么好用的屬性芝发。
這里說一下由于一些事件在不同的瀏覽器行為是不同的苛谷,如果要兼容某個版本的瀏覽器腹殿,所以最好調(diào)試的時候看看預(yù)先各個瀏覽器的事件行為溜哮。這里在 Firefox 以及 Google 是沒什么差異的构挤,所以就不截圖了萝究。

onmousedown 下的 event

這里可以看到有二個 layer 前綴的屬性婴栽,它總是能得到 相對于 event.target 元素的 點擊位置爱谁。這個特性就很好用访敌!

于是決定使用 event.layerYevent.layerX 來進(jìn)行光標(biāo)位置的計算衣盾!
這里要插一句_(:3」∠)...势决,在 demo 中 Y的位置 是使用 event.target.id.split[SIGN][1]來獲得的阻塑,但是在以后的開發(fā)中,記得會出現(xiàn)一些麻煩的判斷陈莽,所以這里用 layerY 嘗試一下据悔。嘛,各有利弊朱盐,layerY的話兵琳,也可能得考慮padding margin height line-heightcss造成的計算偏差骇径。

位置: serval/script/harusame-serval.js
為了突出主要說明部分,刪掉了其他代碼

;
(function () {
    var Serval = function (config) {
        this._bindMouseEvent()
    }

    Serval.prototype = {
        constructor: Serval,

        /**
         * 綁定各種鼠標(biāo)事件
         */
        _bindMouseEvent: function () {
            var self = this

            /**
             * addEventListener 是指自己寫的方法清女,見最下面
             * 當(dāng) mousedown 時嫡丙,就對光標(biāo)位置進(jìn)行計算
             */
            addEventListener(self.$serval_container, 'mousedown', function (event) {
                self.allocTask(function (v_cursor) {
                    v_cursor.psysicalY = event.layerY
                    v_cursor.psysicalX = event.layerX
                })
            })
        },

        /**
         * 為所有光標(biāo)分配任務(wù)
         * 對光標(biāo)操作時,統(tǒng)一通過這個接口
         * 這里約定必須傳入一個回調(diào)函數(shù)拥刻,所以不使用 v_task && v_task() 進(jìn)行判斷
         */
        allocTask: function (v_task) {
            var self = this

            for (var i = 0, len = self.cursor_list.length; i < len; i++) {
                v_task(self.cursor_list[i])
            }
        },
    }
})()

做好了入口般哼,接下來去 serval/script/harusame-cursor.js 寫邏輯哦惠窄。
先寫logicalYpsysicalY部分

文件位置 serval/script/harusame-cursor.js

;
(function () {
    /**
     * 這里先約(寫)定(死) 行高
     */
    var LINE_HEIGHT = 20

    /**
     * 1. 光標(biāo)本身的元素節(jié)點
     * 2. 光標(biāo)所在行的元素節(jié)點
     */
    var Cursor = function (config) {
        this.$ref = null /* 1 */
        this.$line = null /* 2 */
        this._logicalY = 0
        this._logicalX = 0
        this._psysicalY = 0
        this._psysicalX = 0

        this.selection_start = null

        this._generateCursor()
        this._setObserver()
    }

    Cursor.prototype = {
        constructor: Cursor,

        /**
         * 創(chuàng)建一個游標(biāo)對象
         */
        _generateCursor: function () {
            this.$ref = SatoriDom.compile(
                e('i', {'class': 'fake-cursor'})
            )
        },

        /**
         * 綁定 邏輯位置 與 物理位置 之間的關(guān)系
         */
        _setObserver: function () {
            /**
             * 這里的 self 由于也是 js關(guān)鍵字睬捶,所以會高亮
             * self 原本指向 window,一般用不到
             */
            var self = this

            /**
             * 1. 這里賦值的是  _logicalY 哦臀晃,下面也是
             * 2. 更新 psysicalY 的值
             * 3. 更新 DOM 位置
             * 4. 寫到這里發(fā)現(xiàn)有點問題......
             */
            Object.defineProperty(self, 'logicalY', {
                set: function (v_logicalY) {
                    self._logicalY = v_logicalY /* 1 */
                    self._psysicalY = self.calcPsysicalY(v_logicalY) /* 2 */
                    self.setY(self._psysicalY) /* 3 */

                    self.$line = document.getElementById(LINE) /* 4 */
                },

                get: function () {
                    return self._logicalY
                }
            })

            Object.defineProperty(self, 'psysicalY', {
                set: function (v_psysicalY) {
                    self.logicalY = self.calcLogicalY(v_psysicalY)
                },

                get: function () {
                    return self._psysicalY
                }
            })
        },

        setY: function (v_psysicalY) {
            this.$ref.style.top = v_psysicalY + 'px'
        },

        /**
         * 計算 物理 Y
         */
        calcPsysicalY: function (v_logicalY) {
            return v_logicalY * LINE_HEIGHT
        },

        /**
         * 計算 邏輯 Y
         */
        calcLogicalY: function (v_psysicalY) {
            return parseInt(v_psysicalY / LINE_HEIGHT)
        }
    }

    window.Cursor = Cursor
})()

這里可以看看效果徽惋,截圖軟件截不到鼠標(biāo)_(:3」∠)...险绘。

但是也注意到了設(shè)計上的問題:在self.logicalY 中誉碴,需要用到 $line黔帕,這個東西不應(yīng)該分在Cursor中,畢竟他不屬于光標(biāo)呐芥,以及LINE_HEIGHT 屬性奋岁,也不屬于闻伶。甚至在接下來的 self.logicalX 中,也需要用到 $line铡买,這個時候霎箍,不如把與 有關(guān)的,再單獨抽象成一個類會比較合適景埃。

休息會

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谷徙,一起剝皮案震驚了整個濱河市完慧,隨后出現(xiàn)的幾起案子剩失,更是在濱河造成了極大的恐慌,老刑警劉巖脾歧,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鞭执,死亡現(xiàn)場離奇詭異芒粹,居然都是意外死亡,警方通過查閱死者的電腦和手機估脆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進(jìn)店門旁蔼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來疙教,“玉大人贞谓,你說我怎么就攤上這事∷钔” “怎么了?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵泞坦,是天一觀的道長贰锁。 經(jīng)常有香客問我滤蝠,道長,這世上最難降的妖魔是什么锣险? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任芯肤,我火速辦了婚禮焕济,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘掩幢。我一直安慰自己上鞠,他們只是感情好芍阎,可當(dāng)我...
    茶點故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著轮听,像睡著了一般血巍。 火紅的嫁衣襯著肌膚如雪珊随。 梳的紋絲不亂的頭發(fā)上柿隙,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天禀崖,我揣著相機與錄音螟炫,去河邊找鬼不恭。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的钥星。 我是一名探鬼主播谦炒,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼宁改,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了爹耗?” 一聲冷哼從身側(cè)響起谜喊,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤斗遏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后账蓉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體逾一,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡嬉荆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了椅亚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片舱污。...
    茶點故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡扩灯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出惧磺,到底是詐尸還是另有隱情捻撑,我是刑警寧澤,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站江解,受9級特大地震影響犁河,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜耕魄,卻給世界環(huán)境...
    茶點故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一彭谁、第九天 我趴在偏房一處隱蔽的房頂上張望缠局。 院中可真熱鬧,春花似錦读处、人聲如沸唱矛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽包个。三九已至,卻和暖如春树灶,著一層夾襖步出監(jiān)牢的瞬間糯而,已是汗流浹背歧蒋。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人阐虚。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓实束,卻偏偏與公主長得像逊彭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子避矢,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,630評論 2 359

推薦閱讀更多精彩內(nèi)容