#6 从零开始制作在线 代码编辑器

上一篇
#5 从零开始制作在线 代码编辑器

复制 与 剪切 与 粘贴


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

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

监听事件

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

  1. oncopy - 复制
  2. oncut - 剪切
  3. 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

图6-1.png

关于下面那行,是由于这里指定的是 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)操作...

因为一般编辑器插入文字的情况有两种:

  1. 插入无需换行的文字
  2. 插入 多行文字

为了调用 _insertContent 方便,会在该函数中加入判断以应对两个不同的情况:

  1. 插入无需换行的文字时候,调用时允许传入一个字符串 或者 一个 长度为1 的数组
  2. 插入多行文字的时候,约定传入一个数组

另外在该函数中,会处理行号问题以及光标的位置

这个时候写这个函数已经有点微妙的恶心了_(: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

图6-2.gif

剪切 与 删除选区内容

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

  1. 复制内容
  2. 删除选区内容

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

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

删除选区内容

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

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

简短说一下:

  1. 获取选区终点之后的内容
  2. 删除 选取范围内 除了选区起点所在的行的 其他行
  3. 将选区终点之后的内容贴到起点之后
  4. 修正行号
@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,只要能看到选区被删除了就可以了。

图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

图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:

图6-5.gif

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


上一篇
#5 从零开始制作在线 代码编辑器

下一篇
还没有

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • 上一篇#4 从零开始制作在线 代码编辑器 删除 与 BackSpace 与 Delete BackSpace 为了...
    春雨棲姬阅读 1,458评论 0 0
  • 命令历史 以:和/开头的命令都有历史纪录,可以首先键入:或/然后按上下箭头来选择某个历史命令。 启动vim 在命令...
    im蚂蚁阅读 30,720评论 3 48
  • 目录(?)[-] 关于Vim 1 Vim的几种模式 启动Vim 文档操作 光标的移动 1 基本移动 2 翻屏 3 ...
    SunnyLeong阅读 8,627评论 0 32
  • 真希望你没有女朋友 第一次遇见你的时候,我也这么想。 让我追悔莫及的是 当时我没有告诉你, 我,愿意等你。
    SweetCC阅读 80评论 0 0
  • 诗歌一《被吃定了的感觉》 是春天的景色太美,还是空中的歌声在飞 刚刚烘培好的嫩绿 点缀了枯燥颓废 咬一口翠绿 香甜...
    是孙轶呀阅读 521评论 0 4