自己做在线HTML编辑器,#6 从零开始制作在线 代码编辑器

复制 与 剪切 与 粘贴

能获得选区内容后,就可以做进一步的操作啦。

剪切的话,实现原理同复制,只不过需要附加一个删除操作而已,所以先不管他。

监听事件

在浏览器中可以使用以下监听器来捕获到事件:

oncopy - 复制

oncut - 剪切

onpaste - 粘贴

剪贴板

复制/剪切后的数据会寄存在剪贴板中(clipboard)。幸运的是在浏览器中可以操作剪贴板。

在 oncopy oncut onpaste 中,可以使用 event.clipboardData 来获得剪贴板,然后使用 setData(type, content) getData(type) 来操作数据,比如有以下用法

addEventListener(self.$serval_container, 'copy', function (event) {

event.clipboardData.setData('text/plain', 'Hello, world!');

event.clipboardData.setData('text/html', 'Hello, world!');

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

26bcb09af203

图6-1.png

关于下面那行,是由于这里指定的是 text/html。在 HTML 中,Hello, world! 这样的 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是不是不太好。

26bcb09af203

这里看来,避免过早进行优化 && 过早想的复杂 又一次引以为戒。这里在没有做正式的测试的情况下就改变原本的想法,算是一个惩罚吧 _(:3」∠)...

嘛... 至少是实现了功能。见 图6-2

26bcb09af203

图6-2.gif

剪切 与 删除选区内容

在这里,剪切实际上是两个操作,依次是:

复制内容

删除选区内容

复制内容已经做好了,反正也就一句话,相比来说删除选区内容就麻烦多啦~。

不过挺好的是,他很重要,因为很多地方都会用到他。做好了删除选取内容,选区这块基本就完成了~

26bcb09af203

删除选区内容

其实做到现在都是单光标的情况,逻辑上都不复杂,就加快进度了(偷懒),解释都会加在代码中,以注释的形式存在。

同样地,这里的代码完全是为了只考虑实现功能存在了,有很多地方不合理,这些问题已经记在小本子上

简短说一下:

获取选区终点之后的内容

删除 选取范围内 除了选区起点所在的行的 其他行

将选区终点之后的内容贴到起点之后

修正行号

@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,只要能看到选区被删除了就可以了。

26bcb09af203

图6-3.gif

关于删除选区

这里先把删除选区放一放...

在剪切中的删除选区实际上虽然能解决问题,但是是饶了很多弯强行实现的 _(: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

26bcb09af203

图6-4.gif

这个也放到优化后再说。_(: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:

26bcb09af203

图6-5.gif

这个甚至不够差强人意的编辑器终于要第一次优化了~

26bcb09af203

下一篇

还没有

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值