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

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

输入功能


简单的原理

输入功能的话,利用一个不可见的 <textarea>( 这里叫它inputer)来接受键盘事件,当用户将内容输入到inputer中,通过监听事件oninput的回调函数将inputer中的内容($inputer.value)获取到,然后复制给当前行的文本节点中(Line.$ref.textContent = $inputer.value),最后清空inputer中的内容($inputer.value = '')。
另外为了能让inputer一直有效保持聚焦状态,每次鼠标点击在编辑器的内部时,都要去进行一次聚焦操作...吧?!

现在可能有个问题就是:假如有多个光标(之后的每一个功能都会优先考虑多光标的情况哦~),对每个光标所在的那一行需要输入一些文字,但是Line所管理的当前行只有一个,代码写起来会有点别扭..
就像一桌人在饭店吃饭,但是筷子却只有一双,只有一个人吃完才把筷子留给下个人用的感觉...希望的是,每个人都有一双筷子,会比较爽...~
对于计算姬们来说,不是不行,甚至可能是更好的方法,毕竟节省了的资源。但是在开发初期,只管开发者爽会让项目进展的更快吧..?(

所以这里更改下LineCursor的代码,让每一个Cursor实例去维护一个记录当前行的Line实例

code

文件位置 serval/script/harusame-line.js
由于 Line 需要被实例化,而且考虑到方便与扩展起见,几乎改了整个harusame-line.js,所以这里会贴出完整的代码

;
;
/**
 * 1. 行号 的元素节点的 id前缀
 * 2. 行内容 的元素节点的 id前缀
 * 3. 初始行号
 * 4. 行 的高度,同样,这里先约(写)定(死),暴露给外面使用
 */
(function (config) {
    var Line = function () {
        this.$line_content = null
    }

    var self = Line

    self.line_number_sign = 'LNS' /* 1 */
    self.line_content_sign = 'LCS' /* 2 */
    self.start_line = 1 /* 3 */
    self.line_height = 20 /* 4 */

    /**
     * 获得该行的行号DOM
     */
    self.getLineNumberByLogicalY = function (v_line_number) {
        return document.getElementById(self.line_number_sign + v_line_number)
    }

    /**
     * 获得该行的行内容的DOM
     */
    self.getLineContentByLogicalY = function (v_line_number) {
        return document.getElementById(self.line_content_sign + v_line_number)
    }

    /**
     * 生成一行
     * @param content {string} 初始内容
     */
    self.generateLine = function (v_content) {
        var line_number = self.max_line_number
        var initial_content = v_content || ''
        return Template.line({
            line_number: line_number,
            initial_content: initial_content,
            line_content_sign: self.line_content_sign,
            line_number_sign: self.line_number_sign,
            start_line: self.start_line
        })
    }

    /**
     * 生成最大行号
     */
    var PROXY_max_line_number = 0
    Object.defineProperty(self, 'max_line_number', {
        set: function (v_max_line_number) {
            PROXY_max_line_number = v_max_line_number
        },

        get: function () {
            return PROXY_max_line_number++
        }
    })


    /**
     * set:
     * 1. 记录当前行
     * 2. 记录当前行的 DOM
     * get:
     * 1. 返回当前行
     */
    var PROXY_line = 0
    Object.defineProperty(self, 'line', {
        set: function (v_logicalY) {
            PROXY_line = v_logicalY /* 1 */
            self.$ref = document.getElementById(self.line_content_sign + v_logicalY) /* 2 */
        },

        get: function () {
            return PROXY_line
        }
    })

    window.Line = Line
})()
文件位置 serval/script/harusame-template.js
同样是 line 处

/**
 * 行
 * @param line_number {string} 行号
 * @param initial_content {string} 该行初始内容
 */
line: function (params) {
    var line_number = params.line_number
    return SatoriDom.compile(
        e('div', {'class': 'line'}, [
            e('div', {'class': 'line-number-wrap'}, [
                e('span', {'id': params.line_number_sign + line_number, 'class': 'line-number'}, line_number + params.start_line + '')
            ]),
            e('div', {'class': 'code-wrap'}, [
                e('code', {'id': params.line_content_sign + line_number, 'class': 'code-content'}, params.initial_content || '')
            ])
        ])
    )
},
文件位置 serval/script/harusame-serval.js
部分改动

var Serval = function (config) {
    // ...
    this._bindMouseEvent()
    this._bindKeyboardEvent() /* 新增 */
}

Serval.prototype = {
    // ...
    /**
     * 绑定各种鼠标事件
     */
    _bindMouseEvent: function () {
        var self = this

        /**
         * addEventListener 是指自己写的方法,见最下面
         * 当 mousedown 时,就对光标位置进行计算
         * 1. 取消鼠标默认的行为,否则 2 不会生效
         * 2. 让编辑器总是能够接受键盘事件
         * 3. 定位鼠标
         */
        addEventListener(self.$serval_container, 'mousedown', function (event) {
            event.preventDefault() /* 1 */

            self.$inputer.focus() /* 2 */

            self.allocTask(function (v_cursor) {
                v_cursor.psysicalY = event.layerY
                v_cursor.psysicalX = event.layerX
            })
        })
    },

    /**
     * 绑定各种键盘事件
     */
    _bindKeyboardEvent: function () {
        var self = this

        /**
         * 当对 $inputer 进行输入的时候
         * 1. 统一使用 insertContent 进行内容的插入
         * 2. 清除 $inputer 中的文本内容
         */
        addEventListener(self.$inputer, 'input', function (event) {
            var content = self.$inputer.value
            self.allocTask(function (v_cursor) {
                self.insertContent(v_cursor, content)
            })
            self.$inputer.value = ''
        })
    },

    /**
     * 插入内容
     * 1. 缓存该光标所在的行的DOM
     * 2. 缓存该行的文本内容
     * 3. 取得光标之前的字符串
     * 4. 取得光标之后的字符串
     * 5. 拼接出完整的插入内容后的字符串
     * 6. 移动游标
     */
    _insertContent: function (v_cursor, v_content) {
        var $line = v_cursor.line.$line_content /* 1 */
        var textContent = $line.textContent /* 2 */
        var logicalX = v_cursor.logicalX
        var content_before = textContent.substring(0, logicalX) /* 3 */
        var content_after = textContent.substring(logicalX, textContent.length) /* 4 */

        $line.textContent = content_before + v_content + content_after /* 5 */
        v_cursor.logicalX += v_content.length /* 6 */
    },
    // ...
}

现在就可以进行输入啦~(如果出现错误,可能是因为之前的harusame-cursor.js中的calcX中的偷偷做了修改_(:3」∠)... i 改为 i + 1 i - 1 改为 i 以及 calcPsysicalX 中的 <= 改为 <),效果见 图3-1。

图3-1

嗯嗯...看上去很美好,很有成就感,但是还不够!

中文的输入 与 浏览器事件行为的差异

理论上来说,当然实践上也是,输入法会从逻辑上被禁用...还无法输入中文等需要拼写的文字哦。毕竟准备变成中文字符的字母全被'偷'走了。在input的回调函数中加入
console.info('emit input')来看看发生了什么...
在 火狐 中见 图3-2。

图3-2

在 Chrome 中见 图3-3。

图3-3

可以看到在打开输入法的情况下,要拼写的字母直接就被拖进行里面了,并且在火狐中会连续触发三次oninput,而在 Chrome 中只会正常点地触发一次。
虽然这个不同浏览器对事件作出行为的差异与之后的解决方案没有什么直接关系,但是预先记录并提醒一下,在之后也与会遇到类似的不同浏览器之间事件行为的差异,并且会导致编辑器出问题。很幸运,这里不会就是了~

要想使用拼写的能力,这时候需要compositionstartcompositionend的两个事件来配合使用解决问题啦。
compositionstartcompositionend 往往用在输入法的处理方面。

MDN 中有相关解释。
这里作简单地解释,就像在键盘上按下一个键,会依次触发keydown keyup一样,当输入(拼写)文字的时候,也会依次触发compositionstart compositionend。拿敲入nihao 为例的话,在敲n的时候,compositionstart会触发,期间每次敲入一个字母都会触发compositionupdate(这个事件的意思听名字就能猜出来了,虽然这里没有用到),在敲完nihao,按下空格键、或者回车键、或者鼠标选择文字等把拼写后的内容(你好尼壕你号什么的)进行输出的时候,才会触发compositionend事件。常理是这样哦~
但是做的时候就遇到问题了,这里就直接说了,在火狐中会有迷の行为。
先把代码改成这样,然后见图 3-4

文件位置 serval/script/harusame-serval.js

_bindKeyboardEvent: function () {
    var self = this
    var typewriting_switch = false /* 用来标识是否正在使用输入法,一般都会这么用 */

    addEventListener(self.$inputer, 'compositionstart', function (event) {
        console.info('emit compositionstart', event)
        typewriting_switch = true
    })

    addEventListener(self.$inputer, 'compositionend', function (event) {
        console.info('emit compositionend', event)
        typewriting_switch = false
    })

    /**
     * 当对 $inputer 进行输入的时候
     * 1. 统一使用 _insertContent 进行内容的插入
     * 2. 清除 $inputer 中的文本内容
     */
    addEventListener(self.$inputer, 'input', function (event) {
        console.info('emit input')
        if (!typewriting_switch) {
            var content = self.$inputer.value
            self.allocTask(function (v_cursor) {
                self._insertContent(v_cursor, content)
            })
            self.$inputer.value = ''
        }
    })
},

图 3-4

可以看到在火狐中,利用输入法敲入nihao后,会依次

  1. 首先触发compositionstart
  2. 触发五次input(因为nihao有五个字母),并且这五个字母不算作$inputer.value中。
  3. 选择你好 进行输出,触发 compositionend,并且在data中可以获取。
  4. 触发一次 input
  5. 再次触发compositionstart
  6. 触发一次 input
  7. 再次触发compositionend,但此时data中是空的
  8. 触发一次 input

这方面的话,我也不是很懂啦...突然触发那么多事件...!?

不过也没关系,再看看 Chrome 中的行为。

图3-5

这就很正常了,并且会发现编辑器中的第一行没有你好输出,这才是正常啊~!因为输出文字是利用input的,Chrome 最后并没有触发 input,而在火狐中肯定是触发了input再输出的你好。这里可以看到火狐跟 Chrome 都能够使用compositionend.data来获取到输出的内容,如果此时停止执行input回调函数中的逻辑的话,这样就能获得完整的输入法体验了。

code

文件位置 serval/script/harusame-serval.js
只改部分哦

/**
 * 绑定各种键盘事件
 */
_bindKeyboardEvent: function () {
    var self = this
    var typewriting_switch = false /* 用来标识是否正在使用输入法,一般都会这么用 */

    /**
     * 当准备使用输入法进行输入时
     * 1. 开启输入法标识
     */
    addEventListener(self.$inputer, 'compositionstart', function (event) {
        typewriting_switch = true
    })

    /**
     * 当准备使用输入法进行输出时
     * 1. 输出内容
     * 2. 清空 $inputer 中的内容
     * 3. 做完这些事后,关闭输入法标识
     */
    addEventListener(self.$inputer, 'compositionend', function (event) {
        var content = event.data
        /* 因为火狐会触发两次 compositionend,而第二次的 data 是没有数据的,所以只需要取有数据的那次 */
        if (content.length !== 0) {
            /* 1 */
            self.allocTask(function (v_cursor) {
                self._insertContent(v_cursor, content)
            })
            self.$inputer.value = '' /* 2 */
        }
        typewriting_switch = false /* 3 */
    })

    /**
     * 当对 $inputer 进行输入的时候
     * 1. 只有输入法未开启时,才使用 input 事件 进行输出
     * 1. 统一使用 _insertContent 进行内容的插入
     * 2. 清除 $inputer 中的文本内容
     */
    addEventListener(self.$inputer, 'input', function (event) {
        /* 1 */
        if (!typewriting_switch) {
            var content = self.$inputer.value
            self.allocTask(function (v_cursor) {
                self._insertContent(v_cursor, content) /* 1 */
            })
            self.$inputer.value = '' /* 2 */
        }
    })
},

来看看效果吧,文字就顺手敲得...图3-6 ~

图3-6

下一篇可能是回车


CHANGELOG

2017年7月20日 22:56
D 删除了 不小心粘贴上来的 剧透内容


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

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

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

推荐阅读更多精彩内容