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

终于有网了

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

Line


harusame-line.js

serval/script/ 下创建 harusame-line.js

文件路径 serval/script/harusame-line.js

;
/**
 * 1. 行 的高度,同样,这里先约(写)定(死)
 */
(function () {
    var Line = {}
    var self = Line

    self.LINE_HEIGHT = 20 /* 1 */

    window.Line = Line
})()

同时修改掉serval/script/harusame-cursor.js 中的 LINE_HEIGHTLine.LINE_HEIGHT

,既然是 Line类,顺便把 serval/script/harusame-serval.js 中的 _generateLine 改造为由 Line 生成,这样会比较合适吧,也顺手把生成行号的方法写一下~

文件路径 serval/script/harusame-serval.js

/**
 * 渲染一行
 */
_generateLine: function (v_content) {
    var $line = Line.generateLine(v_content)
    this.$line_container.appendChild($line)
}
文件路径 serval/script/harusame-line.js

/**
 * 生成一行
 * @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})
},

/**
 * 生成最大行号
 */
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++
    }
})

这时候刷新了下浏览器发现报错了,嗯嗯...别忘记引入harusame-line.js

文件路径 serval/index.html

...
<script src="script/harusame-dom.js"></script>
<script src="script/harusame-template.js"></script>
<script src="script/harusame-line.js"></script>
<script src="script/harusame-cursor.js"></script>
<script src="script/harusame-serval.js"></script>
...

引入位置别忘了只能放在 harusame-cursor.js && harusame-serval.js 以上,harusame-template 以下...

继续 #1 中计算 Cursor 位置的逻辑

嘛,要计算 psysicalX logicalX,要比计算Y复杂一点...而且有各种各样的因素会影响这个,比如letter-spacing 窗口resize 不知道是啥宽度的内容 什么的,不管那么多~先只管正常情况下的英文,中文。

大致的计算思路。

先确定一下最终目的是为了让点击时,光标会自动偏移到一个最适合的它位置,防止写着写着就写到其他地方去了的这种事情......_(:3」∠)...

拿 图2-1 为例,这是 Sublime Text 3 中的放大了很多倍的第一行的内容,当点击到红色圆圈处,触发一个方法calcX,让它去尝试寻找一个可能是最适合的位置,这里是约定寻找一个总是大于且最靠近点击处的位置M1,然后比较M1M1之前的一个字符处的位置,光标靠近哪个就返回那个位置的字符位置(psysicalX)与它的索引(logicalX)。

