
刪除 與 BackSpace 與 Delete
BackSpace
為了方便. 這里所說的刪除的只考慮以下兩種鍵的最簡單的刪除行為:
-
BackSpace往左邊刪除一個字符 -
Delete往右邊刪除一個字符
有選區(qū)狀態(tài)時所做的刪除或者替換. 這里不考慮哦.
因為做了簡化. 這里的流程就會比較簡單. 要說明的是:
當(dāng)光標位于行首時. 再使用BackSpace 的時候. 要刪除當(dāng)前行.
把這個方法加在 harusame-line.js 中
code
@path serval/script/harusame-line.js
// 只有部分代碼
/**
* 刪除指定行
*/
self.deleteLine = function (v_line_number) {
var $line_container = document.querySelector('.line-container') // 同樣. 這里暫時這么寫...
var $line = self.getLineContentByLogicalY(v_line_number).parentNode.parentNode
$line_container.removeChild($line)
}
@path serval/script/harusame-serval.js
// 只有部分代碼
/**
* KEY: BackSpace
* 0. 阻止默認行為
* 1. 如果光標在首行首列, 什么都不干
* 2. 如果光標在該行第0個位置
* 2.1. 得到光標后的內(nèi)容(left_content), 刪除當(dāng)前行
* 2.2. 修正行號 -> 暫時是這樣
* 2.3. 光標上移一行, 且放置到該行最后一列
* 2.4. 將上一行的殘留下來的內(nèi)容(left_content) 追加到該行末尾
* 3. 其他情況下
* 3.1. 光標往左移動一列
* 3.2. 刪除一個字符
*/
Serval.prototype.keydownHandler.'8': function (event) {
event.preventDefault() /* 0 */
var self = this
self.allocTask(function (v_cursor) {
var $line = v_cursor.line.$line_content
var textContent = $line.textContent
var logicalX = v_cursor.logicalX
var left_content = textContent.substring(logicalX, textContent.length)
var logicalY = v_cursor.logicalY
if (logicalY === 0) {
if (logicalX === 0) {
return /* 2 */
}
} else {
if (logicalX === 0) { /* 2 */
Line.deleteLine(v_cursor.logicalY) /* 2.1 */
Line.fixLineNumber(v_cursor.logicalY) /* 2.2 */
v_cursor.logicalY -= 1 /* 2.3 */
v_cursor.logicalX = v_cursor.line.$line_content.textContent /* 2.3 */
var $line = v_cursor.line.$line_content
var textContent = $line.textContent
$line.textContent = textContent + left_content /* 2.4 */
return
}
}
v_cursor.logicalX -= 1 /* 3.1 */
$line.textContent = textContent.substring(0, logicalX - 1) + left_content /* 3.2 */
})
},
效果是這樣... 很普通. 見 圖5-1

Delete
Delete 的原理同 BackSpace. 但是還是有一些差異. 在做的時候. 能感受到之前代碼的不合理(蠢).
在 Delete 中. 當(dāng)光標位于 最后一行 最后一列 時. 需要先得到一共有幾行. 才能做比較. 可是問題在于訪問 Line.max_line_number 會觸發(fā)它的 getter. 并且修改了數(shù)據(jù). 見 圖5-2. 這是一個看似簡單但是會充滿麻煩的行為. 畢竟每次訪問. 他都在變. 導(dǎo)致在做 最后一行 最后一列 的判斷時. 只要按下 一次以上的 Delete. 這個判斷就會失效.

先把問題補了吧...
- 把
++改成刪掉(這里暫時這么改... 事實上沒有必要控制 getter setter 了)

- 每次調(diào)用
Line.generateLine的時候. 最大行數(shù)加一.

- 同樣地. 每次調(diào)用
Line.deleteLine的時候. 最大行數(shù)減一.

