diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-02-19 23:38:52 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-02-19 23:38:52 +0100 |
commit | 3332bebe4da6dfa0fe3e4b2abddc84b1cc62f8f5 (patch) | |
tree | a4f77655fe55b79606e7d3416504686a1ab8b058 /sources/plugins/magicline/plugin.js | |
download | piedsjaloux-ckeditor-component-3332bebe4da6dfa0fe3e4b2abddc84b1cc62f8f5.tar.gz piedsjaloux-ckeditor-component-3332bebe4da6dfa0fe3e4b2abddc84b1cc62f8f5.tar.zst piedsjaloux-ckeditor-component-3332bebe4da6dfa0fe3e4b2abddc84b1cc62f8f5.zip |
Initial commit4.5.7
Diffstat (limited to 'sources/plugins/magicline/plugin.js')
-rw-r--r-- | sources/plugins/magicline/plugin.js | 1874 |
1 files changed, 1874 insertions, 0 deletions
diff --git a/sources/plugins/magicline/plugin.js b/sources/plugins/magicline/plugin.js new file mode 100644 index 0000000..cdb1c23 --- /dev/null +++ b/sources/plugins/magicline/plugin.js | |||
@@ -0,0 +1,1874 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, 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,bg,ca,cs,cy,da,de,de-ch,el,en,en-gb,eo,es,et,eu,fa,fi,fr,fr-ca,gl,he,hr,hu,id,it,ja,km,ko,ku,lv,nb,nl,no,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 | // (#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 (#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. (#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) (#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">↵</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. (#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 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 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 | // (#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 | */ | ||