复制 与 剪切 与 粘贴
能获得选区内容后,就可以做进一步的操作啦。
剪切的话,实现原理同复制,只不过需要附加一个删除操作而已,所以先不管他。
监听事件
在浏览器中可以使用以下监听器来捕获到事件:
- oncopy - 复制
- oncut - 剪切
- onpaste - 粘贴
剪贴板
复制/剪切后的数据会寄存在剪贴板中(clipboard)。幸运的是在浏览器中可以操作剪贴板。
在 oncopy oncut onpaste
中,可以使用 event.clipboardData
来获得剪贴板,然后使用 setData(type, content)
getData(type)
来操作数据,比如有以下用法
这里的示例改编自 https://www.w3.org/TR/clipboard-apis/#the-copy-action 以便尽量准确 _(:3」∠)... 另外这里为方便叙述做了点调整
addEventListener(self.$serval_container, 'copy', function (event) {
event.clipboardData.setData('text/plain', 'Hello, world!');
event.clipboardData.setData('text/html', '<b>Hello, world!</b>');
event.preventDefault() // 阻止默认行为,避免选区内的数据覆盖掉这里写的
})
addEventListener(self.$inputer, 'paste', function (event) {
console.info(event.clipboardData.getData('text/plain'))
console.info(event.clipboardData.getData('text/html'))
event.preventDefault() // 阻止默认行为,不想要剪贴板的数据贴到编辑器中
})
执行结果(本机 )见 图6-1
关于下面那行,是由于这里指定的是 text/html
。在 HTML
中,<b>Hello, world!</b>
这样的 HTML标签
不能单独存在,所以会 自动拼接出一个最简化的完整的 HTML
这里将会用到的是 event.clipboardData.setData('text/plain', ...)
整体思路
这里从粘贴开始反推到复制,会比较容易。
假设现在剪贴板内已经有了 oncopy
提供的数据。那么在触发 onpaste
的时候,将这些数据插入到当前光标所在的行中。
另外,如果数据是有多行的情况,要在插入的时候,自动创建新的行。最后,将光标移动到插入的这些数据的末尾。
在 getSelectionContent
的时候,当时返回的是带有\n
的字符串。而不是返回一个数组。这是考虑到如果在线上编辑器进行多行内容复制的时候,目的是为了与操作系统上的其他软件进行交互的话,比如把一段代码复制到QQ聊天框里,这个时候还是要手动拼接上\n
换行符,所以干脆统一用字符串来传递,再通过String.prototype.split('\n')
解析成数组,之后再创建行的 DOM,最后渲染。
code
@path serval/script/harusame-serval.js
Serval.prototype._bindKeyboardEvent: function () {
/**
* 复制
* 1. 阻止复制的默认行为,手动处理复制行为
* 2. 当光标有选区的时候将选区内容放进剪贴板
*/
addEventListener(self.$serval_container, 'copy', function (event) {
event.preventDefault() /* 1 */
self.allocTask(function (v_cursor) {
if (v_cursor.isSelectionExist()) {
event.clipboardData.setData('text/plain', v_cursor.getSelectionContent()) /* 2 */
}
})
console.info('execute copy')
})
/**
* 粘贴
* 1. 阻止粘贴的默认行为,手动处理粘贴行为
* 2. 获得剪贴板的数据,只需要获得一次,所以写在外面
* 3. 分割数据成数组
* 4. 插入内容
*/
addEventListener(self.$inputer, 'paste', function (event) {
event.preventDefault() /* 1 */
var data = event.clipboardData.getData('text/plain') /* 2 */
self.allocTask(function (v_cursor) {
var data_array = data.split('\n') /* 3 */
self._insertContent(v_cursor, data_array) /* 4 */
})
console.info('execute paste')
})
},
打断一下!
可以看到在粘贴处使用了 _insertContent
函数,用来往编辑器插入内容,这个函数在前几章使用过,当时还约定了
往编辑器插入内容统一使用这个函数
结果自己回头就忘了这么回事... _(:3」∠)...
嘛... 不过最终还是要统一使用这个函数,目的是为了处理代码高亮以及撤回(Ctrl + z)操作...
因为一般编辑器插入文字的情况有两种:
- 插入无需换行的文字
- 插入 多行文字
为了调用 _insertContent
方便,会在该函数中加入判断以应对两个不同的情况:
- 插入无需换行的文字时候,调用时允许传入一个字符串 或者 一个 长度为1 的数组
- 插入多行文字的时候,约定传入一个数组
另外在该函数中,会处理行号问题以及光标的位置
这个时候写这个函数已经有点微妙的恶心了_(:3」∠)... 可以不用看
@path serval/script/harusame-serval.js
/**
* 插入内容
* 1. 缓存该光标所在的行的DOM
* 2. 缓存该行的文本内容
* 3. 取得光标之前的字符串
* 4. 取得光标之后的字符串
* 5. 拼接出完整的插入内容后的字符串
* 6. 移动游标
*/
Serval.prototype._insertContent: function (v_cursor, v_content) {
var $line = v_cursor.line.$line_content /* 1 */
var textContent = $line.textContent /* 2 */
var logicalY = v_cursor.logicalY
var logicalX = v_cursor.logicalX
var content_before = textContent.substring(0, logicalX) /* 3 */
var content_after = textContent.substring(logicalX, textContent.length) /* 4 */
var type = v_content.constructor
var array_length = v_content.length
if (type === String) {
$line.textContent = content_before + v_content + content_after /* 5 */
v_cursor.logicalX += v_content.length /* 6 */
} else if (type === Array) {
if (array_length === 1) {
$line.textContent = content_before + v_content[0] + content_after
v_cursor.logicalX += v_content[0].length
return
}
var first_line = v_content[0]
var extraX = first_line.length
$line.textContent = content_before + first_line
if (array_length > 1) {
for (var i = 1; i < array_length - 1; i++) {
Line.createLine(v_cursor.logicalY + i - 1, v_content[i])
Line.fixLineNumber(logicalY)
}
var last_line = v_content[array_length - 1]
Line.createLine(v_cursor.logicalY + array_length - 2, last_line)
Line.fixLineNumber(logicalY)
extraX = last_line.length
v_cursor.logicalY += array_length - 1
v_cursor.logicalX = extraX
} else {
Line.fixLineNumber(logicalY)
v_cursor.logicalY += array_length - 1
v_cursor.logicalX += extraX
}
$line = v_cursor.line.$line_content
$line.textContent += content_after
}
},
想了会有这种感觉的原因是因为:
Line.createLine的运作中使用选择器来得到操作对象,而这个函数本身会打乱行号,需 要重新修改行号 才能让 Line.createLine正常运作
之前考虑过使用数组来存储所有创建的行,这避免了使用选择器来得到DOM,应该会很好用,但是又想想如果会有几千几万行,存那么多DOM是不是不太好。
这里看来,避免过早进行优化 && 过早想的复杂 又一次引以为戒。这里在没有做正式的测试的情况下就改变原本的想法,算是一个惩罚吧 _(:3」∠)...
嘛... 至少是实现了功能。见 图6-2
剪切 与 删除选区内容
在这里,剪切实际上是两个操作,依次是:
- 复制内容
- 删除选区内容
复制内容已经做好了,反正也就一句话,相比来说删除选区内容就麻烦多啦~。
不过挺好的是,他很重要,因为很多地方都会用到他。做好了删除选取内容,选区这块基本就完成了~
删除选区内容
其实做到现在都是单光标的情况,逻辑上都不复杂,就加快进度了(偷懒),解释都会加在代码中,以注释的形式存在。
同样地,这里的代码完全是为了只考虑实现功能存在了,有很多地方不合理,这些问题已经记在小本子上
简短说一下:
- 获取选区终点之后的内容
- 删除 选取范围内 除了选区起点所在的行的 其他行
- 将选区终点之后的内容贴到起点之后
- 修正行号
@path serval/script/harusame-serval
/**
* 剪切
* 1. 阻止剪切的默认行为,手动处理剪切行为
* 2. 当光标有选区的时候将选区内容放进剪贴板
* 3. 删除选区内容
* 4. 清除选区
*/
addEventListener(self.$serval_container, 'cut', function (event) {
event.preventDefault() /* 1 */
self.allocTask(function (v_cursor) {
if (v_cursor.isSelectionExist()) {
event.clipboardData.setData('text/plain', v_cursor.getSelectionContent()) /* 2 */
v_cursor.deleteSelectionContent() /* 3 */
v_cursor.setSelectionBase() /* 4 */
v_cursor.updateSelection() /* 4 */
}
})
console.info('execute cut')
})
@path serval/script/harusame-cursor.js
/**
* 删除选区内容(视图方面)
* @注意保证执行了 findSelection()
* 1. 获得终点之后的内容
* 2. 当选区有多行的时候,删除除了选区起点所在的行
* 3. 将光标移到选区起点(删除光标选区的时候,光标只会在选区起点)
* 4. 将终点之后的内容贴到光标起点之后
* 5. 修正行号
*/
deleteSelectionContent: function () {
var endY = this.selection_end.logicalY
var endX = this.selection_end.logicalX
var $end = Line.getLineContentByLogicalY(endY)
var end_textContent = $end.textContent
var end_content = end_textContent.substring(endX, end_textContent.length) /* 1 */
var startY = this.selection_start.logicalY
var offsetY = endY - startY
if (offsetY === 1) { /* 2 */
Line.deleteLine(endY)
} else if (offsetY > 1) { /* 2 */
for (var i = endY; i > startY; i--) {
Line.deleteLine(i)
}
}
this.logicalY = this.selection_start.logicalY /* 3 */
this.logicalX = this.selection_start.logicalX /* 3 */
var $line = this.line.$line_content
var startX = this.selection_start.logicalX
$line.textContent = $line.textContent.substring(0, startX) + end_content /* 4 */
Line.fixLineNumber(startY) /* 5 */
},
效果见 图6-3,只要能看到选区被删除了就可以了。
关于删除选区
这里先把删除选区放一放...
在剪切中的删除选区实际上虽然能解决问题,但是是饶了很多弯强行实现的 _(:3」∠)...。删除选区同样放在优化后再做。
在有选区的情况下,一般来说输入内容会直接覆盖掉选区。
下面的一段内容都不会考虑有选区的情况
Home && End && 上下左右
Home && End 很简单,没什么好说的。
@path serval/script/harusame-serval.js
Serval.prototype.keydownHandler = {
/**
* KEY: End
*/
'35': function (event) {
this.allocTask(function (v_cursor) {
v_cursor.logicalX = v_cursor.line.$line_content.textContent.length
})
},
/**
* KEY: Home
*/
'36': function (event) {
this.allocTask(function (v_cursor) {
v_cursor.logicalX = 0
})
},
}
上下左右的话,其实有个小细节:
仅对于上(ArrowUp) 下(ArrowDown)键,使用时会记下第一次使用时的 psysicalX
值,如果之后再次使用上或下,除了改变光标的 logicalY
,这次的位置会移到所说的psysicalX
处最近的地方。当使用其他按键时,会重设这个 psysicalX
见 图6-4
这个也放到优化后再说。_(:3」∠)...
@path serval/script/harusame-serval.js
Serval.prototype.keydownHandler = {
/**
* KEY: ArrowLeft
* 1. 如果光标在行首
* 1.1. 且光标在第一行
* 1.1.1. 那么什么都不做,返回
* 1.2. 光标移到上一行的末尾
* 2. 普通情况就让 x - 1
*/
'37': function (event) {
this.allocTask(function (v_cursor) {
var logicalY = v_cursor.logicalY
var logicalX = v_cursor.logicalX
/* 1 */
if (logicalX === 0) {
if (logicalY === 0) { /* 1.1 */
return /* 1.1.1 */
}
v_cursor.logicalY -= 1 /* 1.2 */
v_cursor.logicalX = v_cursor.line.$line_content.textContent.length /* 1.2 */
return
}
v_cursor.logicalX -= 1 /* 2 */
})
},
/**
* KEY: ArrowUp
* 1. 如果光标在第一行,什么都不做
* 2. 否则 y - 1
*/
'38': function (event) {
this.allocTask(function (v_cursor) {
if (v_cursor.logicalY === 0) {
return /* 1 */
}
v_cursor.logicalY -= 1 /* 2 */
})
},
/**
* KEY: ArrowRight
*/
'39': function (event) {
this.allocTask(function (v_cursor) {
var logicalY = v_cursor.logicalY
var logicalX = v_cursor.logicalX
if (logicalX === v_cursor.line.$line_content.textContent.length) {
if (logicalY === Line.max_line_number - 1) {
return
}
v_cursor.logicalY += 1
v_cursor.logicalX = 0
return
}
v_cursor.logicalX += 1
})
},
/**
* KEY: ArrowDown
*/
'40': function (event) {
this.allocTask(function (v_cursor) {
if (v_cursor.logicalY === Line.max_line_number - 1) {
return
}
v_cursor.logicalY += 1
})
},
}
效果见 图6-5:
这个甚至不够差强人意的编辑器终于要第一次优化了~
下一篇
还没有