]> git.wh0rd.org - tt-rss.git/blob - lib/dijit/_editor/plugins/EnterKeyHandling.js.uncompressed.js
modify dojo rebuild script to remove uncompressed files
[tt-rss.git] / lib / dijit / _editor / plugins / EnterKeyHandling.js.uncompressed.js
1 define("dijit/_editor/plugins/EnterKeyHandling", [
2 "dojo/_base/declare", // declare
3 "dojo/dom-construct", // domConstruct.destroy domConstruct.place
4 "dojo/_base/event", // event.stop
5 "dojo/keys", // keys.ENTER
6 "dojo/_base/lang",
7 "dojo/sniff", // has("ie") has("mozilla") has("webkit")
8 "dojo/_base/window", // win.withGlobal
9 "dojo/window", // winUtils.scrollIntoView
10 "../_Plugin",
11 "../RichText",
12 "../range",
13 "../../_base/focus"
14 ], function(declare, domConstruct, event, keys, lang, has, win, winUtils, _Plugin, RichText, rangeapi, baseFocus){
15
16 // module:
17 // dijit/_editor/plugins/EnterKeyHandling
18
19 return declare("dijit._editor.plugins.EnterKeyHandling", _Plugin, {
20 // summary:
21 // This plugin tries to make all browsers behave consistently with regard to
22 // how ENTER behaves in the editor window. It traps the ENTER key and alters
23 // the way DOM is constructed in certain cases to try to commonize the generated
24 // DOM and behaviors across browsers.
25 //
26 // description:
27 // This plugin has three modes:
28 //
29 // - blockNodeForEnter=BR
30 // - blockNodeForEnter=DIV
31 // - blockNodeForEnter=P
32 //
33 // In blockNodeForEnter=P, the ENTER key starts a new
34 // paragraph, and shift-ENTER starts a new line in the current paragraph.
35 // For example, the input:
36 //
37 // | first paragraph <shift-ENTER>
38 // | second line of first paragraph <ENTER>
39 // | second paragraph
40 //
41 // will generate:
42 //
43 // | <p>
44 // | first paragraph
45 // | <br/>
46 // | second line of first paragraph
47 // | </p>
48 // | <p>
49 // | second paragraph
50 // | </p>
51 //
52 // In BR and DIV mode, the ENTER key conceptually goes to a new line in the
53 // current paragraph, and users conceptually create a new paragraph by pressing ENTER twice.
54 // For example, if the user enters text into an editor like this:
55 //
56 // | one <ENTER>
57 // | two <ENTER>
58 // | three <ENTER>
59 // | <ENTER>
60 // | four <ENTER>
61 // | five <ENTER>
62 // | six <ENTER>
63 //
64 // It will appear on the screen as two 'paragraphs' of three lines each. Markupwise, this generates:
65 //
66 // BR:
67 // | one<br/>
68 // | two<br/>
69 // | three<br/>
70 // | <br/>
71 // | four<br/>
72 // | five<br/>
73 // | six<br/>
74 //
75 // DIV:
76 // | <div>one</div>
77 // | <div>two</div>
78 // | <div>three</div>
79 // | <div>&nbsp;</div>
80 // | <div>four</div>
81 // | <div>five</div>
82 // | <div>six</div>
83
84 // blockNodeForEnter: String
85 // This property decides the behavior of Enter key. It can be either P,
86 // DIV, BR, or empty (which means disable this feature). Anything else
87 // will trigger errors. The default is 'BR'
88 //
89 // See class description for more details.
90 blockNodeForEnter: 'BR',
91
92 constructor: function(args){
93 if(args){
94 if("blockNodeForEnter" in args){
95 args.blockNodeForEnter = args.blockNodeForEnter.toUpperCase();
96 }
97 lang.mixin(this,args);
98 }
99 },
100
101 setEditor: function(editor){
102 // Overrides _Plugin.setEditor().
103 if(this.editor === editor){ return; }
104 this.editor = editor;
105 if(this.blockNodeForEnter == 'BR'){
106 // While Moz has a mode tht mostly works, it's still a little different,
107 // So, try to just have a common mode and be consistent. Which means
108 // we need to enable customUndo, if not already enabled.
109 this.editor.customUndo = true;
110 editor.onLoadDeferred.then(lang.hitch(this,function(d){
111 this.connect(editor.document, "onkeypress", function(e){
112 if(e.charOrCode == keys.ENTER){
113 // Just do it manually. The handleEnterKey has a shift mode that
114 // Always acts like <br>, so just use it.
115 var ne = lang.mixin({},e);
116 ne.shiftKey = true;
117 if(!this.handleEnterKey(ne)){
118 event.stop(e);
119 }
120 }
121 });
122 if(has("ie") >= 9){
123 this.connect(editor.document, "onpaste", function(e){
124 setTimeout(dojo.hitch(this, function(){
125 // Use the old range/selection code to kick IE 9 into updating
126 // its range by moving it back, then forward, one 'character'.
127 var r = this.editor.document.selection.createRange();
128 r.move('character',-1);
129 r.select();
130 r.move('character',1);
131 r.select();
132 }),0);
133 });
134 }
135 return d;
136 }));
137 }else if(this.blockNodeForEnter){
138 // add enter key handler
139 // FIXME: need to port to the new event code!!
140 var h = lang.hitch(this,this.handleEnterKey);
141 editor.addKeyHandler(13, 0, 0, h); //enter
142 editor.addKeyHandler(13, 0, 1, h); //shift+enter
143 this.connect(this.editor,'onKeyPressed','onKeyPressed');
144 }
145 },
146 onKeyPressed: function(){
147 // summary:
148 // Handler for keypress events.
149 // tags:
150 // private
151 if(this._checkListLater){
152 if(win.withGlobal(this.editor.window, 'isCollapsed', baseFocus)){
153 var liparent = this.editor._sCall('getAncestorElement', ['LI']);
154 if(!liparent){
155 // circulate the undo detection code by calling RichText::execCommand directly
156 RichText.prototype.execCommand.call(this.editor, 'formatblock',this.blockNodeForEnter);
157 // set the innerHTML of the new block node
158 var block = this.editor._sCall('getAncestorElement', [this.blockNodeForEnter]);
159 if(block){
160 block.innerHTML=this.bogusHtmlContent;
161 if(has("ie") <= 9){
162 // move to the start by moving backwards one char
163 var r = this.editor.document.selection.createRange();
164 r.move('character',-1);
165 r.select();
166 }
167 }else{
168 console.error('onKeyPressed: Cannot find the new block node'); // FIXME
169 }
170 }else{
171 if(has("mozilla")){
172 if(liparent.parentNode.parentNode.nodeName == 'LI'){
173 liparent=liparent.parentNode.parentNode;
174 }
175 }
176 var fc=liparent.firstChild;
177 if(fc && fc.nodeType == 1 && (fc.nodeName == 'UL' || fc.nodeName == 'OL')){
178 liparent.insertBefore(fc.ownerDocument.createTextNode('\xA0'),fc);
179 var newrange = rangeapi.create(this.editor.window);
180 newrange.setStart(liparent.firstChild,0);
181 var selection = rangeapi.getSelection(this.editor.window, true);
182 selection.removeAllRanges();
183 selection.addRange(newrange);
184 }
185 }
186 }
187 this._checkListLater = false;
188 }
189 if(this._pressedEnterInBlock){
190 // the new created is the original current P, so we have previousSibling below
191 if(this._pressedEnterInBlock.previousSibling){
192 this.removeTrailingBr(this._pressedEnterInBlock.previousSibling);
193 }
194 delete this._pressedEnterInBlock;
195 }
196 },
197
198 // bogusHtmlContent: [private] String
199 // HTML to stick into a new empty block
200 bogusHtmlContent: '&#160;', // &nbsp;
201
202 // blockNodes: [private] Regex
203 // Regex for testing if a given tag is a block level (display:block) tag
204 blockNodes: /^(?:P|H1|H2|H3|H4|H5|H6|LI)$/,
205
206 handleEnterKey: function(e){
207 // summary:
208 // Handler for enter key events when blockNodeForEnter is DIV or P.
209 // description:
210 // Manually handle enter key event to make the behavior consistent across
211 // all supported browsers. See class description for details.
212 // tags:
213 // private
214
215 var selection, range, newrange, startNode, endNode, brNode, doc=this.editor.document,br,rs,txt;
216 if(e.shiftKey){ // shift+enter always generates <br>
217 var parent = this.editor._sCall('getParentElement', []);
218 var header = rangeapi.getAncestor(parent,this.blockNodes);
219 if(header){
220 if(header.tagName == 'LI'){
221 return true; // let browser handle
222 }
223 selection = rangeapi.getSelection(this.editor.window);
224 range = selection.getRangeAt(0);
225 if(!range.collapsed){
226 range.deleteContents();
227 selection = rangeapi.getSelection(this.editor.window);
228 range = selection.getRangeAt(0);
229 }
230 if(rangeapi.atBeginningOfContainer(header, range.startContainer, range.startOffset)){
231 br=doc.createElement('br');
232 newrange = rangeapi.create(this.editor.window);
233 header.insertBefore(br,header.firstChild);
234 newrange.setStartAfter(br);
235 selection.removeAllRanges();
236 selection.addRange(newrange);
237 }else if(rangeapi.atEndOfContainer(header, range.startContainer, range.startOffset)){
238 newrange = rangeapi.create(this.editor.window);
239 br=doc.createElement('br');
240 header.appendChild(br);
241 header.appendChild(doc.createTextNode('\xA0'));
242 newrange.setStart(header.lastChild,0);
243 selection.removeAllRanges();
244 selection.addRange(newrange);
245 }else{
246 rs = range.startContainer;
247 if(rs && rs.nodeType == 3){
248 // Text node, we have to split it.
249 txt = rs.nodeValue;
250 startNode = doc.createTextNode(txt.substring(0, range.startOffset));
251 endNode = doc.createTextNode(txt.substring(range.startOffset));
252 brNode = doc.createElement("br");
253
254 if(endNode.nodeValue == "" && has("webkit")){
255 endNode = doc.createTextNode('\xA0')
256 }
257 domConstruct.place(startNode, rs, "after");
258 domConstruct.place(brNode, startNode, "after");
259 domConstruct.place(endNode, brNode, "after");
260 domConstruct.destroy(rs);
261 newrange = rangeapi.create(this.editor.window);
262 newrange.setStart(endNode,0);
263 selection.removeAllRanges();
264 selection.addRange(newrange);
265 return false;
266 }
267 return true; // let browser handle
268 }
269 }else{
270 selection = rangeapi.getSelection(this.editor.window);
271 if(selection.rangeCount){
272 range = selection.getRangeAt(0);
273 if(range && range.startContainer){
274 if(!range.collapsed){
275 range.deleteContents();
276 selection = rangeapi.getSelection(this.editor.window);
277 range = selection.getRangeAt(0);
278 }
279 rs = range.startContainer;
280 if(rs && rs.nodeType == 3){
281 // Text node, we have to split it.
282 var endEmpty = false;
283
284 var offset = range.startOffset;
285 if(rs.length < offset){
286 //We are not splitting the right node, try to locate the correct one
287 ret = this._adjustNodeAndOffset(rs, offset);
288 rs = ret.node;
289 offset = ret.offset;
290 }
291 txt = rs.nodeValue;
292
293 startNode = doc.createTextNode(txt.substring(0, offset));
294 endNode = doc.createTextNode(txt.substring(offset));
295 brNode = doc.createElement("br");
296
297 if(!endNode.length){
298 endNode = doc.createTextNode('\xA0');
299 endEmpty = true;
300 }
301
302 if(startNode.length){
303 domConstruct.place(startNode, rs, "after");
304 }else{
305 startNode = rs;
306 }
307 domConstruct.place(brNode, startNode, "after");
308 domConstruct.place(endNode, brNode, "after");
309 domConstruct.destroy(rs);
310 newrange = rangeapi.create(this.editor.window);
311 newrange.setStart(endNode,0);
312 newrange.setEnd(endNode, endNode.length);
313 selection.removeAllRanges();
314 selection.addRange(newrange);
315 if(endEmpty && !has("webkit")){
316 this.editor._sCall("remove", []);
317 }else{
318 this.editor._sCall("collapse", [true]);
319 }
320 }else{
321 var targetNode;
322 if(range.startOffset >= 0){
323 targetNode = rs.childNodes[range.startOffset];
324 }
325 var brNode = doc.createElement("br");
326 var endNode = doc.createTextNode('\xA0');
327 if(!targetNode){
328 rs.appendChild(brNode);
329 rs.appendChild(endNode);
330 }else{
331 domConstruct.place(brNode, targetNode, "before");
332 domConstruct.place(endNode, brNode, "after");
333 }
334 newrange = rangeapi.create(this.editor.window);
335 newrange.setStart(endNode,0);
336 newrange.setEnd(endNode, endNode.length);
337 selection.removeAllRanges();
338 selection.addRange(newrange);
339 this.editor._sCall("collapse", [true]);
340 }
341 }
342 }else{
343 // don't change this: do not call this.execCommand, as that may have other logic in subclass
344 RichText.prototype.execCommand.call(this.editor, 'inserthtml', '<br>');
345 }
346 }
347 return false;
348 }
349 var _letBrowserHandle = true;
350
351 // first remove selection
352 selection = rangeapi.getSelection(this.editor.window);
353 range = selection.getRangeAt(0);
354 if(!range.collapsed){
355 range.deleteContents();
356 selection = rangeapi.getSelection(this.editor.window);
357 range = selection.getRangeAt(0);
358 }
359
360 var block = rangeapi.getBlockAncestor(range.endContainer, null, this.editor.editNode);
361 var blockNode = block.blockNode;
362
363 // if this is under a LI or the parent of the blockNode is LI, just let browser to handle it
364 if((this._checkListLater = (blockNode && (blockNode.nodeName == 'LI' || blockNode.parentNode.nodeName == 'LI')))){
365 if(has("mozilla")){
366 // press enter in middle of P may leave a trailing <br/>, let's remove it later
367 this._pressedEnterInBlock = blockNode;
368 }
369 // if this li only contains spaces, set the content to empty so the browser will outdent this item
370 if(/^(\s|&nbsp;|&#160;|\xA0|<span\b[^>]*\bclass=['"]Apple-style-span['"][^>]*>(\s|&nbsp;|&#160;|\xA0)<\/span>)?(<br>)?$/.test(blockNode.innerHTML)){
371 // empty LI node
372 blockNode.innerHTML = '';
373 if(has("webkit")){ // WebKit tosses the range when innerHTML is reset
374 newrange = rangeapi.create(this.editor.window);
375 newrange.setStart(blockNode, 0);
376 selection.removeAllRanges();
377 selection.addRange(newrange);
378 }
379 this._checkListLater = false; // nothing to check since the browser handles outdent
380 }
381 return true;
382 }
383
384 // text node directly under body, let's wrap them in a node
385 if(!block.blockNode || block.blockNode===this.editor.editNode){
386 try{
387 RichText.prototype.execCommand.call(this.editor, 'formatblock',this.blockNodeForEnter);
388 }catch(e2){ /*squelch FF3 exception bug when editor content is a single BR*/ }
389 // get the newly created block node
390 // FIXME
391 block = {blockNode: this.editor._sCall('getAncestorElement', [this.blockNodeForEnter]),
392 blockContainer: this.editor.editNode};
393 if(block.blockNode){
394 if(block.blockNode != this.editor.editNode &&
395 (!(block.blockNode.textContent || block.blockNode.innerHTML).replace(/^\s+|\s+$/g, "").length)){
396 this.removeTrailingBr(block.blockNode);
397 return false;
398 }
399 }else{ // we shouldn't be here if formatblock worked
400 block.blockNode = this.editor.editNode;
401 }
402 selection = rangeapi.getSelection(this.editor.window);
403 range = selection.getRangeAt(0);
404 }
405
406 var newblock = doc.createElement(this.blockNodeForEnter);
407 newblock.innerHTML=this.bogusHtmlContent;
408 this.removeTrailingBr(block.blockNode);
409 var endOffset = range.endOffset;
410 var node = range.endContainer;
411 if(node.length < endOffset){
412 //We are not checking the right node, try to locate the correct one
413 var ret = this._adjustNodeAndOffset(node, endOffset);
414 node = ret.node;
415 endOffset = ret.offset;
416 }
417 if(rangeapi.atEndOfContainer(block.blockNode, node, endOffset)){
418 if(block.blockNode === block.blockContainer){
419 block.blockNode.appendChild(newblock);
420 }else{
421 domConstruct.place(newblock, block.blockNode, "after");
422 }
423 _letBrowserHandle = false;
424 // lets move caret to the newly created block
425 newrange = rangeapi.create(this.editor.window);
426 newrange.setStart(newblock, 0);
427 selection.removeAllRanges();
428 selection.addRange(newrange);
429 if(this.editor.height){
430 winUtils.scrollIntoView(newblock);
431 }
432 }else if(rangeapi.atBeginningOfContainer(block.blockNode,
433 range.startContainer, range.startOffset)){
434 domConstruct.place(newblock, block.blockNode, block.blockNode === block.blockContainer ? "first" : "before");
435 if(newblock.nextSibling && this.editor.height){
436 // position input caret - mostly WebKit needs this
437 newrange = rangeapi.create(this.editor.window);
438 newrange.setStart(newblock.nextSibling, 0);
439 selection.removeAllRanges();
440 selection.addRange(newrange);
441 // browser does not scroll the caret position into view, do it manually
442 winUtils.scrollIntoView(newblock.nextSibling);
443 }
444 _letBrowserHandle = false;
445 }else{ //press enter in the middle of P/DIV/Whatever/
446 if(block.blockNode === block.blockContainer){
447 block.blockNode.appendChild(newblock);
448 }else{
449 domConstruct.place(newblock, block.blockNode, "after");
450 }
451 _letBrowserHandle = false;
452
453 // Clone any block level styles.
454 if(block.blockNode.style){
455 if(newblock.style){
456 if(block.blockNode.style.cssText){
457 newblock.style.cssText = block.blockNode.style.cssText;
458 }
459 }
460 }
461
462 // Okay, we probably have to split.
463 rs = range.startContainer;
464 var firstNodeMoved;
465 if(rs && rs.nodeType == 3){
466 // Text node, we have to split it.
467 var nodeToMove, tNode;
468 endOffset = range.endOffset;
469 if(rs.length < endOffset){
470 //We are not splitting the right node, try to locate the correct one
471 ret = this._adjustNodeAndOffset(rs, endOffset);
472 rs = ret.node;
473 endOffset = ret.offset;
474 }
475
476 txt = rs.nodeValue;
477 startNode = doc.createTextNode(txt.substring(0, endOffset));
478 endNode = doc.createTextNode(txt.substring(endOffset, txt.length));
479
480 // Place the split, then remove original nodes.
481 domConstruct.place(startNode, rs, "before");
482 domConstruct.place(endNode, rs, "after");
483 domConstruct.destroy(rs);
484
485 // Okay, we split the text. Now we need to see if we're
486 // parented to the block element we're splitting and if
487 // not, we have to split all the way up. Ugh.
488 var parentC = startNode.parentNode;
489 while(parentC !== block.blockNode){
490 var tg = parentC.tagName;
491 var newTg = doc.createElement(tg);
492 // Clone over any 'style' data.
493 if(parentC.style){
494 if(newTg.style){
495 if(parentC.style.cssText){
496 newTg.style.cssText = parentC.style.cssText;
497 }
498 }
499 }
500 // If font also need to clone over any font data.
501 if(parentC.tagName === "FONT"){
502 if(parentC.color){
503 newTg.color = parentC.color;
504 }
505 if(parentC.face){
506 newTg.face = parentC.face;
507 }
508 if(parentC.size){ // this check was necessary on IE
509 newTg.size = parentC.size;
510 }
511 }
512
513 nodeToMove = endNode;
514 while(nodeToMove){
515 tNode = nodeToMove.nextSibling;
516 newTg.appendChild(nodeToMove);
517 nodeToMove = tNode;
518 }
519 domConstruct.place(newTg, parentC, "after");
520 startNode = parentC;
521 endNode = newTg;
522 parentC = parentC.parentNode;
523 }
524
525 // Lastly, move the split out tags to the new block.
526 // as they should now be split properly.
527 nodeToMove = endNode;
528 if(nodeToMove.nodeType == 1 || (nodeToMove.nodeType == 3 && nodeToMove.nodeValue)){
529 // Non-blank text and non-text nodes need to clear out that blank space
530 // before moving the contents.
531 newblock.innerHTML = "";
532 }
533 firstNodeMoved = nodeToMove;
534 while(nodeToMove){
535 tNode = nodeToMove.nextSibling;
536 newblock.appendChild(nodeToMove);
537 nodeToMove = tNode;
538 }
539 }
540
541 //lets move caret to the newly created block
542 newrange = rangeapi.create(this.editor.window);
543 var nodeForCursor;
544 var innerMostFirstNodeMoved = firstNodeMoved;
545 if(this.blockNodeForEnter !== 'BR'){
546 while(innerMostFirstNodeMoved){
547 nodeForCursor = innerMostFirstNodeMoved;
548 tNode = innerMostFirstNodeMoved.firstChild;
549 innerMostFirstNodeMoved = tNode;
550 }
551 if(nodeForCursor && nodeForCursor.parentNode){
552 newblock = nodeForCursor.parentNode;
553 newrange.setStart(newblock, 0);
554 selection.removeAllRanges();
555 selection.addRange(newrange);
556 if(this.editor.height){
557 winUtils.scrollIntoView(newblock);
558 }
559 if(has("mozilla")){
560 // press enter in middle of P may leave a trailing <br/>, let's remove it later
561 this._pressedEnterInBlock = block.blockNode;
562 }
563 }else{
564 _letBrowserHandle = true;
565 }
566 }else{
567 newrange.setStart(newblock, 0);
568 selection.removeAllRanges();
569 selection.addRange(newrange);
570 if(this.editor.height){
571 winUtils.scrollIntoView(newblock);
572 }
573 if(has("mozilla")){
574 // press enter in middle of P may leave a trailing <br/>, let's remove it later
575 this._pressedEnterInBlock = block.blockNode;
576 }
577 }
578 }
579 return _letBrowserHandle;
580 },
581
582 _adjustNodeAndOffset: function(/*DomNode*/node, /*Int*/offset){
583 // summary:
584 // In the case there are multiple text nodes in a row the offset may not be within the node. If the offset is larger than the node length, it will attempt to find
585 // the next text sibling until it locates the text node in which the offset refers to
586 // node:
587 // The node to check.
588 // offset:
589 // The position to find within the text node
590 // tags:
591 // private.
592 while(node.length < offset && node.nextSibling && node.nextSibling.nodeType==3){
593 //Adjust the offset and node in the case of multiple text nodes in a row
594 offset = offset - node.length;
595 node = node.nextSibling;
596 }
597 return {"node": node, "offset": offset};
598 },
599
600 removeTrailingBr: function(container){
601 // summary:
602 // If last child of container is a `<br>`, then remove it.
603 // tags:
604 // private
605 var para = /P|DIV|LI/i.test(container.tagName) ?
606 container : this.editor._sCall("getParentOfType", [container,['P','DIV','LI']]);
607
608 if(!para){ return; }
609 if(para.lastChild){
610 if((para.childNodes.length > 1 && para.lastChild.nodeType == 3 && /^[\s\xAD]*$/.test(para.lastChild.nodeValue)) ||
611 para.lastChild.tagName=='BR'){
612
613 domConstruct.destroy(para.lastChild);
614 }
615 }
616 if(!para.childNodes.length){
617 para.innerHTML=this.bogusHtmlContent;
618 }
619 }
620 });
621
622 });