code
@path serval/script/harusame-serval.js
// 部分代碼
/**
* KEY: Delete
* 0. 阻止默認行為
* 1. 如果光標在最后一行最后一列, 什么都不干
* 2. 如果光標在該行最后一個位置
* 2.1. 得到下一行的光標后的內(nèi)容(left_content, 另外 也肯定是該行全部的內(nèi)容)
* 2.2. 刪除下一行
* 2.3. 修正行號 -> 暫時是這樣
* 2.4. 將 left_content 內(nèi)容 追加到當(dāng)前行末尾
* 3. 其他情況下
* 3.1. 向右刪除一個字符
*/
Serval.prototype.keydownHandler. '46': function (event) {
event.preventDefault() /* 0 */
var self = this
self.allocTask(function (v_cursor) {
var $line = v_cursor.line.$line_content
var textContent = $line.textContent
var logicalX = v_cursor.logicalX
var left_content = textContent.substring(logicalX + 1, textContent.length)
var logicalY = v_cursor.logicalY
var max = Line.max_line_number - 1
console.log('max', max)
if (logicalY === max) {
if (logicalX === textContent.length) {
return /* 1 */
}
} else {
if (logicalX === textContent.length) {
var left_content = Line.getLineContentByLogicalY(logicalY + 1).textContent /* 2.1 */
Line.deleteLine(logicalY + 1) /* 2.2 */
Line.fixLineNumber(v_cursor.logicalY) /* 2.3 */
$line.textContent = v_cursor.line.$line_content.textContent + left_content /* 2.4 */
return
}
}
$line.textContent = textContent.substring(0, logicalX) + left_content /* 3.1 */
})
},
看看效果的說. 很普通. 見圖 5-3. 順便又試了下 BackSpace

選區(qū) 與 selection
在進行復(fù)制等操作前. 需要讓計算姬知道哪些對象需要操作. 選區(qū)就是這樣一個東西. 感覺也沒什么好說的.

編寫選區(qū)的整體思路
鑒于常識與操作習(xí)慣. 這里規(guī)定一個光標只能有一個選區(qū) 并且 選擇的內(nèi)容必須是連續(xù)的.
對于一個選區(qū)... 只要
- 擁有起點與終點. 由于區(qū)域已經(jīng)規(guī)定必須是連續(xù)的. 那么
- 他所包含的區(qū)域就可以計算出來.
- 選區(qū)中的內(nèi)容才可以獲得.
- 選區(qū)也才可以繪制出來.
但是有個問題!
可能很早之前說了.? 終點總是光標的當(dāng)前位置. 所以只要記下選區(qū)的起點就行了.
然而所說的起點并不是 鼠標按下時候(onmousedown)的那個點.! 這里為了方便記錄. 把鼠標按下時候的那個點叫做 基準點 ..
因為選區(qū)可能是從 基準點開始往左/左上角拉 或者 往右/右下角拉. 圖5-4 這樣.

需要做一個判斷來確定選區(qū)真正的 起點 和 終點. 這樣
- 繪制選區(qū)才會比較方便的找準點...
- 截取內(nèi)容時. 才會正確?.(下面會說這個問號是為什么)
獲得選區(qū)內(nèi)容
觀察已有的編輯器功能. 可以了解到選區(qū)的創(chuàng)建方式:
- 按下 鼠標的時候確定一個選區(qū)基準點
- 拖動 鼠標來選取內(nèi)容. (也就是根據(jù)選區(qū)基準點 與 光標當(dāng)前位置 計算選區(qū)起點與終點. 之后根據(jù)起點與終點繪制視圖).
-
放開 鼠標后確定選取的內(nèi)容. (實際上步驟同
2)
這里也剛好對應(yīng)三個常用的鼠標事件:
- 按下 - onmousedown
- 拖動 - onmousemove
- 放開 - onmouseup
具體功能邏輯的話... 其實原理很簡單.. 只是由于設(shè)計上的問題. 導(dǎo)致代碼暫時有點臃腫. 就直接通過代碼來顯示了.
繪制選區(qū)視圖
最后是關(guān)于選區(qū)的視圖...
根據(jù)選區(qū)必須是連續(xù)的特性以及盒子模型一般是矩形的特性... 可以將選區(qū)分為三種類型 以方便地適應(yīng)所有情況. 見 圖5-5.

