美文网首页这是前端嘛Web前端之路
#1 从零开始制作在线 代码编辑器

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

作者: 春雨棲姬 | 来源:发表于2017-06-14 17:24 被阅读735次

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

    目录结构和说明


    目录初始结构

    创建一些目录以及文件,如图1-1 所示。这里只是随手创的,也可以自定目录结构。

    1-1 目录结构

    目录说明

    • serval/script/harusame-dom.js 提供了创建 DOM 的工具,比createElement(tagName) 那种要稍微方便一点,创建方式借鉴了虚拟DOM。使用方式就在这里说明了,之后也不细说。
    // 创建元素节点
    var $node = SatoriDom.compile(e('div', {'class': 'demo', 'id': 'demo'}, '这里是文本节点: parent', [
        e('div', {'class': 'child'}, [
            e('p', '这里是文本节点: child x child')
        ])
    ]))
    
    console.log($node.tagName) // DIV
    
    // 其他可能用到的方式
    function template_submit (data) {
        var $event_node = SatoriDom.compile(e('input', {'class': 'i-have-event', 'type': 'submit'}))
    
        $event_node.addEventListener('click', function (event) {
            console.log('You click me:' + data.content)
        })
    
        return SatoriDom.compile(e('form', {'class': 'this-is-a-form'}, [
            $event_node
        ]))
    }
    
    
    • serval/script/harusame-template.js 存放了一些模板,把与创建DOM 相关的代码抽离出来,避免其在逻辑代码中占用很多篇幅。

    • serval/script/harusame-cursor.js 光标会被单独抽象成一个类。

    • serval/script/harusame-serval.js 绑定整个编辑器的事件,以及逻辑的处理,最重要的部分。

    • serval/style/normalize.scssnormalize.scss 官网,这里改了后缀只是方便后缀格式相同....

    • serval/style/harusame-code.scss 是目录 serval/style/code/ 下所有需要高亮的语言的样式的 入口,这样便于以后做其他语言的样式扩展。这里先有能力解决 js 的语法高亮再想着其他语言吧。

    • serval/style/harusame-serval.scss 描述了编辑器样式。

    先直接描绘出成型后的编辑器


    对着已有的优秀的编辑器观察,把看到的东西抽象成几部分,再根据这些编写成 DOMs 。
    这里对着 Sublime Text 3 截了一张图 1-2:

    1-2 Sublime Text 3

    挺小也挺简单的一张图,但是包含了想做的编辑器的大部分所需 DOMs,或者称他为组件,这里说下明确当前要做的部分:

    • 行号 ( 6 ~ 15)
    • 光标 ( 第七行最后的白色竖线),与多重光标(7, 9, 15)
    • 选中行提示(比如第七行)
    • 选中内容提示(比如第九行,第 13 ~ 15 行)
      这里插入一点,以下内容会是显而易见的,但是也是编辑器组成中最最重要的,尽量把这些理所当然的也把它梳理出来,毕竟他们也需要 DOM 来显示
    • 行的内容(比如第七行的 state = {
    • 编辑器样式(比如这里深灰色,行号的灰色,非关键词的白色)
      以下是一些肉眼看不到的
    • 行能够接受键盘输入
    • 点击一个位置,光标会自动偏移到该行中,离点击字符最近的位置。
    • 行号无法选中

    至于一些其他的细节:代码高亮,成对的括号提示等,这里会放在以后再做/说。

    于是根据这些梳理好的内容,转换成 DOMs:

    • 编辑器容器 设置编辑器的背景颜色等,也是存放 其他容器的容器(父节点)
    • 光标容器 存放 所有光标 的元素节点
    • 行容器 存放 一行 的元素节点,包括它的行号,以及行的内容
    • 选择容器 存放比如 当前选择行,选择内容 的背景高亮的元素节点
    • 键盘输入事件接收器 正如之前所说(好像说了),一个 div 本身是不能接受键盘事件的,需要一个接受键盘事件的容器,再将事件的处理逻辑与相应的元素节点绑定。

    以上就是需要的 DOMs 了,包含关系也基本没啥问题了:

    文件路径 serval/index
    
    <div id="input-container" class="input-container">
        <!-- 创建一个 serval 专属的区域,且总是铺满 编辑器的容器 -->
        <div class="serval theme-harusame">
            <!-- 包裹区域,总是由 行 的高度所撑开 -->
            <div class="serval-container">
                <!-- 所有 行 的 容器 -->
                <div class="line-container">
                    <div class="line">
                            <div class="line-number-wrap">
                                <span class="line-number">1</span>
                            </div>
                           <!--  在pre中,添加使用等宽字体的 buff,使用等宽字体是为了之后,便于文字宽度的计算 -->
                            <pre class="code-wrap">
                                <!-- 因为一般放的是代码,codeMirror 是用的 span,试试 code -->
                                <code class="code-content">const PI = 3.1415</code>
                            </pre>
                    </div>
                    <div class="line">
                            <div class="line-number-wrap">
                                <span class="line-number">2</span>
                            </div>
                            <pre class="code-wrap">
                                <code class="code-content">console.info('PI', PI)</code>
                            </pre>
                    </div>
                    <div class="line">
                            <div class="line-number-wrap">
                                <span class="line-number">3</span>
                            </div>
                            <pre class="code-wrap">
                                <code class="code-content"></code>
                            </pre>
                    </div>
                </div>
                <!--  接受键盘事件 的容器 的容器 -->
                <div class="inputer-container">
                    <textarea class="inputer"> 
                    <!-- 这里使用一个 看不见的(并不是隐藏哦) textarea 来接受键盘事件 -->
                </div>
                <!--  所有 光标 的容器 -->
                <div class="cursor-container">
                    <i class="fake-cursor blink"></i>
                </div>
                <!-- 选择行 提示 -->
                <div class="selected-container">
                    <div class="selected-line"></div>
                </div>
            </div>
        </div>
    </div>
    
    
    文件路径 serval/style/harusame-serval.css
    这里就直接给了编译后的
    还是挺暴力的,所有都直接嵌套,防止命名冲突
    
    .serval {
      position: relative;
      height: 100%;
    }
    .serval .serval-container {
      height: 100%;
    }
    .serval .serval-container .line-container {
      margin-left: 50px;
      font-size: 0;
    }
    .serval .serval-container .line-container .line .line-number-wrap {
      position: absolute;
      left: 0;
      text-align: right;
    }
    .serval .serval-container .line-container .line .line-number-wrap .line-number {
      display: inline-block;
      padding-right: 15px;
      box-sizing: border-box;
      width: 50px;
      height: 20px;
      line-height: 20px;
      font-size: 12px;
    }
    .serval .serval-container .line-container .line .code-wrap .code-content {
      display: block;
      height: 20px;
      line-height: 20px;
      font-size: 12px;
    }
    .serval .serval-container .cursor-container {
      position: absolute;
      top: 0;
      margin-left: 50px;
    }
    .serval .serval-container .cursor-container .fake-cursor {
      display: block;
      position: absolute;
      left: 0;
      top: 0;
      width: 0;
      height: 18px;
      margin-top: 1px;
    }
    .serval .serval-container .selected-container {
      position: absolute;
      top: 0;
      width: 100%;
    }
    .serval .serval-container .selected-container .selected-line {
      position: absolute;
      left: 0;
      top: 0;
      width: 100%;
      height: 20px;
    }
    .serval .serval-container .inputer-container {
      position: absolute;
      top: 0;
      margin-left: 50px;
    }
    .serval .serval-container .inputer-container .inputer {
      position: absolute;
      left: 0;
      top: 0;
      border: 0;
      resize: none;
      width: 0;
      height: 20px;
      line-height: 20px;
      opacity: 0;
    }
    
    .theme-harusame {
      background-color: rgba(0, 0, 0, 0.8);
    }
    .theme-harusame .serval-container .line-container {
      font-family: Consolas;
    }
    .theme-harusame .serval-container .line-container .line .line-number-wrap .line-number {
      color: white;
      cursor: default;
    }
    .theme-harusame .serval-container .line-container .line .code-wrap {
      cursor: text;
    }
    .theme-harusame .serval-container .line-container .line .code-wrap .code-content {
      font-family: Consolas;
      color: white;
    }
    .theme-harusame .serval-container .cursor-container .fake-cursor {
      border-right: 1px solid rgba(255, 255, 255, 0.9);
    }
    .theme-harusame .serval-container .selected-container .selected-line {
      background-color: rgba(255, 255, 255, 0.15);
    }
    
    

    效果图,见图1-3 ,嗯嗯,不错的感觉。

    1-3 效果图

    HTML 方面应该没有什么问题,就把它们转换成为模板,写入 template.js中,并且删掉 index.html 中刚才写的 HTML标签。

    文件路径 serval/script/harusame-template.js
    
    ;
    (function () {
        Template = {
            /**
             * 编辑器
             */
            editor: function () {
                var $line_container = SatoriDom.compile(e('div', {'class': 'line-container'}))
                var $inputer_container = SatoriDom.compile(e('div', {'class': 'inputer-container'}))
                var $cursor_container = SatoriDom.compile(e('div', {'class': 'cursor-container'}))
                var $selected_container = SatoriDom.compile(e('div', {'class': 'selected-container'}))
    
                var $serval_container = SatoriDom.compile(e('div', {'class': 'serval-container'}, [
                    $inputer_container,
                    $selected_container,
                    $cursor_container,
                    $line_container
                ]))
    
                var $fragment = SatoriDom.compile(
                    e('div', {'class': 'serval theme-harusame'}, [
                        $serval_container
                    ])
                )
    
                return {
                    $editor: $fragment,
                    nodes: {
                        $serval_container: $serval_container,
                        $line_container: $line_container,
                        $inputer_container: $inputer_container,
                        $cursor_container: $cursor_container,
                        $selected_container: $selected_container
                    }
                }
            },
    
            /**
             * 行
             * @param line_number {string} 行号
             * @param initial_content {string} 该行初始内容
             */
            line: function (params) {
                console.info('params', params)
                return SatoriDom.compile(
                    e('div', {'class': 'line'}, [
                        e('div', {'class': 'line-number-wrap'}, [
                            e('span', {'class': 'line-number'}, params.line_number)
                        ]),
                        e('div', {'class': 'code-wrap'}, [
                            e('code', {'class': 'code-content'}, params.initial_content || '')
                        ])
                    ])
                )
            },
    
            /**
             * 当前选择行
             */
            selectedLine: function () {
                return SatoriDom.compile(
                    e('div', {'class': 'selected-line'})
                )
            },
    
            /**
             * 光标
             */
            cursor: function () {
                return SatoriDom.compile(
                    e('i', {'class': 'fake-cursor blink'})
                )
            },
    
            /**
             * 键盘事件接收器
             */
            inputer: function () {
                return SatoriDom.compile(
                    e('textarea', {'class': 'inputer'})
                )
            }
        }
    
        window.Template = Template
    })()
    
    文件路径 serval/script/cursor.js
    
    ;
    (function () {
        var Cursor = function () {
            this.$ref = null
    
            this._generateCursor()
        }
    
        Cursor.prototype = {
            constructor: Cursor,
    
            /**
             * 创建一个游标对象
             */
            _generateCursor: function () {
                this.$ref = SatoriDom.compile(
                    e('i', {'class': 'fake-cursor'})
                )
            }
        }
    
        window.Cursor = Cursor
    })()
    
    文件路径 serval/script/serval.js
    
    ;
    (function () {
        /**
         * 1. 存放所有的存在游标
         */
        var Serval = function (config) {
            /* 1 */
            this.cursor_list = []
    
            this._generateEditor(config)
            this._generateInputer()
            this._generateCursor()
            this._generateSelectedLine()
    
            this._generateLine() // 测试用!!!
        }
    
        Serval.prototype = {
            constructor: Serval,
    
            /**
             * 生成编辑器的主要 DOM结构
             */
            _generateEditor: function (config) {
                var temp = Template.editor()
                var nodes = temp.nodes
    
                this.$serval_container = nodes.$serval_container
                this.$line_container = nodes.$line_container
                this.$inputer_container = nodes.$inputer_container
                this.$cursor_container = nodes.$cursor_container
                this.$selected_container = nodes.$selected_container
    
                config['editor-container'].appendChild(temp.$editor)
            },
    
            /**
             * 生成键盘事件接收器,并渲染
             */
            _generateInputer: function () {
                var $inputer = Template.inputer()
    
                this.$inputer = $inputer
                this.$inputer_container.appendChild($inputer)
            },
    
            /**
             * 生成一个光标,并渲染
             * 1. 创建 cursor 实例
             * 2. 持久化该 cursor 实例
             * 3. 得到该 cursor 实例的元素节点,渲染到 光标容器 中
             */
            _generateCursor: function () {
                var cursor = new Cursor() /* 1 */
    
                this.cursor_list.push(cursor) /* 1 */
    
                this.$cursor_container.appendChild(cursor.$ref) /* 3 */
            },
    
            /**
             * 生成一行,并渲染
             */
            _generateLine: function () {
                var $line = Template.line({line_number: '1', initial_content: '初始化内容'})
    
                this.$line_container.appendChild($line)
            },
    
            /**
             * 生成当前选择行的背景颜色提示,并渲染
             */
            _generateSelectedLine: function () {
                var $selected_line = Template.selectedLine()
    
                this.$selected_line = $selected_line
                this.$selected_container.appendChild($selected_line)
            }
        }
    
        window.Serval = Serval
    })()
    
    

    效果图,见图1-4

    1-4 效果图

    嗯嗯,好像没什么毛病...
    随手框选了一下,发现,这几个字没法选中,有点不符合预期,在之后做选取内容时,肯定会坑,所以立马填了。
    额...因为在当前层叠上下文中,选择行<.selected_container> 的层叠样式最大(他在最下面) 并且 他有position: absolute 的属性,导致覆盖了<.line_container>,所以

    1. template.js 中调整一下位置,令他们的覆盖情况更合逻辑。
    2. <.line_container> 添加一个 position: relative 属性
    3. 另外2的副作用就是,由于 <.line_container>具备了定位属性,行的 left: 0,得手工设置为left: -50px了,不然布局会乱哦

    做这些理论上就可以了,当然如果懒得改的话,设置z-indexpointer-events之类的也可以,这里就不记了:

    文件路径 serval/script/harusame-template.js
    改成这样 ↓
    
    var $fragment = SatoriDom.compile(
        e('div', {'class': 'serval theme-harusame'}, [
            e('div', {'class': 'serval-container'}, [
                $inputer_container,
                $selected_container,
                $cursor_container,
                $line_container
            ])
        ])
    )
    
    文件路径 serval/style/harusame-serval.scss
    
    .line-container {
        // 添加一条
        position: relative;
    }
    
    .line-number-wrap {
        // 添加一条
        left: -50px;
        // 这里的 50px 取决于 .line-number 的 width 属性
    }
    

    光标


    先说说光标的逻辑。
    可以在 #0 节的例子中了解到

    // ...
    ... substring(0, logicalX) ...
    // ...
    

    编辑器实际上通过 字符的位置 来添加,删除文本/代码,这里所说的字符的位置,我自作主张把它称为 逻辑位置,对应到 Cursor实例 中,给他命名为 logical,相应地,也有 物理位置(视图位置),对应到 Cursor实例 的DOM 中,给他命名为 psysical

    logicalY 决定了光标在编辑器中的 行号
    psysicalY 决定了光标 DOM 的 top
    logicalX 决定了光标在编辑器中的 列号,或者说 第 logicalX 个字符
    psysicalX 决定了光标 DOM 的 left

    除了这些,光标还需要一个保存选区的属性,这里命名为selection

    selection 是一个对象,它包含一个起始点坐标start,与一个终点坐标end

    举个栗子,对于图 1-5:

    1-5

    为了计数方便,删除了部分缩进,以及这里的缩进都是 4个空格。
    有以下光标:

    • 第三行的光标 cursor_1
    • 第四行的光标 cursor_2
    • 第五行的光标 cursor_3
    • 第六行的光标 cursor_4

    它们分别有以下属性:
    (如果看懂了文字说明,以下代码就随便看看)

    cursor_1 = {
        logicalY: 2, /* 从零开始计算 */
        psysicalY: 40, /* 约定一行的高度为 20px */
    
        logicalX: 2, /* 从零开始计算 */
        psysicalX: 14, 
        /* 字符'l',字符'e'的宽度,通过标尺类工具可以得到一个字符(英文)的宽度约等于 7px
         * 手工写死的字符宽度面对不同的情况或者说样式上变化,会比较难维护
         * 接下来马上会说怎么使用浏览器的计算,虽然也不怎么优雅......_(:3」∠)...
         * /
    
        selection: null
    }
    
    cursor_2 = {
        logicalY: 3, 
        psysicalY: 60, 
    
        logicalX: 0, 
        psysicalX: 0, 
    
        selection: null
    }
    
    cursor_3 = {
        logicalY: 4, 
        psysicalY: 80, 
    
        logicalX: 5, 
        psysicalX: 35, // 5 * 7
    
        selection = {
            start: {
                logicalY: 4,
                psysicalY: 80,
                logicalX: 0,
                psysicalX: 0
            },
    
           /* end: {}
            * 写到这里的时候发现,end 的位置总是与当前位置相同,于是决定不要这个冗余的部分,
            * 虽然与 start 相对应的 end 缺失了,可能会 '引起不适',
            * 但是暂时觉得没必要多出一份冗余数据
            * 所以这里改变以下 selection的写法 ↓,把 selection 删了,只留下 selection_start
            */
        }
    
        selection_start = {
            logicalY: 4,
            psysicalY: 80,
            logicalX: 0,
            psysicalX: 0
        }
    }
    
    cursor_4 = {
        logicalY: 5, 
        psysicalY: 100,
        logicalX: 4,
        /*
         * 刚好四个空格,观察仔细会发现,图1-5 的距离不会是 4格,
         * 这里因为偷懒用了 tab进行缩进,导致如此。
         * 不要在意细节..._(:3」∠)...啊,不对,编程还是得很注意细节的
         */
    
        psysicalX: 28, /* 空格也约为 7px */
    
        selection_start = {
            logicalY: 5,
            psysicalY: 100,
            logicalX: 4,
            psysicalX: 28
        }
    }
    
    

    如果看完了甚至没看上面的代码,都可以 显然 得到:

    psysicalY = line_height * logicalY
    logicalY = psysicalY / line_height
    $current_line = document.querySelector(SIGN_LINE + logicalY)

    psysicalX = single_byte_length * 7 + double_byte_length * 12
    logicalX = single_byte_length + double_byte_length

    • 这里的 line_height,不是指 css 中的 line-height 哦,是指 该行的高度

    • 这里的SIGN_LINE,是一个自己约定的名字,这里倾向于使用id,而不是class,个人觉得理由如下:

      1.每行都是唯一的
      2.性能稍微高一点
      3.可以利用锚点方便地跳转到该行位置

    比如 github 中的代码编辑器(也可以看 codeMirror

    github中的代码编辑器
    • single_byte_length 这个变量名字译为单字节字符的长度(数量),同理double_byte_length 变量名字译为双字节字符的长度(数量)。
      (这个名字我不清楚对不对,之后再查阅下文献。

    为什么这样分呢?
    因为在计算光标的psysicalX时,需要计算字符的 宽度,所以根据宽度来分。
    font-size: 12px 的前提下,
    单字节字符的宽度,比如数字,英文字符,半角标点符号
    a b c 1 2 3 . , /
    等等 都约为 7px

    双字节字符的宽度,比如汉字,日语,全角标点符号
    你 好 啊 こ は お ア 。,、
    等等 都约为 12px

    说明就到这里。
    之所以把这些类似公式一样的东西列出来,就是想说它们之间总是一一对应,改变其中一个,另外一个也相应地改变。这个情况,是不是有点类似于双向绑定的概念呢。提到双向绑定,在如今,前端大佬们有个比较好的处理方法就是使用Object.defineProperty哦。

    接下来终于可以开始写代码。

    计算光标位置

    首先 先去serval/script/harusame-serval.js 中提供事件

    文件位置 serval/script/harusame-serval.js
    为了能显示更多的有效内容,先把写过的代码在这里删掉了。
    
    ;
    (function () {
        var Serval = function (config) {
            this._bindMouseEvent()
        }
    
        Serval.prototype = {
            /**
             * 绑定各种鼠标事件
             */
            _bindMouseEvent: function () {
                var self = this
    
                /**
                 * addEventListener 是指自己写的方法,见最下面
                 * mousedown 时,就对光标位置进行计算
                 */
                addEventListener(self.$serval_container, 'mousedown', function (event) {
                    console.log(event) // 先看看 event 中有什么好用的属性。
                })
            },
        }
    
        /**
         * 可能会对 addEventListener 进行一些兼容处理
         * 实际上并没有处理,不过也好,至少留一条后路...
         */
        function addEventListener (v_el, v_type, v_callback) {
            v_el.addEventListener(v_type, v_callback)
        }
    
        window.Serval = Serval
    })()
    

    先看看 event 中有什么好用的属性。
    这里说一下由于一些事件在不同的浏览器行为是不同的,如果要兼容某个版本的浏览器,所以最好调试的时候看看预先各个浏览器的事件行为。这里在 Firefox 以及 Google 是没什么差异的,所以就不截图了。

    onmousedown 下的 event

    这里可以看到有二个 layer 前缀的属性,它总是能得到 相对于 event.target 元素的 点击位置。这个特性就很好用!

    于是决定使用 event.layerYevent.layerX 来进行光标位置的计算!
    这里要插一句_(:3」∠)...,在 demo 中 Y的位置 是使用 event.target.id.split[SIGN][1]来获得的,但是在以后的开发中,记得会出现一些麻烦的判断,所以这里用 layerY 尝试一下。嘛,各有利弊,layerY的话,也可能得考虑padding margin height line-heightcss造成的计算偏差。

    位置: serval/script/harusame-serval.js
    为了突出主要说明部分,删掉了其他代码
    
    ;
    (function () {
        var Serval = function (config) {
            this._bindMouseEvent()
        }
    
        Serval.prototype = {
            constructor: Serval,
    
            /**
             * 绑定各种鼠标事件
             */
            _bindMouseEvent: function () {
                var self = this
    
                /**
                 * addEventListener 是指自己写的方法,见最下面
                 * 当 mousedown 时,就对光标位置进行计算
                 */
                addEventListener(self.$serval_container, 'mousedown', function (event) {
                    self.allocTask(function (v_cursor) {
                        v_cursor.psysicalY = event.layerY
                        v_cursor.psysicalX = event.layerX
                    })
                })
            },
    
            /**
             * 为所有光标分配任务
             * 对光标操作时,统一通过这个接口
             * 这里约定必须传入一个回调函数,所以不使用 v_task && v_task() 进行判断
             */
            allocTask: function (v_task) {
                var self = this
    
                for (var i = 0, len = self.cursor_list.length; i < len; i++) {
                    v_task(self.cursor_list[i])
                }
            },
        }
    })()
    
    

    做好了入口,接下来去 serval/script/harusame-cursor.js 写逻辑哦。
    先写logicalYpsysicalY部分

    文件位置 serval/script/harusame-cursor.js
    
    ;
    (function () {
        /**
         * 这里先约(写)定(死) 行高
         */
        var LINE_HEIGHT = 20
    
        /**
         * 1. 光标本身的元素节点
         * 2. 光标所在行的元素节点
         */
        var Cursor = function (config) {
            this.$ref = null /* 1 */
            this.$line = null /* 2 */
            this._logicalY = 0
            this._logicalX = 0
            this._psysicalY = 0
            this._psysicalX = 0
    
            this.selection_start = null
    
            this._generateCursor()
            this._setObserver()
        }
    
        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
                    }
                })
            },
    
            setY: function (v_psysicalY) {
                this.$ref.style.top = v_psysicalY + 'px'
            },
    
            /**
             * 计算 物理 Y
             */
            calcPsysicalY: function (v_logicalY) {
                return v_logicalY * LINE_HEIGHT
            },
    
            /**
             * 计算 逻辑 Y
             */
            calcLogicalY: function (v_psysicalY) {
                return parseInt(v_psysicalY / LINE_HEIGHT)
            }
        }
    
        window.Cursor = Cursor
    })()
    
    

    这里可以看看效果,截图软件截不到鼠标_(:3」∠)...。

    但是也注意到了设计上的问题:在self.logicalY 中,需要用到 $line,这个东西不应该分在Cursor中,毕竟他不属于光标,以及LINE_HEIGHT 属性,也不属于。甚至在接下来的 self.logicalX 中,也需要用到 $line,这个时候,不如把与 有关的,再单独抽象成一个类会比较合适。

    休息会

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

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

    相关文章

      网友评论

        本文标题:#1 从零开始制作在线 代码编辑器

        本文链接:https://www.haomeiwen.com/subject/tcnhqxtx.html