图 2-1 其实就是离哪个地方近就去哪
逻辑步骤
  1. 在初始化的时候,计算并存储英文字母和中文字母的宽度single_byte_width double_byte_width。由于之前就设置了等宽字体,所以按理来说,字符间都是宽度相等的,但是一眼就能看出英文字母的宽度是一类,中文字母的宽度是一类。因为字符方面的知识并不充足,只能下意识地怀疑是单字节字符是一类宽度,双字节字符是一类宽度...于是去找了相关的正则,做出来发现这么分类竟然没什么问题..!?(之后再补习._(:3」∠)...

  2. 用户点击某处,得到event.layerX,作为参数v_psysicalX 传入 能够计算偏差后的psysicalX 的方法calcX中(因为要返回二个结果,所以不叫他calcPsysicalX or calcLogicalX

  3. 得到该行的字符串(textContent),转化为数组content_array

  4. 声明一个保存字符累加长度的变量current_width,创建一个循环体

  5. 在循环体中,当current_width < v_psysicalX 的时候(也就是尝试寻找在点击处右边 && 离点击处最近光标位置),执行 5,否则执行6

  6. 声明一个变量char_width用来存储当前字符content_array[index]的宽度,它通过计算得到。最终即char_width = calcCharWidth(content_array[index])

  7. 如果current_width >= v_psysicalX(找到了4中所说的该位置),这时候去判断:点击位置离左边的光标处更近一点还是右边的光标处更近一点,返回更近一点的光标位置。(也就是让光标进行偏移到两个字符之间的位置。不然点哪光标在哪,光标会遮挡文字,而且可能会让用户觉得懵逼..?!..
    不过说到这里,突然想到了一个没什么用的模拟修改液的功能.XD...)

接下来又能写代码了...

各个地方的代码
文件位置 serval/script/harusame-cursor.js
加的挺多的,直接贴完全了~

;
(function () {
    /**
     * 1. 光标本身的元素节点
     */
    var Cursor = function (config) {
        this.$ref = null /* 1 */

        this._logicalY = 0
        this._logicalX = 0
        this._psysicalY = 0
        this._psysicalX = 0

        this.selection_start = null

        Cursor.preCheck()
        this._generateCursor()
        this._setObserver()
    }

    /**
     * 得到浏览器计算后的宽度(width)
     * getComputedStyle(v_node).width 是一个带单位的字符串,用 parseFloat 隐式转化为数字类型,并去除'px'单位,且保证精准度
     */
    function getComputedWidth (v_node) {
        return parseFloat(getComputedStyle(v_node).width)
    }

    /**
     * 检测字符宽度
     * a && 雨是随便打的字符
     */
    Cursor.preCheck = function () {
        Line.line = 0
        Line.$ref.textContent = 'a'
        this.single_byte_width = getComputedWidth(Line.$ref)
        Line.$ref.textContent = '雨'
        this.double_byte_width = getComputedWidth(Line.$ref)

        // 这句话是测试用的,等能够输入了的时候,记得删除
        Line.$ref.textContent = 'hello 你好'
        console.info('single_byte_width', this.single_byte_width)
        console.info('double_byte_width', this.double_byte_width)
    }

    /**
     * 判断字符宽度
     * /[\x00-\xff]/ ASCII 编码在 0-255 的字符哦
     */
    Cursor.calcCharWidth = function (v_char) {
        if (/[\x00-\xff]/.test(v_char)) {
            return this.single_byte_width
        } else {
            return this.double_byte_width
        }
    }


    Cursor.prototype = {
        constructor: Cursor,

        /**
         * 创建一个游标对象
         */
        _generateCursor: function () {
            this.$ref = SatoriDom.compile(
                e('i', {'class': 'fake-cursor'})
            )
        },

        /**
         * 绑定 逻辑位置 与 物理位置 之间的关系
         */
        _setObserver: function () {
            /**
             * 这里的 self 由于也是 js关键字,所以会高亮
             * self 原本指向 window,一般用不到
             */
            var self = this

            /**
             * 1. 这里赋值的是  _logicalY 哦,下面也是
             * 2. 更新 psysicalY 的值
             * 3. 更新 DOM 位置

             * 4. 写到这里发现有点问题......
             */
            Object.defineProperty(self, 'logicalY', {
                set: function (v_logicalY) {
                    self._logicalY = v_logicalY /* 1 */
                    self._psysicalY = self.calcPsysicalY(v_logicalY) /* 2 */
                    self._setY(self._psysicalY) /* 3 */

                    // self.$line = document.getElementById(LINE) /* 4 */
                },

                get: function () {
                    return self._logicalY
                }
            })

            Object.defineProperty(self, 'psysicalY', {
                set: function (v_psysicalY) {
                    self.logicalY = self.calcLogicalY(v_psysicalY)
                },

                get: function () {
                    return self._psysicalY
                }
            })

            Object.defineProperty(self, 'logicalX', {
                set: function (v_logicalX) {
                    self._logicalX = v_logicalX
                    self._psysicalX = self.calcPsysicalX(v_logicalX)
                    self._setX(self._psysicalX)
                },

                get: function () {
                    return self._logicalX
                }
            })


            Object.defineProperty(self, 'psysicalX', {
                set: function (v_psysicalX) {
                    var _proxy = self.calcX(v_psysicalX)
                    self._psysicalX = _proxy.psysicalX
                    self._logicalX = _proxy.logicalX
                    self._setX(self._psysicalX)
                },

                get: function () {
                    return self._psysicalX
                }
            })
        },

        _setX: function (v_psysicalX) {
            this.$ref.style.left = v_psysicalX + 'px'
        },

        _setY: function (v_psysicalY) {
            this.$ref.style.top = v_psysicalY + 'px'
        },

        /**
         * 计算 物理 Y
         */
        calcPsysicalY: function (v_logicalY) {
            return v_logicalY * Line.LINE_HEIGHT
        },

        /**
         * 计算 逻辑 Y
         */
        calcLogicalY: function (v_psysicalY) {
            return parseInt(v_psysicalY / Line.LINE_HEIGHT)
        },

        /**
         * 计算 物理 X
         */
        calcPsysicalX: function (v_logicalX) {
            var content_array = Line.$ref.textContent.split('')
            var current_width = 0

            for (var i = 0; i < v_logicalX; i++) {
                current_width += Cursor.calcCharWidth(content_array[i])
            }

            return current_width
        },

        /**
         * 用于计算 逻辑 X
         */
        calcX: function (v_psysicalX) {
            var psysicalX = getComputedWidth(Line.$ref)
            var textContent = Line.$ref.textContent
            /**
             * 如果点击的位置大于该行长度,直接将光标放在该行末尾
             */
            if (psysicalX <= v_psysicalX) {
                return {
                    psysicalX: psysicalX,
                    logicalX: textContent.length
                }
            }

            var content_array = textContent.split('')
            var current_width = 0

            for (var i = 0; i < content_array.length; i++) {
                var char_width = Cursor.calcCharWidth(content_array[i])
                current_width += char_width
                if (current_width >= v_psysicalX) {
                    var point_right = current_width
                    var point_left = current_width - char_width

                    var offset_right = point_right - v_psysicalX
                    var offset_left = v_psysicalX - point_left

                    if (offset_right < offset_left) {
                        return {
                            psysicalX: point_right,
                            logicalX: i + 1
                        }
                    } else {
                        return {
                            psysicalX: point_left,
                            logicalX: i
                        }
                    }
                }
            }
        }
    }

    window.Cursor = Cursor
})()

文件位置 serval/script/harusame-line.js
加的挺多的,直接贴完全了~

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

    self.LINE_HEIGHT = 20 /* 4 */

    var LINE_NUMBER_SIGN = 'LNS' /* 1 */
    var LINE_CONTENT_SIGN = 'LCS' /* 2 */
    var START_LINE = 1 /* 3 */

    /**
     * 生成一行
     * @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: LINE_CONTENT_SIGN,
            LINE_NUMBER_SIGN: LINE_NUMBER_SIGN,
            START_LINE: 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(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) {
    console.info(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 || '')
            ])
        ])
    )
},

来看看浏览器中的效果,图 2-2:

图 2-2

目的是达到了,理论上来说不想有的也都有:

  1. 点在1所在的元素节点上,也会有光标的定位效果;
  2. 点在没有行的部分,光标也会定位过去。而希望光标能定位到最后一行,最后一列
  3. 点在 h 的前面的时候,不怎么容易点到,体验比较差...关于体验的话,感觉能在后面单独修正...

嘛..也想提供一些选项,让用户自定义行为,但是感觉反正我自己是不会用的...就不做了。

因为主要目标已经达到了..(可能)...其他的细节留给以后再说..
接下来是可能是输入内容与换行~


CHANGELOG

2017年7月12日 18:11
U 修改 calcPsysicalX 中的 <= 为 <
U 修改 calcX 中的 i 与 i - 1 为 i + 1 与 i

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

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

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

推荐阅读更多精彩内容

  • 上一篇#0 从零开始制作在线 代码编辑器 目录结构和说明 目录初始结构 创建一些目录以及文件,如图1-1 所示。这...
    春雨棲姬阅读 3,526评论 0 6
  • 问答题47 /72 常见浏览器兼容性问题与解决方案? 参考答案 (1)浏览器兼容问题一:不同浏览器的标签默认的外补...
    _Yfling阅读 13,727评论 1 92
  • 上一篇#4 从零开始制作在线 代码编辑器 删除 与 BackSpace 与 Delete BackSpace 为了...
    春雨棲姬阅读 1,458评论 0 0
  • 选择qi:是表达式 标签选择器 类选择器 属性选择器 继承属性: color,font,text-align,li...
    wzhiq896阅读 1,730评论 0 2
  • 选择qi:是表达式 标签选择器 类选择器 属性选择器 继承属性: color,font,text-align,li...
    love2013阅读 2,303评论 0 11