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

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

輸入功能


簡單的原理

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

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

所以這里更改下LineCursor的代碼沿后,讓每一個Cursor實例去維護一個記錄當(dāng)前行的Line實例尖滚。

code

文件位置 serval/script/harusame-line.js
由于 Line 需要被實例化,而且考慮到方便與擴展起見睦裳,幾乎改了整個harusame-line.js廉邑,所以這里會貼出完整的代碼

;
;
/**
 * 1. 行號 的元素節(jié)點的 id前綴
 * 2. 行內(nèi)容 的元素節(jié)點的 id前綴
 * 3. 初始行號
 * 4. 行 的高度倒谷,同樣恨锚,這里先約(寫)定(死)猴伶,暴露給外面使用
 */
(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 */

    /**
     * 獲得該行的行號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
        })
    }

    /**
     * 生成最大行號
     */
    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} 行號
 * @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
部分改動

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

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

        /**
         * addEventListener 是指自己寫的方法,見最下面
         * 當(dāng) mousedown 時办桨,就對光標(biāo)位置進行計算
         * 1. 取消鼠標(biāo)默認的行為呢撞,否則 2 不會生效
         * 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)對 $inputer 進行輸入的時候
         * 1. 統(tǒng)一使用 insertContent 進行內(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. 移動游標(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)在就可以進行輸入啦~(如果出現(xiàn)錯誤殊霞,可能是因為之前的harusame-cursor.js中的calcX中的偷偷做了修改_(:3」∠)... i 改為 i + 1 i - 1 改為 i 以及 calcPsysicalX 中的 <= 改為 <)绷蹲,效果見 圖3-1祝钢。

圖3-1

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

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

理論上來說做裙,當(dāng)然實踐上也是锚贱,輸入法會從邏輯上被禁用...還無法輸入中文等需要拼寫的文字哦。畢竟準備變成中文字符的字母全被'偷'走了监徘。在input的回調(diào)函數(shù)中加入
console.info('emit input')來看看發(fā)生了什么...
在 火狐 中見 圖3-2凰盔。

圖3-2

在 Chrome 中見 圖3-3户敬。

圖3-3

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

要想使用拼寫的能力笆包,這時候需要compositionstartcompositionend的兩個事件來配合使用解決問題啦庵佣。
compositionstartcompositionend 往往用在輸入法的處理方面。

MDN 中有相關(guān)解釋通今。
這里作簡單地解釋辫塌,就像在鍵盤上按下一個鍵派哲,會依次觸發(fā)keydown keyup一樣芭届,當(dāng)輸入(拼寫)文字的時候,也會依次觸發(fā)compositionstart compositionend。拿敲入nihao 為例的話树叽,在敲n的時候题诵,compositionstart會觸發(fā)层皱,期間每次敲入一個字母都會觸發(fā)compositionupdate(這個事件的意思聽名字就能猜出來了叫胖,雖然這里沒有用到),在敲完nihao,按下空格鍵哩俭、或者回車鍵凡资、或者鼠標(biāo)選擇文字等把拼寫后的內(nèi)容(你好谬运、尼壕梆暖、你號什么的)進行輸出的時候式廷,才會觸發(fā)compositionend事件。常理是這樣哦~
但是做的時候就遇到問題了蝗肪,這里就直接說了薛闪,在火狐中會有迷の行為俺陋。
先把代碼改成這樣腊状,然后見圖 3-4

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

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

    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)對 $inputer 進行輸入的時候
     * 1. 統(tǒng)一使用 _insertContent 進行內(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后苟鸯,會依次

  1. 首先觸發(fā)compositionstart
  2. 觸發(fā)五次input(因為nihao有五個字母)早处,并且這五個字母不算作$inputer.value中砌梆。
  3. 選擇你好 進行輸出,觸發(fā) compositionend傻丝,并且在data中可以獲取葡缰。
  4. 觸發(fā)一次 input
  5. 再次觸發(fā)compositionstart
  6. 觸發(fā)一次 input
  7. 再次觸發(fā)compositionend忱反,但此時data中是空的
  8. 觸發(fā)一次 input

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

不過也沒關(guān)系注竿,再看看 Chrome 中的行為巩割。

圖3-5

這就很正常了宣谈,并且會發(fā)現(xiàn)編輯器中的第一行沒有你好輸出,這才是正常啊~漩怎!因為輸出文字是利用input的勋锤,Chrome 最后并沒有觸發(fā) input,而在火狐中肯定是觸發(fā)了input再輸出的你好。這里可以看到火狐跟 Chrome 都能夠使用compositionend.data來獲取到輸出的內(nèi)容,如果此時停止執(zhí)行input回調(diào)函數(shù)中的邏輯的話,這樣就能獲得完整的輸入法體驗了猖吴。

code

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

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

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

    /**
     * 當(dāng)準備使用輸入法進行輸出時
     * 1. 輸出內(nèi)容
     * 2. 清空 $inputer 中的內(nèi)容
     * 3. 做完這些事后障陶,關(guān)閉輸入法標(biāo)識
     */
    addEventListener(self.$inputer, 'compositionend', function (event) {
        var content = event.data
        /* 因為火狐會觸發(fā)兩次 compositionend抱究,而第二次的 data 是沒有數(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)對 $inputer 進行輸入的時候
     * 1. 只有輸入法未開啟時,才使用 input 事件 進行輸出
     * 1. 統(tǒng)一使用 _insertContent 進行內(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 */
        }
    })
},

來看看效果吧敢靡,文字就順手敲得...圖3-6 ~

圖3-6

下一篇可能是回車


CHANGELOG

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


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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市纺念,隨后出現(xiàn)的幾起案子柠辞,更是在濱河造成了極大的恐慌主胧,老刑警劉巖踪栋,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件夷都,死亡現(xiàn)場離奇詭異,居然都是意外死亡冬阳,警方通過查閱死者的電腦和手機肝陪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進店門氯窍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來狼讨,“玉大人,你說我怎么就攤上這事播聪∪埽” “怎么了执泰?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長术吝。 經(jīng)常有香客問我计济,道長,這世上最難降的妖魔是什么排苍? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任沦寂,我火速辦了婚禮,結(jié)果婚禮上淘衙,老公的妹妹穿的比我還像新娘传藏。我一直安慰自己,他們只是感情好彤守,可當(dāng)我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著具垫,像睡著了一般侈离。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上筝蚕,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天卦碾,我揣著相機與錄音,去河邊找鬼起宽。 笑死洲胖,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的坯沪。 我是一名探鬼主播绿映,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼屏箍!你這毒婦竟也來了绘梦?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤赴魁,失蹤者是張志新(化名)和其女友劉穎卸奉,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體颖御,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡榄棵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了潘拱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疹鳄。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖芦岂,靈堂內(nèi)的尸體忽然破棺而出瘪弓,到底是詐尸還是另有隱情,我是刑警寧澤禽最,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布腺怯,位于F島的核電站,受9級特大地震影響川无,放射性物質(zhì)發(fā)生泄漏呛占。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一懦趋、第九天 我趴在偏房一處隱蔽的房頂上張望晾虑。 院中可真熱鬧,春花似錦仅叫、人聲如沸帜篇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽坠狡。三九已至,卻和暖如春遂跟,著一層夾襖步出監(jiān)牢的瞬間逃沿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工幻锁, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留凯亮,地道東北人。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓哄尔,卻偏偏與公主長得像假消,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子岭接,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,728評論 2 351

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