上一篇
#4 从零开始制作在线 代码编辑器
删除 与 BackSpace 与 Delete
BackSpace
为了方便. 这里所说的删除的只考虑以下两种键的最简单的删除行为:
-
BackSpace
往左边删除一个字符 -
Delete
往右边删除一个字符
有选区状态时所做的删除或者替换. 这里不考虑哦.
因为做了简化. 这里的流程就会比较简单. 要说明的是:
当光标位于行首时. 再使用BackSpace
的时候. 要删除当前行.
把这个方法加在 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. 得到光标后的内容(left_content), 删除当前行
* 2.2. 修正行号 -> 暂时是这样
* 2.3. 光标上移一行, 且放置到该行最后一列
* 2.4. 将上一行的残留下来的内容(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
中. 当光标位于 最后一行 最后一列
时. 需要先得到一共有几行. 才能做比较. 可是问题在于访问 Line.max_line_number
会触发它的 getter
. 并且修改了数据. 见 图5-2. 这是一个看似简单但是会充满麻烦的行为. 毕竟每次访问. 他都在变. 导致在做 最后一行 最后一列
的判断时. 只要按下 一次以上的 Delete
. 这个判断就会失效.
先把问题补了吧...
- 把
++
改成删掉
(这里暂时这么改... 事实上没有必要控制 getter setter 了)
- 每次调用
Line.generateLine
的时候. 最大行数加一.
- 同样地. 每次调用
Line.deleteLine
的时候. 最大行数减一.
code
@path serval/script/harusame-serval.js
// 部分代码
/**
* KEY: Delete
* 0. 阻止默认行为
* 1. 如果光标在最后一行最后一列, 什么都不干
* 2. 如果光标在该行最后一个位置
* 2.1. 得到下一行的光标后的内容(left_content, 另外 也肯定是该行全部的内容)
* 2.2. 删除下一行
* 2.3. 修正行号 -> 暂时是这样
* 2.4. 将 left_content 内容 追加到当前行末尾
* 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
选区 与 selection
在进行复制等操作前. 需要让计算姬知道哪些对象需要操作. 选区就是这样一个东西. 感觉也没什么好说的.
编写选区的整体思路
鉴于常识与操作习惯. 这里规定一个光标只能有一个选区 并且 选择的内容必须是连续的.
对于一个选区... 只要
- 拥有起点与终点. 由于区域已经规定必须是连续的. 那么
- 他所包含的区域就可以计算出来.
- 选区中的内容才可以获得.
- 选区也才可以绘制出来.
但是有个问题!
可能很早之前说了.? 终点总是光标的当前位置. 所以只要记下选区的起点就行了.
然而所说的起点并不是 鼠标按下时候(onmousedown)的那个点.! 这里为了方便记录. 把鼠标按下时候的那个点叫做 基准点 ..
因为选区可能是从 基准点开始往左/左上角拉 或者 往右/右下角拉. 图5-4 这样.
需要做一个判断来确定选区真正的 起点 和 终点. 这样
- 绘制选区才会比较方便的找准点...
- 截取内容时. 才会正确?.(下面会说这个问号是为什么)
获得选区内容
观察已有的编辑器功能. 可以了解到选区的创建方式:
- 按下 鼠标的时候确定一个选区基准点
- 拖动 鼠标来选取内容. (也就是根据选区基准点 与 光标当前位置 计算选区起点与终点. 之后根据起点与终点绘制视图).
- 放开 鼠标后确定选取的内容. (实际上步骤同
2
)
这里也刚好对应三个常用的鼠标事件:
- 按下 - onmousedown
- 拖动 - onmousemove
- 放开 - onmouseup
具体功能逻辑的话... 其实原理很简单.. 只是由于设计上的问题. 导致代码暂时有点臃肿. 就直接通过代码来显示了.
绘制选区视图
最后是关于选区的视图...
根据选区必须是连续的特性以及盒子模型一般是矩形的特性... 可以将选区分为三种类型 以方便地适应所有情况. 见 图5-5.
- 只有一行的: 用一个带背景颜色的
并控制他的偏移量与宽度 来显示
- 只有二行的: 那就用二个
- 多于三行的: 无论多少行都可以用三个来表示
记得最初的时候创建了selected-container
.
就把 放进这层中. 另外
selected-container
这名字好有问题... 不如改成 selection
. 再加上这几个 container
有明确的覆盖关系... 最终会改成 selection-layer
这种形式 可能比较好一点... 这里暂时不会改
code -
获得选区内容
首先来绑定事件... 因为逻辑比较清晰啦... 这里是就直接先调用还不存在的函数... 然后再去写函数内部的具体逻辑..(从界面进入到逻辑)
@path serval/script/harusame-serval.js
// 目标代码
Serval.prototype._bindMouseEvent: function () {
var self = this
var isMouseDown = false
/**
* addEventListener 是指自己写的方法,见最下面
* 当 mousedown 时,就对光标位置进行计算
* 1. 取消鼠标默认的行为,否则 2 不会生效
* 2. 让编辑器总是能够接受键盘事件
* 3. 定位鼠标
* 4. 设置选区基准点
* 5. 记忆鼠标已经点击还未弹起, 用来避免鼠标没有点击就一直更新选区
*/
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 */
})
/**
* 这里先不管触发的频次是否频繁什么的...
* 1. 当 mousemove 时 且 鼠标 按下 时,更新光标位置
* 2. 更新选区起点 与 终点 与 视图
*/
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. 标记鼠标已经弹起
* 2. 更新光标位置
* 3. 更新选区起点 与 终点 与 视图
* 4. 当存在选区的时候
* 4.1. 获得选区内容, ---> 这里先测试下是否能获取到选区内容 <---
*/
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
/**
* 设定选区基准点
*/
Cursor.prototype.setSelectionBase: function () {
this.mousedown_point = {
logicalY: this.logicalY,
logicalX: this.logicalX,
psysicalY: this.psysicalY,
psysicalX: this.psysicalX
}
},
/**
* 更新选区
*/
Cursor.prototype.updateSelection: function () {
this.findSelection()
// this.updateSelectionView() 用来更新选区视图
},
/**
* 找到选区的 起点 与 终点
* @ 这个函数名字不怎么合适.. 而且代码很丑..
* @ 如果把坐标单独做成一个类 就会好看很多(大概
* @ 甚至可以比如 point_end = this.mousedown_point
* @ 但是就现在来说的话干脆就让他更臃肿 反而好看点.(大概
* @ _(: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
},
/**
* 判断是否有选区
*/
Cursor.prototype.isSelectionExist: function () {
if (this.logicalY === this.mousedown_point.logicalY && this.logicalX === this.mousedown_point .logicalX) {
return false
}
return true
},
/**
* 获得选区内容
* 1. 如果选区只有一行
* 1.1. 截取 起点 与 终点 的内容,且不需要换行
* 2. 如果选区只有二行
* 2.1. 截取 起点 到 起点行末尾 的内容
* 2.2. 截取 终点行开始 到 终点 的内容,且不需要换行
* 3. 如果选区大于二行
* 3.1. 截取 起点 到 起点行末尾 的内容
* 3.2. 遍历除了 起点行 与 终点行 的其他行
* 3.2.1. 截取该行的整段内容
* 3.3. 截取 终点行开始 到 终点 的内容,且不需要换行
*/
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('--> 选区类型 : 一行 <--')
result += start_line_text.substring(point_start.logicalX, point_end.logicalX) /* 1.1 */
/* 2 */
} else if (count === 1) {
console.log('--> 选区类型 : 二行 <--')
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('--> 选区类型 : 多行 <--')
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
},
就是这样.先来调试一下.保证选区数据是返回正确的再来做视图哦... 来看看效果... 见图5-6.
... 嗯嗯.. 内容能用各种姿势获取到.~
这里额外说一下... 好早之前的版本中忘记做了单行选区的 选区起点 终点的判断
会产生比如这样的事情 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 -
获得选区内容
确保选区的数据获得是正确的之后. 来尝试做选区的视图部分...
之前也说过.. 这里的选区最多只会划分为三段... 这是为了防止操作太多的DOM起见...(偷懒.
当然像一般的编辑器那样. 每一行单独一段高亮的选区也不是不行啦... 只是还没做就感觉会卡(hen)卡(ma)的(fan).
CSS
为了能快地看到成型后的效果. 先以最快速度把 先规定一下: 选区中的最上面这行. 比如 选区中的中间的部分. 比如 选区中的最下面这行. 比如 模拟过程比如像这样. 见图5-8: 可以看到由于样式方面的原因.(可能算是问题).. 计算选区视图大小的时候要额外算上行号所在的空间的宽度. 是 绘制视图的整个流程就是: 当 2.1. 如果 2.2. 如果 2.3. 如果 现在测试都基本没问题了. 如果测试内容对实际要做的东西会有干扰. 就考虑删掉哦.. 有以下这个: 还原为 然后在 接下来把之前想要做的流程转换为代码.. 来看看有没有问题... 见 图5-9. 嗯... 选区应该没有什么问题. 说起来示例gif 里的内容都是无意义的数字之类的...因为复制粘贴什么的还没有做..就暂时用这些代替了.. 接下来可能是 复制 剪切 粘贴 Home End ↑ ↓ ← → ... 在做完这些最基础的功能之后... 重新调整与优化代码.. 之后再做多个光标.. 代码高亮&&智能提示 之类的东西 塞进
选区的控制是通过改变 top
left
right
与 height
来实现的.
见图5-5 中 (这里复制过来了..
1
3
7
行. 之后记为 $selection_top
8-9
行. 之后记为 $selection_middle
4
10
行. 之后记为 $selection_bottom
50px
.mousemove
或者 mouseup
的时候. 比对当前光标的位置 与 选区起点 是否出了偏差 (计算 logicalY
)... 如果有就更新选区视图
Y
上的差
var diffY = point_end.logicalY - point_start.logicalY
diffY === 0
. 更新
$selection_top
的 DOM 的 top
left
diffY === 1
. 更新
$selection_top
的 top
left
&&
$selection_bottom
的 top
right
diffY > 1
. 更新
$selection_top
的 top
left
&&
$selection_middle
的 top
height
&&
$selection_bottom
的 top
right
JS
template
中加入这个@path serval/script/harusame-template.js
/**
* 选区片段
*/
selectionPart: function () {
return SatoriDom.compile(
e('div', {'class': 'selection-part'})
)
}
@path serval/script/harusame-cursor.js
/**
* 1. 光标本身的元素节点
* 2. 之前所说的基准点可以做一个初始化.
*/
var Cursor = function (config) {
// ...
this.mousedown_point = {} /* 2 */
this.$selection_top = Template.selectionPart()
this.$selection_middle = Template.selectionPart()
this.$selection_bottom = Template.selectionPart()
// ...
}
/**
* 更新选区的 值 与 视图
*/
Cursor.prototype.updateSelection: function () {
this.findSelection()
this.updateSelectionView()
},
/**
* 更新选区视图
*/
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
}
},
感觉内容好多... 其实感觉依旧好水_(:3」∠)...
CHANGELOG
2017年8月10日14:14:00
F
在 getSelectionContent 中 修复了多余的 \n
上一篇
#4 从零开始制作在线 代码编辑器下一篇
#6 从零开始制作在线 代码编辑器