这是前端嘛Web前端之路

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

2017-07-13  本文已影响275人  春雨棲姬

上一篇
#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 从零开始制作在线 代码编辑器

上一篇 下一篇

猜你喜欢

热点阅读