]>
git.wh0rd.org - tt-rss.git/blob - 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
7 "dojo/sniff" , // has("ie") has("mozilla") has("webkit")
8 "dojo/_base/window" , // win.withGlobal
9 "dojo/window" , // winUtils.scrollIntoView
14 ], function ( declare
, domConstruct
, event
, keys
, lang
, has
, win
, winUtils
, _Plugin
, RichText
, rangeapi
, baseFocus
){
17 // dijit/_editor/plugins/EnterKeyHandling
19 return declare ( "dijit._editor.plugins.EnterKeyHandling" , _Plugin
, {
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.
27 // This plugin has three modes:
29 // - blockNodeForEnter=BR
30 // - blockNodeForEnter=DIV
31 // - blockNodeForEnter=P
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:
37 // | first paragraph <shift-ENTER>
38 // | second line of first paragraph <ENTER>
46 // | second line of first paragraph
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:
64 // It will appear on the screen as two 'paragraphs' of three lines each. Markupwise, this generates:
79 // | <div> </div>
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'
89 // See class description for more details.
90 blockNodeForEnter
: 'BR' ,
92 constructor : function ( args
){
94 if ( "blockNodeForEnter" in args
){
95 args
. blockNodeForEnter
= args
. blockNodeForEnter
. toUpperCase ();
97 lang
. mixin ( this , args
);
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
);
117 if (! this . handleEnterKey ( ne
)){
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 );
130 r
. move ( 'character' , 1 );
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' );
146 onKeyPressed : function (){
148 // Handler for keypress events.
151 if ( this . _checkListLater
){
152 if ( win
. withGlobal ( this . editor
. window
, 'isCollapsed' , baseFocus
)){
153 var liparent
= this . editor
. _sCall ( 'getAncestorElement' , [ 'LI' ]);
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
]);
160 block
. innerHTML
= this . bogusHtmlContent
;
162 // move to the start by moving backwards one char
163 var r
= this . editor
. document
. selection
. createRange ();
164 r
. move ( 'character' ,- 1 );
168 console
. error ( 'onKeyPressed: Cannot find the new block node' ); // FIXME
172 if ( liparent
. parentNode
. parentNode
. nodeName
== 'LI' ){
173 liparent
= liparent
. parentNode
. parentNode
;
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
);
187 this . _checkListLater
= false ;
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
);
194 delete this . _pressedEnterInBlock
;
198 // bogusHtmlContent: [private] String
199 // HTML to stick into a new empty block
200 bogusHtmlContent
: ' ' , //
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)$/ ,
206 handleEnterKey : function ( e
){
208 // Handler for enter key events when blockNodeForEnter is DIV or P.
210 // Manually handle enter key event to make the behavior consistent across
211 // all supported browsers. See class description for details.
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
);
220 if ( header
. tagName
== 'LI' ){
221 return true ; // let browser handle
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 );
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
);
246 rs
= range
. startContainer
;
247 if ( rs
&& rs
. nodeType
== 3 ){
248 // Text node, we have to split it.
250 startNode
= doc
. createTextNode ( txt
. substring ( 0 , range
. startOffset
));
251 endNode
= doc
. createTextNode ( txt
. substring ( range
. startOffset
));
252 brNode
= doc
. createElement ( "br" );
254 if ( endNode
. nodeValue
== "" && has ( "webkit" )){
255 endNode
= doc
. createTextNode ( ' \xA0 ' )
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
);
267 return true ; // let browser handle
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 );
279 rs
= range
. startContainer
;
280 if ( rs
&& rs
. nodeType
== 3 ){
281 // Text node, we have to split it.
282 var endEmpty
= false ;
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
);
293 startNode
= doc
. createTextNode ( txt
. substring ( 0 , offset
));
294 endNode
= doc
. createTextNode ( txt
. substring ( offset
));
295 brNode
= doc
. createElement ( "br" );
298 endNode
= doc
. createTextNode ( ' \xA0 ' );
302 if ( startNode
. length
){
303 domConstruct
. place ( startNode
, rs
, "after" );
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" , []);
318 this . editor
. _sCall ( "collapse" , [ true ]);
322 if ( range
. startOffset
>= 0 ){
323 targetNode
= rs
. childNodes
[ range
. startOffset
];
325 var brNode
= doc
. createElement ( "br" );
326 var endNode
= doc
. createTextNode ( ' \xA0 ' );
328 rs
. appendChild ( brNode
);
329 rs
. appendChild ( endNode
);
331 domConstruct
. place ( brNode
, targetNode
, "before" );
332 domConstruct
. place ( endNode
, brNode
, "after" );
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 ]);
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>' );
349 var _letBrowserHandle
= true ;
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 );
360 var block
= rangeapi
. getBlockAncestor ( range
. endContainer
, null , this . editor
. editNode
);
361 var blockNode
= block
. blockNode
;
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' )))){
366 // press enter in middle of P may leave a trailing <br/>, let's remove it later
367 this . _pressedEnterInBlock
= blockNode
;
369 // if this li only contains spaces, set the content to empty so the browser will outdent this item
370 if ( /^(\s| | |\xA0|<span\b[^>]*\bclass=['"]Apple-style-span['"][^>]*>(\s| | |\xA0)<\/span>)?(<br>)?$/ . test ( blockNode
. innerHTML
)){
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
);
379 this . _checkListLater
= false ; // nothing to check since the browser handles outdent
384 // text node directly under body, let's wrap them in a node
385 if (! block
. blockNode
|| block
. blockNode
=== this . editor
. editNode
){
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
391 block
= { blockNode
: this . editor
. _sCall ( 'getAncestorElement' , [ this . blockNodeForEnter
]),
392 blockContainer
: this . editor
. editNode
};
394 if ( block
. blockNode
!= this . editor
. editNode
&&
395 (!( block
. blockNode
. textContent
|| block
. blockNode
. innerHTML
). replace ( /^\s+|\s+$/g , "" ). length
)){
396 this . removeTrailingBr ( block
. blockNode
);
399 } else { // we shouldn't be here if formatblock worked
400 block
. blockNode
= this . editor
. editNode
;
402 selection
= rangeapi
. getSelection ( this . editor
. window
);
403 range
= selection
. getRangeAt ( 0 );
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
);
415 endOffset
= ret
. offset
;
417 if ( rangeapi
. atEndOfContainer ( block
. blockNode
, node
, endOffset
)){
418 if ( block
. blockNode
=== block
. blockContainer
){
419 block
. blockNode
. appendChild ( newblock
);
421 domConstruct
. place ( newblock
, block
. blockNode
, "after" );
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
);
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
);
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
);
449 domConstruct
. place ( newblock
, block
. blockNode
, "after" );
451 _letBrowserHandle
= false ;
453 // Clone any block level styles.
454 if ( block
. blockNode
. style
){
456 if ( block
. blockNode
. style
. cssText
){
457 newblock
. style
. cssText
= block
. blockNode
. style
. cssText
;
462 // Okay, we probably have to split.
463 rs
= range
. startContainer
;
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
);
473 endOffset
= ret
. offset
;
477 startNode
= doc
. createTextNode ( txt
. substring ( 0 , endOffset
));
478 endNode
= doc
. createTextNode ( txt
. substring ( endOffset
, txt
. length
));
480 // Place the split, then remove original nodes.
481 domConstruct
. place ( startNode
, rs
, "before" );
482 domConstruct
. place ( endNode
, rs
, "after" );
483 domConstruct
. destroy ( rs
);
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.
495 if ( parentC
. style
. cssText
){
496 newTg
. style
. cssText
= parentC
. style
. cssText
;
500 // If font also need to clone over any font data.
501 if ( parentC
. tagName
=== "FONT" ){
503 newTg
. color
= parentC
. color
;
506 newTg
. face
= parentC
. face
;
508 if ( parentC
. size
){ // this check was necessary on IE
509 newTg
. size
= parentC
. size
;
513 nodeToMove
= endNode
;
515 tNode
= nodeToMove
. nextSibling
;
516 newTg
. appendChild ( nodeToMove
);
519 domConstruct
. place ( newTg
, parentC
, "after" );
522 parentC
= parentC
. parentNode
;
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
= "" ;
533 firstNodeMoved
= nodeToMove
;
535 tNode
= nodeToMove
. nextSibling
;
536 newblock
. appendChild ( nodeToMove
);
541 //lets move caret to the newly created block
542 newrange
= rangeapi
. create ( this . editor
. window
);
544 var innerMostFirstNodeMoved
= firstNodeMoved
;
545 if ( this . blockNodeForEnter
!== 'BR' ){
546 while ( innerMostFirstNodeMoved
){
547 nodeForCursor
= innerMostFirstNodeMoved
;
548 tNode
= innerMostFirstNodeMoved
. firstChild
;
549 innerMostFirstNodeMoved
= tNode
;
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
);
560 // press enter in middle of P may leave a trailing <br/>, let's remove it later
561 this . _pressedEnterInBlock
= block
. blockNode
;
564 _letBrowserHandle
= true ;
567 newrange
. setStart ( newblock
, 0 );
568 selection
. removeAllRanges ();
569 selection
. addRange ( newrange
);
570 if ( this . editor
. height
){
571 winUtils
. scrollIntoView ( newblock
);
574 // press enter in middle of P may leave a trailing <br/>, let's remove it later
575 this . _pressedEnterInBlock
= block
. blockNode
;
579 return _letBrowserHandle
;
582 _adjustNodeAndOffset : function ( /*DomNode*/ node
, /*Int*/ offset
){
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
587 // The node to check.
589 // The position to find within the text node
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
;
597 return { "node" : node
, "offset" : offset
};
600 removeTrailingBr : function ( container
){
602 // If last child of container is a `<br>`, then remove it.
605 var para
= /P|DIV|LI/i . test ( container
. tagName
) ?
606 container
: this . editor
. _sCall ( "getParentOfType" , [ container
,[ 'P' , 'DIV' , 'LI' ]]);
610 if (( para
. childNodes
. length
> 1 && para
. lastChild
. nodeType
== 3 && /^[\s\xAD]*$/ . test ( para
. lastChild
. nodeValue
)) ||
611 para
. lastChild
. tagName
== 'BR' ){
613 domConstruct
. destroy ( para
. lastChild
);
616 if (! para
. childNodes
. length
){
617 para
. innerHTML
= this . bogusHtmlContent
;