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

終于有網(wǎng)了

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

Line


harusame-line.js

serval/script/ 下創(chuàng)建 harusame-line.js

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

;
/**
 * 1. 行 的高度,同樣,這里先約(寫)定(死)
 */
(function () {
    var Line = {}
    var self = Line

    self.LINE_HEIGHT = 20 /* 1 */

    window.Line = Line
})()

同時(shí)修改掉serval/script/harusame-cursor.js 中的 LINE_HEIGHTLine.LINE_HEIGHT

,既然是 Line類,順便把 serval/script/harusame-serval.js 中的 _generateLine 改造為由 Line 生成,這樣會(huì)比較合適吧,也順手把生成行號(hào)的方法寫一下~

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

/**
 * 渲染一行
 */
_generateLine: function (v_content) {
    var $line = Line.generateLine(v_content)
    this.$line_container.appendChild($line)
}
文件路徑 serval/script/harusame-line.js

/**
 * 生成一行
 * @param content {string} 初始內(nèi)容
 */
self.generateLine = function (v_content) {
    var line_number = self.max_line_number + ""
    var initial_content = v_content || ''
    return Template.line({line_number: line_number, initial_content: initial_content})
},

/**
 * 生成最大行號(hào)
 */
var PROXY_max_line_number = 0
Object.defineProperty(self, 'max_line_number', {
    set: function (v_max_line_number) {
        PROXY_max_line_number = v_max_line_number
    },

    get: function () {
        return PROXY_max_line_number++
    }
})

這時(shí)候刷新了下瀏覽器發(fā)現(xiàn)報(bào)錯(cuò)了,嗯嗯...別忘記引入harusame-line.js。

文件路徑 serval/index.html

...
<script src="script/harusame-dom.js"></script>
<script src="script/harusame-template.js"></script>
<script src="script/harusame-line.js"></script>
<script src="script/harusame-cursor.js"></script>
<script src="script/harusame-serval.js"></script>
...

引入位置別忘了只能放在 harusame-cursor.js && harusame-serval.js 以上,harusame-template 以下...

繼續(xù) #1 中計(jì)算 Cursor 位置的邏輯

嘛,要計(jì)算 psysicalX logicalX,要比計(jì)算Y復(fù)雜一點(diǎn)...而且有各種各樣的因素會(huì)影響這個(gè),比如letter-spacing 窗口resize 不知道是啥寬度的內(nèi)容 什么的,不管那么多~先只管正常情況下的英文,中文。

大致的計(jì)算思路。

先確定一下最終目的是為了讓點(diǎn)擊時(shí),光標(biāo)會(huì)自動(dòng)偏移到一個(gè)最適合的它位置,防止寫著寫著就寫到其他地方去了的這種事情......_(:3」∠)...

拿 圖2-1 為例,這是 Sublime Text 3 中的放大了很多倍的第一行的內(nèi)容,當(dāng)點(diǎn)擊到紅色圓圈處,觸發(fā)一個(gè)方法calcX,讓它去嘗試尋找一個(gè)可能是最適合的位置,這里是約定尋找一個(gè)總是大于且最靠近點(diǎn)擊處的位置M1,然后比較M1M1之前的一個(gè)字符處的位置,光標(biāo)靠近哪個(gè)就返回那個(gè)位置的字符位置(psysicalX)與它的索引(logicalX)。

