
輸入功能
簡(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)展的更快吧..?(

所以這里更改下Line與Cursor的代碼,讓每一個(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。

嗯嗯...看上去很美好,很有成就感,但是還不夠!
中文的輸入 與 瀏覽器事件行為的差異
理論上來(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。

在 Chrome 中見(jiàn) 圖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í)候需要compositionstart與compositionend的兩個(gè)事件來(lái)配合使用解決問(wèn)題啦。
compositionstart與compositionend 往往用在輸入法的處理方面。
在 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 = ''
}
})
},

可以看到在火狐中,利用輸入法敲入nihao后,會(huì)依次
- 首先觸發(fā)
compositionstart - 觸發(fā)五次
input(因?yàn)?code>nihao有五個(gè)字母),并且這五個(gè)字母不算作$inputer.value中。 - 選擇
你好進(jìn)行輸出,觸發(fā)compositionend,并且在data中可以獲取。 - 觸發(fā)一次
input - 再次觸發(fā)
compositionstart - 觸發(fā)一次
input - 再次觸發(fā)
compositionend,但此時(shí)data中是空的 - 觸發(fā)一次
input
這方面的話,我也不是很懂啦...突然觸發(fā)那么多事件...!?

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

這就很正常了,并且會(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 ~


下一篇可能是回車
CHANGELOG
2017年7月20日 22:56
D 刪除了 不小心粘貼上來(lái)的 劇透內(nèi)容