2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
7 * @fileOverview The [Magic Line](http://ckeditor.com/addon/magicline) plugin that makes it easier to access some document areas that
8 * are difficult to focus.
14 CKEDITOR
.plugins
.add( 'magicline', {
15 lang: 'af,ar,az,bg,ca,cs,cy,da,de,de-ch,el,en,en-gb,eo,es,et,eu,fa,fi,fr,fr-ca,gl,he,hr,hu,id,it,ja,km,ko,ku,lv,nb,nl,no,oc,pl,pt,pt-br,ru,si,sk,sl,sq,sv,tr,tt,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
19 // Activates the box inside of an editor.
20 function initPlugin( editor
) {
22 var config
= editor
.config
,
23 triggerOffset
= config
.magicline_triggerOffset
|| 30,
24 enterMode
= config
.enterMode
,
26 // Global stuff is being initialized here.
29 triggerOffset: triggerOffset
,
30 holdDistance: 0 | triggerOffset
* ( config
.magicline_holdDistance
|| 0.5 ),
31 boxColor: config
.magicline_color
|| '#ff0000',
32 rtl: config
.contentsLangDirection
== 'rtl',
33 tabuList: [ 'data-cke-hidden-sel' ].concat( config
.magicline_tabuList
|| [] ),
34 triggers: config
.magicline_everywhere
? DTD_BLOCK : { table: 1, hr: 1, div: 1, ul: 1, ol: 1, dl: 1, form: 1, blockquote: 1 }
36 scrollTimeout
, checkMouseTimeoutPending
, checkMouseTimer
;
39 // Internal DEBUG uses tools located in the topmost window.
41 // (#9701) Due to security limitations some browsers may throw
42 // errors when accessing window.top object. Do it safely first then.
44 that
.debug
= window
.top
.DEBUG
;
48 that
.debug
= that
.debug
|| {
49 groupEnd: function() {},
50 groupStart: function() {},
52 logElements: function() {},
53 logElementsEnd: function() {},
54 logEnd: function() {},
55 mousePos: function() {},
56 showHidden: function() {},
57 showTrigger: function() {},
58 startTimer: function() {},
59 stopTimer: function() {}
63 // Simple irrelevant elements filter.
64 that
.isRelevant = function( node
) {
65 return isHtml( node
) && // -> Node must be an existing HTML element.
66 !isLine( that
, node
) && // -> Node can be neither the box nor its child.
67 !isFlowBreaker( node
); // -> Node can be neither floated nor positioned nor aligned.
70 editor
.on( 'contentDom', addListeners
, this );
72 function addListeners() {
73 var editable
= editor
.editable(),
74 doc
= editor
.document
,
77 // Global stuff is being initialized here.
80 inInlineMode: editable
.isInline(),
86 // This is the boundary of the editor. For inline the boundary is editable itself.
87 // For classic (`iframe`-based) editor, the HTML element is a real boundary.
88 that
.boundary
= that
.inInlineMode
? that
.editable : that
.doc
.getDocumentElement();
90 // Enabling the box inside of inline editable is pointless.
91 // There's no need to access spaces inside paragraphs, links, spans, etc.
92 if ( editable
.is( dtd
.$inline
) )
95 // Handle in-line editing by setting appropriate position.
96 // If current position is static, make it relative and clear top/left coordinates.
97 if ( that
.inInlineMode
&& !isPositioned( editable
) ) {
104 // Enable the box. Let it produce children elements, initialize
105 // event handlers and own methods.
106 initLine
.call( this, that
);
108 // Get view dimensions and scroll positions.
109 // At this stage (before any checkMouse call) it is used mostly
110 // by tests. Nevertheless it a crucial thing.
111 updateWindowSize( that
);
113 // Remove the box before an undo image is created.
114 // This is important. If we didn't do that, the *undo thing* would revert the box into an editor.
115 // Thanks to that, undo doesn't even know about the existence of the box.
116 editable
.attachListener( editor
, 'beforeUndoImage', function() {
120 // Removes the box HTML from editor data string if getData is called.
121 // Thanks to that, an editor never yields data polluted by the box.
122 // Listen with very high priority, so line will be removed before other
123 // listeners will see it.
124 editable
.attachListener( editor
, 'beforeGetData', function() {
125 // If the box is in editable, remove it.
126 if ( that
.line
.wrap
.getParent() ) {
129 // Restore line in the last listener for 'getData'.
130 editor
.once( 'getData', function() {
132 }, null, null, 1000 );
136 // Hide the box on mouseout if mouse leaves document.
137 editable
.attachListener( that
.inInlineMode
? doc : doc
.getWindow().getFrame(), 'mouseout', function( event
) {
138 if ( editor
.mode
!= 'wysiwyg' )
141 // Check for inline-mode editor. If so, check mouse position
142 // and remove the box if mouse outside of an editor.
143 if ( that
.inInlineMode
) {
145 x: event
.data
.$.clientX
,
146 y: event
.data
.$.clientY
149 updateWindowSize( that
);
150 updateEditableSize( that
, true );
152 var size
= that
.view
.editable
,
153 scroll
= that
.view
.scroll
;
155 // If outside of an editor...
156 if ( !inBetween( mouse
.x
, size
.left
- scroll
.x
, size
.right
- scroll
.x
) || !inBetween( mouse
.y
, size
.top
- scroll
.y
, size
.bottom
- scroll
.y
) ) {
157 clearTimeout( checkMouseTimer
);
158 checkMouseTimer
= null;
164 clearTimeout( checkMouseTimer
);
165 checkMouseTimer
= null;
170 // This one deactivates hidden mode of an editor which
171 // prevents the box from being shown.
172 editable
.attachListener( editable
, 'keyup', function() {
174 that
.debug
.showHidden( that
.hiddenMode
); // %REMOVE_LINE%
177 editable
.attachListener( editable
, 'keydown', function( event
) {
178 if ( editor
.mode
!= 'wysiwyg' )
181 var keyStroke
= event
.data
.getKeystroke();
183 switch ( keyStroke
) {
191 that
.debug
.showHidden( that
.hiddenMode
); // %REMOVE_LINE%
194 // This method ensures that checkMouse aren't executed
195 // in parallel and no more frequently than specified in timeout function.
196 // In classic (`iframe`-based) editor, document is used as a trigger, to provide magicline
197 // functionality when mouse is below the body (short content, short body).
198 editable
.attachListener( that
.inInlineMode
? editable : doc
, 'mousemove', function( event
) {
199 checkMouseTimeoutPending
= true;
201 if ( editor
.mode
!= 'wysiwyg' || editor
.readOnly
|| checkMouseTimer
)
204 // IE<9 requires this event-driven object to be created
205 // outside of the setTimeout statement.
206 // Otherwise it loses the event object with its properties.
208 x: event
.data
.$.clientX
,
209 y: event
.data
.$.clientY
212 checkMouseTimer
= setTimeout( function() {
214 }, 30 ); // balances performance and accessibility
217 // This one removes box on scroll event.
218 // It is to avoid box displacement.
219 editable
.attachListener( win
, 'scroll', function() {
220 if ( editor
.mode
!= 'wysiwyg' )
225 // To figure this out just look at the mouseup
226 // event handler below.
230 clearTimeout( scrollTimeout
);
231 scrollTimeout
= setTimeout( function() {
232 // Don't leave hidden mode until mouse remains pressed and
233 // scroll is being used, i.e. when dragging something.
234 if ( !that
.mouseDown
)
236 that
.debug
.showHidden( that
.hiddenMode
); // %REMOVE_LINE%
239 that
.debug
.showHidden( that
.hiddenMode
); // %REMOVE_LINE%
243 // Those event handlers remove the box on mousedown
244 // and don't reveal it until the mouse is released.
245 // It is to prevent box insertion e.g. while scrolling
246 // (w/ scrollbar), selecting and so on.
247 editable
.attachListener( env_ie8
? doc : win
, 'mousedown', function() {
248 if ( editor
.mode
!= 'wysiwyg' )
255 that
.debug
.showHidden( that
.hiddenMode
); // %REMOVE_LINE%
258 // Google Chrome doesn't trigger this on the scrollbar (since 2009...)
259 // so it is totally useless to check for scroll finish
260 // see: http://code.google.com/p/chromium/issues/detail?id=14204
261 editable
.attachListener( env_ie8
? doc : win
, 'mouseup', function() {
264 that
.debug
.showHidden( that
.hiddenMode
); // %REMOVE_LINE%
267 // Editor commands for accessing difficult focus spaces.
268 editor
.addCommand( 'accessPreviousSpace', accessFocusSpaceCmd( that
) );
269 editor
.addCommand( 'accessNextSpace', accessFocusSpaceCmd( that
, true ) );
271 editor
.setKeystroke( [
272 [ config
.magicline_keystrokePrevious
, 'accessPreviousSpace' ],
273 [ config
.magicline_keystrokeNext
, 'accessNextSpace' ]
276 // Revert magicline hot node on undo/redo.
277 editor
.on( 'loadSnapshot', function() {
278 var elements
, element
, i
;
280 for ( var t
in { p: 1, br: 1, div: 1 } ) {
281 // document.find is not available in QM (#11149).
282 elements
= editor
.document
.getElementsByTag( t
);
284 for ( i
= elements
.count(); i
--; ) {
285 if ( ( element
= elements
.getItem( i
) ).data( 'cke-magicline-hot' ) ) {
287 that
.hotNode
= element
;
288 // Restore last access direction
289 that
.lastCmdDirection
= element
.data( 'cke-magicline-dir' ) === 'true' ? true : false;
297 // This method handles mousemove mouse for box toggling.
298 // It uses mouse position to determine underlying element, then
299 // it tries to use different trigger type in order to place the box
300 // in correct place. The following procedure is executed periodically.
301 function checkMouse( mouse
) {
302 that
.debug
.groupStart( 'CheckMouse' ); // %REMOVE_LINE%
303 that
.debug
.startTimer(); // %REMOVE_LINE%
308 checkMouseTimer
= null;
309 updateWindowSize( that
);
312 checkMouseTimeoutPending
&& // There must be an event pending.
313 !that
.hiddenMode
&& // Can't be in hidden mode.
314 editor
.focusManager
.hasFocus
&& // Editor must have focus.
315 !that
.line
.mouseNear() && // Mouse pointer can't be close to the box.
316 ( that
.element
= elementFromMouse( that
, true ) ) // There must be valid element.
318 // If trigger exists, and trigger is correct -> show the box.
319 // Don't show the line if trigger is a descendant of some tabu-list element.
320 if ( ( that
.trigger
= triggerEditable( that
) || triggerEdge( that
) || triggerExpand( that
) ) &&
321 !isInTabu( that
, that
.trigger
.upper
|| that
.trigger
.lower
) ) {
322 that
.line
.attach().place();
325 // Otherwise remove the box
331 that
.debug
.showTrigger( that
.trigger
); // %REMOVE_LINE%
332 that
.debug
.mousePos( mouse
.y
, that
.element
); // %REMOVE_LINE%
334 checkMouseTimeoutPending
= false;
337 that
.debug
.stopTimer(); // %REMOVE_LINE%
338 that
.debug
.groupEnd(); // %REMOVE_LINE%
341 // This one allows testing and debugging. It reveals some
342 // inner methods to the world.
344 accessFocusSpace: accessFocusSpace
,
345 boxTrigger: boxTrigger
,
347 getAscendantTrigger: getAscendantTrigger
,
348 getNonEmptyNeighbour: getNonEmptyNeighbour
,
351 triggerEdge: triggerEdge
,
352 triggerEditable: triggerEditable
,
353 triggerExpand: triggerExpand
358 // Some shorthands for common methods to save bytes
359 var extend
= CKEDITOR
.tools
.extend
,
360 newElement
= CKEDITOR
.dom
.element
,
361 newElementFromHtml
= newElement
.createFromHtml
,
363 env_ie8
= CKEDITOR
.env
.ie
&& CKEDITOR
.env
.version
< 9,
366 // Global object associating enter modes with elements.
369 // Constant values, types and so on.
378 WHITE_SPACE
= '\u00A0',
379 DTD_LISTITEM
= dtd
.$listItem
,
380 DTD_TABLECONTENT
= dtd
.$tableContent
,
381 DTD_NONACCESSIBLE
= extend( {}, dtd
.$nonEditable
, dtd
.$empty
),
382 DTD_BLOCK
= dtd
.$block
,
384 // Minimum time that must elapse between two update*Size calls.
385 // It prevents constant getComuptedStyle calls and improves performance.
388 // Shared CSS stuff for box elements
389 CSS_COMMON
= 'width:0px;height:0px;padding:0px;margin:0px;display:block;' + 'z-index:9999;color:#fff;position:absolute;font-size: 0px;line-height:0px;',
390 CSS_TRIANGLE
= CSS_COMMON
+ 'border-color:transparent;display:block;border-style:solid;',
391 TRIANGLE_HTML
= '<span>' + WHITE_SPACE
+ '</span>';
393 enterElements
[ CKEDITOR
.ENTER_BR
] = 'br';
394 enterElements
[ CKEDITOR
.ENTER_P
] = 'p';
395 enterElements
[ CKEDITOR
.ENTER_DIV
] = 'div';
397 function areSiblings( that
, upper
, lower
) {
398 return isHtml( upper
) && isHtml( lower
) && lower
.equals( upper
.getNext( function( node
) {
399 return !( isEmptyTextNode( node
) || isComment( node
) || isFlowBreaker( node
) );
403 // boxTrigger is an abstract type which describes
404 // the relationship between elements that may result
405 // in showing the box.
407 // The following type is used by numerous methods
408 // to share information about the hypothetical box placement
409 // and look by referring to boxTrigger properties.
410 function boxTrigger( triggerSetup
) {
411 this.upper
= triggerSetup
[ 0 ];
412 this.lower
= triggerSetup
[ 1 ];
413 this.set.apply( this, triggerSetup
.slice( 2 ) );
416 boxTrigger
.prototype = {
417 set: function( edge
, type
, look
) {
418 this.properties
= edge
+ type
+ ( look
|| LOOK_NORMAL
);
422 is: function( property
) {
423 return ( this.properties
& property
) == property
;
427 var elementFromMouse
= ( function() {
428 function elementFromPoint( doc
, mouse
) {
429 var pointedElement
= doc
.$.elementFromPoint( mouse
.x
, mouse
.y
);
431 // IE9QM: from times to times it will return an empty object on scroll bar hover. (#12185)
432 return pointedElement
&& pointedElement
.nodeType
?
433 new CKEDITOR
.dom
.element( pointedElement
) :
437 return function( that
, ignoreBox
, forceMouse
) {
442 lineWrap
= that
.line
.wrap
,
443 mouse
= forceMouse
|| that
.mouse
,
444 // Note: element might be null.
445 element
= elementFromPoint( doc
, mouse
);
447 // If ignoreBox is set and element is the box, it means that we
448 // need to hide the box for a while, repeat elementFromPoint
449 // and show it again.
450 if ( ignoreBox
&& isLine( that
, element
) ) {
452 element
= elementFromPoint( doc
, mouse
);
456 // Return nothing if:
457 // \-> Element is not HTML.
458 if ( !( element
&& element
.type
== CKEDITOR
.NODE_ELEMENT
&& element
.$ ) )
461 // Also return nothing if:
462 // \-> We're IE<9 and element is out of the top-level element (editable for inline and HTML for classic (`iframe`-based)).
463 // This is due to the bug which allows IE<9 firing mouse events on element
464 // with contenteditable=true while doing selection out (far, away) of the element.
465 // Thus we must always be sure that we stay in editable or HTML.
466 if ( env
.ie
&& env
.version
< 9 ) {
467 if ( !( that
.boundary
.equals( element
) || that
.boundary
.contains( element
) ) )
475 // Gets the closest parent node that belongs to triggers group.
476 function getAscendantTrigger( that
) {
477 var node
= that
.element
,
480 if ( node
&& isHtml( node
) ) {
481 trigger
= node
.getAscendant( that
.triggers
, true );
483 // If trigger is an element, neither editable nor editable's ascendant.
484 if ( trigger
&& that
.editable
.contains( trigger
) ) {
485 // Check for closest editable limit.
486 // Don't consider trigger as a limit as it may be nested editable (includeSelf=false) (#12009).
487 var limit
= getClosestEditableLimit( trigger
);
489 // Trigger in nested editable area.
490 if ( limit
.getAttribute( 'contenteditable' ) == 'true' )
492 // Trigger in non-editable area.
493 else if ( limit
.is( that
.triggers
) )
505 function getMidpoint( that
, upper
, lower
) {
506 updateSize( that
, upper
);
507 updateSize( that
, lower
);
509 var upperSizeBottom
= upper
.size
.bottom
,
510 lowerSizeTop
= lower
.size
.top
;
512 return upperSizeBottom
&& lowerSizeTop
? 0 | ( upperSizeBottom
+ lowerSizeTop
) / 2 : upperSizeBottom
|| lowerSizeTop
;
515 // Get nearest node (either text or HTML), but:
516 // \-> Omit all empty text nodes (containing white characters only).
517 // \-> Omit BR elements
518 // \-> Omit flow breakers.
519 function getNonEmptyNeighbour( that
, node
, goBack
) {
520 node
= node
[ goBack
? 'getPrevious' : 'getNext' ]( function( node
) {
521 return ( isTextNode( node
) && !isEmptyTextNode( node
) ) ||
522 ( isHtml( node
) && !isFlowBreaker( node
) && !isLine( that
, node
) );
528 function inBetween( val
, lower
, upper
) {
529 return val
> lower
&& val
< upper
;
532 // Returns the closest ancestor that has contenteditable attribute.
533 // Such ancestor is the limit of (non-)editable DOM branch that element
534 // belongs to. This method omits editor editable.
535 function getClosestEditableLimit( element
, includeSelf
) {
536 if ( element
.data( 'cke-editable' ) )
540 element
= element
.getParent();
543 if ( element
.data( 'cke-editable' ) )
546 if ( element
.hasAttribute( 'contenteditable' ) )
549 element
= element
.getParent();
555 // Access space line consists of a few elements (spans):
558 // \-> Line triangles: left triangle (LT), right triangle (RT).
559 // \-> Button handler (BTN).
561 // +--------------------------------------------------- line.wrap (span) -----+
562 // | +---------------------------------------------------- line (span) -----+ |
563 // | | +- LT \ +- BTN -+ / RT -+ | |
564 // | | | \ | | | / | | |
565 // | | | / | <__| | \ | | |
566 // | | +-----/ +-------+ \-----+ | |
567 // | +----------------------------------------------------------------------+ |
568 // +--------------------------------------------------------------------------+
570 function initLine( that
) {
572 // This the main box element that holds triangles and the insertion button
573 line
= newElementFromHtml( '<span contenteditable="false" style="' + CSS_COMMON
+ 'position:absolute;border-top:1px dashed ' + that
.boxColor
+ '"></span>', doc
),
574 iconPath
= CKEDITOR
.getUrl( this.path
+ 'images/' + ( env
.hidpi
? 'hidpi/' : '' ) + 'icon' + ( that
.rtl
? '-rtl' : '' ) + '.png' );
579 // Only if not already attached
580 if ( !this.wrap
.getParent() )
581 this.wrap
.appendTo( that
.editable
, true );
586 // Looks are as follows: [ LOOK_TOP, LOOK_BOTTOM, LOOK_NORMAL ].
590 '<span title="' + that
.editor
.lang
.magicline
.title
+
591 '" contenteditable="false">↵</span>', doc
593 base: CSS_COMMON
+ 'height:17px;width:17px;' + ( that
.rtl
? 'left' : 'right' ) + ':17px;' +
594 'background:url(' + iconPath
+ ') center no-repeat ' + that
.boxColor
+ ';cursor:pointer;' +
595 ( env
.hc
? 'font-size: 15px;line-height:14px;border:1px solid #fff;text-align:center;' : '' ) +
596 ( env
.hidpi
? 'background-size: 9px 10px;' : '' ),
598 'top:-8px; border-radius: 2px;',
599 'top:-17px; border-radius: 2px 2px 0px 0px;',
600 'top:-1px; border-radius: 0px 0px 2px 2px;'
604 extend( newElementFromHtml( TRIANGLE_HTML
, doc
), {
605 base: CSS_TRIANGLE
+ 'left:0px;border-left-color:' + that
.boxColor
+ ';',
607 'border-width:8px 0 8px 8px;top:-8px',
608 'border-width:8px 0 0 8px;top:-8px',
609 'border-width:0 0 8px 8px;top:0px'
612 extend( newElementFromHtml( TRIANGLE_HTML
, doc
), {
613 base: CSS_TRIANGLE
+ 'right:0px;border-right-color:' + that
.boxColor
+ ';',
615 'border-width:8px 8px 8px 0;top:-8px',
616 'border-width:8px 8px 0 0;top:-8px',
617 'border-width:0 8px 8px 0;top:0px'
623 // Detach only if already attached.
624 if ( this.wrap
.getParent() )
630 // Checks whether mouseY is around an element by comparing boundaries and considering
631 // an offset distance.
632 mouseNear: function() {
633 that
.debug
.groupStart( 'mouseNear' ); // %REMOVE_LINE%
635 updateSize( that
, this );
636 var offset
= that
.holdDistance
,
639 // Determine neighborhood by element dimensions and offsets.
640 if ( size
&& inBetween( that
.mouse
.y
, size
.top
- offset
, size
.bottom
+ offset
) && inBetween( that
.mouse
.x
, size
.left
- offset
, size
.right
+ offset
) ) {
641 that
.debug
.logEnd( 'Mouse is near.' ); // %REMOVE_LINE%
645 that
.debug
.logEnd( 'Mouse isn\'t near.' ); // %REMOVE_LINE%
649 // Adjusts position of the box according to the trigger properties.
650 // If also affects look of the box depending on the type of the trigger.
652 var view
= that
.view
,
653 editable
= that
.editable
,
654 trigger
= that
.trigger
,
655 upper
= trigger
.upper
,
656 lower
= trigger
.lower
,
657 any
= upper
|| lower
,
658 parent
= any
.getParent(),
661 // Save recent trigger for further insertion.
662 // It is necessary due to the fact, that that.trigger may
663 // contain different boxTrigger at the moment of insertion
664 // or may be even null.
665 this.trigger
= trigger
;
667 upper
&& updateSize( that
, upper
, true );
668 lower
&& updateSize( that
, lower
, true );
669 updateSize( that
, parent
, true );
671 // Yeah, that's gonna be useful in inline-mode case.
672 if ( that
.inInlineMode
)
673 updateEditableSize( that
, true );
675 // Set X coordinate (left, right, width).
676 if ( parent
.equals( editable
) ) {
677 styleSet
.left
= view
.scroll
.x
;
678 styleSet
.right
= -view
.scroll
.x
;
681 styleSet
.left
= any
.size
.left
- any
.size
.margin
.left
+ view
.scroll
.x
- ( that
.inInlineMode
? view
.editable
.left
+ view
.editable
.border
.left : 0 );
682 styleSet
.width
= any
.size
.outerWidth
+ any
.size
.margin
.left
+ any
.size
.margin
.right
+ view
.scroll
.x
;
686 // Set Y coordinate (top) for trigger consisting of two elements.
687 if ( upper
&& lower
) {
688 // No margins at all or they're equal. Place box right between.
689 if ( upper
.size
.margin
.bottom
=== lower
.size
.margin
.top
)
690 styleSet
.top
= 0 | ( upper
.size
.bottom
+ upper
.size
.margin
.bottom
/ 2 );
692 // Upper margin < lower margin. Place at lower margin.
693 if ( upper
.size
.margin
.bottom
< lower
.size
.margin
.top
)
694 styleSet
.top
= upper
.size
.bottom
+ upper
.size
.margin
.bottom
;
695 // Upper margin > lower margin. Place at upper margin - lower margin.
697 styleSet
.top
= upper
.size
.bottom
+ upper
.size
.margin
.bottom
- lower
.size
.margin
.top
;
700 // Set Y coordinate (top) for single-edge trigger.
702 styleSet
.top
= lower
.size
.top
- lower
.size
.margin
.top
;
704 styleSet
.top
= upper
.size
.bottom
+ upper
.size
.margin
.bottom
;
707 // Set box button modes if close to the viewport horizontal edge
708 // or look forced by the trigger.
709 if ( trigger
.is( LOOK_TOP
) || inBetween( styleSet
.top
, view
.scroll
.y
- 15, view
.scroll
.y
+ 5 ) ) {
710 styleSet
.top
= that
.inInlineMode
? 0 : view
.scroll
.y
;
711 this.look( LOOK_TOP
);
712 } else if ( trigger
.is( LOOK_BOTTOM
) || inBetween( styleSet
.top
, view
.pane
.bottom
- 5, view
.pane
.bottom
+ 15 ) ) {
713 styleSet
.top
= that
.inInlineMode
? (
714 view
.editable
.height
+ view
.editable
.padding
.top
+ view
.editable
.padding
.bottom
719 this.look( LOOK_BOTTOM
);
721 if ( that
.inInlineMode
)
722 styleSet
.top
-= view
.editable
.top
+ view
.editable
.border
.top
;
724 this.look( LOOK_NORMAL
);
727 if ( that
.inInlineMode
) {
731 // Consider the editable to be an element with overflow:scroll
732 // and non-zero scrollTop/scrollLeft value.
733 // For example: divarea editable. (#9383)
734 styleSet
.top
+= view
.editable
.scroll
.top
;
735 styleSet
.left
+= view
.editable
.scroll
.left
;
738 // Append `px` prefixes.
739 for ( var style
in styleSet
)
740 styleSet
[ style
] = CKEDITOR
.tools
.cssLength( styleSet
[ style
] );
742 this.setStyles( styleSet
);
745 // Changes look of the box according to current needs.
746 // Three different styles are available: [ LOOK_TOP, LOOK_BOTTOM, LOOK_NORMAL ].
747 look: function( look
) {
748 if ( this.oldLook
== look
)
751 for ( var i
= this.lineChildren
.length
, child
; i
--; )
752 ( child
= this.lineChildren
[ i
] ).setAttribute( 'style', child
.base
+ child
.looks
[ 0 | look
/ 2 ] );
757 wrap: new newElement( 'span', that
.doc
)
761 // Insert children into the box.
762 for ( var i
= line
.lineChildren
.length
; i
--; )
763 line
.lineChildren
[ i
].appendTo( line
);
765 // Set default look of the box.
766 line
.look( LOOK_NORMAL
);
768 // Using that wrapper prevents IE (8,9) from resizing editable area at the moment
769 // of box insertion. This works thanks to the fact, that positioned box is wrapped by
770 // an inline element. So much tricky.
771 line
.appendTo( line
.wrap
);
773 // Make the box unselectable.
776 // Handle accessSpace node insertion.
777 line
.lineChildren
[ 0 ].on( 'mouseup', function( event
) {
780 accessFocusSpace( that
, function( accessNode
) {
781 // Use old trigger that was saved by 'place' method. Look: line.place
782 var trigger
= that
.line
.trigger
;
784 accessNode
[ trigger
.is( EDGE_TOP
) ? 'insertBefore' : 'insertAfter' ](
785 trigger
.is( EDGE_TOP
) ? trigger
.lower : trigger
.upper
);
790 if ( !env
.ie
&& that
.enterMode
!= CKEDITOR
.ENTER_BR
)
791 that
.hotNode
.scrollIntoView();
793 event
.data
.preventDefault( true );
796 // Prevents IE9 from displaying the resize box and disables drag'n'drop functionality.
797 line
.on( 'mousedown', function( event
) {
798 event
.data
.preventDefault( true );
804 // This function allows accessing any focus space according to the insert function:
805 // * For enterMode ENTER_P it creates P element filled with dummy white-space.
806 // * For enterMode ENTER_DIV it creates DIV element filled with dummy white-space.
807 // * For enterMode ENTER_BR it creates BR element or in IE.
809 // The node is being inserted according to insertFunction. Finally the method
810 // selects the non-breaking space making the node ready for typing.
811 function accessFocusSpace( that
, insertFunction
, doSave
) {
812 var range
= new CKEDITOR
.dom
.range( that
.doc
),
813 editor
= that
.editor
,
816 // IE requires text node of in ENTER_BR mode.
817 if ( env
.ie
&& that
.enterMode
== CKEDITOR
.ENTER_BR
)
818 accessNode
= that
.doc
.createText( WHITE_SPACE
);
820 // In other cases a regular element is used.
822 // Use the enterMode of editable's limit or editor's
823 // enter mode if not in nested editable.
824 var limit
= getClosestEditableLimit( that
.element
, true ),
826 // This is an enter mode for the context. We cannot use
827 // editor.activeEnterMode because the focused nested editable will
828 // have a different enterMode as editor but magicline will be inserted
829 // directly into editor's editable.
830 enterMode
= limit
&& limit
.data( 'cke-enter-mode' ) || that
.enterMode
;
832 accessNode
= new newElement( enterElements
[ enterMode
], that
.doc
);
834 if ( !accessNode
.is( 'br' ) ) {
835 var dummy
= that
.doc
.createText( WHITE_SPACE
);
836 dummy
.appendTo( accessNode
);
840 doSave
&& editor
.fire( 'saveSnapshot' );
842 insertFunction( accessNode
);
843 //dummy.appendTo( accessNode );
844 range
.moveToPosition( accessNode
, CKEDITOR
.POSITION_AFTER_START
);
845 editor
.getSelection().selectRanges( [ range
] );
846 that
.hotNode
= accessNode
;
848 doSave
&& editor
.fire( 'saveSnapshot' );
851 // Access focus space on demand by taking an element under the caret as a reference.
852 // The space is accessed provided the element under the caret is trigger AND:
854 // 1. First/last-child of its parent:
855 // +----------------------- Parent element -+
856 // | +------------------------------ DIV -+ | <-- Access before
859 // | +------------------------------------+ | <-- Access after
860 // +----------------------------------------+
864 // 2. It has a direct sibling element, which is also a trigger:
865 // +-------------------------------- DIV#1 -+
868 // +----------------------------------------+
870 // +-------------------------------- DIV#2 -+
873 // +----------------------------------------+
877 // 3. It has a direct sibling, which is a trigger and has a valid neighbour trigger,
878 // but belongs to dtd.$.empty/nonEditable:
879 // +------------------------------------ P -+
882 // +----------------------------------------+
883 // +----------------------------------- HR -+
885 // +-------------------------------- DIV#2 -+
888 // +----------------------------------------+
890 function accessFocusSpaceCmd( that
, insertAfter
) {
893 modes: { wysiwyg: 1 },
896 // Inserts line (accessNode) at the position by taking target node as a reference.
897 function doAccess( target
) {
898 // Remove old hotNode under certain circumstances.
899 var hotNodeChar
= ( env
.ie
&& env
.version
< 9 ? ' ' : WHITE_SPACE
),
900 removeOld
= that
.hotNode
&& // Old hotNode must exist.
901 that
.hotNode
.getText() == hotNodeChar
&& // Old hotNode hasn't been changed.
902 that
.element
.equals( that
.hotNode
) && // Caret is inside old hotNode.
903 // Command is executed in the same direction.
904 that
.lastCmdDirection
=== !!insertAfter
; // jshint ignore:line
906 accessFocusSpace( that
, function( accessNode
) {
907 if ( removeOld
&& that
.hotNode
)
908 that
.hotNode
.remove();
910 accessNode
[ insertAfter
? 'insertAfter' : 'insertBefore' ]( target
);
912 // Make this element distinguishable. Also remember the direction
913 // it's been inserted into document.
914 accessNode
.setAttributes( {
915 'data-cke-magicline-hot': 1,
916 'data-cke-magicline-dir': !!insertAfter
919 // Save last direction of the command (is insertAfter?).
920 that
.lastCmdDirection
= !!insertAfter
;
923 if ( !env
.ie
&& that
.enterMode
!= CKEDITOR
.ENTER_BR
)
924 that
.hotNode
.scrollIntoView();
926 // Detach the line if was visible (previously triggered by mouse).
930 return function( editor
) {
931 var selected
= editor
.getSelection().getStartElement(),
934 // (#9833) Go down to the closest non-inline element in DOM structure
935 // since inline elements don't participate in in magicline.
936 selected
= selected
.getAscendant( DTD_BLOCK
, 1 );
938 // Stop if selected is a child of a tabu-list element.
939 if ( isInTabu( that
, selected
) )
942 // Sometimes it may happen that there's no parent block below selected element
943 // or, for example, getAscendant reaches editable or editable parent.
944 // We must avoid such pathological cases.
945 if ( !selected
|| selected
.equals( that
.editable
) || selected
.contains( that
.editable
) )
948 // Executing the command directly in nested editable should
949 // access space before/after it.
950 if ( ( limit
= getClosestEditableLimit( selected
) ) && limit
.getAttribute( 'contenteditable' ) == 'false' )
953 // That holds element from mouse. Replace it with the
954 // element under the caret.
955 that
.element
= selected
;
957 // (3.) Handle the following cases where selected neighbour
958 // is a trigger inaccessible for the caret AND:
959 // - Is first/last-child
961 // - Has a sibling, which is also a trigger.
962 var neighbor
= getNonEmptyNeighbour( that
, selected
, !insertAfter
),
965 // Check for a neighbour that belongs to triggers.
966 // Consider only non-accessible elements (they cannot have any children)
967 // since they cannot be given a caret inside, to run the command
968 // the regular way (1. & 2.).
970 isHtml( neighbor
) && neighbor
.is( that
.triggers
) && neighbor
.is( DTD_NONACCESSIBLE
) &&
972 // Check whether neighbor is first/last-child.
973 !getNonEmptyNeighbour( that
, neighbor
, !insertAfter
) ||
974 // Check for a sibling of a neighbour that also is a trigger.
976 ( neighborSibling
= getNonEmptyNeighbour( that
, neighbor
, !insertAfter
) ) &&
977 isHtml( neighborSibling
) &&
978 neighborSibling
.is( that
.triggers
)
982 doAccess( neighbor
);
986 // Look for possible target element DOWN "selected" DOM branch (towards editable)
987 // that belong to that.triggers
988 var target
= getAscendantTrigger( that
, selected
);
990 // No HTML target -> no access.
991 if ( !isHtml( target
) )
994 // (1.) Target is first/last child -> access.
995 if ( !getNonEmptyNeighbour( that
, target
, !insertAfter
) ) {
1000 var sibling
= getNonEmptyNeighbour( that
, target
, !insertAfter
);
1002 // (2.) Target has a sibling that belongs to that.triggers -> access.
1003 if ( sibling
&& isHtml( sibling
) && sibling
.is( that
.triggers
) ) {
1012 function isLine( that
, node
) {
1013 if ( !( node
&& node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.$ ) )
1016 var line
= that
.line
;
1018 return line
.wrap
.equals( node
) || line
.wrap
.contains( node
);
1021 // Is text node containing white-spaces only?
1022 var isEmptyTextNode
= CKEDITOR
.dom
.walker
.whitespaces();
1024 // Is fully visible HTML node?
1025 function isHtml( node
) {
1026 return node
&& node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.$; // IE requires that
1029 function isFloated( element
) {
1030 if ( !isHtml( element
) )
1033 var options
= { left: 1, right: 1, center: 1 };
1035 return !!( options
[ element
.getComputedStyle( 'float' ) ] || options
[ element
.getAttribute( 'align' ) ] );
1038 function isFlowBreaker( element
) {
1039 if ( !isHtml( element
) )
1042 return isPositioned( element
) || isFloated( element
);
1045 // Isn't node of NODE_COMMENT type?
1046 var isComment
= CKEDITOR
.dom
.walker
.nodeType( CKEDITOR
.NODE_COMMENT
);
1048 function isPositioned( element
) {
1049 return !!{ absolute: 1, fixed: 1 }[ element
.getComputedStyle( 'position' ) ];
1053 function isTextNode( node
) {
1054 return node
&& node
.type
== CKEDITOR
.NODE_TEXT
;
1057 function isTrigger( that
, element
) {
1058 return isHtml( element
) ? element
.is( that
.triggers
) : null;
1061 function isInTabu( that
, element
) {
1065 var parents
= element
.getParents( 1 );
1067 for ( var i
= parents
.length
; i
-- ; ) {
1068 for ( var j
= that
.tabuList
.length
; j
-- ; ) {
1069 if ( parents
[ i
].hasAttribute( that
.tabuList
[ j
] ) )
1077 // This function checks vertically is there's a relevant child between element's edge
1079 // \-> Table contents are omitted.
1080 function isChildBetweenPointerAndEdge( that
, parent
, edgeBottom
) {
1081 var edgeChild
= parent
[ edgeBottom
? 'getLast' : 'getFirst' ]( function( node
) {
1082 return that
.isRelevant( node
) && !node
.is( DTD_TABLECONTENT
);
1088 updateSize( that
, edgeChild
);
1090 return edgeBottom
? edgeChild
.size
.top
> that
.mouse
.y : edgeChild
.size
.bottom
< that
.mouse
.y
;
1093 // This method handles edge cases:
1094 // \-> Mouse is around upper or lower edge of view pane.
1095 // \-> Also scroll position is either minimal or maximal.
1096 // \-> It's OK to show LOOK_TOP(BOTTOM) type line.
1098 // This trigger doesn't need additional post-filtering.
1100 // +----------------------------- Editable -+ /--
1101 // | +---------------------- First child -+ | | <-- Top edge (first child)
1103 // | | | | | * Mouse activation area *
1105 // | | ... | | \-- Top edge + trigger offset
1109 // | | ... | | /-- Bottom edge - trigger offset
1111 // | | | | | * Mouse activation area *
1113 // | +----------------------- Last child -+ | | <-- Bottom edge (last child)
1114 // +----------------------------------------+ \--
1116 function triggerEditable( that
) {
1117 that
.debug
.groupStart( 'triggerEditable' ); // %REMOVE_LINE%
1119 var editable
= that
.editable
,
1122 triggerOffset
= that
.triggerOffset
,
1125 // Update editable dimensions.
1126 updateEditableSize( that
);
1128 // This flag determines whether checking bottom trigger.
1129 var bottomTrigger
= mouse
.y
> (
1130 that
.inInlineMode
? (
1131 view
.editable
.top
+ view
.editable
.height
/ 2
1133 // This is to handle case when editable.height / 2 <<< pane.height.
1134 Math
.min( view
.editable
.height
, view
.pane
.height
) / 2
1138 // Edge node according to bottomTrigger.
1139 edgeNode
= editable
[ bottomTrigger
? 'getLast' : 'getFirst' ]( function( node
) {
1140 return !( isEmptyTextNode( node
) || isComment( node
) );
1143 // There's no edge node. Abort.
1145 that
.debug
.logEnd( 'ABORT. No edge node found.' ); // %REMOVE_LINE%
1149 // If the edgeNode in editable is ML, get the next one.
1150 if ( isLine( that
, edgeNode
) ) {
1151 edgeNode
= that
.line
.wrap
[ bottomTrigger
? 'getPrevious' : 'getNext' ]( function( node
) {
1152 return !( isEmptyTextNode( node
) || isComment( node
) );
1156 // Exclude bad nodes (no ML needed then):
1157 // \-> Edge node is text.
1158 // \-> Edge node is floated, etc.
1160 // Edge node *must be* a valid trigger at this stage as well.
1161 if ( !isHtml( edgeNode
) || isFlowBreaker( edgeNode
) || !isTrigger( that
, edgeNode
) ) {
1162 that
.debug
.logEnd( 'ABORT. Invalid edge node.' ); // %REMOVE_LINE%
1166 // Update size of edge node. Dimensions will be necessary.
1167 updateSize( that
, edgeNode
);
1169 // Return appropriate trigger according to bottomTrigger.
1170 // \-> Top edge trigger case first.
1171 if ( !bottomTrigger
&& // Top trigger case.
1172 edgeNode
.size
.top
>= 0 && // Check if the first element is fully visible.
1173 inBetween( mouse
.y
, 0, edgeNode
.size
.top
+ triggerOffset
) ) { // Check if mouse in [0, edgeNode.top + triggerOffset].
1175 // Determine trigger look.
1176 triggerLook
= that
.inInlineMode
|| view
.scroll
.y
=== 0 ?
1177 LOOK_TOP : LOOK_NORMAL
;
1179 that
.debug
.logEnd( 'SUCCESS. Created box trigger. EDGE_TOP.' ); // %REMOVE_LINE%
1181 return new boxTrigger( [ null, edgeNode
,
1189 else if ( bottomTrigger
&&
1190 edgeNode
.size
.bottom
<= view
.pane
.height
&& // Check if the last element is fully visible
1191 inBetween( mouse
.y
, // Check if mouse in...
1192 edgeNode
.size
.bottom
- triggerOffset
, view
.pane
.height
) ) { // [ edgeNode.bottom - triggerOffset, paneHeight ]
1194 // Determine trigger look.
1195 triggerLook
= that
.inInlineMode
||
1196 inBetween( edgeNode
.size
.bottom
, view
.pane
.height
- triggerOffset
, view
.pane
.height
) ?
1197 LOOK_BOTTOM : LOOK_NORMAL
;
1199 that
.debug
.logEnd( 'SUCCESS. Created box trigger. EDGE_BOTTOM.' ); // %REMOVE_LINE%
1201 return new boxTrigger( [ edgeNode
, null,
1208 that
.debug
.logEnd( 'ABORT. No trigger created.' ); // %REMOVE_LINE%
1212 // This method covers cases *inside* of an element:
1213 // \-> The pointer is in the top (bottom) area of an element and there's
1214 // HTML node before (after) this element.
1215 // \-> An element being the first or last child of its parent.
1217 // +----------------------- Parent element -+
1218 // | +----------------------- Element #1 -+ | /--
1219 // | | | | | * Mouse activation area (as first child) *
1222 // | | | | | * Mouse activation area (Element #2) *
1223 // | +------------------------------------+ | \--
1225 // | +----------------------- Element #2 -+ | /--
1226 // | | | | | * Mouse activation area (Element #1) *
1229 // | +------------------------------------+ |
1231 // | Text node is here. |
1233 // | +----------------------- Element #3 -+ |
1237 // | | | | | * Mouse activation area (as last child) *
1238 // | +------------------------------------+ | \--
1239 // +----------------------------------------+
1241 function triggerEdge( that
) {
1242 that
.debug
.groupStart( 'triggerEdge' ); // %REMOVE_LINE%
1244 var mouse
= that
.mouse
,
1246 triggerOffset
= that
.triggerOffset
;
1248 // Get the ascendant trigger basing on elementFromMouse.
1249 var element
= getAscendantTrigger( that
);
1251 that
.debug
.logElements( [ element
], [ 'Ascendant trigger' ], 'First stage' ); // %REMOVE_LINE%
1253 // Abort if there's no appropriate element.
1255 that
.debug
.logEnd( 'ABORT. No element, element is editable or element contains editable.' ); // %REMOVE_LINE%
1259 // Dimensions will be necessary.
1260 updateSize( that
, element
);
1262 // If triggerOffset is larger than a half of element's height,
1263 // use an offset of 1/2 of element's height. If the offset wasn't reduced,
1264 // top area would cover most (all) cases.
1265 var fixedOffset
= Math
.min( triggerOffset
,
1266 0 | ( element
.size
.outerHeight
/ 2 ) ),
1268 // This variable will hold the trigger to be returned.
1272 // This flag determines whether dealing with a bottom trigger.
1276 if ( inBetween( mouse
.y
, element
.size
.top
- 1, element
.size
.top
+ fixedOffset
) )
1277 bottomTrigger
= false;
1278 // \-> Bottom trigger.
1279 else if ( inBetween( mouse
.y
, element
.size
.bottom
- fixedOffset
, element
.size
.bottom
+ 1 ) )
1280 bottomTrigger
= true;
1281 // \-> Abort. Not in a valid trigger space.
1283 that
.debug
.logEnd( 'ABORT. Not around of any edge.' ); // %REMOVE_LINE%
1287 // Reject wrong elements.
1288 // \-> Reject an element which is a flow breaker.
1289 // \-> Reject an element which has a child above/below the mouse pointer.
1290 // \-> Reject an element which belongs to list items.
1292 isFlowBreaker( element
) ||
1293 isChildBetweenPointerAndEdge( that
, element
, bottomTrigger
) ||
1294 element
.getParent().is( DTD_LISTITEM
)
1296 that
.debug
.logEnd( 'ABORT. element is wrong', element
); // %REMOVE_LINE%
1300 // Get sibling according to bottomTrigger.
1301 var elementSibling
= getNonEmptyNeighbour( that
, element
, !bottomTrigger
);
1303 // No sibling element.
1304 // This is a first or last child case.
1305 if ( !elementSibling
) {
1306 // No need to reject the element as it has already been done before.
1307 // Prepare a trigger.
1309 // Determine trigger look.
1310 if ( element
.equals( that
.editable
[ bottomTrigger
? 'getLast' : 'getFirst' ]( that
.isRelevant
) ) ) {
1311 updateEditableSize( that
);
1314 bottomTrigger
&& inBetween( mouse
.y
,
1315 element
.size
.bottom
- fixedOffset
, view
.pane
.height
) &&
1316 inBetween( element
.size
.bottom
, view
.pane
.height
- fixedOffset
, view
.pane
.height
)
1318 triggerLook
= LOOK_BOTTOM
;
1319 } else if ( inBetween( mouse
.y
, 0, element
.size
.top
+ fixedOffset
) ) {
1320 triggerLook
= LOOK_TOP
;
1323 triggerLook
= LOOK_NORMAL
;
1326 triggerSetup
= [ null, element
][ bottomTrigger
? 'reverse' : 'concat' ]().concat( [
1327 bottomTrigger
? EDGE_BOTTOM : EDGE_TOP
,
1330 element
.equals( that
.editable
[ bottomTrigger
? 'getLast' : 'getFirst' ]( that
.isRelevant
) ) ?
1331 ( bottomTrigger
? LOOK_BOTTOM : LOOK_TOP
) : LOOK_NORMAL
1334 that
.debug
.log( 'Configured edge trigger of ' + ( bottomTrigger
? 'EDGE_BOTTOM' : 'EDGE_TOP' ) ); // %REMOVE_LINE%
1337 // Abort. Sibling is a text element.
1338 else if ( isTextNode( elementSibling
) ) {
1339 that
.debug
.logEnd( 'ABORT. Sibling is non-empty text element' ); // %REMOVE_LINE%
1343 // Check if the sibling is a HTML element.
1344 // If so, create an TYPE_EDGE, EDGE_MIDDLE trigger.
1345 else if ( isHtml( elementSibling
) ) {
1346 // Reject wrong elementSiblings.
1347 // \-> Reject an elementSibling which is a flow breaker.
1348 // \-> Reject an elementSibling which isn't a trigger.
1349 // \-> Reject an elementSibling which belongs to list items.
1351 isFlowBreaker( elementSibling
) ||
1352 !isTrigger( that
, elementSibling
) ||
1353 elementSibling
.getParent().is( DTD_LISTITEM
)
1355 that
.debug
.logEnd( 'ABORT. elementSibling is wrong', elementSibling
); // %REMOVE_LINE%
1359 // Prepare a trigger.
1360 triggerSetup
= [ elementSibling
, element
][ bottomTrigger
? 'reverse' : 'concat' ]().concat( [
1365 that
.debug
.log( 'Configured edge trigger of EDGE_MIDDLE' ); // %REMOVE_LINE%
1368 if ( 0 in triggerSetup
) {
1369 that
.debug
.logEnd( 'SUCCESS. Returning a trigger.' ); // %REMOVE_LINE%
1370 return new boxTrigger( triggerSetup
);
1373 that
.debug
.logEnd( 'ABORT. No trigger generated.' ); // %REMOVE_LINE%
1377 // Checks iteratively up and down in search for elements using elementFromMouse method.
1378 // Useful if between two triggers.
1380 // +----------------------- Parent element -+
1381 // | +----------------------- Element #1 -+ |
1385 // | +------------------------------------+ |
1388 // | . +-- Floated -+ | |
1389 // | | | | | | * Mouse activation area *
1390 // | | | IGNORE | | |
1391 // | X | | | | Method searches vertically for sibling elements.
1392 // | | +------------+ | | Start point is X (mouse-y coordinate).
1393 // | | | | Floated elements, comments and empty text nodes are omitted.
1397 // | +----------------------- Element #2 -+ |
1402 // | +------------------------------------+ |
1403 // +----------------------------------------+
1405 var triggerExpand
= ( function() {
1406 // The heart of the procedure. This method creates triggers that are
1407 // filtered by expandFilter method.
1408 function expandEngine( that
) {
1409 that
.debug
.groupStart( 'expandEngine' ); // %REMOVE_LINE%
1411 var startElement
= that
.element
,
1412 upper
, lower
, trigger
;
1414 if ( !isHtml( startElement
) || startElement
.contains( that
.editable
) ) {
1415 that
.debug
.logEnd( 'ABORT. No start element, or start element contains editable.' ); // %REMOVE_LINE%
1419 // Stop searching if element is in non-editable branch of DOM.
1420 if ( startElement
.isReadOnly() )
1423 trigger
= verticalSearch( that
,
1424 function( current
, startElement
) {
1425 return !startElement
.equals( current
); // stop when start element and the current one differ
1426 }, function( that
, mouse
) {
1427 return elementFromMouse( that
, true, mouse
);
1430 upper
= trigger
.upper
,
1431 lower
= trigger
.lower
;
1433 that
.debug
.logElements( [ upper
, lower
], [ 'Upper', 'Lower' ], 'Pair found' ); // %REMOVE_LINE%
1435 // Success: two siblings have been found
1436 if ( areSiblings( that
, upper
, lower
) ) {
1437 that
.debug
.logEnd( 'SUCCESS. Expand trigger created.' ); // %REMOVE_LINE%
1438 return trigger
.set( EDGE_MIDDLE
, TYPE_EXPAND
);
1441 that
.debug
.logElements( [ startElement
, upper
, lower
], // %REMOVE_LINE%
1442 [ 'Start', 'Upper', 'Lower' ], 'Post-processing' ); // %REMOVE_LINE%
1444 // Danger. Dragons ahead.
1445 // No siblings have been found during previous phase, post-processing may be necessary.
1446 // We can traverse DOM until a valid pair of elements around the pointer is found.
1448 // Prepare for post-processing:
1449 // 1. Determine if upper and lower are children of startElement.
1450 // 1.1. If so, find their ascendants that are closest to startElement (one level deeper than startElement).
1451 // 1.2. Otherwise use first/last-child of the startElement as upper/lower. Why?:
1452 // a) upper/lower belongs to another branch of the DOM tree.
1453 // b) verticalSearch encountered an edge of the viewport and failed.
1454 // 1.3. Make sure upper and lower still exist. Why?:
1455 // a) Upper and lower may be not belong to the branch of the startElement (may not exist at all) and
1456 // startElement has no children.
1457 // 2. Perform the post-processing.
1458 // 2.1. Gather dimensions of an upper element.
1459 // 2.2. Abort if lower edge of upper is already under the mouse pointer. Why?:
1460 // a) We expect upper to be above and lower below the mouse pointer.
1461 // 3. Perform iterative search while upper != lower.
1462 // 3.1. Find the upper-next element. If there's no such element, break current search. Why?:
1463 // a) There's no point in further search if there are only text nodes ahead.
1464 // 3.2. Calculate the distance between the middle point of ( upper, upperNext ) and mouse-y.
1465 // 3.3. If the distance is shorter than the previous best, save it (save upper, upperNext as well).
1466 // 3.4. If the optimal pair is found, assign it back to the trigger.
1469 if ( upper
&& startElement
.contains( upper
) ) {
1470 while ( !upper
.getParent().equals( startElement
) )
1471 upper
= upper
.getParent();
1473 upper
= startElement
.getFirst( function( node
) {
1474 return expandSelector( that
, node
);
1478 if ( lower
&& startElement
.contains( lower
) ) {
1479 while ( !lower
.getParent().equals( startElement
) )
1480 lower
= lower
.getParent();
1482 lower
= startElement
.getLast( function( node
) {
1483 return expandSelector( that
, node
);
1488 if ( !upper
|| !lower
) {
1489 that
.debug
.logEnd( 'ABORT. There is no upper or no lower element.' ); // %REMOVE_LINE%
1494 updateSize( that
, upper
);
1495 updateSize( that
, lower
);
1497 if ( !checkMouseBetweenElements( that
, upper
, lower
) ) {
1498 that
.debug
.logEnd( 'ABORT. Mouse is already above upper or below lower.' ); // %REMOVE_LINE%
1502 var minDistance
= Number
.MAX_VALUE
,
1503 currentDistance
, upperNext
, minElement
, minElementNext
;
1505 while ( lower
&& !lower
.equals( upper
) ) {
1507 if ( !( upperNext
= upper
.getNext( that
.isRelevant
) ) )
1511 currentDistance
= Math
.abs( getMidpoint( that
, upper
, upperNext
) - that
.mouse
.y
);
1514 if ( currentDistance
< minDistance
) {
1515 minDistance
= currentDistance
;
1517 minElementNext
= upperNext
;
1521 updateSize( that
, upper
);
1524 that
.debug
.logElements( [ minElement
, minElementNext
], // %REMOVE_LINE%
1525 [ 'Min', 'MinNext' ], 'Post-processing results' ); // %REMOVE_LINE%
1528 if ( !minElement
|| !minElementNext
) {
1529 that
.debug
.logEnd( 'ABORT. No Min or MinNext' ); // %REMOVE_LINE%
1533 if ( !checkMouseBetweenElements( that
, minElement
, minElementNext
) ) {
1534 that
.debug
.logEnd( 'ABORT. Mouse is already above minElement or below minElementNext.' ); // %REMOVE_LINE%
1538 // An element of minimal distance has been found. Assign it to the trigger.
1539 trigger
.upper
= minElement
;
1540 trigger
.lower
= minElementNext
;
1542 // Success: post-processing revealed a pair of elements.
1543 that
.debug
.logEnd( 'SUCCESSFUL post-processing. Trigger created.' ); // %REMOVE_LINE%
1544 return trigger
.set( EDGE_MIDDLE
, TYPE_EXPAND
);
1547 // This is default element selector used by the engine.
1548 function expandSelector( that
, node
) {
1549 return !( isTextNode( node
) ||
1550 isComment( node
) ||
1551 isFlowBreaker( node
) ||
1552 isLine( that
, node
) ||
1553 ( node
.type
== CKEDITOR
.NODE_ELEMENT
&& node
.$ && node
.is( 'br' ) ) );
1556 // This method checks whether mouse-y is between the top edge of upper
1557 // and bottom edge of lower.
1559 // NOTE: This method assumes that updateSize has already been called
1560 // for the elements and is up-to-date.
1562 // +---------------------------- Upper -+ /--
1564 // +------------------------------------+ |
1568 // X | * Return true for mouse-y in this range *
1572 // +---------------------------- Lower -+ |
1574 // +------------------------------------+ \--
1576 function checkMouseBetweenElements( that
, upper
, lower
) {
1577 return inBetween( that
.mouse
.y
, upper
.size
.top
, lower
.size
.bottom
);
1580 // A method for trigger filtering. Accepts or rejects trigger pairs
1581 // by their location in DOM etc.
1582 function expandFilter( that
, trigger
) {
1583 that
.debug
.groupStart( 'expandFilter' ); // %REMOVE_LINE%
1585 var upper
= trigger
.upper
,
1586 lower
= trigger
.lower
;
1589 !upper
|| !lower
|| // NOT: EDGE_MIDDLE trigger ALWAYS has two elements.
1590 isFlowBreaker( lower
) || isFlowBreaker( upper
) || // NOT: one of the elements is floated or positioned
1591 lower
.equals( upper
) || upper
.equals( lower
) || // NOT: two trigger elements, one equals another.
1592 lower
.contains( upper
) || upper
.contains( lower
)
1593 ) { // NOT: two trigger elements, one contains another.
1594 that
.debug
.logEnd( 'REJECTED. No upper or no lower or they contain each other.' ); // %REMOVE_LINE%
1599 // YES: two trigger elements, pure siblings.
1600 else if ( isTrigger( that
, upper
) && isTrigger( that
, lower
) && areSiblings( that
, upper
, lower
) ) {
1601 that
.debug
.logElementsEnd( [ upper
, lower
], // %REMOVE_LINE%
1602 [ 'upper', 'lower' ], 'APPROVED EDGE_MIDDLE' ); // %REMOVE_LINE%
1607 that
.debug
.logElementsEnd( [ upper
, lower
], // %REMOVE_LINE%
1608 [ 'upper', 'lower' ], 'Rejected unknown pair' ); // %REMOVE_LINE%
1613 // Simple wrapper for expandEngine and expandFilter.
1614 return function( that
) {
1615 that
.debug
.groupStart( 'triggerExpand' ); // %REMOVE_LINE%
1617 var trigger
= expandEngine( that
);
1619 that
.debug
.groupEnd(); // %REMOVE_LINE%
1620 return trigger
&& expandFilter( that
, trigger
) ? trigger : null;
1624 // Collects dimensions of an element.
1625 var sizePrefixes
= [ 'top', 'left', 'right', 'bottom' ];
1627 function getSize( that
, element
, ignoreScroll
, force
) {
1628 var docPosition
= element
.getDocumentPosition(),
1634 for ( var i
= sizePrefixes
.length
; i
--; ) {
1635 border
[ sizePrefixes
[ i
] ] = parseInt( getStyle( 'border-' + sizePrefixes
[ i
] + '-width' ), 10 ) || 0;
1636 padding
[ sizePrefixes
[ i
] ] = parseInt( getStyle( 'padding-' + sizePrefixes
[ i
] ), 10 ) || 0;
1637 margin
[ sizePrefixes
[ i
] ] = parseInt( getStyle( 'margin-' + sizePrefixes
[ i
] ), 10 ) || 0;
1640 // updateWindowSize if forced to do so OR NOT ignoring scroll.
1641 if ( !ignoreScroll
|| force
)
1642 updateWindowSize( that
, force
);
1644 box
.top
= docPosition
.y
- ( ignoreScroll
? 0 : that
.view
.scroll
.y
), box
.left
= docPosition
.x
- ( ignoreScroll
? 0 : that
.view
.scroll
.x
),
1646 // w/ borders and paddings.
1647 box
.outerWidth
= element
.$.offsetWidth
, box
.outerHeight
= element
.$.offsetHeight
,
1649 // w/o borders and paddings.
1650 box
.height
= box
.outerHeight
- ( padding
.top
+ padding
.bottom
+ border
.top
+ border
.bottom
), box
.width
= box
.outerWidth
- ( padding
.left
+ padding
.right
+ border
.left
+ border
.right
),
1652 box
.bottom
= box
.top
+ box
.outerHeight
, box
.right
= box
.left
+ box
.outerWidth
;
1654 if ( that
.inInlineMode
) {
1656 top: element
.$.scrollTop
,
1657 left: element
.$.scrollLeft
1665 ignoreScroll: ignoreScroll
1668 function getStyle( propertyName
) {
1669 return element
.getComputedStyle
.call( element
, propertyName
);
1673 function updateSize( that
, element
, ignoreScroll
) {
1674 if ( !isHtml( element
) ) // i.e. an element is hidden
1675 return ( element
.size
= null ); // -> reset size to make it useless for other methods
1677 if ( !element
.size
)
1680 // Abort if there was a similar query performed recently.
1681 // This kind of caching provides great performance improvement.
1682 else if ( element
.size
.ignoreScroll
== ignoreScroll
&& element
.size
.date
> new Date() - CACHE_TIME
) {
1683 that
.debug
.log( 'element.size: get from cache' ); // %REMOVE_LINE%
1687 that
.debug
.log( 'element.size: capture' ); // %REMOVE_LINE%
1689 return extend( element
.size
, getSize( that
, element
, ignoreScroll
), {
1694 // Updates that.view.editable object.
1695 // This one must be called separately outside of updateWindowSize
1696 // to prevent cyclic dependency getSize<->updateWindowSize.
1697 // It calls getSize with force flag to avoid getWindowSize cache (look: getSize).
1698 function updateEditableSize( that
, ignoreScroll
) {
1699 that
.view
.editable
= getSize( that
, that
.editable
, ignoreScroll
, true );
1702 function updateWindowSize( that
, force
) {
1706 var view
= that
.view
;
1708 if ( !force
&& view
&& view
.date
> new Date() - CACHE_TIME
) {
1709 that
.debug
.log( 'win.size: get from cache' ); // %REMOVE_LINE%
1713 that
.debug
.log( 'win.size: capturing' ); // %REMOVE_LINE%
1716 scroll
= win
.getScrollPosition(),
1717 paneSize
= win
.getViewPaneSize();
1719 extend( that
.view
, {
1723 width: that
.doc
.$.documentElement
.scrollWidth
- paneSize
.width
,
1724 height: that
.doc
.$.documentElement
.scrollHeight
- paneSize
.height
1727 width: paneSize
.width
,
1728 height: paneSize
.height
,
1729 bottom: paneSize
.height
+ scroll
.y
1735 // This method searches document vertically using given
1736 // select criterion until stop criterion is fulfilled.
1737 function verticalSearch( that
, stopCondition
, selectCriterion
, startElement
) {
1738 var upper
= startElement
,
1739 lower
= startElement
,
1743 viewPaneHeight
= that
.view
.pane
.height
,
1746 while ( mouse
.y
+ mouseStep
< viewPaneHeight
&& mouse
.y
- mouseStep
> 0 ) {
1748 upperFound
= stopCondition( upper
, startElement
);
1751 lowerFound
= stopCondition( lower
, startElement
);
1753 // Still not found...
1754 if ( !upperFound
&& mouse
.y
- mouseStep
> 0 )
1755 upper
= selectCriterion( that
, { x: mouse
.x
, y: mouse
.y
- mouseStep
} );
1757 if ( !lowerFound
&& mouse
.y
+ mouseStep
< viewPaneHeight
)
1758 lower
= selectCriterion( that
, { x: mouse
.x
, y: mouse
.y
+ mouseStep
} );
1760 if ( upperFound
&& lowerFound
)
1763 // Instead of ++ to reduce the number of invocations by half.
1764 // It's trades off accuracy in some edge cases for improved performance.
1768 return new boxTrigger( [ upper
, lower
, null, null ] );
1774 * Sets the default vertical distance between the edge of the element and the mouse pointer that
1775 * causes the magic line to appear. This option accepts a value in pixels, without the unit (for example:
1776 * `15` for 15 pixels).
1778 * Read more in the [documentation](#!/guide/dev_magicline)
1779 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).
1781 * // Changes the offset to 15px.
1782 * CKEDITOR.config.magicline_triggerOffset = 15;
1784 * @cfg {Number} [magicline_triggerOffset=30]
1785 * @member CKEDITOR.config
1786 * @see CKEDITOR.config#magicline_holdDistance
1790 * Defines the distance between the mouse pointer and the box within
1791 * which the magic line stays revealed and no other focus space is offered to be accessed.
1792 * This value is relative to {@link #magicline_triggerOffset}.
1794 * Read more in the [documentation](#!/guide/dev_magicline)
1795 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).
1797 * // Increases the distance to 80% of CKEDITOR.config.magicline_triggerOffset.
1798 * CKEDITOR.config.magicline_holdDistance = .8;
1800 * @cfg {Number} [magicline_holdDistance=0.5]
1801 * @member CKEDITOR.config
1802 * @see CKEDITOR.config#magicline_triggerOffset
1806 * Defines the default keystroke that accesses the closest unreachable focus space **before**
1807 * the caret (start of the selection). If there is no focus space available, the selection remains unchanged.
1809 * Read more in the [documentation](#!/guide/dev_magicline)
1810 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).
1812 * // Changes the default keystroke to "Ctrl + ,".
1813 * CKEDITOR.config.magicline_keystrokePrevious = CKEDITOR.CTRL + 188;
1815 * @cfg {Number} [magicline_keystrokePrevious=CKEDITOR.CTRL + CKEDITOR.SHIFT + 51 (CTRL + SHIFT + 3)]
1816 * @member CKEDITOR.config
1818 CKEDITOR
.config
.magicline_keystrokePrevious
= CKEDITOR
.CTRL
+ CKEDITOR
.SHIFT
+ 51; // CTRL + SHIFT + 3
1821 * Defines the default keystroke that accesses the closest unreachable focus space **after**
1822 * the caret (start of the selection). If there is no focus space available, the selection remains unchanged.
1824 * Read more in the [documentation](#!/guide/dev_magicline)
1825 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).
1827 * // Changes keystroke to "Ctrl + .".
1828 * CKEDITOR.config.magicline_keystrokeNext = CKEDITOR.CTRL + 190;
1830 * @cfg {Number} [magicline_keystrokeNext=CKEDITOR.CTRL + CKEDITOR.SHIFT + 52 (CTRL + SHIFT + 4)]
1831 * @member CKEDITOR.config
1833 CKEDITOR
.config
.magicline_keystrokeNext
= CKEDITOR
.CTRL
+ CKEDITOR
.SHIFT
+ 52; // CTRL + SHIFT + 4
1836 * Defines a list of attributes that, if assigned to some elements, prevent the magic line from being
1837 * used within these elements.
1839 * Read more in the [documentation](#!/guide/dev_magicline)
1840 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).
1842 * // Adds the "data-tabu" attribute to the magic line tabu list.
1843 * CKEDITOR.config.magicline_tabuList = [ 'data-tabu' ];
1845 * @cfg {Number} [magicline_tabuList=[ 'data-widget-wrapper' ]]
1846 * @member CKEDITOR.config
1850 * Defines the color of the magic line. The color may be adjusted to enhance readability.
1852 * Read more in the [documentation](#!/guide/dev_magicline)
1853 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).
1855 * // Changes magic line color to blue.
1856 * CKEDITOR.config.magicline_color = '#0000FF';
1858 * @cfg {String} [magicline_color='#FF0000']
1859 * @member CKEDITOR.config
1863 * Activates the special all-encompassing mode that considers all focus spaces between
1864 * {@link CKEDITOR.dtd#$block} elements as accessible by the magic line.
1866 * Read more in the [documentation](#!/guide/dev_magicline)
1867 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).
1869 * // Enables the greedy "put everywhere" mode.
1870 * CKEDITOR.config.magicline_everywhere = true;
1872 * @cfg {Boolean} [magicline_everywhere=false]
1873 * @member CKEDITOR.config