圖 2-1 其實(shí)就是離哪個(gè)地方近就去哪
邏輯步驟
  1. 在初始化的時(shí)候,計(jì)算并存儲(chǔ)英文字母和中文字母的寬度single_byte_width double_byte_width。由于之前就設(shè)置了等寬字體,所以按理來說,字符間都是寬度相等的,但是一眼就能看出英文字母的寬度是一類,中文字母的寬度是一類。因?yàn)樽址矫娴闹R(shí)并不充足,只能下意識(shí)地懷疑是單字節(jié)字符是一類寬度,雙字節(jié)字符是一類寬度...于是去找了相關(guān)的正則,做出來發(fā)現(xiàn)這么分類竟然沒什么問題..!?(之后再補(bǔ)習(xí)._(:3」∠)...

  2. 用戶點(diǎn)擊某處,得到event.layerX,作為參數(shù)v_psysicalX 傳入 能夠計(jì)算偏差后的psysicalX 的方法calcX中(因?yàn)橐祷囟€(gè)結(jié)果,所以不叫他calcPsysicalX or calcLogicalX

  3. 得到該行的字符串(textContent),轉(zhuǎn)化為數(shù)組content_array

  4. 聲明一個(gè)保存字符累加長度的變量current_width,創(chuàng)建一個(gè)循環(huán)體

  5. 在循環(huán)體中,當(dāng)current_width < v_psysicalX 的時(shí)候(也就是嘗試尋找在點(diǎn)擊處右邊 && 離點(diǎn)擊處最近光標(biāo)位置),執(zhí)行 5,否則執(zhí)行6

  6. 聲明一個(gè)變量char_width用來存儲(chǔ)當(dāng)前字符content_array[index]的寬度,它通過計(jì)算得到。最終即char_width = calcCharWidth(content_array[index])

  7. 如果current_width >= v_psysicalX(找到了4中所說的該位置),這時(shí)候去判斷:點(diǎn)擊位置離左邊的光標(biāo)處更近一點(diǎn)還是右邊的光標(biāo)處更近一點(diǎn),返回更近一點(diǎn)的光標(biāo)位置。(也就是讓光標(biāo)進(jìn)行偏移到兩個(gè)字符之間的位置。不然點(diǎn)哪光標(biāo)在哪,光標(biāo)會(huì)遮擋文字,而且可能會(huì)讓用戶覺得懵逼..?!..
    不過說到這里,突然想到了一個(gè)沒什么用的模擬修改液的功能.XD...)

接下來又能寫代碼了...

各個(gè)地方的代碼
文件位置 serval/script/harusame-cursor.js
加的挺多的,直接貼完全了~

;
(function () {
    /**
     * 1. 光標(biāo)本身的元素節(jié)點(diǎn)
     */
    var Cursor = function (config) {
        this.$ref = null /* 1 */

        this._logicalY = 0
        this._logicalX = 0
        this._psysicalY = 0
        this._psysicalX = 0

        this.selection_start = null

        Cursor.preCheck()
        this._generateCursor()
        this._setObserver()
    }

    /**
     * 得到瀏覽器計(jì)算后的寬度(width)
     * getComputedStyle(v_node).width 是一個(gè)帶單位的字符串,用 parseFloat 隱式轉(zhuǎn)化為數(shù)字類型,并去除'px'單位,且保證精準(zhǔn)度
     */
    function getComputedWidth (v_node) {
        return parseFloat(getComputedStyle(v_node).width)
    }

    /**
     * 檢測字符寬度
     * a && 雨是隨便打的字符
     */
    Cursor.preCheck = function () {
        Line.line = 0
        Line.$ref.textContent = 'a'
        this.single_byte_width = getComputedWidth(Line.$ref)
        Line.$ref.textContent = '雨'
        this.double_byte_width = getComputedWidth(Line.$ref)

        // 這句話是測試用的,等能夠輸入了的時(shí)候,記得刪除
        Line.$ref.textContent = 'hello 你好'
        console.info('single_byte_width', this.single_byte_width)
        console.info('double_byte_width', this.double_byte_width)
    }

    /**
     * 判斷字符寬度
     * /[\x00-\xff]/ ASCII 編碼在 0-255 的字符哦
     */
    Cursor.calcCharWidth = function (v_char) {
        if (/[\x00-\xff]/.test(v_char)) {
            return this.single_byte_width
        } else {
            return this.double_byte_width
        }
    }


    Cursor.prototype = {
        constructor: Cursor,

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

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

            /**
             * 1. 這里賦值的是  _logicalY 哦,下面也是
             * 2. 更新 psysicalY 的值
             * 3. 更新 DOM 位置

             * 4. 寫到這里發(fā)現(xiàn)有點(diǎ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
                }
            })

            Object.defineProperty(self, 'logicalX', {
                set: function (v_logicalX) {
                    self._logicalX = v_logicalX
                    self._psysicalX = self.calcPsysicalX(v_logicalX)
                    self._setX(self._psysicalX)
                },

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


            Object.defineProperty(self, 'psysicalX', {
                set: function (v_psysicalX) {
                    var _proxy = self.calcX(v_psysicalX)
                    self._psysicalX = _proxy.psysicalX
                    self._logicalX = _proxy.logicalX
                    self._setX(self._psysicalX)
                },

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

        _setX: function (v_psysicalX) {
            this.$ref.style.left = v_psysicalX + 'px'
        },

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

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

        /**
         * 計(jì)算 邏輯 Y
         */
        calcLogicalY: function (v_psysicalY) {
            return parseInt(v_psysicalY / Line.LINE_HEIGHT)
        },

        /**
         * 計(jì)算 物理 X
         */
        calcPsysicalX: function (v_logicalX) {
            var content_array = Line.$ref.textContent.split('')
            var current_width = 0

            for (var i = 0; i < v_logicalX; i++) {
                current_width += Cursor.calcCharWidth(content_array[i])
            }

            return current_width
        },

        /**
         * 用于計(jì)算 邏輯 X
         */
        calcX: function (v_psysicalX) {
            var psysicalX = getComputedWidth(Line.$ref)
            var textContent = Line.$ref.textContent
            /**
             * 如果點(diǎn)擊的位置大于該行長度,直接將光標(biāo)放在該行末尾
             */
            if (psysicalX <= v_psysicalX) {
                return {
                    psysicalX: psysicalX,
                    logicalX: textContent.length
                }
            }

            var content_array = textContent.split('')
            var current_width = 0

            for (var i = 0; i < content_array.length; i++) {
                var char_width = Cursor.calcCharWidth(content_array[i])
                current_width += char_width
                if (current_width >= v_psysicalX) {
                    var point_right = current_width
                    var point_left = current_width - char_width

                    var offset_right = point_right - v_psysicalX
                    var offset_left = v_psysicalX - point_left

                    if (offset_right < offset_left) {
                        return {
                            psysicalX: point_right,
                            logicalX: i + 1
                        }
                    } else {
                        return {
                            psysicalX: point_left,
                            logicalX: i
                        }
                    }
                }
            }
        }
    }

    window.Cursor = Cursor
})()

文件位置 serval/script/harusame-line.js
加的挺多的,直接貼完全了~

;
/**
 * 1. 行號(hào) 的元素節(jié)點(diǎn)的 id前綴
 * 2. 行內(nèi)容 的元素節(jié)點(diǎn)的 id前綴
 * 3. 初始行號(hào)
 * 4. 行 的高度,同樣,這里先約(寫)定(死),暴露給外面使用
 */
(function (config) {
    var Line = {}
    var self = Line

    self.LINE_HEIGHT = 20 /* 4 */

    var LINE_NUMBER_SIGN = 'LNS' /* 1 */
    var LINE_CONTENT_SIGN = 'LCS' /* 2 */
    var START_LINE = 1 /* 3 */

    /**
     * 生成一行
     * @param content {string} 初始內(nèi)容
     */
    self.generateLine = function (v_content) {
        var line_number = self.max_line_number
        var initial_content = v_content || ''
        return Template.line({
            line_number: line_number,
            initial_content: initial_content,
            LINE_CONTENT_SIGN: LINE_CONTENT_SIGN,
            LINE_NUMBER_SIGN: LINE_NUMBER_SIGN,
            START_LINE: START_LINE
        })
    }

    /**
     * 生成最大行號(hào)
     */
    var PROXY_max_line_number = 0
    Object.defineProperty(self, 'max_line_number', {
        set: function (v_max_line_number) {
            PROXY_max_line_number = v_max_line_number
        },

        get: function () {
            return PROXY_max_line_number++
        }
    })


    /**
     * set:
     * 1. 記錄當(dāng)前行
     * 2. 記錄當(dāng)前行的 DOM
     * get:
     * 1. 返回當(dāng)前行
     */
    var PROXY_line = 0
    Object.defineProperty(self, 'line', {
        set: function (v_logicalY) {
            PROXY_line = v_logicalY /* 1 */
            self.$ref = document.getElementById(LINE_CONTENT_SIGN + v_logicalY) /* 2 */
        },

        get: function () {
            return PROXY_line
        }
    })

    window.Line = Line
})()
文件位置 serval/script/harusame-template.js
只需修改 line,其他就沒貼

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

來看看瀏覽器中的效果,圖 2-2:

圖 2-2

目的是達(dá)到了,理論上來說不想有的也都有:

  1. 點(diǎn)在1所在的元素節(jié)點(diǎn)上,也會(huì)有光標(biāo)的定位效果;
  2. 點(diǎn)在沒有行的部分,光標(biāo)也會(huì)定位過去。而希望光標(biāo)能定位到最后一行,最后一列
  3. 點(diǎn)在 h 的前面的時(shí)候,不怎么容易點(diǎn)到,體驗(yàn)比較差...關(guān)于體驗(yàn)的話,感覺能在后面單獨(dú)修正...

嘛..也想提供一些選項(xiàng),讓用戶自定義行為,但是感覺反正我自己是不會(huì)用的...就不做了。

因?yàn)橹饕繕?biāo)已經(jīng)達(dá)到了..(可能)...其他的細(xì)節(jié)留給以后再說..
接下來是可能是輸入內(nèi)容與換行~


