]> git.immae.eu Git - perso/Immae/Projets/packagist/ludivine-ckeditor-component.git/blob - sources/plugins/magicline/plugin.js
Update to 4.7.3
[perso/Immae/Projets/packagist/ludivine-ckeditor-component.git] / sources / plugins / magicline / plugin.js
1 /**
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
4 */
5
6 /**
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.
9 */
10
11 'use strict';
12
13 ( function() {
14 CKEDITOR.plugins.add( 'magicline', {
15 lang: 'af,ar,az,bg,ca,cs,cy,da,de,de-ch,el,en,en-gb,eo,es,es-mx,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%
16 init: initPlugin
17 } );
18
19 // Activates the box inside of an editor.
20 function initPlugin( editor ) {
21 // Configurables
22 var config = editor.config,
23 triggerOffset = config.magicline_triggerOffset || 30,
24 enterMode = config.enterMode,
25 that = {
26 // Global stuff is being initialized here.
27 editor: editor,
28 enterMode: enterMode,
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 }
35 },
36 scrollTimeout, checkMouseTimeoutPending, checkMouseTimer;
37
38 // %REMOVE_START%
39 // Internal DEBUG uses tools located in the topmost window.
40
41 // (http://dev.ckeditor.com/ticket/9701) Due to security limitations some browsers may throw
42 // errors when accessing window.top object. Do it safely first then.
43 try {
44 that.debug = window.top.DEBUG;
45 }
46 catch ( e ) {}
47
48 that.debug = that.debug || {
49 groupEnd: function() {},
50 groupStart: function() {},
51 log: 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() {}
60 };
61 // %REMOVE_END%
62
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.
68 };
69
70 editor.on( 'contentDom', addListeners, this );
71
72 function addListeners() {
73 var editable = editor.editable(),
74 doc = editor.document,
75 win = editor.window;
76
77 // Global stuff is being initialized here.
78 extend( that, {
79 editable: editable,
80 inInlineMode: editable.isInline(),
81 doc: doc,
82 win: win,
83 hotNode: null
84 }, true );
85
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();
89
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 ) )
93 return;
94
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 ) ) {
98 editable.setStyles( {
99 position: 'relative',
100 top: null,
101 left: null
102 } );
103 }
104 // Enable the box. Let it produce children elements, initialize
105 // event handlers and own methods.
106 initLine.call( this, that );
107
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 );
112
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() {
117 that.line.detach();
118 } );
119
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() ) {
127 that.line.detach();
128
129 // Restore line in the last listener for 'getData'.
130 editor.once( 'getData', function() {
131 that.line.attach();
132 }, null, null, 1000 );
133 }
134 }, null, null, 0 );
135
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' )
139 return;
140
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 ) {
144 var mouse = {
145 x: event.data.$.clientX,
146 y: event.data.$.clientY
147 };
148
149 updateWindowSize( that );
150 updateEditableSize( that, true );
151
152 var size = that.view.editable,
153 scroll = that.view.scroll;
154
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;
159 that.line.detach();
160 }
161 }
162
163 else {
164 clearTimeout( checkMouseTimer );
165 checkMouseTimer = null;
166 that.line.detach();
167 }
168 } );
169
170 // This one deactivates hidden mode of an editor which
171 // prevents the box from being shown.
172 editable.attachListener( editable, 'keyup', function() {
173 that.hiddenMode = 0;
174 that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
175 } );
176
177 editable.attachListener( editable, 'keydown', function( event ) {
178 if ( editor.mode != 'wysiwyg' )
179 return;
180
181 var keyStroke = event.data.getKeystroke();
182
183 switch ( keyStroke ) {
184 // Shift pressed
185 case 2228240: // IE
186 case 16:
187 that.hiddenMode = 1;
188 that.line.detach();
189 }
190
191 that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
192 } );
193
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;
200
201 if ( editor.mode != 'wysiwyg' || editor.readOnly || checkMouseTimer )
202 return;
203
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.
207 var mouse = {
208 x: event.data.$.clientX,
209 y: event.data.$.clientY
210 };
211
212 checkMouseTimer = setTimeout( function() {
213 checkMouse( mouse );
214 }, 30 ); // balances performance and accessibility
215 } );
216
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' )
221 return;
222
223 that.line.detach();
224
225 // To figure this out just look at the mouseup
226 // event handler below.
227 if ( env.webkit ) {
228 that.hiddenMode = 1;
229
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 )
235 that.hiddenMode = 0;
236 that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
237 }, 50 );
238
239 that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
240 }
241 } );
242
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' )
249 return;
250
251 that.line.detach();
252 that.hiddenMode = 1;
253 that.mouseDown = 1;
254
255 that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
256 } );
257
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() {
262 that.hiddenMode = 0;
263 that.mouseDown = 0;
264 that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%
265 } );
266
267 // Editor commands for accessing difficult focus spaces.
268 editor.addCommand( 'accessPreviousSpace', accessFocusSpaceCmd( that ) );
269 editor.addCommand( 'accessNextSpace', accessFocusSpaceCmd( that, true ) );
270
271 editor.setKeystroke( [
272 [ config.magicline_keystrokePrevious, 'accessPreviousSpace' ],
273 [ config.magicline_keystrokeNext, 'accessNextSpace' ]
274 ] );
275
276 // Revert magicline hot node on undo/redo.
277 editor.on( 'loadSnapshot', function() {
278 var elements, element, i;
279
280 for ( var t in { p: 1, br: 1, div: 1 } ) {
281 // document.find is not available in QM (http://dev.ckeditor.com/ticket/11149).
282 elements = editor.document.getElementsByTag( t );
283
284 for ( i = elements.count(); i--; ) {
285 if ( ( element = elements.getItem( i ) ).data( 'cke-magicline-hot' ) ) {
286 // Restore hotNode
287 that.hotNode = element;
288 // Restore last access direction
289 that.lastCmdDirection = element.data( 'cke-magicline-dir' ) === 'true' ? true : false;
290
291 return;
292 }
293 }
294 }
295 } );
296
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%
304
305 that.mouse = mouse;
306 that.trigger = null;
307
308 checkMouseTimer = null;
309 updateWindowSize( that );
310
311 if (
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.
317 ) {
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();
323 }
324
325 // Otherwise remove the box
326 else {
327 that.trigger = null;
328 that.line.detach();
329 }
330
331 that.debug.showTrigger( that.trigger ); // %REMOVE_LINE%
332 that.debug.mousePos( mouse.y, that.element ); // %REMOVE_LINE%
333
334 checkMouseTimeoutPending = false;
335 }
336
337 that.debug.stopTimer(); // %REMOVE_LINE%
338 that.debug.groupEnd(); // %REMOVE_LINE%
339 }
340
341 // This one allows testing and debugging. It reveals some
342 // inner methods to the world.
343 this.backdoor = {
344 accessFocusSpace: accessFocusSpace,
345 boxTrigger: boxTrigger,
346 isLine: isLine,
347 getAscendantTrigger: getAscendantTrigger,
348 getNonEmptyNeighbour: getNonEmptyNeighbour,
349 getSize: getSize,
350 that: that,
351 triggerEdge: triggerEdge,
352 triggerEditable: triggerEditable,
353 triggerExpand: triggerExpand
354 };
355 }
356 }
357
358 // Some shorthands for common methods to save bytes
359 var extend = CKEDITOR.tools.extend,
360 newElement = CKEDITOR.dom.element,
361 newElementFromHtml = newElement.createFromHtml,
362 env = CKEDITOR.env,
363 env_ie8 = CKEDITOR.env.ie && CKEDITOR.env.version < 9,
364 dtd = CKEDITOR.dtd,
365
366 // Global object associating enter modes with elements.
367 enterElements = {},
368
369 // Constant values, types and so on.
370 EDGE_TOP = 128,
371 EDGE_BOTTOM = 64,
372 EDGE_MIDDLE = 32,
373 TYPE_EDGE = 16,
374 TYPE_EXPAND = 8,
375 LOOK_TOP = 4,
376 LOOK_BOTTOM = 2,
377 LOOK_NORMAL = 1,
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,
383
384 // Minimum time that must elapse between two update*Size calls.
385 // It prevents constant getComuptedStyle calls and improves performance.
386 CACHE_TIME = 100,
387
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>';
392
393 enterElements[ CKEDITOR.ENTER_BR ] = 'br';
394 enterElements[ CKEDITOR.ENTER_P ] = 'p';
395 enterElements[ CKEDITOR.ENTER_DIV ] = 'div';
396
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 ) );
400 } ) );
401 }
402
403 // boxTrigger is an abstract type which describes
404 // the relationship between elements that may result
405 // in showing the box.
406 //
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 ) );
414 }
415
416 boxTrigger.prototype = {
417 set: function( edge, type, look ) {
418 this.properties = edge + type + ( look || LOOK_NORMAL );
419 return this;
420 },
421
422 is: function( property ) {
423 return ( this.properties & property ) == property;
424 }
425 };
426
427 var elementFromMouse = ( function() {
428 function elementFromPoint( doc, mouse ) {
429 var pointedElement = doc.$.elementFromPoint( mouse.x, mouse.y );
430
431 // IE9QM: from times to times it will return an empty object on scroll bar hover. (http://dev.ckeditor.com/ticket/12185)
432 return pointedElement && pointedElement.nodeType ?
433 new CKEDITOR.dom.element( pointedElement ) :
434 null;
435 }
436
437 return function( that, ignoreBox, forceMouse ) {
438 if ( !that.mouse )
439 return null;
440
441 var doc = that.doc,
442 lineWrap = that.line.wrap,
443 mouse = forceMouse || that.mouse,
444 // Note: element might be null.
445 element = elementFromPoint( doc, mouse );
446
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 ) ) {
451 lineWrap.hide();
452 element = elementFromPoint( doc, mouse );
453 lineWrap.show();
454 }
455
456 // Return nothing if:
457 // \-> Element is not HTML.
458 if ( !( element && element.type == CKEDITOR.NODE_ELEMENT && element.$ ) )
459 return null;
460
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 ) ) )
468 return null;
469 }
470
471 return element;
472 };
473 } )();
474
475 // Gets the closest parent node that belongs to triggers group.
476 function getAscendantTrigger( that ) {
477 var node = that.element,
478 trigger;
479
480 if ( node && isHtml( node ) ) {
481 trigger = node.getAscendant( that.triggers, true );
482
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) (http://dev.ckeditor.com/ticket/12009).
487 var limit = getClosestEditableLimit( trigger );
488
489 // Trigger in nested editable area.
490 if ( limit.getAttribute( 'contenteditable' ) == 'true' )
491 return trigger;
492 // Trigger in non-editable area.
493 else if ( limit.is( that.triggers ) )
494 return limit;
495 else
496 return null;
497 } else {
498 return null;
499 }
500 }
501
502 return null;
503 }
504
505 function getMidpoint( that, upper, lower ) {
506 updateSize( that, upper );
507 updateSize( that, lower );
508
509 var upperSizeBottom = upper.size.bottom,
510 lowerSizeTop = lower.size.top;
511
512 return upperSizeBottom && lowerSizeTop ? 0 | ( upperSizeBottom + lowerSizeTop ) / 2 : upperSizeBottom || lowerSizeTop;
513 }
514
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 ) );
523 } );
524
525 return node;
526 }
527
528 function inBetween( val, lower, upper ) {
529 return val > lower && val < upper;
530 }
531
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' ) )
537 return null;
538
539 if ( !includeSelf )
540 element = element.getParent();
541
542 while ( element ) {
543 if ( element.data( 'cke-editable' ) )
544 return null;
545
546 if ( element.hasAttribute( 'contenteditable' ) )
547 return element;
548
549 element = element.getParent();
550 }
551
552 return null;
553 }
554
555 // Access space line consists of a few elements (spans):
556 // \-> Line wrapper.
557 // \-> Line.
558 // \-> Line triangles: left triangle (LT), right triangle (RT).
559 // \-> Button handler (BTN).
560 //
561 // +--------------------------------------------------- line.wrap (span) -----+
562 // | +---------------------------------------------------- line (span) -----+ |
563 // | | +- LT \ +- BTN -+ / RT -+ | |
564 // | | | \ | | | / | | |
565 // | | | / | <__| | \ | | |
566 // | | +-----/ +-------+ \-----+ | |
567 // | +----------------------------------------------------------------------+ |
568 // +--------------------------------------------------------------------------+
569 //
570 function initLine( that ) {
571 var doc = that.doc,
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' );
575
576 extend( line, {
577
578 attach: function() {
579 // Only if not already attached
580 if ( !this.wrap.getParent() )
581 this.wrap.appendTo( that.editable, true );
582
583 return this;
584 },
585
586 // Looks are as follows: [ LOOK_TOP, LOOK_BOTTOM, LOOK_NORMAL ].
587 lineChildren: [
588 extend(
589 newElementFromHtml(
590 '<span title="' + that.editor.lang.magicline.title +
591 '" contenteditable="false">&#8629;</span>', doc
592 ), {
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;' : '' ),
597 looks: [
598 'top:-8px; border-radius: 2px;',
599 'top:-17px; border-radius: 2px 2px 0px 0px;',
600 'top:-1px; border-radius: 0px 0px 2px 2px;'
601 ]
602 }
603 ),
604 extend( newElementFromHtml( TRIANGLE_HTML, doc ), {
605 base: CSS_TRIANGLE + 'left:0px;border-left-color:' + that.boxColor + ';',
606 looks: [
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'
610 ]
611 } ),
612 extend( newElementFromHtml( TRIANGLE_HTML, doc ), {
613 base: CSS_TRIANGLE + 'right:0px;border-right-color:' + that.boxColor + ';',
614 looks: [
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'
618 ]
619 } )
620 ],
621
622 detach: function() {
623 // Detach only if already attached.
624 if ( this.wrap.getParent() )
625 this.wrap.remove();
626
627 return this;
628 },
629
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%
634
635 updateSize( that, this );
636 var offset = that.holdDistance,
637 size = this.size;
638
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%
642 return true;
643 }
644
645 that.debug.logEnd( 'Mouse isn\'t near.' ); // %REMOVE_LINE%
646 return false;
647 },
648
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.
651 place: function() {
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(),
659 styleSet = {};
660
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;
666
667 upper && updateSize( that, upper, true );
668 lower && updateSize( that, lower, true );
669 updateSize( that, parent, true );
670
671 // Yeah, that's gonna be useful in inline-mode case.
672 if ( that.inInlineMode )
673 updateEditableSize( that, true );
674
675 // Set X coordinate (left, right, width).
676 if ( parent.equals( editable ) ) {
677 styleSet.left = view.scroll.x;
678 styleSet.right = -view.scroll.x;
679 styleSet.width = '';
680 } else {
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;
683 styleSet.right = '';
684 }
685
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 );
691 else {
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.
696 else
697 styleSet.top = upper.size.bottom + upper.size.margin.bottom - lower.size.margin.top;
698 }
699 }
700 // Set Y coordinate (top) for single-edge trigger.
701 else if ( !upper )
702 styleSet.top = lower.size.top - lower.size.margin.top;
703 else if ( !lower ) {
704 styleSet.top = upper.size.bottom + upper.size.margin.bottom;
705 }
706
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
715 ) : (
716 view.pane.bottom - 1
717 );
718
719 this.look( LOOK_BOTTOM );
720 } else {
721 if ( that.inInlineMode )
722 styleSet.top -= view.editable.top + view.editable.border.top;
723
724 this.look( LOOK_NORMAL );
725 }
726
727 if ( that.inInlineMode ) {
728 // 1px bug here...
729 styleSet.top--;
730
731 // Consider the editable to be an element with overflow:scroll
732 // and non-zero scrollTop/scrollLeft value.
733 // For example: divarea editable. (http://dev.ckeditor.com/ticket/9383)
734 styleSet.top += view.editable.scroll.top;
735 styleSet.left += view.editable.scroll.left;
736 }
737
738 // Append `px` prefixes.
739 for ( var style in styleSet )
740 styleSet[ style ] = CKEDITOR.tools.cssLength( styleSet[ style ] );
741
742 this.setStyles( styleSet );
743 },
744
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 )
749 return;
750
751 for ( var i = this.lineChildren.length, child; i--; )
752 ( child = this.lineChildren[ i ] ).setAttribute( 'style', child.base + child.looks[ 0 | look / 2 ] );
753
754 this.oldLook = look;
755 },
756
757 wrap: new newElement( 'span', that.doc )
758
759 } );
760
761 // Insert children into the box.
762 for ( var i = line.lineChildren.length; i--; )
763 line.lineChildren[ i ].appendTo( line );
764
765 // Set default look of the box.
766 line.look( LOOK_NORMAL );
767
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 );
772
773 // Make the box unselectable.
774 line.unselectable();
775
776 // Handle accessSpace node insertion.
777 line.lineChildren[ 0 ].on( 'mouseup', function( event ) {
778 line.detach();
779
780 accessFocusSpace( that, function( accessNode ) {
781 // Use old trigger that was saved by 'place' method. Look: line.place
782 var trigger = that.line.trigger;
783
784 accessNode[ trigger.is( EDGE_TOP ) ? 'insertBefore' : 'insertAfter' ](
785 trigger.is( EDGE_TOP ) ? trigger.lower : trigger.upper );
786 }, true );
787
788 that.editor.focus();
789
790 if ( !env.ie && that.enterMode != CKEDITOR.ENTER_BR )
791 that.hotNode.scrollIntoView();
792
793 event.data.preventDefault( true );
794 } );
795
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 );
799 } );
800
801 that.line = line;
802 }
803
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 &nbsp; in IE.
808 //
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,
814 accessNode;
815
816 // IE requires text node of &nbsp; in ENTER_BR mode.
817 if ( env.ie && that.enterMode == CKEDITOR.ENTER_BR )
818 accessNode = that.doc.createText( WHITE_SPACE );
819
820 // In other cases a regular element is used.
821 else {
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 ),
825
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;
831
832 accessNode = new newElement( enterElements[ enterMode ], that.doc );
833
834 if ( !accessNode.is( 'br' ) ) {
835 var dummy = that.doc.createText( WHITE_SPACE );
836 dummy.appendTo( accessNode );
837 }
838 }
839
840 doSave && editor.fire( 'saveSnapshot' );
841
842 insertFunction( accessNode );
843 //dummy.appendTo( accessNode );
844 range.moveToPosition( accessNode, CKEDITOR.POSITION_AFTER_START );
845 editor.getSelection().selectRanges( [ range ] );
846 that.hotNode = accessNode;
847
848 doSave && editor.fire( 'saveSnapshot' );
849 }
850
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:
853 //
854 // 1. First/last-child of its parent:
855 // +----------------------- Parent element -+
856 // | +------------------------------ DIV -+ | <-- Access before
857 // | | Foo^ | |
858 // | | | |
859 // | +------------------------------------+ | <-- Access after
860 // +----------------------------------------+
861 //
862 // OR
863 //
864 // 2. It has a direct sibling element, which is also a trigger:
865 // +-------------------------------- DIV#1 -+
866 // | Foo^ |
867 // | |
868 // +----------------------------------------+
869 // <-- Access here
870 // +-------------------------------- DIV#2 -+
871 // | Bar |
872 // | |
873 // +----------------------------------------+
874 //
875 // OR
876 //
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 -+
880 // | Foo^ |
881 // | |
882 // +----------------------------------------+
883 // +----------------------------------- HR -+
884 // <-- Access here
885 // +-------------------------------- DIV#2 -+
886 // | Bar |
887 // | |
888 // +----------------------------------------+
889 //
890 function accessFocusSpaceCmd( that, insertAfter ) {
891 return {
892 canUndo: true,
893 modes: { wysiwyg: 1 },
894 exec: ( function() {
895
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
905
906 accessFocusSpace( that, function( accessNode ) {
907 if ( removeOld && that.hotNode )
908 that.hotNode.remove();
909
910 accessNode[ insertAfter ? 'insertAfter' : 'insertBefore' ]( target );
911
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
917 } );
918
919 // Save last direction of the command (is insertAfter?).
920 that.lastCmdDirection = !!insertAfter;
921 } );
922
923 if ( !env.ie && that.enterMode != CKEDITOR.ENTER_BR )
924 that.hotNode.scrollIntoView();
925
926 // Detach the line if was visible (previously triggered by mouse).
927 that.line.detach();
928 }
929
930 return function( editor ) {
931 var selected = editor.getSelection().getStartElement(),
932 limit;
933
934 // (http://dev.ckeditor.com/ticket/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 );
937
938 // Stop if selected is a child of a tabu-list element.
939 if ( isInTabu( that, selected ) )
940 return;
941
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 ) )
946 return;
947
948 // Executing the command directly in nested editable should
949 // access space before/after it.
950 if ( ( limit = getClosestEditableLimit( selected ) ) && limit.getAttribute( 'contenteditable' ) == 'false' )
951 selected = limit;
952
953 // That holds element from mouse. Replace it with the
954 // element under the caret.
955 that.element = selected;
956
957 // (3.) Handle the following cases where selected neighbour
958 // is a trigger inaccessible for the caret AND:
959 // - Is first/last-child
960 // OR
961 // - Has a sibling, which is also a trigger.
962 var neighbor = getNonEmptyNeighbour( that, selected, !insertAfter ),
963 neighborSibling;
964
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.).
969 if (
970 isHtml( neighbor ) && neighbor.is( that.triggers ) && neighbor.is( DTD_NONACCESSIBLE ) &&
971 (
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.
975 (
976 ( neighborSibling = getNonEmptyNeighbour( that, neighbor, !insertAfter ) ) &&
977 isHtml( neighborSibling ) &&
978 neighborSibling.is( that.triggers )
979 )
980 )
981 ) {
982 doAccess( neighbor );
983 return;
984 }
985
986 // Look for possible target element DOWN "selected" DOM branch (towards editable)
987 // that belong to that.triggers
988 var target = getAscendantTrigger( that, selected );
989
990 // No HTML target -> no access.
991 if ( !isHtml( target ) )
992 return;
993
994 // (1.) Target is first/last child -> access.
995 if ( !getNonEmptyNeighbour( that, target, !insertAfter ) ) {
996 doAccess( target );
997 return;
998 }
999
1000 var sibling = getNonEmptyNeighbour( that, target, !insertAfter );
1001
1002 // (2.) Target has a sibling that belongs to that.triggers -> access.
1003 if ( sibling && isHtml( sibling ) && sibling.is( that.triggers ) ) {
1004 doAccess( target );
1005 return;
1006 }
1007 };
1008 } )()
1009 };
1010 }
1011
1012 function isLine( that, node ) {
1013 if ( !( node && node.type == CKEDITOR.NODE_ELEMENT && node.$ ) )
1014 return false;
1015
1016 var line = that.line;
1017
1018 return line.wrap.equals( node ) || line.wrap.contains( node );
1019 }
1020
1021 // Is text node containing white-spaces only?
1022 var isEmptyTextNode = CKEDITOR.dom.walker.whitespaces();
1023
1024 // Is fully visible HTML node?
1025 function isHtml( node ) {
1026 return node && node.type == CKEDITOR.NODE_ELEMENT && node.$; // IE requires that
1027 }
1028
1029 function isFloated( element ) {
1030 if ( !isHtml( element ) )
1031 return false;
1032
1033 var options = { left: 1, right: 1, center: 1 };
1034
1035 return !!( options[ element.getComputedStyle( 'float' ) ] || options[ element.getAttribute( 'align' ) ] );
1036 }
1037
1038 function isFlowBreaker( element ) {
1039 if ( !isHtml( element ) )
1040 return false;
1041
1042 return isPositioned( element ) || isFloated( element );
1043 }
1044
1045 // Isn't node of NODE_COMMENT type?
1046 var isComment = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_COMMENT );
1047
1048 function isPositioned( element ) {
1049 return !!{ absolute: 1, fixed: 1 }[ element.getComputedStyle( 'position' ) ];
1050 }
1051
1052 // Is text node?
1053 function isTextNode( node ) {
1054 return node && node.type == CKEDITOR.NODE_TEXT;
1055 }
1056
1057 function isTrigger( that, element ) {
1058 return isHtml( element ) ? element.is( that.triggers ) : null;
1059 }
1060
1061 function isInTabu( that, element ) {
1062 if ( !element )
1063 return false;
1064
1065 var parents = element.getParents( 1 );
1066
1067 for ( var i = parents.length ; i-- ; ) {
1068 for ( var j = that.tabuList.length ; j-- ; ) {
1069 if ( parents[ i ].hasAttribute( that.tabuList[ j ] ) )
1070 return true;
1071 }
1072 }
1073
1074 return false;
1075 }
1076
1077 // This function checks vertically is there's a relevant child between element's edge
1078 // and the pointer.
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 );
1083 } );
1084
1085 if ( !edgeChild )
1086 return false;
1087
1088 updateSize( that, edgeChild );
1089
1090 return edgeBottom ? edgeChild.size.top > that.mouse.y : edgeChild.size.bottom < that.mouse.y;
1091 }
1092
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.
1097 //
1098 // This trigger doesn't need additional post-filtering.
1099 //
1100 // +----------------------------- Editable -+ /--
1101 // | +---------------------- First child -+ | | <-- Top edge (first child)
1102 // | | | | |
1103 // | | | | | * Mouse activation area *
1104 // | | | | |
1105 // | | ... | | \-- Top edge + trigger offset
1106 // | . . |
1107 // | |
1108 // | . . |
1109 // | | ... | | /-- Bottom edge - trigger offset
1110 // | | | | |
1111 // | | | | | * Mouse activation area *
1112 // | | | | |
1113 // | +----------------------- Last child -+ | | <-- Bottom edge (last child)
1114 // +----------------------------------------+ \--
1115 //
1116 function triggerEditable( that ) {
1117 that.debug.groupStart( 'triggerEditable' ); // %REMOVE_LINE%
1118
1119 var editable = that.editable,
1120 mouse = that.mouse,
1121 view = that.view,
1122 triggerOffset = that.triggerOffset,
1123 triggerLook;
1124
1125 // Update editable dimensions.
1126 updateEditableSize( that );
1127
1128 // This flag determines whether checking bottom trigger.
1129 var bottomTrigger = mouse.y > (
1130 that.inInlineMode ? (
1131 view.editable.top + view.editable.height / 2
1132 ) : (
1133 // This is to handle case when editable.height / 2 <<< pane.height.
1134 Math.min( view.editable.height, view.pane.height ) / 2
1135 )
1136 ),
1137
1138 // Edge node according to bottomTrigger.
1139 edgeNode = editable[ bottomTrigger ? 'getLast' : 'getFirst' ]( function( node ) {
1140 return !( isEmptyTextNode( node ) || isComment( node ) );
1141 } );
1142
1143 // There's no edge node. Abort.
1144 if ( !edgeNode ) {
1145 that.debug.logEnd( 'ABORT. No edge node found.' ); // %REMOVE_LINE%
1146 return null;
1147 }
1148
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 ) );
1153 } );
1154 }
1155
1156 // Exclude bad nodes (no ML needed then):
1157 // \-> Edge node is text.
1158 // \-> Edge node is floated, etc.
1159 //
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%
1163 return null;
1164 }
1165
1166 // Update size of edge node. Dimensions will be necessary.
1167 updateSize( that, edgeNode );
1168
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].
1174
1175 // Determine trigger look.
1176 triggerLook = that.inInlineMode || view.scroll.y === 0 ?
1177 LOOK_TOP : LOOK_NORMAL;
1178
1179 that.debug.logEnd( 'SUCCESS. Created box trigger. EDGE_TOP.' ); // %REMOVE_LINE%
1180
1181 return new boxTrigger( [ null, edgeNode,
1182 EDGE_TOP,
1183 TYPE_EDGE,
1184 triggerLook
1185 ] );
1186 }
1187
1188 // \-> Bottom case.
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 ]
1193
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;
1198
1199 that.debug.logEnd( 'SUCCESS. Created box trigger. EDGE_BOTTOM.' ); // %REMOVE_LINE%
1200
1201 return new boxTrigger( [ edgeNode, null,
1202 EDGE_BOTTOM,
1203 TYPE_EDGE,
1204 triggerLook
1205 ] );
1206 }
1207
1208 that.debug.logEnd( 'ABORT. No trigger created.' ); // %REMOVE_LINE%
1209 return null;
1210 }
1211
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.
1216 //
1217 // +----------------------- Parent element -+
1218 // | +----------------------- Element #1 -+ | /--
1219 // | | | | | * Mouse activation area (as first child) *
1220 // | | | | \--
1221 // | | | | /--
1222 // | | | | | * Mouse activation area (Element #2) *
1223 // | +------------------------------------+ | \--
1224 // | |
1225 // | +----------------------- Element #2 -+ | /--
1226 // | | | | | * Mouse activation area (Element #1) *
1227 // | | | | \--
1228 // | | | |
1229 // | +------------------------------------+ |
1230 // | |
1231 // | Text node is here. |
1232 // | |
1233 // | +----------------------- Element #3 -+ |
1234 // | | | |
1235 // | | | |
1236 // | | | | /--
1237 // | | | | | * Mouse activation area (as last child) *
1238 // | +------------------------------------+ | \--
1239 // +----------------------------------------+
1240 //
1241 function triggerEdge( that ) {
1242 that.debug.groupStart( 'triggerEdge' ); // %REMOVE_LINE%
1243
1244 var mouse = that.mouse,
1245 view = that.view,
1246 triggerOffset = that.triggerOffset;
1247
1248 // Get the ascendant trigger basing on elementFromMouse.
1249 var element = getAscendantTrigger( that );
1250
1251 that.debug.logElements( [ element ], [ 'Ascendant trigger' ], 'First stage' ); // %REMOVE_LINE%
1252
1253 // Abort if there's no appropriate element.
1254 if ( !element ) {
1255 that.debug.logEnd( 'ABORT. No element, element is editable or element contains editable.' ); // %REMOVE_LINE%
1256 return null;
1257 }
1258
1259 // Dimensions will be necessary.
1260 updateSize( that, element );
1261
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 ) ),
1267
1268 // This variable will hold the trigger to be returned.
1269 triggerSetup = [],
1270 triggerLook,
1271
1272 // This flag determines whether dealing with a bottom trigger.
1273 bottomTrigger;
1274
1275 // \-> Top 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.
1282 else {
1283 that.debug.logEnd( 'ABORT. Not around of any edge.' ); // %REMOVE_LINE%
1284 return null;
1285 }
1286
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.
1291 if (
1292 isFlowBreaker( element ) ||
1293 isChildBetweenPointerAndEdge( that, element, bottomTrigger ) ||
1294 element.getParent().is( DTD_LISTITEM )
1295 ) {
1296 that.debug.logEnd( 'ABORT. element is wrong', element ); // %REMOVE_LINE%
1297 return null;
1298 }
1299
1300 // Get sibling according to bottomTrigger.
1301 var elementSibling = getNonEmptyNeighbour( that, element, !bottomTrigger );
1302
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.
1308
1309 // Determine trigger look.
1310 if ( element.equals( that.editable[ bottomTrigger ? 'getLast' : 'getFirst' ]( that.isRelevant ) ) ) {
1311 updateEditableSize( that );
1312
1313 if (
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 )
1317 ) {
1318 triggerLook = LOOK_BOTTOM;
1319 } else if ( inBetween( mouse.y, 0, element.size.top + fixedOffset ) ) {
1320 triggerLook = LOOK_TOP;
1321 }
1322 } else {
1323 triggerLook = LOOK_NORMAL;
1324 }
1325
1326 triggerSetup = [ null, element ][ bottomTrigger ? 'reverse' : 'concat' ]().concat( [
1327 bottomTrigger ? EDGE_BOTTOM : EDGE_TOP,
1328 TYPE_EDGE,
1329 triggerLook,
1330 element.equals( that.editable[ bottomTrigger ? 'getLast' : 'getFirst' ]( that.isRelevant ) ) ?
1331 ( bottomTrigger ? LOOK_BOTTOM : LOOK_TOP ) : LOOK_NORMAL
1332 ] );
1333
1334 that.debug.log( 'Configured edge trigger of ' + ( bottomTrigger ? 'EDGE_BOTTOM' : 'EDGE_TOP' ) ); // %REMOVE_LINE%
1335 }
1336
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%
1340 return null;
1341 }
1342
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.
1350 if (
1351 isFlowBreaker( elementSibling ) ||
1352 !isTrigger( that, elementSibling ) ||
1353 elementSibling.getParent().is( DTD_LISTITEM )
1354 ) {
1355 that.debug.logEnd( 'ABORT. elementSibling is wrong', elementSibling ); // %REMOVE_LINE%
1356 return null;
1357 }
1358
1359 // Prepare a trigger.
1360 triggerSetup = [ elementSibling, element ][ bottomTrigger ? 'reverse' : 'concat' ]().concat( [
1361 EDGE_MIDDLE,
1362 TYPE_EDGE
1363 ] );
1364
1365 that.debug.log( 'Configured edge trigger of EDGE_MIDDLE' ); // %REMOVE_LINE%
1366 }
1367
1368 if ( 0 in triggerSetup ) {
1369 that.debug.logEnd( 'SUCCESS. Returning a trigger.' ); // %REMOVE_LINE%
1370 return new boxTrigger( triggerSetup );
1371 }
1372
1373 that.debug.logEnd( 'ABORT. No trigger generated.' ); // %REMOVE_LINE%
1374 return null;
1375 }
1376
1377 // Checks iteratively up and down in search for elements using elementFromMouse method.
1378 // Useful if between two triggers.
1379 //
1380 // +----------------------- Parent element -+
1381 // | +----------------------- Element #1 -+ |
1382 // | | | |
1383 // | | | |
1384 // | | | |
1385 // | +------------------------------------+ |
1386 // | | /--
1387 // | . | |
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.
1394 // | . | |
1395 // | . | |
1396 // | | \--
1397 // | +----------------------- Element #2 -+ |
1398 // | | | |
1399 // | | | |
1400 // | | | |
1401 // | | | |
1402 // | +------------------------------------+ |
1403 // +----------------------------------------+
1404 //
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%
1410
1411 var startElement = that.element,
1412 upper, lower, trigger;
1413
1414 if ( !isHtml( startElement ) || startElement.contains( that.editable ) ) {
1415 that.debug.logEnd( 'ABORT. No start element, or start element contains editable.' ); // %REMOVE_LINE%
1416 return null;
1417 }
1418
1419 // Stop searching if element is in non-editable branch of DOM.
1420 if ( startElement.isReadOnly() )
1421 return null;
1422
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 );
1428 }, startElement ),
1429
1430 upper = trigger.upper,
1431 lower = trigger.lower;
1432
1433 that.debug.logElements( [ upper, lower ], [ 'Upper', 'Lower' ], 'Pair found' ); // %REMOVE_LINE%
1434
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 );
1439 }
1440
1441 that.debug.logElements( [ startElement, upper, lower ], // %REMOVE_LINE%
1442 [ 'Start', 'Upper', 'Lower' ], 'Post-processing' ); // %REMOVE_LINE%
1443
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.
1447
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.
1467
1468 // 1.1., 1.2.
1469 if ( upper && startElement.contains( upper ) ) {
1470 while ( !upper.getParent().equals( startElement ) )
1471 upper = upper.getParent();
1472 } else {
1473 upper = startElement.getFirst( function( node ) {
1474 return expandSelector( that, node );
1475 } );
1476 }
1477
1478 if ( lower && startElement.contains( lower ) ) {
1479 while ( !lower.getParent().equals( startElement ) )
1480 lower = lower.getParent();
1481 } else {
1482 lower = startElement.getLast( function( node ) {
1483 return expandSelector( that, node );
1484 } );
1485 }
1486
1487 // 1.3.
1488 if ( !upper || !lower ) {
1489 that.debug.logEnd( 'ABORT. There is no upper or no lower element.' ); // %REMOVE_LINE%
1490 return null;
1491 }
1492
1493 // 2.1.
1494 updateSize( that, upper );
1495 updateSize( that, lower );
1496
1497 if ( !checkMouseBetweenElements( that, upper, lower ) ) {
1498 that.debug.logEnd( 'ABORT. Mouse is already above upper or below lower.' ); // %REMOVE_LINE%
1499 return null;
1500 }
1501
1502 var minDistance = Number.MAX_VALUE,
1503 currentDistance, upperNext, minElement, minElementNext;
1504
1505 while ( lower && !lower.equals( upper ) ) {
1506 // 3.1.
1507 if ( !( upperNext = upper.getNext( that.isRelevant ) ) )
1508 break;
1509
1510 // 3.2.
1511 currentDistance = Math.abs( getMidpoint( that, upper, upperNext ) - that.mouse.y );
1512
1513 // 3.3.
1514 if ( currentDistance < minDistance ) {
1515 minDistance = currentDistance;
1516 minElement = upper;
1517 minElementNext = upperNext;
1518 }
1519
1520 upper = upperNext;
1521 updateSize( that, upper );
1522 }
1523
1524 that.debug.logElements( [ minElement, minElementNext ], // %REMOVE_LINE%
1525 [ 'Min', 'MinNext' ], 'Post-processing results' ); // %REMOVE_LINE%
1526
1527 // 3.4.
1528 if ( !minElement || !minElementNext ) {
1529 that.debug.logEnd( 'ABORT. No Min or MinNext' ); // %REMOVE_LINE%
1530 return null;
1531 }
1532
1533 if ( !checkMouseBetweenElements( that, minElement, minElementNext ) ) {
1534 that.debug.logEnd( 'ABORT. Mouse is already above minElement or below minElementNext.' ); // %REMOVE_LINE%
1535 return null;
1536 }
1537
1538 // An element of minimal distance has been found. Assign it to the trigger.
1539 trigger.upper = minElement;
1540 trigger.lower = minElementNext;
1541
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 );
1545 }
1546
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' ) ) );
1554 }
1555
1556 // This method checks whether mouse-y is between the top edge of upper
1557 // and bottom edge of lower.
1558 //
1559 // NOTE: This method assumes that updateSize has already been called
1560 // for the elements and is up-to-date.
1561 //
1562 // +---------------------------- Upper -+ /--
1563 // | | |
1564 // +------------------------------------+ |
1565 // |
1566 // ... |
1567 // |
1568 // X | * Return true for mouse-y in this range *
1569 // |
1570 // ... |
1571 // |
1572 // +---------------------------- Lower -+ |
1573 // | | |
1574 // +------------------------------------+ \--
1575 //
1576 function checkMouseBetweenElements( that, upper, lower ) {
1577 return inBetween( that.mouse.y, upper.size.top, lower.size.bottom );
1578 }
1579
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%
1584
1585 var upper = trigger.upper,
1586 lower = trigger.lower;
1587
1588 if (
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%
1595
1596 return false;
1597 }
1598
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%
1603
1604 return true;
1605 }
1606
1607 that.debug.logElementsEnd( [ upper, lower ], // %REMOVE_LINE%
1608 [ 'upper', 'lower' ], 'Rejected unknown pair' ); // %REMOVE_LINE%
1609
1610 return false;
1611 }
1612
1613 // Simple wrapper for expandEngine and expandFilter.
1614 return function( that ) {
1615 that.debug.groupStart( 'triggerExpand' ); // %REMOVE_LINE%
1616
1617 var trigger = expandEngine( that );
1618
1619 that.debug.groupEnd(); // %REMOVE_LINE%
1620 return trigger && expandFilter( that, trigger ) ? trigger : null;
1621 };
1622 } )();
1623
1624 // Collects dimensions of an element.
1625 var sizePrefixes = [ 'top', 'left', 'right', 'bottom' ];
1626
1627 function getSize( that, element, ignoreScroll, force ) {
1628 var docPosition = element.getDocumentPosition(),
1629 border = {},
1630 margin = {},
1631 padding = {},
1632 box = {};
1633
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;
1638 }
1639
1640 // updateWindowSize if forced to do so OR NOT ignoring scroll.
1641 if ( !ignoreScroll || force )
1642 updateWindowSize( that, force );
1643
1644 box.top = docPosition.y - ( ignoreScroll ? 0 : that.view.scroll.y ), box.left = docPosition.x - ( ignoreScroll ? 0 : that.view.scroll.x ),
1645
1646 // w/ borders and paddings.
1647 box.outerWidth = element.$.offsetWidth, box.outerHeight = element.$.offsetHeight,
1648
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 ),
1651
1652 box.bottom = box.top + box.outerHeight, box.right = box.left + box.outerWidth;
1653
1654 if ( that.inInlineMode ) {
1655 box.scroll = {
1656 top: element.$.scrollTop,
1657 left: element.$.scrollLeft
1658 };
1659 }
1660
1661 return extend( {
1662 border: border,
1663 padding: padding,
1664 margin: margin,
1665 ignoreScroll: ignoreScroll
1666 }, box, true );
1667
1668 function getStyle( propertyName ) {
1669 return element.getComputedStyle.call( element, propertyName );
1670 }
1671 }
1672
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
1676
1677 if ( !element.size )
1678 element.size = {};
1679
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%
1684 return null;
1685 }
1686
1687 that.debug.log( 'element.size: capture' ); // %REMOVE_LINE%
1688
1689 return extend( element.size, getSize( that, element, ignoreScroll ), {
1690 date: +new Date()
1691 }, true );
1692 }
1693
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 );
1700 }
1701
1702 function updateWindowSize( that, force ) {
1703 if ( !that.view )
1704 that.view = {};
1705
1706 var view = that.view;
1707
1708 if ( !force && view && view.date > new Date() - CACHE_TIME ) {
1709 that.debug.log( 'win.size: get from cache' ); // %REMOVE_LINE%
1710 return;
1711 }
1712
1713 that.debug.log( 'win.size: capturing' ); // %REMOVE_LINE%
1714
1715 var win = that.win,
1716 scroll = win.getScrollPosition(),
1717 paneSize = win.getViewPaneSize();
1718
1719 extend( that.view, {
1720 scroll: {
1721 x: scroll.x,
1722 y: scroll.y,
1723 width: that.doc.$.documentElement.scrollWidth - paneSize.width,
1724 height: that.doc.$.documentElement.scrollHeight - paneSize.height
1725 },
1726 pane: {
1727 width: paneSize.width,
1728 height: paneSize.height,
1729 bottom: paneSize.height + scroll.y
1730 },
1731 date: +new Date()
1732 }, true );
1733 }
1734
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,
1740 mouseStep = 0,
1741 upperFound = false,
1742 lowerFound = false,
1743 viewPaneHeight = that.view.pane.height,
1744 mouse = that.mouse;
1745
1746 while ( mouse.y + mouseStep < viewPaneHeight && mouse.y - mouseStep > 0 ) {
1747 if ( !upperFound )
1748 upperFound = stopCondition( upper, startElement );
1749
1750 if ( !lowerFound )
1751 lowerFound = stopCondition( lower, startElement );
1752
1753 // Still not found...
1754 if ( !upperFound && mouse.y - mouseStep > 0 )
1755 upper = selectCriterion( that, { x: mouse.x, y: mouse.y - mouseStep } );
1756
1757 if ( !lowerFound && mouse.y + mouseStep < viewPaneHeight )
1758 lower = selectCriterion( that, { x: mouse.x, y: mouse.y + mouseStep } );
1759
1760 if ( upperFound && lowerFound )
1761 break;
1762
1763 // Instead of ++ to reduce the number of invocations by half.
1764 // It's trades off accuracy in some edge cases for improved performance.
1765 mouseStep += 2;
1766 }
1767
1768 return new boxTrigger( [ upper, lower, null, null ] );
1769 }
1770
1771 } )();
1772
1773 /**
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).
1777 *
1778 * Read more in the [documentation](#!/guide/dev_magicline)
1779 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).
1780 *
1781 * // Changes the offset to 15px.
1782 * CKEDITOR.config.magicline_triggerOffset = 15;
1783 *
1784 * @cfg {Number} [magicline_triggerOffset=30]
1785 * @member CKEDITOR.config
1786 * @see CKEDITOR.config#magicline_holdDistance
1787 */
1788
1789 /**
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}.
1793 *
1794 * Read more in the [documentation](#!/guide/dev_magicline)
1795 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).
1796 *
1797 * // Increases the distance to 80% of CKEDITOR.config.magicline_triggerOffset.
1798 * CKEDITOR.config.magicline_holdDistance = .8;
1799 *
1800 * @cfg {Number} [magicline_holdDistance=0.5]
1801 * @member CKEDITOR.config
1802 * @see CKEDITOR.config#magicline_triggerOffset
1803 */
1804
1805 /**
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.
1808 *
1809 * Read more in the [documentation](#!/guide/dev_magicline)
1810 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).
1811 *
1812 * // Changes the default keystroke to "Ctrl + ,".
1813 * CKEDITOR.config.magicline_keystrokePrevious = CKEDITOR.CTRL + 188;
1814 *
1815 * @cfg {Number} [magicline_keystrokePrevious=CKEDITOR.CTRL + CKEDITOR.SHIFT + 51 (CTRL + SHIFT + 3)]
1816 * @member CKEDITOR.config
1817 */
1818 CKEDITOR.config.magicline_keystrokePrevious = CKEDITOR.CTRL + CKEDITOR.SHIFT + 51; // CTRL + SHIFT + 3
1819
1820 /**
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.
1823 *
1824 * Read more in the [documentation](#!/guide/dev_magicline)
1825 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).
1826 *
1827 * // Changes keystroke to "Ctrl + .".
1828 * CKEDITOR.config.magicline_keystrokeNext = CKEDITOR.CTRL + 190;
1829 *
1830 * @cfg {Number} [magicline_keystrokeNext=CKEDITOR.CTRL + CKEDITOR.SHIFT + 52 (CTRL + SHIFT + 4)]
1831 * @member CKEDITOR.config
1832 */
1833 CKEDITOR.config.magicline_keystrokeNext = CKEDITOR.CTRL + CKEDITOR.SHIFT + 52; // CTRL + SHIFT + 4
1834
1835 /**
1836 * Defines a list of attributes that, if assigned to some elements, prevent the magic line from being
1837 * used within these elements.
1838 *
1839 * Read more in the [documentation](#!/guide/dev_magicline)
1840 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).
1841 *
1842 * // Adds the "data-tabu" attribute to the magic line tabu list.
1843 * CKEDITOR.config.magicline_tabuList = [ 'data-tabu' ];
1844 *
1845 * @cfg {Number} [magicline_tabuList=[ 'data-widget-wrapper' ]]
1846 * @member CKEDITOR.config
1847 */
1848
1849 /**
1850 * Defines the color of the magic line. The color may be adjusted to enhance readability.
1851 *
1852 * Read more in the [documentation](#!/guide/dev_magicline)
1853 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).
1854 *
1855 * // Changes magic line color to blue.
1856 * CKEDITOR.config.magicline_color = '#0000FF';
1857 *
1858 * @cfg {String} [magicline_color='#FF0000']
1859 * @member CKEDITOR.config
1860 */
1861
1862 /**
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.
1865 *
1866 * Read more in the [documentation](#!/guide/dev_magicline)
1867 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).
1868 *
1869 * // Enables the greedy "put everywhere" mode.
1870 * CKEDITOR.config.magicline_everywhere = true;
1871 *
1872 * @cfg {Boolean} [magicline_everywhere=false]
1873 * @member CKEDITOR.config
1874 */