]> git.immae.eu Git - perso/Immae/Projets/packagist/ludivine-ckeditor-component.git/blame - sources/plugins/magicline/plugin.js
Update to 4.7.3
[perso/Immae/Projets/packagist/ludivine-ckeditor-component.git] / sources / plugins / magicline / plugin.js
CommitLineData
c63493c8
IB
1/**\r
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
3 * For licensing, see LICENSE.md or http://ckeditor.com/license\r
4 */\r
5\r
6/**\r
7 * @fileOverview The [Magic Line](http://ckeditor.com/addon/magicline) plugin that makes it easier to access some document areas that\r
8 * are difficult to focus.\r
9 */\r
10\r
11'use strict';\r
12\r
13( function() {\r
14 CKEDITOR.plugins.add( 'magicline', {\r
1794320d 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%\r
c63493c8
IB
16 init: initPlugin\r
17 } );\r
18\r
19 // Activates the box inside of an editor.\r
20 function initPlugin( editor ) {\r
21 // Configurables\r
22 var config = editor.config,\r
23 triggerOffset = config.magicline_triggerOffset || 30,\r
24 enterMode = config.enterMode,\r
25 that = {\r
26 // Global stuff is being initialized here.\r
27 editor: editor,\r
28 enterMode: enterMode,\r
29 triggerOffset: triggerOffset,\r
30 holdDistance: 0 | triggerOffset * ( config.magicline_holdDistance || 0.5 ),\r
31 boxColor: config.magicline_color || '#ff0000',\r
32 rtl: config.contentsLangDirection == 'rtl',\r
33 tabuList: [ 'data-cke-hidden-sel' ].concat( config.magicline_tabuList || [] ),\r
34 triggers: config.magicline_everywhere ? DTD_BLOCK : { table: 1, hr: 1, div: 1, ul: 1, ol: 1, dl: 1, form: 1, blockquote: 1 }\r
35 },\r
36 scrollTimeout, checkMouseTimeoutPending, checkMouseTimer;\r
37\r
38 // %REMOVE_START%\r
39 // Internal DEBUG uses tools located in the topmost window.\r
40\r
1794320d 41 // (http://dev.ckeditor.com/ticket/9701) Due to security limitations some browsers may throw\r
c63493c8
IB
42 // errors when accessing window.top object. Do it safely first then.\r
43 try {\r
44 that.debug = window.top.DEBUG;\r
45 }\r
46 catch ( e ) {}\r
47\r
48 that.debug = that.debug || {\r
49 groupEnd: function() {},\r
50 groupStart: function() {},\r
51 log: function() {},\r
52 logElements: function() {},\r
53 logElementsEnd: function() {},\r
54 logEnd: function() {},\r
55 mousePos: function() {},\r
56 showHidden: function() {},\r
57 showTrigger: function() {},\r
58 startTimer: function() {},\r
59 stopTimer: function() {}\r
60 };\r
61 // %REMOVE_END%\r
62\r
63 // Simple irrelevant elements filter.\r
64 that.isRelevant = function( node ) {\r
65 return isHtml( node ) && // -> Node must be an existing HTML element.\r
66 !isLine( that, node ) && // -> Node can be neither the box nor its child.\r
67 !isFlowBreaker( node ); // -> Node can be neither floated nor positioned nor aligned.\r
68 };\r
69\r
70 editor.on( 'contentDom', addListeners, this );\r
71\r
72 function addListeners() {\r
73 var editable = editor.editable(),\r
74 doc = editor.document,\r
75 win = editor.window;\r
76\r
77 // Global stuff is being initialized here.\r
78 extend( that, {\r
79 editable: editable,\r
80 inInlineMode: editable.isInline(),\r
81 doc: doc,\r
82 win: win,\r
83 hotNode: null\r
84 }, true );\r
85\r
86 // This is the boundary of the editor. For inline the boundary is editable itself.\r
87 // For classic (`iframe`-based) editor, the HTML element is a real boundary.\r
88 that.boundary = that.inInlineMode ? that.editable : that.doc.getDocumentElement();\r
89\r
90 // Enabling the box inside of inline editable is pointless.\r
91 // There's no need to access spaces inside paragraphs, links, spans, etc.\r
92 if ( editable.is( dtd.$inline ) )\r
93 return;\r
94\r
95 // Handle in-line editing by setting appropriate position.\r
96 // If current position is static, make it relative and clear top/left coordinates.\r
97 if ( that.inInlineMode && !isPositioned( editable ) ) {\r
98 editable.setStyles( {\r
99 position: 'relative',\r
100 top: null,\r
101 left: null\r
102 } );\r
103 }\r
104 // Enable the box. Let it produce children elements, initialize\r
105 // event handlers and own methods.\r
106 initLine.call( this, that );\r
107\r
108 // Get view dimensions and scroll positions.\r
109 // At this stage (before any checkMouse call) it is used mostly\r
110 // by tests. Nevertheless it a crucial thing.\r
111 updateWindowSize( that );\r
112\r
113 // Remove the box before an undo image is created.\r
114 // This is important. If we didn't do that, the *undo thing* would revert the box into an editor.\r
115 // Thanks to that, undo doesn't even know about the existence of the box.\r
116 editable.attachListener( editor, 'beforeUndoImage', function() {\r
117 that.line.detach();\r
118 } );\r
119\r
120 // Removes the box HTML from editor data string if getData is called.\r
121 // Thanks to that, an editor never yields data polluted by the box.\r
122 // Listen with very high priority, so line will be removed before other\r
123 // listeners will see it.\r
124 editable.attachListener( editor, 'beforeGetData', function() {\r
125 // If the box is in editable, remove it.\r
126 if ( that.line.wrap.getParent() ) {\r
127 that.line.detach();\r
128\r
129 // Restore line in the last listener for 'getData'.\r
130 editor.once( 'getData', function() {\r
131 that.line.attach();\r
132 }, null, null, 1000 );\r
133 }\r
134 }, null, null, 0 );\r
135\r
136 // Hide the box on mouseout if mouse leaves document.\r
137 editable.attachListener( that.inInlineMode ? doc : doc.getWindow().getFrame(), 'mouseout', function( event ) {\r
138 if ( editor.mode != 'wysiwyg' )\r
139 return;\r
140\r
141 // Check for inline-mode editor. If so, check mouse position\r
142 // and remove the box if mouse outside of an editor.\r
143 if ( that.inInlineMode ) {\r
144 var mouse = {\r
145 x: event.data.$.clientX,\r
146 y: event.data.$.clientY\r
147 };\r
148\r
149 updateWindowSize( that );\r
150 updateEditableSize( that, true );\r
151\r
152 var size = that.view.editable,\r
153 scroll = that.view.scroll;\r
154\r
155 // If outside of an editor...\r
156 if ( !inBetween( mouse.x, size.left - scroll.x, size.right - scroll.x ) || !inBetween( mouse.y, size.top - scroll.y, size.bottom - scroll.y ) ) {\r
157 clearTimeout( checkMouseTimer );\r
158 checkMouseTimer = null;\r
159 that.line.detach();\r
160 }\r
161 }\r
162\r
163 else {\r
164 clearTimeout( checkMouseTimer );\r
165 checkMouseTimer = null;\r
166 that.line.detach();\r
167 }\r
168 } );\r
169\r
170 // This one deactivates hidden mode of an editor which\r
171 // prevents the box from being shown.\r
172 editable.attachListener( editable, 'keyup', function() {\r
173 that.hiddenMode = 0;\r
174 that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%\r
175 } );\r
176\r
177 editable.attachListener( editable, 'keydown', function( event ) {\r
178 if ( editor.mode != 'wysiwyg' )\r
179 return;\r
180\r
181 var keyStroke = event.data.getKeystroke();\r
182\r
183 switch ( keyStroke ) {\r
184 // Shift pressed\r
185 case 2228240: // IE\r
186 case 16:\r
187 that.hiddenMode = 1;\r
188 that.line.detach();\r
189 }\r
190\r
191 that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%\r
192 } );\r
193\r
194 // This method ensures that checkMouse aren't executed\r
195 // in parallel and no more frequently than specified in timeout function.\r
196 // In classic (`iframe`-based) editor, document is used as a trigger, to provide magicline\r
197 // functionality when mouse is below the body (short content, short body).\r
198 editable.attachListener( that.inInlineMode ? editable : doc, 'mousemove', function( event ) {\r
199 checkMouseTimeoutPending = true;\r
200\r
201 if ( editor.mode != 'wysiwyg' || editor.readOnly || checkMouseTimer )\r
202 return;\r
203\r
204 // IE<9 requires this event-driven object to be created\r
205 // outside of the setTimeout statement.\r
206 // Otherwise it loses the event object with its properties.\r
207 var mouse = {\r
208 x: event.data.$.clientX,\r
209 y: event.data.$.clientY\r
210 };\r
211\r
212 checkMouseTimer = setTimeout( function() {\r
213 checkMouse( mouse );\r
214 }, 30 ); // balances performance and accessibility\r
215 } );\r
216\r
217 // This one removes box on scroll event.\r
218 // It is to avoid box displacement.\r
219 editable.attachListener( win, 'scroll', function() {\r
220 if ( editor.mode != 'wysiwyg' )\r
221 return;\r
222\r
223 that.line.detach();\r
224\r
225 // To figure this out just look at the mouseup\r
226 // event handler below.\r
227 if ( env.webkit ) {\r
228 that.hiddenMode = 1;\r
229\r
230 clearTimeout( scrollTimeout );\r
231 scrollTimeout = setTimeout( function() {\r
232 // Don't leave hidden mode until mouse remains pressed and\r
233 // scroll is being used, i.e. when dragging something.\r
234 if ( !that.mouseDown )\r
235 that.hiddenMode = 0;\r
236 that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%\r
237 }, 50 );\r
238\r
239 that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%\r
240 }\r
241 } );\r
242\r
243 // Those event handlers remove the box on mousedown\r
244 // and don't reveal it until the mouse is released.\r
245 // It is to prevent box insertion e.g. while scrolling\r
246 // (w/ scrollbar), selecting and so on.\r
247 editable.attachListener( env_ie8 ? doc : win, 'mousedown', function() {\r
248 if ( editor.mode != 'wysiwyg' )\r
249 return;\r
250\r
251 that.line.detach();\r
252 that.hiddenMode = 1;\r
253 that.mouseDown = 1;\r
254\r
255 that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%\r
256 } );\r
257\r
258 // Google Chrome doesn't trigger this on the scrollbar (since 2009...)\r
259 // so it is totally useless to check for scroll finish\r
260 // see: http://code.google.com/p/chromium/issues/detail?id=14204\r
261 editable.attachListener( env_ie8 ? doc : win, 'mouseup', function() {\r
262 that.hiddenMode = 0;\r
263 that.mouseDown = 0;\r
264 that.debug.showHidden( that.hiddenMode ); // %REMOVE_LINE%\r
265 } );\r
266\r
267 // Editor commands for accessing difficult focus spaces.\r
268 editor.addCommand( 'accessPreviousSpace', accessFocusSpaceCmd( that ) );\r
269 editor.addCommand( 'accessNextSpace', accessFocusSpaceCmd( that, true ) );\r
270\r
271 editor.setKeystroke( [\r
272 [ config.magicline_keystrokePrevious, 'accessPreviousSpace' ],\r
273 [ config.magicline_keystrokeNext, 'accessNextSpace' ]\r
274 ] );\r
275\r
276 // Revert magicline hot node on undo/redo.\r
277 editor.on( 'loadSnapshot', function() {\r
278 var elements, element, i;\r
279\r
280 for ( var t in { p: 1, br: 1, div: 1 } ) {\r
1794320d 281 // document.find is not available in QM (http://dev.ckeditor.com/ticket/11149).\r
c63493c8
IB
282 elements = editor.document.getElementsByTag( t );\r
283\r
284 for ( i = elements.count(); i--; ) {\r
285 if ( ( element = elements.getItem( i ) ).data( 'cke-magicline-hot' ) ) {\r
286 // Restore hotNode\r
287 that.hotNode = element;\r
288 // Restore last access direction\r
289 that.lastCmdDirection = element.data( 'cke-magicline-dir' ) === 'true' ? true : false;\r
290\r
291 return;\r
292 }\r
293 }\r
294 }\r
295 } );\r
296\r
297 // This method handles mousemove mouse for box toggling.\r
298 // It uses mouse position to determine underlying element, then\r
299 // it tries to use different trigger type in order to place the box\r
300 // in correct place. The following procedure is executed periodically.\r
301 function checkMouse( mouse ) {\r
302 that.debug.groupStart( 'CheckMouse' ); // %REMOVE_LINE%\r
303 that.debug.startTimer(); // %REMOVE_LINE%\r
304\r
305 that.mouse = mouse;\r
306 that.trigger = null;\r
307\r
308 checkMouseTimer = null;\r
309 updateWindowSize( that );\r
310\r
311 if (\r
312 checkMouseTimeoutPending && // There must be an event pending.\r
313 !that.hiddenMode && // Can't be in hidden mode.\r
314 editor.focusManager.hasFocus && // Editor must have focus.\r
315 !that.line.mouseNear() && // Mouse pointer can't be close to the box.\r
316 ( that.element = elementFromMouse( that, true ) ) // There must be valid element.\r
317 ) {\r
318 // If trigger exists, and trigger is correct -> show the box.\r
319 // Don't show the line if trigger is a descendant of some tabu-list element.\r
320 if ( ( that.trigger = triggerEditable( that ) || triggerEdge( that ) || triggerExpand( that ) ) &&\r
321 !isInTabu( that, that.trigger.upper || that.trigger.lower ) ) {\r
322 that.line.attach().place();\r
323 }\r
324\r
325 // Otherwise remove the box\r
326 else {\r
327 that.trigger = null;\r
328 that.line.detach();\r
329 }\r
330\r
331 that.debug.showTrigger( that.trigger ); // %REMOVE_LINE%\r
332 that.debug.mousePos( mouse.y, that.element ); // %REMOVE_LINE%\r
333\r
334 checkMouseTimeoutPending = false;\r
335 }\r
336\r
337 that.debug.stopTimer(); // %REMOVE_LINE%\r
338 that.debug.groupEnd(); // %REMOVE_LINE%\r
339 }\r
340\r
341 // This one allows testing and debugging. It reveals some\r
342 // inner methods to the world.\r
343 this.backdoor = {\r
344 accessFocusSpace: accessFocusSpace,\r
345 boxTrigger: boxTrigger,\r
346 isLine: isLine,\r
347 getAscendantTrigger: getAscendantTrigger,\r
348 getNonEmptyNeighbour: getNonEmptyNeighbour,\r
349 getSize: getSize,\r
350 that: that,\r
351 triggerEdge: triggerEdge,\r
352 triggerEditable: triggerEditable,\r
353 triggerExpand: triggerExpand\r
354 };\r
355 }\r
356 }\r
357\r
358 // Some shorthands for common methods to save bytes\r
359 var extend = CKEDITOR.tools.extend,\r
360 newElement = CKEDITOR.dom.element,\r
361 newElementFromHtml = newElement.createFromHtml,\r
362 env = CKEDITOR.env,\r
363 env_ie8 = CKEDITOR.env.ie && CKEDITOR.env.version < 9,\r
364 dtd = CKEDITOR.dtd,\r
365\r
366 // Global object associating enter modes with elements.\r
367 enterElements = {},\r
368\r
369 // Constant values, types and so on.\r
370 EDGE_TOP = 128,\r
371 EDGE_BOTTOM = 64,\r
372 EDGE_MIDDLE = 32,\r
373 TYPE_EDGE = 16,\r
374 TYPE_EXPAND = 8,\r
375 LOOK_TOP = 4,\r
376 LOOK_BOTTOM = 2,\r
377 LOOK_NORMAL = 1,\r
378 WHITE_SPACE = '\u00A0',\r
379 DTD_LISTITEM = dtd.$listItem,\r
380 DTD_TABLECONTENT = dtd.$tableContent,\r
381 DTD_NONACCESSIBLE = extend( {}, dtd.$nonEditable, dtd.$empty ),\r
382 DTD_BLOCK = dtd.$block,\r
383\r
384 // Minimum time that must elapse between two update*Size calls.\r
385 // It prevents constant getComuptedStyle calls and improves performance.\r
386 CACHE_TIME = 100,\r
387\r
388 // Shared CSS stuff for box elements\r
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;',\r
390 CSS_TRIANGLE = CSS_COMMON + 'border-color:transparent;display:block;border-style:solid;',\r
391 TRIANGLE_HTML = '<span>' + WHITE_SPACE + '</span>';\r
392\r
393 enterElements[ CKEDITOR.ENTER_BR ] = 'br';\r
394 enterElements[ CKEDITOR.ENTER_P ] = 'p';\r
395 enterElements[ CKEDITOR.ENTER_DIV ] = 'div';\r
396\r
397 function areSiblings( that, upper, lower ) {\r
398 return isHtml( upper ) && isHtml( lower ) && lower.equals( upper.getNext( function( node ) {\r
399 return !( isEmptyTextNode( node ) || isComment( node ) || isFlowBreaker( node ) );\r
400 } ) );\r
401 }\r
402\r
403 // boxTrigger is an abstract type which describes\r
404 // the relationship between elements that may result\r
405 // in showing the box.\r
406 //\r
407 // The following type is used by numerous methods\r
408 // to share information about the hypothetical box placement\r
409 // and look by referring to boxTrigger properties.\r
410 function boxTrigger( triggerSetup ) {\r
411 this.upper = triggerSetup[ 0 ];\r
412 this.lower = triggerSetup[ 1 ];\r
413 this.set.apply( this, triggerSetup.slice( 2 ) );\r
414 }\r
415\r
416 boxTrigger.prototype = {\r
417 set: function( edge, type, look ) {\r
418 this.properties = edge + type + ( look || LOOK_NORMAL );\r
419 return this;\r
420 },\r
421\r
422 is: function( property ) {\r
423 return ( this.properties & property ) == property;\r
424 }\r
425 };\r
426\r
427 var elementFromMouse = ( function() {\r
428 function elementFromPoint( doc, mouse ) {\r
429 var pointedElement = doc.$.elementFromPoint( mouse.x, mouse.y );\r
430\r
1794320d 431 // IE9QM: from times to times it will return an empty object on scroll bar hover. (http://dev.ckeditor.com/ticket/12185)\r
c63493c8
IB
432 return pointedElement && pointedElement.nodeType ?\r
433 new CKEDITOR.dom.element( pointedElement ) :\r
434 null;\r
435 }\r
436\r
437 return function( that, ignoreBox, forceMouse ) {\r
438 if ( !that.mouse )\r
439 return null;\r
440\r
441 var doc = that.doc,\r
442 lineWrap = that.line.wrap,\r
443 mouse = forceMouse || that.mouse,\r
444 // Note: element might be null.\r
445 element = elementFromPoint( doc, mouse );\r
446\r
447 // If ignoreBox is set and element is the box, it means that we\r
448 // need to hide the box for a while, repeat elementFromPoint\r
449 // and show it again.\r
450 if ( ignoreBox && isLine( that, element ) ) {\r
451 lineWrap.hide();\r
452 element = elementFromPoint( doc, mouse );\r
453 lineWrap.show();\r
454 }\r
455\r
456 // Return nothing if:\r
457 // \-> Element is not HTML.\r
458 if ( !( element && element.type == CKEDITOR.NODE_ELEMENT && element.$ ) )\r
459 return null;\r
460\r
461 // Also return nothing if:\r
462 // \-> We're IE<9 and element is out of the top-level element (editable for inline and HTML for classic (`iframe`-based)).\r
463 // This is due to the bug which allows IE<9 firing mouse events on element\r
464 // with contenteditable=true while doing selection out (far, away) of the element.\r
465 // Thus we must always be sure that we stay in editable or HTML.\r
466 if ( env.ie && env.version < 9 ) {\r
467 if ( !( that.boundary.equals( element ) || that.boundary.contains( element ) ) )\r
468 return null;\r
469 }\r
470\r
471 return element;\r
472 };\r
473 } )();\r
474\r
475 // Gets the closest parent node that belongs to triggers group.\r
476 function getAscendantTrigger( that ) {\r
477 var node = that.element,\r
478 trigger;\r
479\r
480 if ( node && isHtml( node ) ) {\r
481 trigger = node.getAscendant( that.triggers, true );\r
482\r
483 // If trigger is an element, neither editable nor editable's ascendant.\r
484 if ( trigger && that.editable.contains( trigger ) ) {\r
485 // Check for closest editable limit.\r
1794320d 486 // Don't consider trigger as a limit as it may be nested editable (includeSelf=false) (http://dev.ckeditor.com/ticket/12009).\r
c63493c8
IB
487 var limit = getClosestEditableLimit( trigger );\r
488\r
489 // Trigger in nested editable area.\r
490 if ( limit.getAttribute( 'contenteditable' ) == 'true' )\r
491 return trigger;\r
492 // Trigger in non-editable area.\r
493 else if ( limit.is( that.triggers ) )\r
494 return limit;\r
495 else\r
496 return null;\r
497 } else {\r
498 return null;\r
499 }\r
500 }\r
501\r
502 return null;\r
503 }\r
504\r
505 function getMidpoint( that, upper, lower ) {\r
506 updateSize( that, upper );\r
507 updateSize( that, lower );\r
508\r
509 var upperSizeBottom = upper.size.bottom,\r
510 lowerSizeTop = lower.size.top;\r
511\r
512 return upperSizeBottom && lowerSizeTop ? 0 | ( upperSizeBottom + lowerSizeTop ) / 2 : upperSizeBottom || lowerSizeTop;\r
513 }\r
514\r
515 // Get nearest node (either text or HTML), but:\r
516 // \-> Omit all empty text nodes (containing white characters only).\r
517 // \-> Omit BR elements\r
518 // \-> Omit flow breakers.\r
519 function getNonEmptyNeighbour( that, node, goBack ) {\r
520 node = node[ goBack ? 'getPrevious' : 'getNext' ]( function( node ) {\r
521 return ( isTextNode( node ) && !isEmptyTextNode( node ) ) ||\r
522 ( isHtml( node ) && !isFlowBreaker( node ) && !isLine( that, node ) );\r
523 } );\r
524\r
525 return node;\r
526 }\r
527\r
528 function inBetween( val, lower, upper ) {\r
529 return val > lower && val < upper;\r
530 }\r
531\r
532 // Returns the closest ancestor that has contenteditable attribute.\r
533 // Such ancestor is the limit of (non-)editable DOM branch that element\r
534 // belongs to. This method omits editor editable.\r
535 function getClosestEditableLimit( element, includeSelf ) {\r
536 if ( element.data( 'cke-editable' ) )\r
537 return null;\r
538\r
539 if ( !includeSelf )\r
540 element = element.getParent();\r
541\r
542 while ( element ) {\r
543 if ( element.data( 'cke-editable' ) )\r
544 return null;\r
545\r
546 if ( element.hasAttribute( 'contenteditable' ) )\r
547 return element;\r
548\r
549 element = element.getParent();\r
550 }\r
551\r
552 return null;\r
553 }\r
554\r
555 // Access space line consists of a few elements (spans):\r
556 // \-> Line wrapper.\r
557 // \-> Line.\r
558 // \-> Line triangles: left triangle (LT), right triangle (RT).\r
559 // \-> Button handler (BTN).\r
560 //\r
561 // +--------------------------------------------------- line.wrap (span) -----+\r
562 // | +---------------------------------------------------- line (span) -----+ |\r
563 // | | +- LT \ +- BTN -+ / RT -+ | |\r
564 // | | | \ | | | / | | |\r
565 // | | | / | <__| | \ | | |\r
566 // | | +-----/ +-------+ \-----+ | |\r
567 // | +----------------------------------------------------------------------+ |\r
568 // +--------------------------------------------------------------------------+\r
569 //\r
570 function initLine( that ) {\r
571 var doc = that.doc,\r
572 // This the main box element that holds triangles and the insertion button\r
573 line = newElementFromHtml( '<span contenteditable="false" style="' + CSS_COMMON + 'position:absolute;border-top:1px dashed ' + that.boxColor + '"></span>', doc ),\r
574 iconPath = CKEDITOR.getUrl( this.path + 'images/' + ( env.hidpi ? 'hidpi/' : '' ) + 'icon' + ( that.rtl ? '-rtl' : '' ) + '.png' );\r
575\r
576 extend( line, {\r
577\r
578 attach: function() {\r
579 // Only if not already attached\r
580 if ( !this.wrap.getParent() )\r
581 this.wrap.appendTo( that.editable, true );\r
582\r
583 return this;\r
584 },\r
585\r
586 // Looks are as follows: [ LOOK_TOP, LOOK_BOTTOM, LOOK_NORMAL ].\r
587 lineChildren: [\r
588 extend(\r
589 newElementFromHtml(\r
590 '<span title="' + that.editor.lang.magicline.title +\r
591 '" contenteditable="false">&#8629;</span>', doc\r
592 ), {\r
593 base: CSS_COMMON + 'height:17px;width:17px;' + ( that.rtl ? 'left' : 'right' ) + ':17px;' +\r
594 'background:url(' + iconPath + ') center no-repeat ' + that.boxColor + ';cursor:pointer;' +\r
595 ( env.hc ? 'font-size: 15px;line-height:14px;border:1px solid #fff;text-align:center;' : '' ) +\r
596 ( env.hidpi ? 'background-size: 9px 10px;' : '' ),\r
597 looks: [\r
598 'top:-8px; border-radius: 2px;',\r
599 'top:-17px; border-radius: 2px 2px 0px 0px;',\r
600 'top:-1px; border-radius: 0px 0px 2px 2px;'\r
601 ]\r
602 }\r
603 ),\r
604 extend( newElementFromHtml( TRIANGLE_HTML, doc ), {\r
605 base: CSS_TRIANGLE + 'left:0px;border-left-color:' + that.boxColor + ';',\r
606 looks: [\r
607 'border-width:8px 0 8px 8px;top:-8px',\r
608 'border-width:8px 0 0 8px;top:-8px',\r
609 'border-width:0 0 8px 8px;top:0px'\r
610 ]\r
611 } ),\r
612 extend( newElementFromHtml( TRIANGLE_HTML, doc ), {\r
613 base: CSS_TRIANGLE + 'right:0px;border-right-color:' + that.boxColor + ';',\r
614 looks: [\r
615 'border-width:8px 8px 8px 0;top:-8px',\r
616 'border-width:8px 8px 0 0;top:-8px',\r
617 'border-width:0 8px 8px 0;top:0px'\r
618 ]\r
619 } )\r
620 ],\r
621\r
622 detach: function() {\r
623 // Detach only if already attached.\r
624 if ( this.wrap.getParent() )\r
625 this.wrap.remove();\r
626\r
627 return this;\r
628 },\r
629\r
630 // Checks whether mouseY is around an element by comparing boundaries and considering\r
631 // an offset distance.\r
632 mouseNear: function() {\r
633 that.debug.groupStart( 'mouseNear' ); // %REMOVE_LINE%\r
634\r
635 updateSize( that, this );\r
636 var offset = that.holdDistance,\r
637 size = this.size;\r
638\r
639 // Determine neighborhood by element dimensions and offsets.\r
640 if ( size && inBetween( that.mouse.y, size.top - offset, size.bottom + offset ) && inBetween( that.mouse.x, size.left - offset, size.right + offset ) ) {\r
641 that.debug.logEnd( 'Mouse is near.' ); // %REMOVE_LINE%\r
642 return true;\r
643 }\r
644\r
645 that.debug.logEnd( 'Mouse isn\'t near.' ); // %REMOVE_LINE%\r
646 return false;\r
647 },\r
648\r
649 // Adjusts position of the box according to the trigger properties.\r
650 // If also affects look of the box depending on the type of the trigger.\r
651 place: function() {\r
652 var view = that.view,\r
653 editable = that.editable,\r
654 trigger = that.trigger,\r
655 upper = trigger.upper,\r
656 lower = trigger.lower,\r
657 any = upper || lower,\r
658 parent = any.getParent(),\r
659 styleSet = {};\r
660\r
661 // Save recent trigger for further insertion.\r
662 // It is necessary due to the fact, that that.trigger may\r
663 // contain different boxTrigger at the moment of insertion\r
664 // or may be even null.\r
665 this.trigger = trigger;\r
666\r
667 upper && updateSize( that, upper, true );\r
668 lower && updateSize( that, lower, true );\r
669 updateSize( that, parent, true );\r
670\r
671 // Yeah, that's gonna be useful in inline-mode case.\r
672 if ( that.inInlineMode )\r
673 updateEditableSize( that, true );\r
674\r
675 // Set X coordinate (left, right, width).\r
676 if ( parent.equals( editable ) ) {\r
677 styleSet.left = view.scroll.x;\r
678 styleSet.right = -view.scroll.x;\r
679 styleSet.width = '';\r
680 } else {\r
681 styleSet.left = any.size.left - any.size.margin.left + view.scroll.x - ( that.inInlineMode ? view.editable.left + view.editable.border.left : 0 );\r
682 styleSet.width = any.size.outerWidth + any.size.margin.left + any.size.margin.right + view.scroll.x;\r
683 styleSet.right = '';\r
684 }\r
685\r
686 // Set Y coordinate (top) for trigger consisting of two elements.\r
687 if ( upper && lower ) {\r
688 // No margins at all or they're equal. Place box right between.\r
689 if ( upper.size.margin.bottom === lower.size.margin.top )\r
690 styleSet.top = 0 | ( upper.size.bottom + upper.size.margin.bottom / 2 );\r
691 else {\r
692 // Upper margin < lower margin. Place at lower margin.\r
693 if ( upper.size.margin.bottom < lower.size.margin.top )\r
694 styleSet.top = upper.size.bottom + upper.size.margin.bottom;\r
695 // Upper margin > lower margin. Place at upper margin - lower margin.\r
696 else\r
697 styleSet.top = upper.size.bottom + upper.size.margin.bottom - lower.size.margin.top;\r
698 }\r
699 }\r
700 // Set Y coordinate (top) for single-edge trigger.\r
701 else if ( !upper )\r
702 styleSet.top = lower.size.top - lower.size.margin.top;\r
703 else if ( !lower ) {\r
704 styleSet.top = upper.size.bottom + upper.size.margin.bottom;\r
705 }\r
706\r
707 // Set box button modes if close to the viewport horizontal edge\r
708 // or look forced by the trigger.\r
709 if ( trigger.is( LOOK_TOP ) || inBetween( styleSet.top, view.scroll.y - 15, view.scroll.y + 5 ) ) {\r
710 styleSet.top = that.inInlineMode ? 0 : view.scroll.y;\r
711 this.look( LOOK_TOP );\r
712 } else if ( trigger.is( LOOK_BOTTOM ) || inBetween( styleSet.top, view.pane.bottom - 5, view.pane.bottom + 15 ) ) {\r
713 styleSet.top = that.inInlineMode ? (\r
714 view.editable.height + view.editable.padding.top + view.editable.padding.bottom\r
715 ) : (\r
716 view.pane.bottom - 1\r
717 );\r
718\r
719 this.look( LOOK_BOTTOM );\r
720 } else {\r
721 if ( that.inInlineMode )\r
722 styleSet.top -= view.editable.top + view.editable.border.top;\r
723\r
724 this.look( LOOK_NORMAL );\r
725 }\r
726\r
727 if ( that.inInlineMode ) {\r
728 // 1px bug here...\r
729 styleSet.top--;\r
730\r
731 // Consider the editable to be an element with overflow:scroll\r
732 // and non-zero scrollTop/scrollLeft value.\r
1794320d 733 // For example: divarea editable. (http://dev.ckeditor.com/ticket/9383)\r
c63493c8
IB
734 styleSet.top += view.editable.scroll.top;\r
735 styleSet.left += view.editable.scroll.left;\r
736 }\r
737\r
738 // Append `px` prefixes.\r
739 for ( var style in styleSet )\r
740 styleSet[ style ] = CKEDITOR.tools.cssLength( styleSet[ style ] );\r
741\r
742 this.setStyles( styleSet );\r
743 },\r
744\r
745 // Changes look of the box according to current needs.\r
746 // Three different styles are available: [ LOOK_TOP, LOOK_BOTTOM, LOOK_NORMAL ].\r
747 look: function( look ) {\r
748 if ( this.oldLook == look )\r
749 return;\r
750\r
751 for ( var i = this.lineChildren.length, child; i--; )\r
752 ( child = this.lineChildren[ i ] ).setAttribute( 'style', child.base + child.looks[ 0 | look / 2 ] );\r
753\r
754 this.oldLook = look;\r
755 },\r
756\r
757 wrap: new newElement( 'span', that.doc )\r
758\r
759 } );\r
760\r
761 // Insert children into the box.\r
762 for ( var i = line.lineChildren.length; i--; )\r
763 line.lineChildren[ i ].appendTo( line );\r
764\r
765 // Set default look of the box.\r
766 line.look( LOOK_NORMAL );\r
767\r
768 // Using that wrapper prevents IE (8,9) from resizing editable area at the moment\r
769 // of box insertion. This works thanks to the fact, that positioned box is wrapped by\r
770 // an inline element. So much tricky.\r
771 line.appendTo( line.wrap );\r
772\r
773 // Make the box unselectable.\r
774 line.unselectable();\r
775\r
776 // Handle accessSpace node insertion.\r
777 line.lineChildren[ 0 ].on( 'mouseup', function( event ) {\r
778 line.detach();\r
779\r
780 accessFocusSpace( that, function( accessNode ) {\r
781 // Use old trigger that was saved by 'place' method. Look: line.place\r
782 var trigger = that.line.trigger;\r
783\r
784 accessNode[ trigger.is( EDGE_TOP ) ? 'insertBefore' : 'insertAfter' ](\r
785 trigger.is( EDGE_TOP ) ? trigger.lower : trigger.upper );\r
786 }, true );\r
787\r
788 that.editor.focus();\r
789\r
790 if ( !env.ie && that.enterMode != CKEDITOR.ENTER_BR )\r
791 that.hotNode.scrollIntoView();\r
792\r
793 event.data.preventDefault( true );\r
794 } );\r
795\r
796 // Prevents IE9 from displaying the resize box and disables drag'n'drop functionality.\r
797 line.on( 'mousedown', function( event ) {\r
798 event.data.preventDefault( true );\r
799 } );\r
800\r
801 that.line = line;\r
802 }\r
803\r
804 // This function allows accessing any focus space according to the insert function:\r
805 // * For enterMode ENTER_P it creates P element filled with dummy white-space.\r
806 // * For enterMode ENTER_DIV it creates DIV element filled with dummy white-space.\r
807 // * For enterMode ENTER_BR it creates BR element or &nbsp; in IE.\r
808 //\r
809 // The node is being inserted according to insertFunction. Finally the method\r
810 // selects the non-breaking space making the node ready for typing.\r
811 function accessFocusSpace( that, insertFunction, doSave ) {\r
812 var range = new CKEDITOR.dom.range( that.doc ),\r
813 editor = that.editor,\r
814 accessNode;\r
815\r
816 // IE requires text node of &nbsp; in ENTER_BR mode.\r
817 if ( env.ie && that.enterMode == CKEDITOR.ENTER_BR )\r
818 accessNode = that.doc.createText( WHITE_SPACE );\r
819\r
820 // In other cases a regular element is used.\r
821 else {\r
822 // Use the enterMode of editable's limit or editor's\r
823 // enter mode if not in nested editable.\r
824 var limit = getClosestEditableLimit( that.element, true ),\r
825\r
826 // This is an enter mode for the context. We cannot use\r
827 // editor.activeEnterMode because the focused nested editable will\r
828 // have a different enterMode as editor but magicline will be inserted\r
829 // directly into editor's editable.\r
830 enterMode = limit && limit.data( 'cke-enter-mode' ) || that.enterMode;\r
831\r
832 accessNode = new newElement( enterElements[ enterMode ], that.doc );\r
833\r
834 if ( !accessNode.is( 'br' ) ) {\r
835 var dummy = that.doc.createText( WHITE_SPACE );\r
836 dummy.appendTo( accessNode );\r
837 }\r
838 }\r
839\r
840 doSave && editor.fire( 'saveSnapshot' );\r
841\r
842 insertFunction( accessNode );\r
843 //dummy.appendTo( accessNode );\r
844 range.moveToPosition( accessNode, CKEDITOR.POSITION_AFTER_START );\r
845 editor.getSelection().selectRanges( [ range ] );\r
846 that.hotNode = accessNode;\r
847\r
848 doSave && editor.fire( 'saveSnapshot' );\r
849 }\r
850\r
851 // Access focus space on demand by taking an element under the caret as a reference.\r
852 // The space is accessed provided the element under the caret is trigger AND:\r
853 //\r
854 // 1. First/last-child of its parent:\r
855 // +----------------------- Parent element -+\r
856 // | +------------------------------ DIV -+ | <-- Access before\r
857 // | | Foo^ | |\r
858 // | | | |\r
859 // | +------------------------------------+ | <-- Access after\r
860 // +----------------------------------------+\r
861 //\r
862 // OR\r
863 //\r
864 // 2. It has a direct sibling element, which is also a trigger:\r
865 // +-------------------------------- DIV#1 -+\r
866 // | Foo^ |\r
867 // | |\r
868 // +----------------------------------------+\r
869 // <-- Access here\r
870 // +-------------------------------- DIV#2 -+\r
871 // | Bar |\r
872 // | |\r
873 // +----------------------------------------+\r
874 //\r
875 // OR\r
876 //\r
877 // 3. It has a direct sibling, which is a trigger and has a valid neighbour trigger,\r
878 // but belongs to dtd.$.empty/nonEditable:\r
879 // +------------------------------------ P -+\r
880 // | Foo^ |\r
881 // | |\r
882 // +----------------------------------------+\r
883 // +----------------------------------- HR -+\r
884 // <-- Access here\r
885 // +-------------------------------- DIV#2 -+\r
886 // | Bar |\r
887 // | |\r
888 // +----------------------------------------+\r
889 //\r
890 function accessFocusSpaceCmd( that, insertAfter ) {\r
891 return {\r
892 canUndo: true,\r
893 modes: { wysiwyg: 1 },\r
894 exec: ( function() {\r
895\r
896 // Inserts line (accessNode) at the position by taking target node as a reference.\r
897 function doAccess( target ) {\r
898 // Remove old hotNode under certain circumstances.\r
899 var hotNodeChar = ( env.ie && env.version < 9 ? ' ' : WHITE_SPACE ),\r
900 removeOld = that.hotNode && // Old hotNode must exist.\r
901 that.hotNode.getText() == hotNodeChar && // Old hotNode hasn't been changed.\r
902 that.element.equals( that.hotNode ) && // Caret is inside old hotNode.\r
903 // Command is executed in the same direction.\r
904 that.lastCmdDirection === !!insertAfter; // jshint ignore:line\r
905\r
906 accessFocusSpace( that, function( accessNode ) {\r
907 if ( removeOld && that.hotNode )\r
908 that.hotNode.remove();\r
909\r
910 accessNode[ insertAfter ? 'insertAfter' : 'insertBefore' ]( target );\r
911\r
912 // Make this element distinguishable. Also remember the direction\r
913 // it's been inserted into document.\r
914 accessNode.setAttributes( {\r
915 'data-cke-magicline-hot': 1,\r
916 'data-cke-magicline-dir': !!insertAfter\r
917 } );\r
918\r
919 // Save last direction of the command (is insertAfter?).\r
920 that.lastCmdDirection = !!insertAfter;\r
921 } );\r
922\r
923 if ( !env.ie && that.enterMode != CKEDITOR.ENTER_BR )\r
924 that.hotNode.scrollIntoView();\r
925\r
926 // Detach the line if was visible (previously triggered by mouse).\r
927 that.line.detach();\r
928 }\r
929\r
930 return function( editor ) {\r
931 var selected = editor.getSelection().getStartElement(),\r
932 limit;\r
933\r
1794320d 934 // (http://dev.ckeditor.com/ticket/9833) Go down to the closest non-inline element in DOM structure\r
c63493c8
IB
935 // since inline elements don't participate in in magicline.\r
936 selected = selected.getAscendant( DTD_BLOCK, 1 );\r
937\r
938 // Stop if selected is a child of a tabu-list element.\r
939 if ( isInTabu( that, selected ) )\r
940 return;\r
941\r
942 // Sometimes it may happen that there's no parent block below selected element\r
943 // or, for example, getAscendant reaches editable or editable parent.\r
944 // We must avoid such pathological cases.\r
945 if ( !selected || selected.equals( that.editable ) || selected.contains( that.editable ) )\r
946 return;\r
947\r
948 // Executing the command directly in nested editable should\r
949 // access space before/after it.\r
950 if ( ( limit = getClosestEditableLimit( selected ) ) && limit.getAttribute( 'contenteditable' ) == 'false' )\r
951 selected = limit;\r
952\r
953 // That holds element from mouse. Replace it with the\r
954 // element under the caret.\r
955 that.element = selected;\r
956\r
957 // (3.) Handle the following cases where selected neighbour\r
958 // is a trigger inaccessible for the caret AND:\r
959 // - Is first/last-child\r
960 // OR\r
961 // - Has a sibling, which is also a trigger.\r
962 var neighbor = getNonEmptyNeighbour( that, selected, !insertAfter ),\r
963 neighborSibling;\r
964\r
965 // Check for a neighbour that belongs to triggers.\r
966 // Consider only non-accessible elements (they cannot have any children)\r
967 // since they cannot be given a caret inside, to run the command\r
968 // the regular way (1. & 2.).\r
969 if (\r
970 isHtml( neighbor ) && neighbor.is( that.triggers ) && neighbor.is( DTD_NONACCESSIBLE ) &&\r
971 (\r
972 // Check whether neighbor is first/last-child.\r
973 !getNonEmptyNeighbour( that, neighbor, !insertAfter ) ||\r
974 // Check for a sibling of a neighbour that also is a trigger.\r
975 (\r
976 ( neighborSibling = getNonEmptyNeighbour( that, neighbor, !insertAfter ) ) &&\r
977 isHtml( neighborSibling ) &&\r
978 neighborSibling.is( that.triggers )\r
979 )\r
980 )\r
981 ) {\r
982 doAccess( neighbor );\r
983 return;\r
984 }\r
985\r
986 // Look for possible target element DOWN "selected" DOM branch (towards editable)\r
987 // that belong to that.triggers\r
988 var target = getAscendantTrigger( that, selected );\r
989\r
990 // No HTML target -> no access.\r
991 if ( !isHtml( target ) )\r
992 return;\r
993\r
994 // (1.) Target is first/last child -> access.\r
995 if ( !getNonEmptyNeighbour( that, target, !insertAfter ) ) {\r
996 doAccess( target );\r
997 return;\r
998 }\r
999\r
1000 var sibling = getNonEmptyNeighbour( that, target, !insertAfter );\r
1001\r
1002 // (2.) Target has a sibling that belongs to that.triggers -> access.\r
1003 if ( sibling && isHtml( sibling ) && sibling.is( that.triggers ) ) {\r
1004 doAccess( target );\r
1005 return;\r
1006 }\r
1007 };\r
1008 } )()\r
1009 };\r
1010 }\r
1011\r
1012 function isLine( that, node ) {\r
1013 if ( !( node && node.type == CKEDITOR.NODE_ELEMENT && node.$ ) )\r
1014 return false;\r
1015\r
1016 var line = that.line;\r
1017\r
1018 return line.wrap.equals( node ) || line.wrap.contains( node );\r
1019 }\r
1020\r
1021 // Is text node containing white-spaces only?\r
1022 var isEmptyTextNode = CKEDITOR.dom.walker.whitespaces();\r
1023\r
1024 // Is fully visible HTML node?\r
1025 function isHtml( node ) {\r
1026 return node && node.type == CKEDITOR.NODE_ELEMENT && node.$; // IE requires that\r
1027 }\r
1028\r
1029 function isFloated( element ) {\r
1030 if ( !isHtml( element ) )\r
1031 return false;\r
1032\r
1033 var options = { left: 1, right: 1, center: 1 };\r
1034\r
1035 return !!( options[ element.getComputedStyle( 'float' ) ] || options[ element.getAttribute( 'align' ) ] );\r
1036 }\r
1037\r
1038 function isFlowBreaker( element ) {\r
1039 if ( !isHtml( element ) )\r
1040 return false;\r
1041\r
1042 return isPositioned( element ) || isFloated( element );\r
1043 }\r
1044\r
1045 // Isn't node of NODE_COMMENT type?\r
1046 var isComment = CKEDITOR.dom.walker.nodeType( CKEDITOR.NODE_COMMENT );\r
1047\r
1048 function isPositioned( element ) {\r
1049 return !!{ absolute: 1, fixed: 1 }[ element.getComputedStyle( 'position' ) ];\r
1050 }\r
1051\r
1052 // Is text node?\r
1053 function isTextNode( node ) {\r
1054 return node && node.type == CKEDITOR.NODE_TEXT;\r
1055 }\r
1056\r
1057 function isTrigger( that, element ) {\r
1058 return isHtml( element ) ? element.is( that.triggers ) : null;\r
1059 }\r
1060\r
1061 function isInTabu( that, element ) {\r
1062 if ( !element )\r
1063 return false;\r
1064\r
1065 var parents = element.getParents( 1 );\r
1066\r
1067 for ( var i = parents.length ; i-- ; ) {\r
1068 for ( var j = that.tabuList.length ; j-- ; ) {\r
1069 if ( parents[ i ].hasAttribute( that.tabuList[ j ] ) )\r
1070 return true;\r
1071 }\r
1072 }\r
1073\r
1074 return false;\r
1075 }\r
1076\r
1077 // This function checks vertically is there's a relevant child between element's edge\r
1078 // and the pointer.\r
1079 // \-> Table contents are omitted.\r
1080 function isChildBetweenPointerAndEdge( that, parent, edgeBottom ) {\r
1081 var edgeChild = parent[ edgeBottom ? 'getLast' : 'getFirst' ]( function( node ) {\r
1082 return that.isRelevant( node ) && !node.is( DTD_TABLECONTENT );\r
1083 } );\r
1084\r
1085 if ( !edgeChild )\r
1086 return false;\r
1087\r
1088 updateSize( that, edgeChild );\r
1089\r
1090 return edgeBottom ? edgeChild.size.top > that.mouse.y : edgeChild.size.bottom < that.mouse.y;\r
1091 }\r
1092\r
1093 // This method handles edge cases:\r
1094 // \-> Mouse is around upper or lower edge of view pane.\r
1095 // \-> Also scroll position is either minimal or maximal.\r
1096 // \-> It's OK to show LOOK_TOP(BOTTOM) type line.\r
1097 //\r
1098 // This trigger doesn't need additional post-filtering.\r
1099 //\r
1100 // +----------------------------- Editable -+ /--\r
1101 // | +---------------------- First child -+ | | <-- Top edge (first child)\r
1102 // | | | | |\r
1103 // | | | | | * Mouse activation area *\r
1104 // | | | | |\r
1105 // | | ... | | \-- Top edge + trigger offset\r
1106 // | . . |\r
1107 // | |\r
1108 // | . . |\r
1109 // | | ... | | /-- Bottom edge - trigger offset\r
1110 // | | | | |\r
1111 // | | | | | * Mouse activation area *\r
1112 // | | | | |\r
1113 // | +----------------------- Last child -+ | | <-- Bottom edge (last child)\r
1114 // +----------------------------------------+ \--\r
1115 //\r
1116 function triggerEditable( that ) {\r
1117 that.debug.groupStart( 'triggerEditable' ); // %REMOVE_LINE%\r
1118\r
1119 var editable = that.editable,\r
1120 mouse = that.mouse,\r
1121 view = that.view,\r
1122 triggerOffset = that.triggerOffset,\r
1123 triggerLook;\r
1124\r
1125 // Update editable dimensions.\r
1126 updateEditableSize( that );\r
1127\r
1128 // This flag determines whether checking bottom trigger.\r
1129 var bottomTrigger = mouse.y > (\r
1130 that.inInlineMode ? (\r
1131 view.editable.top + view.editable.height / 2\r
1132 ) : (\r
1133 // This is to handle case when editable.height / 2 <<< pane.height.\r
1134 Math.min( view.editable.height, view.pane.height ) / 2\r
1135 )\r
1136 ),\r
1137\r
1138 // Edge node according to bottomTrigger.\r
1139 edgeNode = editable[ bottomTrigger ? 'getLast' : 'getFirst' ]( function( node ) {\r
1140 return !( isEmptyTextNode( node ) || isComment( node ) );\r
1141 } );\r
1142\r
1143 // There's no edge node. Abort.\r
1144 if ( !edgeNode ) {\r
1145 that.debug.logEnd( 'ABORT. No edge node found.' ); // %REMOVE_LINE%\r
1146 return null;\r
1147 }\r
1148\r
1149 // If the edgeNode in editable is ML, get the next one.\r
1150 if ( isLine( that, edgeNode ) ) {\r
1151 edgeNode = that.line.wrap[ bottomTrigger ? 'getPrevious' : 'getNext' ]( function( node ) {\r
1152 return !( isEmptyTextNode( node ) || isComment( node ) );\r
1153 } );\r
1154 }\r
1155\r
1156 // Exclude bad nodes (no ML needed then):\r
1157 // \-> Edge node is text.\r
1158 // \-> Edge node is floated, etc.\r
1159 //\r
1160 // Edge node *must be* a valid trigger at this stage as well.\r
1161 if ( !isHtml( edgeNode ) || isFlowBreaker( edgeNode ) || !isTrigger( that, edgeNode ) ) {\r
1162 that.debug.logEnd( 'ABORT. Invalid edge node.' ); // %REMOVE_LINE%\r
1163 return null;\r
1164 }\r
1165\r
1166 // Update size of edge node. Dimensions will be necessary.\r
1167 updateSize( that, edgeNode );\r
1168\r
1169 // Return appropriate trigger according to bottomTrigger.\r
1170 // \-> Top edge trigger case first.\r
1171 if ( !bottomTrigger && // Top trigger case.\r
1172 edgeNode.size.top >= 0 && // Check if the first element is fully visible.\r
1173 inBetween( mouse.y, 0, edgeNode.size.top + triggerOffset ) ) { // Check if mouse in [0, edgeNode.top + triggerOffset].\r
1174\r
1175 // Determine trigger look.\r
1176 triggerLook = that.inInlineMode || view.scroll.y === 0 ?\r
1177 LOOK_TOP : LOOK_NORMAL;\r
1178\r
1179 that.debug.logEnd( 'SUCCESS. Created box trigger. EDGE_TOP.' ); // %REMOVE_LINE%\r
1180\r
1181 return new boxTrigger( [ null, edgeNode,\r
1182 EDGE_TOP,\r
1183 TYPE_EDGE,\r
1184 triggerLook\r
1185 ] );\r
1186 }\r
1187\r
1188 // \-> Bottom case.\r
1189 else if ( bottomTrigger &&\r
1190 edgeNode.size.bottom <= view.pane.height && // Check if the last element is fully visible\r
1191 inBetween( mouse.y, // Check if mouse in...\r
1192 edgeNode.size.bottom - triggerOffset, view.pane.height ) ) { // [ edgeNode.bottom - triggerOffset, paneHeight ]\r
1193\r
1194 // Determine trigger look.\r
1195 triggerLook = that.inInlineMode ||\r
1196 inBetween( edgeNode.size.bottom, view.pane.height - triggerOffset, view.pane.height ) ?\r
1197 LOOK_BOTTOM : LOOK_NORMAL;\r
1198\r
1199 that.debug.logEnd( 'SUCCESS. Created box trigger. EDGE_BOTTOM.' ); // %REMOVE_LINE%\r
1200\r
1201 return new boxTrigger( [ edgeNode, null,\r
1202 EDGE_BOTTOM,\r
1203 TYPE_EDGE,\r
1204 triggerLook\r
1205 ] );\r
1206 }\r
1207\r
1208 that.debug.logEnd( 'ABORT. No trigger created.' ); // %REMOVE_LINE%\r
1209 return null;\r
1210 }\r
1211\r
1212 // This method covers cases *inside* of an element:\r
1213 // \-> The pointer is in the top (bottom) area of an element and there's\r
1214 // HTML node before (after) this element.\r
1215 // \-> An element being the first or last child of its parent.\r
1216 //\r
1217 // +----------------------- Parent element -+\r
1218 // | +----------------------- Element #1 -+ | /--\r
1219 // | | | | | * Mouse activation area (as first child) *\r
1220 // | | | | \--\r
1221 // | | | | /--\r
1222 // | | | | | * Mouse activation area (Element #2) *\r
1223 // | +------------------------------------+ | \--\r
1224 // | |\r
1225 // | +----------------------- Element #2 -+ | /--\r
1226 // | | | | | * Mouse activation area (Element #1) *\r
1227 // | | | | \--\r
1228 // | | | |\r
1229 // | +------------------------------------+ |\r
1230 // | |\r
1231 // | Text node is here. |\r
1232 // | |\r
1233 // | +----------------------- Element #3 -+ |\r
1234 // | | | |\r
1235 // | | | |\r
1236 // | | | | /--\r
1237 // | | | | | * Mouse activation area (as last child) *\r
1238 // | +------------------------------------+ | \--\r
1239 // +----------------------------------------+\r
1240 //\r
1241 function triggerEdge( that ) {\r
1242 that.debug.groupStart( 'triggerEdge' ); // %REMOVE_LINE%\r
1243\r
1244 var mouse = that.mouse,\r
1245 view = that.view,\r
1246 triggerOffset = that.triggerOffset;\r
1247\r
1248 // Get the ascendant trigger basing on elementFromMouse.\r
1249 var element = getAscendantTrigger( that );\r
1250\r
1251 that.debug.logElements( [ element ], [ 'Ascendant trigger' ], 'First stage' ); // %REMOVE_LINE%\r
1252\r
1253 // Abort if there's no appropriate element.\r
1254 if ( !element ) {\r
1255 that.debug.logEnd( 'ABORT. No element, element is editable or element contains editable.' ); // %REMOVE_LINE%\r
1256 return null;\r
1257 }\r
1258\r
1259 // Dimensions will be necessary.\r
1260 updateSize( that, element );\r
1261\r
1262 // If triggerOffset is larger than a half of element's height,\r
1263 // use an offset of 1/2 of element's height. If the offset wasn't reduced,\r
1264 // top area would cover most (all) cases.\r
1265 var fixedOffset = Math.min( triggerOffset,\r
1266 0 | ( element.size.outerHeight / 2 ) ),\r
1267\r
1268 // This variable will hold the trigger to be returned.\r
1269 triggerSetup = [],\r
1270 triggerLook,\r
1271\r
1272 // This flag determines whether dealing with a bottom trigger.\r
1273 bottomTrigger;\r
1274\r
1275 // \-> Top trigger.\r
1276 if ( inBetween( mouse.y, element.size.top - 1, element.size.top + fixedOffset ) )\r
1277 bottomTrigger = false;\r
1278 // \-> Bottom trigger.\r
1279 else if ( inBetween( mouse.y, element.size.bottom - fixedOffset, element.size.bottom + 1 ) )\r
1280 bottomTrigger = true;\r
1281 // \-> Abort. Not in a valid trigger space.\r
1282 else {\r
1283 that.debug.logEnd( 'ABORT. Not around of any edge.' ); // %REMOVE_LINE%\r
1284 return null;\r
1285 }\r
1286\r
1287 // Reject wrong elements.\r
1288 // \-> Reject an element which is a flow breaker.\r
1289 // \-> Reject an element which has a child above/below the mouse pointer.\r
1290 // \-> Reject an element which belongs to list items.\r
1291 if (\r
1292 isFlowBreaker( element ) ||\r
1293 isChildBetweenPointerAndEdge( that, element, bottomTrigger ) ||\r
1294 element.getParent().is( DTD_LISTITEM )\r
1295 ) {\r
1296 that.debug.logEnd( 'ABORT. element is wrong', element ); // %REMOVE_LINE%\r
1297 return null;\r
1298 }\r
1299\r
1300 // Get sibling according to bottomTrigger.\r
1301 var elementSibling = getNonEmptyNeighbour( that, element, !bottomTrigger );\r
1302\r
1303 // No sibling element.\r
1304 // This is a first or last child case.\r
1305 if ( !elementSibling ) {\r
1306 // No need to reject the element as it has already been done before.\r
1307 // Prepare a trigger.\r
1308\r
1309 // Determine trigger look.\r
1310 if ( element.equals( that.editable[ bottomTrigger ? 'getLast' : 'getFirst' ]( that.isRelevant ) ) ) {\r
1311 updateEditableSize( that );\r
1312\r
1313 if (\r
1314 bottomTrigger && inBetween( mouse.y,\r
1315 element.size.bottom - fixedOffset, view.pane.height ) &&\r
1316 inBetween( element.size.bottom, view.pane.height - fixedOffset, view.pane.height )\r
1317 ) {\r
1318 triggerLook = LOOK_BOTTOM;\r
1319 } else if ( inBetween( mouse.y, 0, element.size.top + fixedOffset ) ) {\r
1320 triggerLook = LOOK_TOP;\r
1321 }\r
1322 } else {\r
1323 triggerLook = LOOK_NORMAL;\r
1324 }\r
1325\r
1326 triggerSetup = [ null, element ][ bottomTrigger ? 'reverse' : 'concat' ]().concat( [\r
1327 bottomTrigger ? EDGE_BOTTOM : EDGE_TOP,\r
1328 TYPE_EDGE,\r
1329 triggerLook,\r
1330 element.equals( that.editable[ bottomTrigger ? 'getLast' : 'getFirst' ]( that.isRelevant ) ) ?\r
1331 ( bottomTrigger ? LOOK_BOTTOM : LOOK_TOP ) : LOOK_NORMAL\r
1332 ] );\r
1333\r
1334 that.debug.log( 'Configured edge trigger of ' + ( bottomTrigger ? 'EDGE_BOTTOM' : 'EDGE_TOP' ) ); // %REMOVE_LINE%\r
1335 }\r
1336\r
1337 // Abort. Sibling is a text element.\r
1338 else if ( isTextNode( elementSibling ) ) {\r
1339 that.debug.logEnd( 'ABORT. Sibling is non-empty text element' ); // %REMOVE_LINE%\r
1340 return null;\r
1341 }\r
1342\r
1343 // Check if the sibling is a HTML element.\r
1344 // If so, create an TYPE_EDGE, EDGE_MIDDLE trigger.\r
1345 else if ( isHtml( elementSibling ) ) {\r
1346 // Reject wrong elementSiblings.\r
1347 // \-> Reject an elementSibling which is a flow breaker.\r
1348 // \-> Reject an elementSibling which isn't a trigger.\r
1349 // \-> Reject an elementSibling which belongs to list items.\r
1350 if (\r
1351 isFlowBreaker( elementSibling ) ||\r
1352 !isTrigger( that, elementSibling ) ||\r
1353 elementSibling.getParent().is( DTD_LISTITEM )\r
1354 ) {\r
1355 that.debug.logEnd( 'ABORT. elementSibling is wrong', elementSibling ); // %REMOVE_LINE%\r
1356 return null;\r
1357 }\r
1358\r
1359 // Prepare a trigger.\r
1360 triggerSetup = [ elementSibling, element ][ bottomTrigger ? 'reverse' : 'concat' ]().concat( [\r
1361 EDGE_MIDDLE,\r
1362 TYPE_EDGE\r
1363 ] );\r
1364\r
1365 that.debug.log( 'Configured edge trigger of EDGE_MIDDLE' ); // %REMOVE_LINE%\r
1366 }\r
1367\r
1368 if ( 0 in triggerSetup ) {\r
1369 that.debug.logEnd( 'SUCCESS. Returning a trigger.' ); // %REMOVE_LINE%\r
1370 return new boxTrigger( triggerSetup );\r
1371 }\r
1372\r
1373 that.debug.logEnd( 'ABORT. No trigger generated.' ); // %REMOVE_LINE%\r
1374 return null;\r
1375 }\r
1376\r
1377 // Checks iteratively up and down in search for elements using elementFromMouse method.\r
1378 // Useful if between two triggers.\r
1379 //\r
1380 // +----------------------- Parent element -+\r
1381 // | +----------------------- Element #1 -+ |\r
1382 // | | | |\r
1383 // | | | |\r
1384 // | | | |\r
1385 // | +------------------------------------+ |\r
1386 // | | /--\r
1387 // | . | |\r
1388 // | . +-- Floated -+ | |\r
1389 // | | | | | | * Mouse activation area *\r
1390 // | | | IGNORE | | |\r
1391 // | X | | | | Method searches vertically for sibling elements.\r
1392 // | | +------------+ | | Start point is X (mouse-y coordinate).\r
1393 // | | | | Floated elements, comments and empty text nodes are omitted.\r
1394 // | . | |\r
1395 // | . | |\r
1396 // | | \--\r
1397 // | +----------------------- Element #2 -+ |\r
1398 // | | | |\r
1399 // | | | |\r
1400 // | | | |\r
1401 // | | | |\r
1402 // | +------------------------------------+ |\r
1403 // +----------------------------------------+\r
1404 //\r
1405 var triggerExpand = ( function() {\r
1406 // The heart of the procedure. This method creates triggers that are\r
1407 // filtered by expandFilter method.\r
1408 function expandEngine( that ) {\r
1409 that.debug.groupStart( 'expandEngine' ); // %REMOVE_LINE%\r
1410\r
1411 var startElement = that.element,\r
1412 upper, lower, trigger;\r
1413\r
1414 if ( !isHtml( startElement ) || startElement.contains( that.editable ) ) {\r
1415 that.debug.logEnd( 'ABORT. No start element, or start element contains editable.' ); // %REMOVE_LINE%\r
1416 return null;\r
1417 }\r
1418\r
1419 // Stop searching if element is in non-editable branch of DOM.\r
1420 if ( startElement.isReadOnly() )\r
1421 return null;\r
1422\r
1423 trigger = verticalSearch( that,\r
1424 function( current, startElement ) {\r
1425 return !startElement.equals( current ); // stop when start element and the current one differ\r
1426 }, function( that, mouse ) {\r
1427 return elementFromMouse( that, true, mouse );\r
1428 }, startElement ),\r
1429\r
1430 upper = trigger.upper,\r
1431 lower = trigger.lower;\r
1432\r
1433 that.debug.logElements( [ upper, lower ], [ 'Upper', 'Lower' ], 'Pair found' ); // %REMOVE_LINE%\r
1434\r
1435 // Success: two siblings have been found\r
1436 if ( areSiblings( that, upper, lower ) ) {\r
1437 that.debug.logEnd( 'SUCCESS. Expand trigger created.' ); // %REMOVE_LINE%\r
1438 return trigger.set( EDGE_MIDDLE, TYPE_EXPAND );\r
1439 }\r
1440\r
1441 that.debug.logElements( [ startElement, upper, lower ], // %REMOVE_LINE%\r
1442 [ 'Start', 'Upper', 'Lower' ], 'Post-processing' ); // %REMOVE_LINE%\r
1443\r
1444 // Danger. Dragons ahead.\r
1445 // No siblings have been found during previous phase, post-processing may be necessary.\r
1446 // We can traverse DOM until a valid pair of elements around the pointer is found.\r
1447\r
1448 // Prepare for post-processing:\r
1449 // 1. Determine if upper and lower are children of startElement.\r
1450 // 1.1. If so, find their ascendants that are closest to startElement (one level deeper than startElement).\r
1451 // 1.2. Otherwise use first/last-child of the startElement as upper/lower. Why?:\r
1452 // a) upper/lower belongs to another branch of the DOM tree.\r
1453 // b) verticalSearch encountered an edge of the viewport and failed.\r
1454 // 1.3. Make sure upper and lower still exist. Why?:\r
1455 // a) Upper and lower may be not belong to the branch of the startElement (may not exist at all) and\r
1456 // startElement has no children.\r
1457 // 2. Perform the post-processing.\r
1458 // 2.1. Gather dimensions of an upper element.\r
1459 // 2.2. Abort if lower edge of upper is already under the mouse pointer. Why?:\r
1460 // a) We expect upper to be above and lower below the mouse pointer.\r
1461 // 3. Perform iterative search while upper != lower.\r
1462 // 3.1. Find the upper-next element. If there's no such element, break current search. Why?:\r
1463 // a) There's no point in further search if there are only text nodes ahead.\r
1464 // 3.2. Calculate the distance between the middle point of ( upper, upperNext ) and mouse-y.\r
1465 // 3.3. If the distance is shorter than the previous best, save it (save upper, upperNext as well).\r
1466 // 3.4. If the optimal pair is found, assign it back to the trigger.\r
1467\r
1468 // 1.1., 1.2.\r
1469 if ( upper && startElement.contains( upper ) ) {\r
1470 while ( !upper.getParent().equals( startElement ) )\r
1471 upper = upper.getParent();\r
1472 } else {\r
1473 upper = startElement.getFirst( function( node ) {\r
1474 return expandSelector( that, node );\r
1475 } );\r
1476 }\r
1477\r
1478 if ( lower && startElement.contains( lower ) ) {\r
1479 while ( !lower.getParent().equals( startElement ) )\r
1480 lower = lower.getParent();\r
1481 } else {\r
1482 lower = startElement.getLast( function( node ) {\r
1483 return expandSelector( that, node );\r
1484 } );\r
1485 }\r
1486\r
1487 // 1.3.\r
1488 if ( !upper || !lower ) {\r
1489 that.debug.logEnd( 'ABORT. There is no upper or no lower element.' ); // %REMOVE_LINE%\r
1490 return null;\r
1491 }\r
1492\r
1493 // 2.1.\r
1494 updateSize( that, upper );\r
1495 updateSize( that, lower );\r
1496\r
1497 if ( !checkMouseBetweenElements( that, upper, lower ) ) {\r
1498 that.debug.logEnd( 'ABORT. Mouse is already above upper or below lower.' ); // %REMOVE_LINE%\r
1499 return null;\r
1500 }\r
1501\r
1502 var minDistance = Number.MAX_VALUE,\r
1503 currentDistance, upperNext, minElement, minElementNext;\r
1504\r
1505 while ( lower && !lower.equals( upper ) ) {\r
1506 // 3.1.\r
1507 if ( !( upperNext = upper.getNext( that.isRelevant ) ) )\r
1508 break;\r
1509\r
1510 // 3.2.\r
1511 currentDistance = Math.abs( getMidpoint( that, upper, upperNext ) - that.mouse.y );\r
1512\r
1513 // 3.3.\r
1514 if ( currentDistance < minDistance ) {\r
1515 minDistance = currentDistance;\r
1516 minElement = upper;\r
1517 minElementNext = upperNext;\r
1518 }\r
1519\r
1520 upper = upperNext;\r
1521 updateSize( that, upper );\r
1522 }\r
1523\r
1524 that.debug.logElements( [ minElement, minElementNext ], // %REMOVE_LINE%\r
1525 [ 'Min', 'MinNext' ], 'Post-processing results' ); // %REMOVE_LINE%\r
1526\r
1527 // 3.4.\r
1528 if ( !minElement || !minElementNext ) {\r
1529 that.debug.logEnd( 'ABORT. No Min or MinNext' ); // %REMOVE_LINE%\r
1530 return null;\r
1531 }\r
1532\r
1533 if ( !checkMouseBetweenElements( that, minElement, minElementNext ) ) {\r
1534 that.debug.logEnd( 'ABORT. Mouse is already above minElement or below minElementNext.' ); // %REMOVE_LINE%\r
1535 return null;\r
1536 }\r
1537\r
1538 // An element of minimal distance has been found. Assign it to the trigger.\r
1539 trigger.upper = minElement;\r
1540 trigger.lower = minElementNext;\r
1541\r
1542 // Success: post-processing revealed a pair of elements.\r
1543 that.debug.logEnd( 'SUCCESSFUL post-processing. Trigger created.' ); // %REMOVE_LINE%\r
1544 return trigger.set( EDGE_MIDDLE, TYPE_EXPAND );\r
1545 }\r
1546\r
1547 // This is default element selector used by the engine.\r
1548 function expandSelector( that, node ) {\r
1549 return !( isTextNode( node ) ||\r
1550 isComment( node ) ||\r
1551 isFlowBreaker( node ) ||\r
1552 isLine( that, node ) ||\r
1553 ( node.type == CKEDITOR.NODE_ELEMENT && node.$ && node.is( 'br' ) ) );\r
1554 }\r
1555\r
1556 // This method checks whether mouse-y is between the top edge of upper\r
1557 // and bottom edge of lower.\r
1558 //\r
1559 // NOTE: This method assumes that updateSize has already been called\r
1560 // for the elements and is up-to-date.\r
1561 //\r
1562 // +---------------------------- Upper -+ /--\r
1563 // | | |\r
1564 // +------------------------------------+ |\r
1565 // |\r
1566 // ... |\r
1567 // |\r
1568 // X | * Return true for mouse-y in this range *\r
1569 // |\r
1570 // ... |\r
1571 // |\r
1572 // +---------------------------- Lower -+ |\r
1573 // | | |\r
1574 // +------------------------------------+ \--\r
1575 //\r
1576 function checkMouseBetweenElements( that, upper, lower ) {\r
1577 return inBetween( that.mouse.y, upper.size.top, lower.size.bottom );\r
1578 }\r
1579\r
1580 // A method for trigger filtering. Accepts or rejects trigger pairs\r
1581 // by their location in DOM etc.\r
1582 function expandFilter( that, trigger ) {\r
1583 that.debug.groupStart( 'expandFilter' ); // %REMOVE_LINE%\r
1584\r
1585 var upper = trigger.upper,\r
1586 lower = trigger.lower;\r
1587\r
1588 if (\r
1589 !upper || !lower || // NOT: EDGE_MIDDLE trigger ALWAYS has two elements.\r
1590 isFlowBreaker( lower ) || isFlowBreaker( upper ) || // NOT: one of the elements is floated or positioned\r
1591 lower.equals( upper ) || upper.equals( lower ) || // NOT: two trigger elements, one equals another.\r
1592 lower.contains( upper ) || upper.contains( lower )\r
1593 ) { // NOT: two trigger elements, one contains another.\r
1594 that.debug.logEnd( 'REJECTED. No upper or no lower or they contain each other.' ); // %REMOVE_LINE%\r
1595\r
1596 return false;\r
1597 }\r
1598\r
1599 // YES: two trigger elements, pure siblings.\r
1600 else if ( isTrigger( that, upper ) && isTrigger( that, lower ) && areSiblings( that, upper, lower ) ) {\r
1601 that.debug.logElementsEnd( [ upper, lower ], // %REMOVE_LINE%\r
1602 [ 'upper', 'lower' ], 'APPROVED EDGE_MIDDLE' ); // %REMOVE_LINE%\r
1603\r
1604 return true;\r
1605 }\r
1606\r
1607 that.debug.logElementsEnd( [ upper, lower ], // %REMOVE_LINE%\r
1608 [ 'upper', 'lower' ], 'Rejected unknown pair' ); // %REMOVE_LINE%\r
1609\r
1610 return false;\r
1611 }\r
1612\r
1613 // Simple wrapper for expandEngine and expandFilter.\r
1614 return function( that ) {\r
1615 that.debug.groupStart( 'triggerExpand' ); // %REMOVE_LINE%\r
1616\r
1617 var trigger = expandEngine( that );\r
1618\r
1619 that.debug.groupEnd(); // %REMOVE_LINE%\r
1620 return trigger && expandFilter( that, trigger ) ? trigger : null;\r
1621 };\r
1622 } )();\r
1623\r
1624 // Collects dimensions of an element.\r
1625 var sizePrefixes = [ 'top', 'left', 'right', 'bottom' ];\r
1626\r
1627 function getSize( that, element, ignoreScroll, force ) {\r
1628 var docPosition = element.getDocumentPosition(),\r
1629 border = {},\r
1630 margin = {},\r
1631 padding = {},\r
1632 box = {};\r
1633\r
1634 for ( var i = sizePrefixes.length; i--; ) {\r
1635 border[ sizePrefixes[ i ] ] = parseInt( getStyle( 'border-' + sizePrefixes[ i ] + '-width' ), 10 ) || 0;\r
1636 padding[ sizePrefixes[ i ] ] = parseInt( getStyle( 'padding-' + sizePrefixes[ i ] ), 10 ) || 0;\r
1637 margin[ sizePrefixes[ i ] ] = parseInt( getStyle( 'margin-' + sizePrefixes[ i ] ), 10 ) || 0;\r
1638 }\r
1639\r
1640 // updateWindowSize if forced to do so OR NOT ignoring scroll.\r
1641 if ( !ignoreScroll || force )\r
1642 updateWindowSize( that, force );\r
1643\r
1644 box.top = docPosition.y - ( ignoreScroll ? 0 : that.view.scroll.y ), box.left = docPosition.x - ( ignoreScroll ? 0 : that.view.scroll.x ),\r
1645\r
1646 // w/ borders and paddings.\r
1647 box.outerWidth = element.$.offsetWidth, box.outerHeight = element.$.offsetHeight,\r
1648\r
1649 // w/o borders and paddings.\r
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 ),\r
1651\r
1652 box.bottom = box.top + box.outerHeight, box.right = box.left + box.outerWidth;\r
1653\r
1654 if ( that.inInlineMode ) {\r
1655 box.scroll = {\r
1656 top: element.$.scrollTop,\r
1657 left: element.$.scrollLeft\r
1658 };\r
1659 }\r
1660\r
1661 return extend( {\r
1662 border: border,\r
1663 padding: padding,\r
1664 margin: margin,\r
1665 ignoreScroll: ignoreScroll\r
1666 }, box, true );\r
1667\r
1668 function getStyle( propertyName ) {\r
1669 return element.getComputedStyle.call( element, propertyName );\r
1670 }\r
1671 }\r
1672\r
1673 function updateSize( that, element, ignoreScroll ) {\r
1674 if ( !isHtml( element ) ) // i.e. an element is hidden\r
1675 return ( element.size = null ); // -> reset size to make it useless for other methods\r
1676\r
1677 if ( !element.size )\r
1678 element.size = {};\r
1679\r
1680 // Abort if there was a similar query performed recently.\r
1681 // This kind of caching provides great performance improvement.\r
1682 else if ( element.size.ignoreScroll == ignoreScroll && element.size.date > new Date() - CACHE_TIME ) {\r
1683 that.debug.log( 'element.size: get from cache' ); // %REMOVE_LINE%\r
1684 return null;\r
1685 }\r
1686\r
1687 that.debug.log( 'element.size: capture' ); // %REMOVE_LINE%\r
1688\r
1689 return extend( element.size, getSize( that, element, ignoreScroll ), {\r
1690 date: +new Date()\r
1691 }, true );\r
1692 }\r
1693\r
1694 // Updates that.view.editable object.\r
1695 // This one must be called separately outside of updateWindowSize\r
1696 // to prevent cyclic dependency getSize<->updateWindowSize.\r
1697 // It calls getSize with force flag to avoid getWindowSize cache (look: getSize).\r
1698 function updateEditableSize( that, ignoreScroll ) {\r
1699 that.view.editable = getSize( that, that.editable, ignoreScroll, true );\r
1700 }\r
1701\r
1702 function updateWindowSize( that, force ) {\r
1703 if ( !that.view )\r
1704 that.view = {};\r
1705\r
1706 var view = that.view;\r
1707\r
1708 if ( !force && view && view.date > new Date() - CACHE_TIME ) {\r
1709 that.debug.log( 'win.size: get from cache' ); // %REMOVE_LINE%\r
1710 return;\r
1711 }\r
1712\r
1713 that.debug.log( 'win.size: capturing' ); // %REMOVE_LINE%\r
1714\r
1715 var win = that.win,\r
1716 scroll = win.getScrollPosition(),\r
1717 paneSize = win.getViewPaneSize();\r
1718\r
1719 extend( that.view, {\r
1720 scroll: {\r
1721 x: scroll.x,\r
1722 y: scroll.y,\r
1723 width: that.doc.$.documentElement.scrollWidth - paneSize.width,\r
1724 height: that.doc.$.documentElement.scrollHeight - paneSize.height\r
1725 },\r
1726 pane: {\r
1727 width: paneSize.width,\r
1728 height: paneSize.height,\r
1729 bottom: paneSize.height + scroll.y\r
1730 },\r
1731 date: +new Date()\r
1732 }, true );\r
1733 }\r
1734\r
1735 // This method searches document vertically using given\r
1736 // select criterion until stop criterion is fulfilled.\r
1737 function verticalSearch( that, stopCondition, selectCriterion, startElement ) {\r
1738 var upper = startElement,\r
1739 lower = startElement,\r
1740 mouseStep = 0,\r
1741 upperFound = false,\r
1742 lowerFound = false,\r
1743 viewPaneHeight = that.view.pane.height,\r
1744 mouse = that.mouse;\r
1745\r
1746 while ( mouse.y + mouseStep < viewPaneHeight && mouse.y - mouseStep > 0 ) {\r
1747 if ( !upperFound )\r
1748 upperFound = stopCondition( upper, startElement );\r
1749\r
1750 if ( !lowerFound )\r
1751 lowerFound = stopCondition( lower, startElement );\r
1752\r
1753 // Still not found...\r
1754 if ( !upperFound && mouse.y - mouseStep > 0 )\r
1755 upper = selectCriterion( that, { x: mouse.x, y: mouse.y - mouseStep } );\r
1756\r
1757 if ( !lowerFound && mouse.y + mouseStep < viewPaneHeight )\r
1758 lower = selectCriterion( that, { x: mouse.x, y: mouse.y + mouseStep } );\r
1759\r
1760 if ( upperFound && lowerFound )\r
1761 break;\r
1762\r
1763 // Instead of ++ to reduce the number of invocations by half.\r
1764 // It's trades off accuracy in some edge cases for improved performance.\r
1765 mouseStep += 2;\r
1766 }\r
1767\r
1768 return new boxTrigger( [ upper, lower, null, null ] );\r
1769 }\r
1770\r
1771} )();\r
1772\r
1773/**\r
1774 * Sets the default vertical distance between the edge of the element and the mouse pointer that\r
1775 * causes the magic line to appear. This option accepts a value in pixels, without the unit (for example:\r
1776 * `15` for 15 pixels).\r
1777 *\r
1778 * Read more in the [documentation](#!/guide/dev_magicline)\r
1779 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).\r
1780 *\r
1781 * // Changes the offset to 15px.\r
1782 * CKEDITOR.config.magicline_triggerOffset = 15;\r
1783 *\r
1784 * @cfg {Number} [magicline_triggerOffset=30]\r
1785 * @member CKEDITOR.config\r
1786 * @see CKEDITOR.config#magicline_holdDistance\r
1787 */\r
1788\r
1789/**\r
1790 * Defines the distance between the mouse pointer and the box within\r
1791 * which the magic line stays revealed and no other focus space is offered to be accessed.\r
1792 * This value is relative to {@link #magicline_triggerOffset}.\r
1793 *\r
1794 * Read more in the [documentation](#!/guide/dev_magicline)\r
1795 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).\r
1796 *\r
1797 * // Increases the distance to 80% of CKEDITOR.config.magicline_triggerOffset.\r
1798 * CKEDITOR.config.magicline_holdDistance = .8;\r
1799 *\r
1800 * @cfg {Number} [magicline_holdDistance=0.5]\r
1801 * @member CKEDITOR.config\r
1802 * @see CKEDITOR.config#magicline_triggerOffset\r
1803 */\r
1804\r
1805/**\r
1806 * Defines the default keystroke that accesses the closest unreachable focus space **before**\r
1807 * the caret (start of the selection). If there is no focus space available, the selection remains unchanged.\r
1808 *\r
1809 * Read more in the [documentation](#!/guide/dev_magicline)\r
1810 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).\r
1811 *\r
1812 * // Changes the default keystroke to "Ctrl + ,".\r
1813 * CKEDITOR.config.magicline_keystrokePrevious = CKEDITOR.CTRL + 188;\r
1814 *\r
1815 * @cfg {Number} [magicline_keystrokePrevious=CKEDITOR.CTRL + CKEDITOR.SHIFT + 51 (CTRL + SHIFT + 3)]\r
1816 * @member CKEDITOR.config\r
1817 */\r
1818CKEDITOR.config.magicline_keystrokePrevious = CKEDITOR.CTRL + CKEDITOR.SHIFT + 51; // CTRL + SHIFT + 3\r
1819\r
1820/**\r
1821 * Defines the default keystroke that accesses the closest unreachable focus space **after**\r
1822 * the caret (start of the selection). If there is no focus space available, the selection remains unchanged.\r
1823 *\r
1824 * Read more in the [documentation](#!/guide/dev_magicline)\r
1825 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).\r
1826 *\r
1827 * // Changes keystroke to "Ctrl + .".\r
1828 * CKEDITOR.config.magicline_keystrokeNext = CKEDITOR.CTRL + 190;\r
1829 *\r
1830 * @cfg {Number} [magicline_keystrokeNext=CKEDITOR.CTRL + CKEDITOR.SHIFT + 52 (CTRL + SHIFT + 4)]\r
1831 * @member CKEDITOR.config\r
1832 */\r
1833CKEDITOR.config.magicline_keystrokeNext = CKEDITOR.CTRL + CKEDITOR.SHIFT + 52; // CTRL + SHIFT + 4\r
1834\r
1835/**\r
1836 * Defines a list of attributes that, if assigned to some elements, prevent the magic line from being\r
1837 * used within these elements.\r
1838 *\r
1839 * Read more in the [documentation](#!/guide/dev_magicline)\r
1840 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).\r
1841 *\r
1842 * // Adds the "data-tabu" attribute to the magic line tabu list.\r
1843 * CKEDITOR.config.magicline_tabuList = [ 'data-tabu' ];\r
1844 *\r
1845 * @cfg {Number} [magicline_tabuList=[ 'data-widget-wrapper' ]]\r
1846 * @member CKEDITOR.config\r
1847 */\r
1848\r
1849/**\r
1850 * Defines the color of the magic line. The color may be adjusted to enhance readability.\r
1851 *\r
1852 * Read more in the [documentation](#!/guide/dev_magicline)\r
1853 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).\r
1854 *\r
1855 * // Changes magic line color to blue.\r
1856 * CKEDITOR.config.magicline_color = '#0000FF';\r
1857 *\r
1858 * @cfg {String} [magicline_color='#FF0000']\r
1859 * @member CKEDITOR.config\r
1860 */\r
1861\r
1862/**\r
1863 * Activates the special all-encompassing mode that considers all focus spaces between\r
1864 * {@link CKEDITOR.dtd#$block} elements as accessible by the magic line.\r
1865 *\r
1866 * Read more in the [documentation](#!/guide/dev_magicline)\r
1867 * and see the [SDK sample](http://sdk.ckeditor.com/samples/magicline.html).\r
1868 *\r
1869 * // Enables the greedy "put everywhere" mode.\r
1870 * CKEDITOR.config.magicline_everywhere = true;\r
1871 *\r
1872 * @cfg {Boolean} [magicline_everywhere=false]\r
1873 * @member CKEDITOR.config\r
1874 */\r