- 只有一行的: 用一個帶背景顏色的
<div class="selection-part" />并控制他的偏移量與寬度 來顯示 - 只有二行的: 那就用二個
<div class="selection-part" /> - 多于三行的: 無論多少行都可以用三個來表示
<div class="selection-part" />
記得最初的時候創(chuàng)建了selected-container.

就把 <div class="selection-part" /> 放進這層中. 另外 selected-container 這名字好有問題... 不如改成 selection. 再加上這幾個 container 有明確的覆蓋關(guān)系... 最終會改成 selection-layer 這種形式 可能比較好一點... 這里暫時不會改
code - 獲得選區(qū)內(nèi)容
首先來綁定事件... 因為邏輯比較清晰啦... 這里是就直接先調(diào)用還不存在的函數(shù)... 然后再去寫函數(shù)內(nèi)部的具體邏輯..(從界面進入到邏輯)
@path serval/script/harusame-serval.js
// 目標代碼
Serval.prototype._bindMouseEvent: function () {
var self = this
var isMouseDown = false
/**
* addEventListener 是指自己寫的方法,見最下面
* 當(dāng) mousedown 時,就對光標位置進行計算
* 1. 取消鼠標默認的行為,否則 2 不會生效
* 2. 讓編輯器總是能夠接受鍵盤事件
* 3. 定位鼠標
* 4. 設(shè)置選區(qū)基準點
* 5. 記憶鼠標已經(jīng)點擊還未彈起, 用來避免鼠標沒有點擊就一直更新選區(qū)
*/
addEventListener(self.$serval_container, 'mousedown', function (event) {
event.preventDefault() /* 1 */
self.$inputer.focus() /* 2 */
self.allocTask(function (v_cursor) {
v_cursor.psysicalY = event.layerY /* 3 */
v_cursor.psysicalX = event.layerX /* 3 */
v_cursor.setSelectionBase() /* 4 */
})
isMouseDown = true /* 5 */
})
/**
* 這里先不管觸發(fā)的頻次是否頻繁什么的...
* 1. 當(dāng) mousemove 時 且 鼠標 按下 時,更新光標位置
* 2. 更新選區(qū)起點 與 終點 與 視圖
*/
addEventListener(self.$serval_container, 'mousemove', function (event) {
event.preventDefault()
if (isMouseDown) {
self.allocTask(function (v_cursor) {
v_cursor.psysicalY = event.layerY /* 1 */
v_cursor.psysicalX = event.layerX /* 1 */
v_cursor.updateSelection() /* 2 */
})
}
})
/**
* 1. 標記鼠標已經(jīng)彈起
* 2. 更新光標位置
* 3. 更新選區(qū)起點 與 終點 與 視圖
* 4. 當(dāng)存在選區(qū)的時候
* 4.1. 獲得選區(qū)內(nèi)容, ---> 這里先測試下是否能獲取到選區(qū)內(nèi)容 <---
*/
addEventListener(self.$serval_container, 'mouseup', function (event) {
event.preventDefault()
isMouseDown = false /* 1 */
self.allocTask(function (v_cursor) {
v_cursor.psysicalY = event.layerY /* 2 */
v_cursor.psysicalX = event.layerX /* 2 */
v_cursor.updateSelection() /* 3 */
if (v_cursor.isSelectionExist()) { /* 4 */
console.log(v_cursor.getSelectionContent()) /* 4.1 */
}
})
})
},
@path serval/script/harusame-cursor.js
/**
* 設(shè)定選區(qū)基準點
*/
Cursor.prototype.setSelectionBase: function () {
this.mousedown_point = {
logicalY: this.logicalY,
logicalX: this.logicalX,
psysicalY: this.psysicalY,
psysicalX: this.psysicalX
}
},
/**
* 更新選區(qū)
*/
Cursor.prototype.updateSelection: function () {
this.findSelection()
// this.updateSelectionView() 用來更新選區(qū)視圖
},
/**
* 找到選區(qū)的 起點 與 終點
* @ 這個函數(shù)名字不怎么合適.. 而且代碼很丑..
* @ 如果把坐標單獨做成一個類 就會好看很多(大概
* @ 甚至可以比如 point_end = this.mousedown_point
* @ 但是就現(xiàn)在來說的話干脆就讓他更臃腫 反而好看點.(大概
* @ _(:3」∠)_
*/
Cursor.prototype.findSelection: function () {
var point_start = {}
var point_end = {}
if (this.logicalY < this.mousedown_point.logicalY) {
point_start.logicalY = this.logicalY
point_start.logicalX = this.logicalX
point_start.psysicalY = this.psysicalY
point_start.psysicalX = this.psysicalX
point_end.logicalY = this.mousedown_point.logicalY
point_end.logicalX = this.mousedown_point.logicalX
point_end.psysicalY = this.mousedown_point.psysicalY
point_end.psysicalX = this.mousedown_point.psysicalX
} else if (this.logicalY === this.mousedown_point.logicalY) {
if (this.logicalX < this.mousedown_point.logicalX) {
point_start.logicalY = this.logicalY
point_start.logicalX = this.logicalX
point_start.psysicalY = this.psysicalY
point_start.psysicalX = this.psysicalX
point_end.logicalY = this.mousedown_point.logicalY
point_end.logicalX = this.mousedown_point.logicalX
point_end.psysicalY = this.mousedown_point.psysicalY
point_end.psysicalX = this.mousedown_point.psysicalX
} else {
point_start.logicalY = this.mousedown_point.logicalY
point_start.logicalX = this.mousedown_point.logicalX
point_start.psysicalY = this.mousedown_point.psysicalY
point_start.psysicalX = this.mousedown_point.psysicalX
point_end.logicalY = this.logicalY
point_end.logicalX = this.logicalX
point_end.psysicalY = this.psysicalY
point_end.psysicalX = this.psysicalX
}
} else {
point_start.logicalY = this.mousedown_point.logicalY
point_start.logicalX = this.mousedown_point.logicalX
point_start.psysicalY = this.mousedown_point.psysicalY
point_start.psysicalX = this.mousedown_point.psysicalX
point_end.logicalY = this.logicalY
point_end.logicalX = this.logicalX
point_end.psysicalY = this.psysicalY
point_end.psysicalX = this.psysicalX
}
this.selection_start = point_start
this.selection_end = point_end
},
/**
* 判斷是否有選區(qū)
*/
Cursor.prototype.isSelectionExist: function () {
if (this.logicalY === this.mousedown_point.logicalY && this.logicalX === this.mousedown_point .logicalX) {
return false
}
return true
},
/**
* 獲得選區(qū)內(nèi)容
* 1. 如果選區(qū)只有一行
* 1.1. 截取 起點 與 終點 的內(nèi)容,且不需要換行
* 2. 如果選區(qū)只有二行
* 2.1. 截取 起點 到 起點行末尾 的內(nèi)容
* 2.2. 截取 終點行開始 到 終點 的內(nèi)容,且不需要換行
* 3. 如果選區(qū)大于二行
* 3.1. 截取 起點 到 起點行末尾 的內(nèi)容
* 3.2. 遍歷除了 起點行 與 終點行 的其他行
* 3.2.1. 截取該行的整段內(nèi)容
* 3.3. 截取 終點行開始 到 終點 的內(nèi)容,且不需要換行
*/
getSelectionContent: function () {
var point_start = this.selection_start
var point_end = this.selection_end
var result = ''
var count = point_end.logicalY - point_start.logicalY
var start_line_text = Line.getLineContentByLogicalY(point_start.logicalY).textContent
/* 1 */
if (count === 0) {
console.log('--> 選區(qū)類型 : 一行 <--')
result += start_line_text.substring(point_start.logicalX, point_end.logicalX) /* 1.1 */
/* 2 */
} else if (count === 1) {
console.log('--> 選區(qū)類型 : 二行 <--')
result += start_line_text.substring(point_start.logicalX, start_line_text.length) + '\n' /* 2.1 */
var end_line_text = Line.getLineContentByLogicalY(point_end.logicalY).textContent
result += end_line_text.substring(0, point_end.logicalX) /* 2.2 */
/* 3 */
} else {
console.log('--> 選區(qū)類型 : 多行 <--')
result += start_line_text.substring(point_start.logicalX, start_line_text.length) + '\n' /* 3.1 */
/* 3.2 */
for (var i = point_start.logicalY + 1; i < point_end.logicalY; i++) {
result += Line.getLineContentByLogicalY(i).textContent + '\n' /* 3.2.1 */
}
var end_line_text = Line.getLineContentByLogicalY(point_end.logicalY).textContent
result += end_line_text.substring(0, point_end.logicalX) /* 3.3 */
}
return result
},
就是這樣.先來調(diào)試一下.保證選區(qū)數(shù)據(jù)是返回正確的再來做視圖哦... 來看看效果... 見圖5-6.

