#3 從零開(kāi)始制作在線 代碼編輯器

上一篇
#2 從零開(kāi)始制作在線 代碼編輯器

輸入功能


簡(jiǎn)單的原理

輸入功能的話,利用一個(gè)不可見(jiàn)的 <textarea>( 這里叫它inputer)來(lái)接受鍵盤事件,當(dāng)用戶將內(nèi)容輸入到inputer中,通過(guò)監(jiān)聽(tīng)事件oninput的回調(diào)函數(shù)將inputer中的內(nèi)容($inputer.value)獲取到,然后復(fù)制給當(dāng)前行的文本節(jié)點(diǎn)中(Line.$ref.textContent = $inputer.value),最后清空inputer中的內(nèi)容($inputer.value = '')。
另外為了能讓inputer一直有效保持聚焦?fàn)顟B(tài),每次鼠標(biāo)點(diǎn)擊在編輯器的內(nèi)部時(shí),都要去進(jìn)行一次聚焦操作...吧?!

現(xiàn)在可能有個(gè)問(wèn)題就是:假如有多個(gè)光標(biāo)(之后的每一個(gè)功能都會(huì)優(yōu)先考慮多光標(biāo)的情況哦~),對(duì)每個(gè)光標(biāo)所在的那一行需要輸入一些文字,但是Line所管理的當(dāng)前行只有一個(gè),代碼寫(xiě)起來(lái)會(huì)有點(diǎn)別扭..
就像一桌人在飯店吃飯,但是筷子卻只有一雙,只有一個(gè)人吃完才把筷子留給下個(gè)人用的感覺(jué)...希望的是,每個(gè)人都有一雙筷子,會(huì)比較爽...~
對(duì)于計(jì)算姬們來(lái)說(shuō),不是不行,甚至可能是更好的方法,畢竟節(jié)省了的資源。但是在開(kāi)發(fā)初期,只管開(kāi)發(fā)者爽會(huì)讓項(xiàng)目進(jìn)展的更快吧..?(

所以這里更改下LineCursor的代碼,讓每一個(gè)Cursor實(shí)例去維護(hù)一個(gè)記錄當(dāng)前行的Line實(shí)例。

code

文件位置 serval/script/harusame-line.js
由于 Line 需要被實(shí)例化,而且考慮到方便與擴(kuò)展起見(jiàn),幾乎改了整個(gè)harusame-line.js,所以這里會(huì)貼出完整的代碼

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

    var self = Line

    self.line_number_sign = 'LNS' /* 1 */
    self.line_content_sign = 'LCS' /* 2 */
    self.start_line = 1 /* 3 */
    self.line_height = 20 /* 4 */

    /**
     * 獲得該行的行號(hào)DOM
     */
    self.getLineNumberByLogicalY = function (v_line_number) {
        return document.getElementById(self.line_number_sign + v_line_number)
    }

    /**
     * 獲得該行的行內(nèi)容的DOM
     */
    self.getLineContentByLogicalY = function (v_line_number) {
        return document.getElementById(self.line_content_sign + v_line_number)
    }

    /**
     * 生成一行
     * @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: self.line_content_sign,
            line_number_sign: self.line_number_sign,
            start_line: self.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(self.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) {
    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 || '')
            ])
        ])
    )
},
文件位置 serval/script/harusame-serval.js
部分改動(dòng)

var Serval = function (config) {
    // ...
    this._bindMouseEvent()
    this._bindKeyboardEvent() /* 新增 */
}

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

        /**
         * addEventListener 是指自己寫(xiě)的方法,見(jiàn)最下面
         * 當(dāng) mousedown 時(shí),就對(duì)光標(biāo)位置進(jìn)行計(jì)算
         * 1. 取消鼠標(biāo)默認(rèn)的行為,否則 2 不會(huì)生效
         * 2. 讓編輯器總是能夠接受鍵盤事件
         * 3. 定位鼠標(biāo)
         */
        addEventListener(self.$serval_container, 'mousedown', function (event) {
            event.preventDefault() /* 1 */

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

            self.allocTask(function (v_cursor) {
                v_cursor.psysicalY = event.layerY
                v_cursor.psysicalX = event.layerX
            })
        })
    },

    /**
     * 綁定各種鍵盤事件
     */
    _bindKeyboardEvent: function () {
        var self = this

        /**
         * 當(dāng)對(duì) $inputer 進(jìn)行輸入的時(shí)候
         * 1. 統(tǒng)一使用 insertContent 進(jìn)行內(nèi)容的插入
         * 2. 清除 $inputer 中的文本內(nèi)容
         */
        addEventListener(self.$inputer, 'input', function (event) {
            var content = self.$inputer.value
            self.allocTask(function (v_cursor) {
                self.insertContent(v_cursor, content)
            })
            self.$inputer.value = ''
        })
    },

    /**
     * 插入內(nèi)容
     * 1. 緩存該光標(biāo)所在的行的DOM
     * 2. 緩存該行的文本內(nèi)容
     * 3. 取得光標(biāo)之前的字符串
     * 4. 取得光標(biāo)之后的字符串
     * 5. 拼接出完整的插入內(nèi)容后的字符串
     * 6. 移動(dòng)游標(biāo)
     */
    _insertContent: function (v_cursor, v_content) {
        var $line = v_cursor.line.$line_content /* 1 */
        var textContent = $line.textContent /* 2 */
        var logicalX = v_cursor.logicalX
        var content_before = textContent.substring(0, logicalX) /* 3 */
        var content_after = textContent.substring(logicalX, textContent.length) /* 4 */

        $line.textContent = content_before + v_content + content_after /* 5 */
        v_cursor.logicalX += v_content.length /* 6 */
    },
    // ...
}

