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