-
Notifications
You must be signed in to change notification settings - Fork 25
/
Copy pathteditor.js
424 lines (405 loc) · 17.1 KB
/
teditor.js
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
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
/**
* TEditor (Tencent HTML5 Rich Editor)
* Copyright (c) 2012, webpluz.org, All rights reserved.
* https://github.com/qwt/TEditor
*
* @version 1.0
* @author Azrael(<a href="mailto:[email protected]">[email protected]</a>)
*
*/
/**
* @description
* HTML5 富文本编辑器
* 基于Jx框架
*
*/
;Jx().$package('TE', function(J){
var CURSOR_PLACEHOLDER_TYPE = 'IMG';
var CURSOR_PLACEHOLDER_STYLE = 'width: 1px; height: 1px;';
var INIT_PLACEHOLDER_TYPE = 'BR';
var LINE_NODE_TYPE = 'DIV';
var WORD_NODE_TYPE = 'SPAN';
/**
* TEditor 类定义
*/
var TEditor = new J.Class({
init: function(option) {
option = option || {};
var container = option.container;
if(!container){
throw new Error('must assign a container!');
}
if(option.toolbar){
this.createToolbar(option.toolbar);
}
this.createDom(container);
this.initEvents();
this.setEditable(true);
this.newline();
},
initEvents: function(){
//dom event
J.event.on(this.body, 'keydown', J.bind(observer.onKeyDown, this))
//custom event
J.event.addObserver(this, 'delete', J.bind(observer.onDelete, this));
},
createToolbar: function(option){
//TODO
},
createDom: function(container){
//create dom
var panel = J.dom.node('div', {
'class': 'teditor-container'
});
container.appendChild(panel);
var iframe = J.dom.node('iframe', {
src: 'about:blank',
frameBorder: 0,
'class': 'teditor-iframe'
});
panel.appendChild(iframe);
J.dom.addClass(textarea, 'teditor-textarea');
var textarea = J.dom.node('textarea', {
'class': 'teditor-textarea'
});
panel.appendChild(textarea);
this.iframe = iframe;
this.textarea = textarea;
this.win = TE.util.getWindow(iframe);
this.doc = this.win.document;
this.body = this.doc.body;
},
createCursorPlaceholder: function(){
var node = document.createElement(CURSOR_PLACEHOLDER_TYPE);
node.style.cssText = CURSOR_PLACEHOLDER_STYLE;
node.setAttribute('tcursor', 'tcursor');
return node;
},
createLineNode: function(cssText){
var node = document.createElement(LINE_NODE_TYPE);
node.setAttribute('tline', 'tline');
node.style.cssText = cssText || '';
return node;
},
createWordNode: function(cssText){
var node = document.createElement(WORD_NODE_TYPE);
node.setAttribute('tword', 'tword');
node.style.cssText = cssText || '';
return node;
},
createCursorNode: function(){
var node = document.createElement(INIT_PLACEHOLDER_TYPE);
node.setAttribute('tcursor', 'tcursor');
return node;
},
handleEmptyLine: function(lineNode, range){
//检查这一行是否是空了, 空的话要插入 一个 文字容器
if(!lineNode.childElementCount){
range.selectNodeContents(lineNode);
var word = this.createWordNode();
var br = this.createCursorNode();
word.appendChild(br);
range.insertNode(word);
range.selectNode(word);
}
},
getLineNode: function(node){
while(node && node !== this.body){
if(node.tagName === LINE_NODE_TYPE){
return node;
}else{
node = node.parentNode;
}
}
return null;
},
getWordNode: function(node){
while(node && node !== this.body){
if(node.tagName === WORD_NODE_TYPE){
return node;
}else{
node = node.parentNode;
}
}
return null;
},
getSelection: function(){
return TE.util.getSelection(this.win);
},
getRange: function(){
this.focus();
return TE.util.getRange(this.win, this.body);
},
restoreRange: function(range){
var selection = this.getSelection();
selection.removeAllRanges();
selection.addRange(range);
},
// 一堆 range 的判断处理
isRangeAtWordStart: function(range){
var word = this.getWordNode(range.startContainer);
return word && range.startOffset === 0;
},
isRangeAtWordEnd: function(range){
var word = this.getWordNode(range.endContainer);
return word && range.endOffset === range.endContainer.length;
},
isRangeAtWholeWord: function(range){
return this.isRangeAtWordStart(range) && this.isRangeAtWordEnd(range);
},
isRangeAtLineStart: function(range){
var line = this.getLineNode(range.startContainer);
return line && range.startOffset === 0;
},
isRangeAtLineEnd: function(range){
var line = this.getLineNode(range.endContainer);
//判断行尾的节点都是已经选中 word 节点的, 所以要用 childElementCount
return line && range.endOffset === line.childElementCount;
},
isRangeAtWholeLine: function(range){
return this.isRangeAtLineStart(range) && this.isRangeAtLineEnd(range);
},
//=================== 对外接口 =====================================
setEditable: function(status){
// if(status){
// this.doc.designMode='on';
// }else{
// this.doc.designMode='off';
// }
if(status){
this.body.contentEditable = true;
}else{
this.body.contentEditable = false;
}
},
focus: function(){
this.body.focus();
},
blur: function(){
this.body.blur();
},
newline: function(){
var range = this.getRange();
var line = this.createLineNode();
var word = this.createWordNode();
var br = this.createCursorNode();
word.appendChild(br);
line.appendChild(word);
range.insertNode(line);
range.selectNode(word);
this.restoreRange(range);
},
clear: function(){
this.body.innerHTML = '';
this.newline();
},
setStyle: function(prop, value){
var range = this.getRange();
console.log(range);
//TODO 插入后的样式是否要合并?
//range是否在同一个节点上?
if(range.startContainer === range.endContainer){
//在同一个节点, 继续判断是否已经被包含了 span ?
var rangeParent = range.commonAncestorContainer;
if(rangeParent.tagName === WORD_NODE_TYPE){
//如果 本身 range就包含了整一个span(包括span本身), 那直接改样式就行了
if(J.dom.getStyle(rangeParent, prop) !== value){
J.dom.setStyle(rangeParent, prop, value);
}
return;
}
rangeParent = rangeParent.parentNode;
//光标重合的处理?
if(rangeParent.tagName === WORD_NODE_TYPE){
//已经被span包含了
//是否已经有这个样式?
if(J.dom.getStyle(rangeParent, prop) === value){
//是也, 返回
return;
}
//是否整个span都在range里面?
if(range.startOffset === 0 && range.endOffset === range.endContainer.length){
//是的整个整个range都在span里, 直接设置parentNode的样式吧
J.dom.setStyle(rangeParent, prop, value);
}else{
//悲剧, range只是span其中一部分, 拆成三段
var oldStyle = rangeParent.style.cssText;
var span = this.createWordNode(oldStyle);
var frag = document.createDocumentFragment();
var tempNode;
var holder = null;
var beforeText = range.startContainer.textContent.substr(0, range.startOffset);
var afterText = range.endContainer.textContent.substr(range.endOffset);
var rangeText = range.toString();
if(beforeText){
tempNode = span.cloneNode(true);
tempNode.innerHTML = beforeText;
frag.appendChild(tempNode);
}
frag.appendChild(span);
if(afterText){
tempNode = span.cloneNode(true);
tempNode.innerHTML = afterText;
frag.appendChild(tempNode);
}
J.dom.setStyle(span, prop, value);
if(rangeText){
span.innerHTML = rangeText;
}else{
//内容为空的时候要插入一个光标占位符
holder = this.createCursorPlaceholder();
span.appendChild(holder);
}
//把需要删除的选中后 delete
range.selectNode(rangeParent);
range.deleteContents();
range.insertNode(frag);
range.selectNode(holder || span);
if(holder){
range.collapse();
this.restoreRange(range);
this.focus();
}else{
this.restoreRange(range);
}
}
}else{//没有包含span, 直接加一个
var span = this.createWordNode(prop + ': ' + value);
range.surroundContents(span);
range.selectNode(span);
this.restoreRange(range);
}
}else{//=================================================================
//跨了几个节点的range
//防止调用 deleteContents 删除之后出现空标签
if(range.startOffset === 0 && range.startContainer.parentNode.tagName === WORD_NODE_TYPE){
//range的开始处于节点的开始处, 整个选中它吧, 父亲不是span就别捣乱
range.setStartBefore(range.startContainer.parentNode);
}
if(range.endOffset === range.endContainer.length && range.endContainer.parentNode.tagName === WORD_NODE_TYPE){
//结束于节点末尾, 也选中他
range.setEndAfter(range.endContainer.parentNode);
}
//clone一份选中节点, cloneContents 方法会自动闭合选中的标签
//TODO 跨多行的处理
var frag = range.cloneContents();
var retFrag = document.createDocumentFragment();
var child, span, firstChild, lastChild;
while(child = frag.childNodes[0]){
if(child.nodeType === 3){//文本节点
if(child.textContent){
span = this.createWordNode(prop + ': ' + value);
span.innerHTML = child.textContent;
retFrag.appendChild(span);
}
frag.removeChild(child);
}else if(child.nodeType === 1){//element
if(child.textContent){
if(J.dom.getStyle(child, prop) !== value){
//已经有这个样式就别搞了嘛
J.dom.setStyle(child, prop, value);
}
retFrag.appendChild(child);
}else{//这个节点没有文本内容, 浪费表情 <_<
frag.removeChild(child);
}
}
}
//deleteContents 会删除选中内容后, 自动闭合周边的标签
range.deleteContents();
range.insertNode(retFrag);
this.restoreRange(range);
}
}//end of setStyle
});
/**
* 观察者方法
* @type {Object}
*/
var observer = {
//dom event
onKeyDown: function(e){
var keyCode = Number(e.keyCode);
var altKey = e.altKey, ctrlKey = e.ctrlKey, shiftKey = e.shiftKey;
if(keyCode === 8 && !altKey){
//TODO 还有delete 键呢?
J.event.notifyObservers(this, 'delete', e);
}else if(keyCode === 13 && !altKey){
J.event.notifyObservers(this, 'enter', e);
}
},
//custom event
onDelete: function(e){
var altKey = e.altKey, ctrlKey = e.ctrlKey, shiftKey = e.shiftKey;
var range = this.getRange();
if(ctrlKey && range.collapsed){//删除到行首
//光标是重合的时候, 按 ctrl + delete 会删除到行首
}else{
var wordNode = this.getWordNode(range.commonAncestorContainer);
if(range.startContainer === range.endContainer && wordNode){
//在同一个word节点里
//有span包含
if((range.startOffset === 1 && range.endOffset === 1 && range.endContainer.length === 1) ||
//出现光标的情况, 光标在第一个字符之后, 切只有一个字符了 eg: <span>a[]</span>
this.isRangeAtWholeWord(range)
//整块选中了
){
e.preventDefault();
range.setEndBefore(wordNode);
range.setStartBefore(wordNode);
var lineNode = this.getLineNode(wordNode);
wordNode.parentNode.removeChild(wordNode);
if(lineNode){
this.handleEmptyLine(lineNode, range);
}
this.restoreRange(range);
}
}else{//range 包含多个节点, 属于选中范围的情况, 直接delete了
e.preventDefault();
var lineNode = this.getLineNode(range.commonAncestorContainer);
var relateParent, relateOffset;
if(this.isRangeAtWordStart(range)){
wordNode = this.getWordNode(range.startContainer);
range.setStartBefore(wordNode);
}
if(this.isRangeAtWordEnd(range)){
wordNode = this.getWordNode(range.endContainer);
range.setEndAfter(wordNode);
}
if(!lineNode){//多行的问题
//没有行节点, 说明选中的是跨行的
if(this.isRangeAtLineEnd(range)){
//最后一行是选择到行末的
lineNode = this.getLineNode(range.endContainer);
range.setEndAfter(lineNode);
lineNode = this.getLineNode(range.startContainer);
}else{
//没有选到行末, 比较悲催
relateParent = range.startContainer;
relateOffset = range.startOffset;
}
}
range.deleteContents();
if(relateParent){
//把光标放到 选区之前
range.setStart(relateParent, relateOffset);
range.collapse(true);
}
if(lineNode){
this.handleEmptyLine(lineNode, range);
}
this.restoreRange(range);
}//end of if(wordNode) else
}
}
}
/**
* 入口函数
* @param {Object} option
* @return {TEditor} instance of TEditor
*/
this.create = function(option) {
//TODO any other actions
return new TEditor(option);
}
});