... 嗯嗯.. 內(nèi)容能用各種姿勢獲取到.~

這里額外說一下... 好早之前的版本中忘記做了單行選區(qū)的 選區(qū)起點 終點的判斷
會產(chǎn)生比如這樣的事情 textContent.substring(6, 0). 但這并沒有報錯...
見挺靠譜的文檔 MDN 其中說到了
If indexStart is greater than indexEnd, then the effect of substring() is as if the two arguments were swapped; for example, str.substring(1, 0) == str.substring(0, 1).
感覺有點神奇... 會做這樣的處理...
code - 獲得選區(qū)內(nèi)容
確保選區(qū)的數(shù)據(jù)獲得是正確的之后. 來嘗試做選區(qū)的視圖部分...


之前也說過.. 這里的選區(qū)最多只會劃分為三段... 這是為了防止操作太多的DOM起見...(偷懶.
當(dāng)然像一般的編輯器那樣. 每一行單獨一段高亮的選區(qū)也不是不行啦... 只是還沒做就感覺會卡(hen)卡(ma)的(fan).
CSS
為了能快地看到成型后的效果. 先以最快速度把<div class="selection-part" /> 塞進 <div class="selected-container /> 中. 再來做 js 的部分.
- 因為選區(qū)的樣式已經(jīng)很直觀了... 這里就先寫的 css.
.selection-content {
position: absolute;
top: 0;
left: 0;
right: 0;
/*
* 這里用 right: 0 讓寬度鋪滿一行
* 不用 width: 100% 是因為在個人習(xí)慣調(diào)試的時候盡量不麻煩其他元素節(jié)點的樣式
* 并且此時任意一個父類也還沒有設(shè)置 overflow: hidden; 就換了個方法_(:3」∠)...
*/
height: 20px;
background-color: rgba(120, 120, 120, .5); // 隨便挑一個常用的灰色做測試
}
- 再放進
<div class="selected-container />中. 嗯嗯..放這里
@path serval/script/harusame-template.js
Template.editor = function () {
// ...
/*
* before:
* var $selected_container = SatoriDom.compile(e('div', {'class': 'selected-container'})
*/
var $selected_container = SatoriDom.compile(e('div', {'class': 'selected-container'}, [
e('div', {'class': 'selection-part'}),
e('div', {'class': 'selection-part'}),
e('div', {'class': 'selection-part'})
]))
// ...
}
- 嗯嗯... 這就是效果. 見圖5-7

- 這里先模擬實際效果. 再把模擬的過程轉(zhuǎn)化為用 js 來控制:
選區(qū)的控制是通過改變topleftright與height來實現(xiàn)的.
見圖5-5 中 (這里復(fù)制過來了..

先規(guī)定一下:
選區(qū)中的最上面這行. 比如
137行. 之后記為$selection_top選區(qū)中的中間的部分. 比如
8-9行. 之后記為$selection_middle選區(qū)中的最下面這行. 比如
410行. 之后記為$selection_bottom
模擬過程比如像這樣. 見圖5-8:

可以看到由于樣式方面的原因.(可能算是問題).. 計算選區(qū)視圖大小的時候要額外算上行號所在的空間的寬度. 是 50px.
繪制視圖的整個流程就是:
當(dāng) mousemove 或者 mouseup 的時候. 比對當(dāng)前光標的位置 與 選區(qū)起點 是否出了偏差 (計算 logicalY)... 如果有就更新選區(qū)視圖
- 計算并存儲兩點間的
Y上的差
var diffY = point_end.logicalY - point_start.logicalY
2.1. 如果 diffY === 0. 更新
$selection_top 的 DOM 的 top left
2.2. 如果 diffY === 1. 更新
$selection_top 的 top left &&
$selection_bottom 的 top right
2.3. 如果 diffY > 1. 更新
$selection_top 的 top left &&
$selection_middle 的 top height &&
$selection_bottom 的 top right
JS
現(xiàn)在測試都基本沒問題了. 如果測試內(nèi)容對實際要做的東西會有干擾. 就考慮刪掉哦.. 有以下這個:

還原為

然后在 template 中加入這個
@path serval/script/harusame-template.js
/**
* 選區(qū)片段
*/
selectionPart: function () {
return SatoriDom.compile(
e('div', {'class': 'selection-part'})
)
}
接下來把之前想要做的流程轉(zhuǎn)換為代碼..
@path serval/script/harusame-cursor.js
/**
* 1. 光標本身的元素節(jié)點
* 2. 之前所說的基準點可以做一個初始化.
*/
var Cursor = function (config) {
// ...
this.mousedown_point = {} /* 2 */
this.$selection_top = Template.selectionPart()
this.$selection_middle = Template.selectionPart()
this.$selection_bottom = Template.selectionPart()
// ...
}
/**
* 更新選區(qū)的 值 與 視圖
*/
Cursor.prototype.updateSelection: function () {
this.findSelection()
this.updateSelectionView()
},
/**
* 更新選區(qū)視圖
*/
Cursor.prototype.updateSelectionView: function () {
var point_start = this.selection_start
var point_end = this.selection_end
var diffY = point_end.logicalY - point_start.logicalY
switch (diffY) {
case 0:
this.$selection_top.style.cssText =
'top:' + point_start.psysicalY + 'px;' +
'right:' + (750 - point_end.psysicalX) + 'px;' +
'left:' + (50 + point_start.psysicalX) + 'px;' +
'display:' + 'block;'
this.$selection_middle.style.display = 'none'
this.$selection_bottom.style.display = 'none'
break
case 1:
this.$selection_top.style.cssText =
'top:' + point_start.psysicalY + 'px;' +
'right:' + 0 + 'px;' +
'left:' + (50 + point_start.psysicalX) + 'px;' +
'display: block;'
this.$selection_middle.style.display = 'none'
this.$selection_bottom.style.cssText =
'top:' + point_end.psysicalY + 'px;' +
'right:' + (750 - point_end.psysicalX) + 'px;' +
'left:' + 50 + 'px;' +
'display: block;'
break
default:
this.$selection_top.style.cssText =
'top:' + point_start.psysicalY + 'px;' +
'right:' + 0 + 'px;' +
'left:' + (50 + point_start.psysicalX) + 'px;' +
'display: block;'
this.$selection_middle.style.cssText =
'top:' + (point_start.psysicalY + Line.line_height) + 'px;' +
'left:' + 50 + 'px;' +
'height:' + (point_end.psysicalY - point_start.psysicalY - Line.line_height) + 'px;' +
'display: block;'
this.$selection_bottom.style.cssText =
'top:' + point_end.psysicalY + 'px;' +
'right:' + (750 - point_end.psysicalX) + 'px;' +
'left:' + 50 + 'px;' +
'display: block;'
break
}
},
來看看有沒有問題... 見 圖5-9.

嗯... 選區(qū)應(yīng)該沒有什么問題.
說起來示例gif 里的內(nèi)容都是無意義的數(shù)字之類的...因為復(fù)制粘貼什么的還沒有做..就暫時用這些代替了..
接下來可能是 復(fù)制 剪切 粘貼 Home End ↑ ↓ ← → ...
感覺內(nèi)容好多... 其實感覺依舊好水_(:3」∠)...
在做完這些最基礎(chǔ)的功能之后... 重新調(diào)整與優(yōu)化代碼.. 之后再做多個光標.. 代碼高亮&&智能提示 之類的東西

CHANGELOG
2017年8月10日14:14:00
F 在 getSelectionContent 中 修復(fù)了多余的 \n