wzp
2021-07-19 58ec6ffd2dc6a3e490e28026dd559352678a273d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
/*
    编辑器构造函数
*/
 
import $ from '../util/dom-core.js'
import _config from '../config.js'
import Menus from '../menus/index.js'
import Text from '../text/index.js'
import Command from '../command/index.js'
import selectionAPI from '../selection/index.js'
import UploadImg from './upload/upload-img.js'
import { arrForEach, objForEach } from '../util/util.js'
import { getRandom } from '../util/util.js'
 
// id,累加
let editorId = 1
 
// 构造函数
function Editor(toolbarSelector, textSelector) {
    if (toolbarSelector == null) {
        // 没有传入任何参数,报错
        throw new Error('错误:初始化编辑器时候未传入任何参数,请查阅文档')
    }
    // id,用以区分单个页面不同的编辑器对象
    this.id = 'wangEditor-' + editorId++
 
    this.toolbarSelector = toolbarSelector
    this.textSelector = textSelector
 
    // 自定义配置
    this.customConfig = {}
}
 
// 修改原型
Editor.prototype = {
    constructor: Editor,
 
    // 初始化配置
    _initConfig: function () {
        // _config 是默认配置,this.customConfig 是用户自定义配置,将它们 merge 之后再赋值
        let target = {}
        this.config = Object.assign(target, _config, this.customConfig)
 
        // 将语言配置,生成正则表达式
        const langConfig = this.config.lang || {}
        const langArgs = []
        objForEach(langConfig, (key, val) => {
            // key 即需要生成正则表达式的规则,如“插入链接”
            // val 即需要被替换成的语言,如“insert link”
            langArgs.push({
                reg: new RegExp(key, 'img'),
                val: val
 
            })
        })
        this.config.langArgs = langArgs
    },
 
    // 初始化 DOM
    _initDom: function () {
        const toolbarSelector = this.toolbarSelector
        const $toolbarSelector = $(toolbarSelector)
        const textSelector = this.textSelector
 
        const config = this.config
        const zIndex = config.zIndex
 
        // 定义变量
        let $toolbarElem, $textContainerElem, $textElem, $children
 
        if (textSelector == null) {
            // 只传入一个参数,即是容器的选择器或元素,toolbar 和 text 的元素自行创建
            $toolbarElem = $('<div></div>')
            $textContainerElem = $('<div></div>')
 
            // 将编辑器区域原有的内容,暂存起来
            $children = $toolbarSelector.children()
 
            // 添加到 DOM 结构中
            $toolbarSelector.append($toolbarElem).append($textContainerElem)
 
            // 自行创建的,需要配置默认的样式
            $toolbarElem.css('background-color', '#f1f1f1')
                            .css('border', '1px solid #ccc')
            $textContainerElem.css('border', '1px solid #ccc')
                            .css('border-top', 'none')
                            .css('height', '300px')
        } else {
            // toolbar 和 text 的选择器都有值,记录属性
            $toolbarElem = $toolbarSelector
            $textContainerElem = $(textSelector)
            // 将编辑器区域原有的内容,暂存起来
            $children = $textContainerElem.children()
        }
 
        // 编辑区域
        $textElem = $('<div></div>')
        $textElem.attr('contenteditable', 'true')
                .css('width', '100%')
                .css('height', '100%')
 
        // 初始化编辑区域内容
        if ($children && $children.length) {
            $textElem.append($children)
        } else {
            $textElem.append($('<p><br></p>'))
        }
 
        // 编辑区域加入DOM
        $textContainerElem.append($textElem)
 
        // 设置通用的 class
        $toolbarElem.addClass('w-e-toolbar')
        $textContainerElem.addClass('w-e-text-container')
        $textContainerElem.css('z-index', zIndex)
        $textElem.addClass('w-e-text')
 
        // 添加 ID
        const toolbarElemId = getRandom('toolbar-elem')
        $toolbarElem.attr('id', toolbarElemId)
        const textElemId = getRandom('text-elem')
        $textElem.attr('id', textElemId)
 
        // 记录属性
        this.$toolbarElem = $toolbarElem
        this.$textContainerElem = $textContainerElem
        this.$textElem = $textElem
        this.toolbarElemId = toolbarElemId
        this.textElemId = textElemId
 
        // 记录输入法的开始和结束
        let compositionEnd = true
        $textContainerElem.on('compositionstart', () => {
            // 输入法开始输入
            compositionEnd = false
        })
        $textContainerElem.on('compositionend', () => {
            // 输入法结束输入
            compositionEnd = true
        })
 
        // 绑定 onchange
        $textContainerElem.on('click keyup', () => {
            // 输入法结束才出发 onchange
            compositionEnd && this.change &&  this.change()
        })
        $toolbarElem.on('click', function () {
            this.change &&  this.change()
        })
 
        //绑定 onfocus 与 onblur 事件
        if(config.onfocus || config.onblur){
            // 当前编辑器是否是焦点状态
            this.isFocus = false
            
            $(document).on('click', (e) => {
                //判断当前点击元素是否在编辑器内
                const isChild = $textElem.isContain($(e.target))
                
                //判断当前点击元素是否为工具栏
                const isToolbar = $toolbarElem.isContain($(e.target))
                const isMenu = $toolbarElem[0] == e.target ? true : false
 
                if (!isChild) {
                    //若为选择工具栏中的功能,则不视为成blur操作
                    if(isToolbar && !isMenu){
                        return
                    }
 
                    if(this.isFocus){
                        this.onblur && this.onblur()
                    }
                    this.isFocus = false
                }else{
                    if(!this.isFocus){
                        this.onfocus && this.onfocus()
                    }
                    this.isFocus = true
                }
            })
        }
 
    },
 
    // 封装 command
    _initCommand: function () {
        this.cmd = new Command(this)
    },
 
    // 封装 selection range API
    _initSelectionAPI: function () {
        this.selection = new selectionAPI(this)
    },
 
    // 添加图片上传
    _initUploadImg: function () {
        this.uploadImg = new UploadImg(this)
    },
 
    // 初始化菜单
    _initMenus: function () {
        this.menus = new Menus(this)
        this.menus.init()
    },
 
    // 添加 text 区域
    _initText: function () {
        this.txt = new Text(this)
        this.txt.init()
    },
 
    // 初始化选区,将光标定位到内容尾部
    initSelection: function (newLine) {
        const $textElem = this.$textElem
        const $children = $textElem.children()
        if (!$children.length) {
            // 如果编辑器区域无内容,添加一个空行,重新设置选区
            $textElem.append($('<p><br></p>'))
            this.initSelection()
            return
        }
 
        const $last = $children.last()
 
        if (newLine) {
            // 新增一个空行
            const html = $last.html().toLowerCase()
            const nodeName = $last.getNodeName()
            if ((html !== '<br>' && html !== '<br\/>') || nodeName !== 'P') {
                // 最后一个元素不是 <p><br></p>,添加一个空行,重新设置选区
                $textElem.append($('<p><br></p>'))
                this.initSelection()
                return
            }
        }
 
        this.selection.createRangeByElem($last, false, true)
        this.selection.restoreSelection()
    },
 
    // 绑定事件
    _bindEvent: function () {
        // -------- 绑定 onchange 事件 --------
        let onChangeTimeoutId = 0
        let beforeChangeHtml = this.txt.html()
        const config = this.config
 
        // onchange 触发延迟时间
        let onchangeTimeout = config.onchangeTimeout
        onchangeTimeout = parseInt(onchangeTimeout, 10)
        if (!onchangeTimeout || onchangeTimeout <= 0) {
            onchangeTimeout = 200
        }
 
        const onchange = config.onchange
        if (onchange && typeof onchange === 'function'){
            // 触发 change 的有三个场景:
            // 1. $textContainerElem.on('click keyup')
            // 2. $toolbarElem.on('click')
            // 3. editor.cmd.do()
            this.change = function () {
                // 判断是否有变化
                let currentHtml = this.txt.html()
 
                if (currentHtml.length === beforeChangeHtml.length) {
                    // 需要比较每一个字符
                    if (currentHtml === beforeChangeHtml) {
                        return
                    }
                }
 
                // 执行,使用节流
                if (onChangeTimeoutId) {
                    clearTimeout(onChangeTimeoutId)
                }
                onChangeTimeoutId = setTimeout(() => {
                    // 触发配置的 onchange 函数
                    onchange(currentHtml)
                    beforeChangeHtml = currentHtml
                }, onchangeTimeout)
            }   
        }
 
        // -------- 绑定 onblur 事件 --------
        const onblur = config.onblur
        if (onblur && typeof onblur === 'function') {
            this.onblur = function () {
                const currentHtml = this.txt.html()
                onblur(currentHtml)
            }
        }
 
        // -------- 绑定 onfocus 事件 --------
        const onfocus = config.onfocus
        if (onfocus && typeof onfocus === 'function') {
            this.onfocus = function () {
                onfocus()
            }
        }
        
    },
 
    // 创建编辑器
    create: function () {
        // 初始化配置信息
        this._initConfig()
 
        // 初始化 DOM
        this._initDom()
 
        // 封装 command API
        this._initCommand()
 
        // 封装 selection range API
        this._initSelectionAPI()
 
        // 添加 text
        this._initText()
 
        // 初始化菜单
        this._initMenus()
 
        // 添加 图片上传
        this._initUploadImg()
 
        // 初始化选区,将光标定位到内容尾部
        this.initSelection(true)
 
        // 绑定事件
        this._bindEvent()
    },
 
    // 解绑所有事件(暂时不对外开放)
    _offAllEvent: function () {
        $.offAll()
    }
}
 
export default Editor