現(xiàn)在就可以進(jìn)行輸入啦~(如果出現(xiàn)錯(cuò)誤,可能是因?yàn)橹暗?code>harusame-cursor.js中的calcX中的偷偷做了修改_(:3」∠)... i 改為 i + 1 i - 1 改為 i 以及 calcPsysicalX 中的 <= 改為 <),效果見(jiàn) 圖3-1。

圖3-1

嗯嗯...看上去很美好,很有成就感,但是還不夠!

中文的輸入 與 瀏覽器事件行為的差異

理論上來(lái)說(shuō),當(dāng)然實(shí)踐上也是,輸入法會(huì)從邏輯上被禁用...還無(wú)法輸入中文等需要拼寫(xiě)的文字哦。畢竟準(zhǔn)備變成中文字符的字母全被'偷'走了。在input的回調(diào)函數(shù)中加入
console.info('emit input')來(lái)看看發(fā)生了什么...
在 火狐 中見(jiàn) 圖3-2。

圖3-2

在 Chrome 中見(jiàn) 圖3-3。

圖3-3

可以看到在打開(kāi)輸入法的情況下,要拼寫(xiě)的字母直接就被拖進(jìn)行里面了,并且在火狐中會(huì)連續(xù)觸發(fā)三次oninput,而在 Chrome 中只會(huì)正常點(diǎn)地觸發(fā)一次。
雖然這個(gè)不同瀏覽器對(duì)事件作出行為的差異與之后的解決方案沒(méi)有什么直接關(guān)系,但是預(yù)先記錄并提醒一下,在之后也與會(huì)遇到類似的不同瀏覽器之間事件行為的差異,并且會(huì)導(dǎo)致編輯器出問(wèn)題。很幸運(yùn),這里不會(huì)就是了~

要想使用拼寫(xiě)的能力,這時(shí)候需要compositionstartcompositionend的兩個(gè)事件來(lái)配合使用解決問(wèn)題啦。
compositionstartcompositionend 往往用在輸入法的處理方面。

MDN 中有相關(guān)解釋。
這里作簡(jiǎn)單地解釋,就像在鍵盤上按下一個(gè)鍵,會(huì)依次觸發(fā)keydown keyup一樣,當(dāng)輸入(拼寫(xiě))文字的時(shí)候,也會(huì)依次觸發(fā)compositionstart compositionend。拿敲入nihao 為例的話,在敲n的時(shí)候,compositionstart會(huì)觸發(fā),期間每次敲入一個(gè)字母都會(huì)觸發(fā)compositionupdate(這個(gè)事件的意思聽(tīng)名字就能猜出來(lái)了,雖然這里沒(méi)有用到),在敲完nihao,按下空格鍵、或者回車鍵、或者鼠標(biāo)選擇文字等把拼寫(xiě)后的內(nèi)容(你好、尼壕、你號(hào)什么的)進(jìn)行輸出的時(shí)候,才會(huì)觸發(fā)compositionend事件。常理是這樣哦~
但是做的時(shí)候就遇到問(wèn)題了,這里就直接說(shuō)了,在火狐中會(huì)有迷の行為。
先把代碼改成這樣,然后見(jiàn)圖 3-4

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

