]>
Commit | Line | Data |
---|---|---|
1 | /**\r | |
2 | * @license Copyright (c) 2003-2016, 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 | |
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%\r | |
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 | |
41 | // (#9701) Due to security limitations some browsers may throw\r | |
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 | |
281 | // document.find is not available in QM (#11149).\r | |
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 | |
431 | // IE9QM: from times to times it will return an empty object on scroll bar hover. (#12185)\r | |
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 | |
486 | // Don't consider trigger as a limit as it may be nested editable (includeSelf=false) (#12009).\r | |
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">↵</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 | |
733 | // For example: divarea editable. (#9383)\r | |
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 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 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 | |
934 | // (#9833) Go down to the closest non-inline element in DOM structure\r | |
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 | |
1818 | CKEDITOR.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 | |
1833 | CKEDITOR.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 |