CHANGELOG

2017年7月12日 18:11
U 修改 calcPsysicalX 中的 <= 為 <
U 修改 calcX 中的 i 與 i - 1 為 i + 1 與 i

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

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

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

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

  • 上一篇#0 從零開始制作在線 代碼編輯器 目錄結(jié)構(gòu)和說明 目錄初始結(jié)構(gòu) 創(chuàng)建一些目錄以及文件,如圖1-1 所示。這...
    春雨棲姬閱讀 3,653評(píng)論 0 6
  • 問答題47 /72 常見瀏覽器兼容性問題與解決方案? 參考答案 (1)瀏覽器兼容問題一:不同瀏覽器的標(biāo)簽?zāi)J(rèn)的外補(bǔ)...
    _Yfling閱讀 14,206評(píng)論 1 92
  • 上一篇#4 從零開始制作在線 代碼編輯器 刪除 與 BackSpace 與 Delete BackSpace 為了...
    春雨棲姬閱讀 1,554評(píng)論 0 0
  • 選擇qi:是表達(dá)式 標(biāo)簽選擇器 類選擇器 屬性選擇器 繼承屬性: color,font,text-align,li...
    wzhiq896閱讀 2,136評(píng)論 0 2
  • 選擇qi:是表達(dá)式 標(biāo)簽選擇器 類選擇器 屬性選擇器 繼承屬性: color,font,text-align,li...
    love2013閱讀 2,460評(píng)論 0 11

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