_bindKeyboardEvent: function () {
    var self = this
    var typewriting_switch = false /* 用來(lái)標(biāo)識(shí)是否正在使用輸入法,一般都會(huì)這么用 */

    addEventListener(self.$inputer, 'compositionstart', function (event) {
        console.info('emit compositionstart', event)
        typewriting_switch = true
    })

    addEventListener(self.$inputer, 'compositionend', function (event) {
        console.info('emit compositionend', event)
        typewriting_switch = false
    })

    /**
     * 當(dāng)對(duì) $inputer 進(jìn)行輸入的時(shí)候
     * 1. 統(tǒng)一使用 _insertContent 進(jìn)行內(nèi)容的插入
     * 2. 清除 $inputer 中的文本內(nèi)容
     */
    addEventListener(self.$inputer, 'input', function (event) {
        console.info('emit input')
        if (!typewriting_switch) {
            var content = self.$inputer.value
            self.allocTask(function (v_cursor) {
                self._insertContent(v_cursor, content)
            })
            self.$inputer.value = ''
        }
    })
},

圖 3-4

可以看到在火狐中,利用輸入法敲入nihao后,會(huì)依次

  1. 首先觸發(fā)compositionstart
  2. 觸發(fā)五次input(因?yàn)?code>nihao有五個(gè)字母),并且這五個(gè)字母不算作$inputer.value中。
  3. 選擇你好 進(jìn)行輸出,觸發(fā) compositionend,并且在data中可以獲取。
  4. 觸發(fā)一次 input
  5. 再次觸發(fā)compositionstart
  6. 觸發(fā)一次 input
  7. 再次觸發(fā)compositionend,但此時(shí)data中是空的
  8. 觸發(fā)一次 input

這方面的話,我也不是很懂啦...突然觸發(fā)那么多事件...!?

不過(guò)也沒(méi)關(guān)系,再看看 Chrome 中的行為。

圖3-5

這就很正常了,并且會(huì)發(fā)現(xiàn)編輯器中的第一行沒(méi)有你好輸出,這才是正常啊~!因?yàn)檩敵鑫淖质抢?code>input的,Chrome 最后并沒(méi)有觸發(fā) input,而在火狐中肯定是觸發(fā)了input再輸出的你好。這里可以看到火狐跟 Chrome 都能夠使用compositionend.data來(lái)獲取到輸出的內(nèi)容,如果此時(shí)停止執(zhí)行input回調(diào)函數(shù)中的邏輯的話,這樣就能獲得完整的輸入法體驗(yàn)了。

code

文件位置 serval/script/harusame-serval.js
只改部分哦

/**
 * 綁定各種鍵盤事件
 */
_bindKeyboardEvent: function () {
    var self = this
    var typewriting_switch = false /* 用來(lái)標(biāo)識(shí)是否正在使用輸入法,一般都會(huì)這么用 */

    /**
     * 當(dāng)準(zhǔn)備使用輸入法進(jìn)行輸入時(shí)
     * 1. 開(kāi)啟輸入法標(biāo)識(shí)
     */
    addEventListener(self.$inputer, 'compositionstart', function (event) {
        typewriting_switch = true
    })

    /**
     * 當(dāng)準(zhǔn)備使用輸入法進(jìn)行輸出時(shí)
     * 1. 輸出內(nèi)容
     * 2. 清空 $inputer 中的內(nèi)容
     * 3. 做完這些事后,關(guān)閉輸入法標(biāo)識(shí)
     */
    addEventListener(self.$inputer, 'compositionend', function (event) {
        var content = event.data
        /* 因?yàn)榛鸷鼤?huì)觸發(fā)兩次 compositionend,而第二次的 data 是沒(méi)有數(shù)據(jù)的,所以只需要取有數(shù)據(jù)的那次 */
        if (content.length !== 0) {
            /* 1 */
            self.allocTask(function (v_cursor) {
                self._insertContent(v_cursor, content)
            })
            self.$inputer.value = '' /* 2 */
        }
        typewriting_switch = false /* 3 */
    })

    /**
     * 當(dāng)對(duì) $inputer 進(jìn)行輸入的時(shí)候
     * 1. 只有輸入法未開(kāi)啟時(shí),才使用 input 事件 進(jìn)行輸出
     * 1. 統(tǒng)一使用 _insertContent 進(jìn)行內(nèi)容的插入
     * 2. 清除 $inputer 中的文本內(nèi)容
     */
    addEventListener(self.$inputer, 'input', function (event) {
        /* 1 */
        if (!typewriting_switch) {
            var content = self.$inputer.value
            self.allocTask(function (v_cursor) {
                self._insertContent(v_cursor, content) /* 1 */
            })
            self.$inputer.value = '' /* 2 */
        }
    })
},

來(lái)看看效果吧,文字就順手敲得...圖3-6 ~

圖3-6

下一篇可能是回車


CHANGELOG

2017年7月20日 22:56
D 刪除了 不小心粘貼上來(lái)的 劇透內(nèi)容


上一篇
#2 從零開(kāi)始制作在線 代碼編輯器

下一篇
#4 從零開(kāi)始制作在線 代碼編輯器

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

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

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