]> git.immae.eu Git - perso/Immae/Projets/packagist/ludivine-ckeditor-component.git/blame - sources/plugins/lineutils/plugin.js
Update to 4.7.3
[perso/Immae/Projets/packagist/ludivine-ckeditor-component.git] / sources / plugins / lineutils / plugin.js
CommitLineData
c63493c8
IB
1/**\r
2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
3 * For licensing, see LICENSE.md or http://ckeditor.com/license\r
4 */\r
5\r
6 /**\r
7 * @fileOverview A set of utilities to find and create horizontal spaces in edited content.\r
8 */\r
9\r
10'use strict';\r
11\r
12( function() {\r
13\r
14 CKEDITOR.plugins.add( 'lineutils' );\r
15\r
16 /**\r
17 * Determines a position relative to an element in DOM (before).\r
18 *\r
19 * @readonly\r
20 * @property {Number} [=0]\r
21 * @member CKEDITOR\r
22 */\r
23 CKEDITOR.LINEUTILS_BEFORE = 1;\r
24\r
25 /**\r
26 * Determines a position relative to an element in DOM (after).\r
27 *\r
28 * @readonly\r
29 * @property {Number} [=2]\r
30 * @member CKEDITOR\r
31 */\r
32 CKEDITOR.LINEUTILS_AFTER = 2;\r
33\r
34 /**\r
35 * Determines a position relative to an element in DOM (inside).\r
36 *\r
37 * @readonly\r
38 * @property {Number} [=4]\r
39 * @member CKEDITOR\r
40 */\r
41 CKEDITOR.LINEUTILS_INSIDE = 4;\r
42\r
43 /**\r
44 * A utility that traverses the DOM tree and discovers elements\r
45 * (relations) matching user-defined lookups.\r
46 *\r
47 * @private\r
48 * @class CKEDITOR.plugins.lineutils.finder\r
49 * @constructor Creates a Finder class instance.\r
50 * @param {CKEDITOR.editor} editor Editor instance that the Finder belongs to.\r
51 * @param {Object} def Finder's definition.\r
52 * @since 4.3\r
53 */\r
54 function Finder( editor, def ) {\r
55 CKEDITOR.tools.extend( this, {\r
56 editor: editor,\r
57 editable: editor.editable(),\r
58 doc: editor.document,\r
59 win: editor.window\r
60 }, def, true );\r
61\r
62 this.inline = this.editable.isInline();\r
63\r
64 if ( !this.inline ) {\r
65 this.frame = this.win.getFrame();\r
66 }\r
67\r
68 this.target = this[ this.inline ? 'editable' : 'doc' ];\r
69 }\r
70\r
71 Finder.prototype = {\r
72 /**\r
73 * Initializes searching for elements with every mousemove event fired.\r
74 * To stop searching use {@link #stop}.\r
75 *\r
76 * @param {Function} [callback] Function executed on every iteration.\r
77 */\r
78 start: function( callback ) {\r
79 var that = this,\r
80 editor = this.editor,\r
81 doc = this.doc,\r
82 el, elfp, x, y;\r
83\r
84 var moveBuffer = CKEDITOR.tools.eventsBuffer( 50, function() {\r
85 if ( editor.readOnly || editor.mode != 'wysiwyg' )\r
86 return;\r
87\r
88 that.relations = {};\r
89\r
90 // Sometimes it happens that elementFromPoint returns null (especially on IE).\r
91 // Any further traversal makes no sense if there's no start point. Abort.\r
92 // Note: In IE8 elementFromPoint may return zombie nodes of undefined nodeType,\r
93 // so rejecting those as well.\r
94 if ( !( elfp = doc.$.elementFromPoint( x, y ) ) || !elfp.nodeType ) {\r
95 return;\r
96 }\r
97\r
98 el = new CKEDITOR.dom.element( elfp );\r
99\r
100 that.traverseSearch( el );\r
101\r
102 if ( !isNaN( x + y ) ) {\r
103 that.pixelSearch( el, x, y );\r
104 }\r
105\r
106 callback && callback( that.relations, x, y );\r
107 } );\r
108\r
109 // Searching starting from element from point on mousemove.\r
110 this.listener = this.editable.attachListener( this.target, 'mousemove', function( evt ) {\r
111 x = evt.data.$.clientX;\r
112 y = evt.data.$.clientY;\r
113\r
114 moveBuffer.input();\r
115 } );\r
116\r
117 this.editable.attachListener( this.inline ? this.editable : this.frame, 'mouseout', function() {\r
118 moveBuffer.reset();\r
119 } );\r
120 },\r
121\r
122 /**\r
123 * Stops observing mouse events attached by {@link #start}.\r
124 */\r
125 stop: function() {\r
126 if ( this.listener ) {\r
127 this.listener.removeListener();\r
128 }\r
129 },\r
130\r
131 /**\r
132 * Returns a range representing the relation, according to its element\r
133 * and type.\r
134 *\r
135 * @param {Object} location Location containing a unique identifier and type.\r
136 * @returns {CKEDITOR.dom.range} Range representing the relation.\r
137 */\r
138 getRange: ( function() {\r
139 var where = {};\r
140\r
141 where[ CKEDITOR.LINEUTILS_BEFORE ] = CKEDITOR.POSITION_BEFORE_START;\r
142 where[ CKEDITOR.LINEUTILS_AFTER ] = CKEDITOR.POSITION_AFTER_END;\r
143 where[ CKEDITOR.LINEUTILS_INSIDE ] = CKEDITOR.POSITION_AFTER_START;\r
144\r
145 return function( location ) {\r
146 var range = this.editor.createRange();\r
147\r
148 range.moveToPosition( this.relations[ location.uid ].element, where[ location.type ] );\r
149\r
150 return range;\r
151 };\r
152 } )(),\r
153\r
154 /**\r
155 * Stores given relation in a {@link #relations} object. Processes the relation\r
156 * to normalize and avoid duplicates.\r
157 *\r
158 * @param {CKEDITOR.dom.element} el Element of the relation.\r
159 * @param {Number} type Relation, one of `CKEDITOR.LINEUTILS_AFTER`, `CKEDITOR.LINEUTILS_BEFORE`, `CKEDITOR.LINEUTILS_INSIDE`.\r
160 */\r
161 store: ( function() {\r
162 function merge( el, type, relations ) {\r
163 var uid = el.getUniqueId();\r
164\r
165 if ( uid in relations ) {\r
166 relations[ uid ].type |= type;\r
167 } else {\r
168 relations[ uid ] = { element: el, type: type };\r
169 }\r
170 }\r
171\r
172 return function( el, type ) {\r
173 var alt;\r
174\r
175 // Normalization to avoid duplicates:\r
176 // CKEDITOR.LINEUTILS_AFTER becomes CKEDITOR.LINEUTILS_BEFORE of el.getNext().\r
177 if ( is( type, CKEDITOR.LINEUTILS_AFTER ) && isStatic( alt = el.getNext() ) && alt.isVisible() ) {\r
178 merge( alt, CKEDITOR.LINEUTILS_BEFORE, this.relations );\r
179 type ^= CKEDITOR.LINEUTILS_AFTER;\r
180 }\r
181\r
182 // Normalization to avoid duplicates:\r
183 // CKEDITOR.LINEUTILS_INSIDE becomes CKEDITOR.LINEUTILS_BEFORE of el.getFirst().\r
184 if ( is( type, CKEDITOR.LINEUTILS_INSIDE ) && isStatic( alt = el.getFirst() ) && alt.isVisible() ) {\r
185 merge( alt, CKEDITOR.LINEUTILS_BEFORE, this.relations );\r
186 type ^= CKEDITOR.LINEUTILS_INSIDE;\r
187 }\r
188\r
189 merge( el, type, this.relations );\r
190 };\r
191 } )(),\r
192\r
193 /**\r
194 * Traverses the DOM tree towards root, checking all ancestors\r
195 * with lookup rules, avoiding duplicates. Stores positive relations\r
196 * in the {@link #relations} object.\r
197 *\r
198 * @param {CKEDITOR.dom.element} el Element which is the starting point.\r
199 */\r
200 traverseSearch: function( el ) {\r
201 var l, type, uid;\r
202\r
203 // Go down DOM towards root (or limit).\r
204 do {\r
205 uid = el.$[ 'data-cke-expando' ];\r
206\r
207 // This element was already visited and checked.\r
208 if ( uid && uid in this.relations ) {\r
209 continue;\r
210 }\r
211\r
212 if ( el.equals( this.editable ) ) {\r
213 return;\r
214 }\r
215\r
216 if ( isStatic( el ) ) {\r
217 // Collect all addresses yielded by lookups for that element.\r
218 for ( l in this.lookups ) {\r
219\r
220 if ( ( type = this.lookups[ l ]( el ) ) ) {\r
221 this.store( el, type );\r
222 }\r
223 }\r
224 }\r
225 } while ( !isLimit( el ) && ( el = el.getParent() ) );\r
226 },\r
227\r
228 /**\r
229 * Iterates vertically pixel-by-pixel within a given element starting\r
230 * from given coordinates, searching for elements in the neighborhood.\r
231 * Once an element is found it is processed by {@link #traverseSearch}.\r
232 *\r
233 * @param {CKEDITOR.dom.element} el Element which is the starting point.\r
234 * @param {Number} [x] Horizontal mouse coordinate relative to the viewport.\r
235 * @param {Number} [y] Vertical mouse coordinate relative to the viewport.\r
236 */\r
237 pixelSearch: ( function() {\r
238 var contains = CKEDITOR.env.ie || CKEDITOR.env.webkit ?\r
239 function( el, found ) {\r
240 return el.contains( found );\r
241 } : function( el, found ) {\r
242 return !!( el.compareDocumentPosition( found ) & 16 );\r
243 };\r
244\r
245 // Iterates pixel-by-pixel from starting coordinates, moving by defined\r
246 // step and getting elementFromPoint in every iteration. Iteration stops when:\r
247 // * A valid element is found.\r
248 // * Condition function returns `false` (i.e. reached boundaries of viewport).\r
249 // * No element is found (i.e. coordinates out of viewport).\r
250 // * Element found is ascendant of starting element.\r
251 //\r
252 // @param {Object} doc Native DOM document.\r
253 // @param {Object} el Native DOM element.\r
254 // @param {Number} xStart Horizontal starting coordinate to use.\r
255 // @param {Number} yStart Vertical starting coordinate to use.\r
256 // @param {Number} step Step of the algorithm.\r
257 // @param {Function} condition A condition relative to current vertical coordinate.\r
258 function iterate( el, xStart, yStart, step, condition ) {\r
259 var y = yStart,\r
260 tryouts = 0,\r
261 found;\r
262\r
263 while ( condition( y ) ) {\r
264 y += step;\r
265\r
266 // If we try and we try, and still nothing's found, let's end\r
267 // that party.\r
268 if ( ++tryouts == 25 ) {\r
269 return;\r
270 }\r
271\r
272 found = this.doc.$.elementFromPoint( xStart, y );\r
273\r
274 // Nothing found. This is crazy... but...\r
275 // It might be that a line, which is in different document,\r
276 // covers that pixel (elementFromPoint is doc-sensitive).\r
277 // Better let's have another try.\r
278 if ( !found ) {\r
279 continue;\r
280 }\r
281\r
282 // Still in the same element.\r
283 else if ( found == el ) {\r
284 tryouts = 0;\r
285 continue;\r
286 }\r
287\r
288 // Reached the edge of an element and found an ancestor or...\r
289 // A line, that covers that pixel. Better let's have another try.\r
290 else if ( !contains( el, found ) ) {\r
291 continue;\r
292 }\r
293\r
294 tryouts = 0;\r
295\r
296 // Found a valid element. Stop iterating.\r
297 if ( isStatic( ( found = new CKEDITOR.dom.element( found ) ) ) ) {\r
298 return found;\r
299 }\r
300 }\r
301 }\r
302\r
303 return function( el, x, y ) {\r
304 var paneHeight = this.win.getViewPaneSize().height,\r
305\r
306 // Try to find an element iterating *up* from the starting point.\r
307 neg = iterate.call( this, el.$, x, y, -1, function( y ) {\r
308 return y > 0;\r
309 } ),\r
310\r
311 // Try to find an element iterating *down* from the starting point.\r
312 pos = iterate.call( this, el.$, x, y, 1, function( y ) {\r
313 return y < paneHeight;\r
314 } );\r
315\r
316 if ( neg ) {\r
317 this.traverseSearch( neg );\r
318\r
319 // Iterate towards DOM root until neg is a direct child of el.\r
320 while ( !neg.getParent().equals( el ) ) {\r
321 neg = neg.getParent();\r
322 }\r
323 }\r
324\r
325 if ( pos ) {\r
326 this.traverseSearch( pos );\r
327\r
328 // Iterate towards DOM root until pos is a direct child of el.\r
329 while ( !pos.getParent().equals( el ) ) {\r
330 pos = pos.getParent();\r
331 }\r
332 }\r
333\r
334 // Iterate forwards starting from neg and backwards from\r
335 // pos to harvest all children of el between those elements.\r
336 // Stop when neg and pos meet each other or there's none of them.\r
337 // TODO (?) reduce number of hops forwards/backwards.\r
338 while ( neg || pos ) {\r
339 if ( neg ) {\r
340 neg = neg.getNext( isStatic );\r
341 }\r
342\r
343 if ( !neg || neg.equals( pos ) ) {\r
344 break;\r
345 }\r
346\r
347 this.traverseSearch( neg );\r
348\r
349 if ( pos ) {\r
350 pos = pos.getPrevious( isStatic );\r
351 }\r
352\r
353 if ( !pos || pos.equals( neg ) ) {\r
354 break;\r
355 }\r
356\r
357 this.traverseSearch( pos );\r
358 }\r
359 };\r
360 } )(),\r
361\r
362 /**\r
363 * Unlike {@link #traverseSearch}, it collects **all** elements from editable's DOM tree\r
364 * and runs lookups for every one of them, collecting relations.\r
365 *\r
366 * @returns {Object} {@link #relations}.\r
367 */\r
368 greedySearch: function() {\r
369 this.relations = {};\r
370\r
371 var all = this.editable.getElementsByTag( '*' ),\r
372 i = 0,\r
373 el, type, l;\r
374\r
375 while ( ( el = all.getItem( i++ ) ) ) {\r
376 // Don't consider editable, as it might be inline,\r
377 // and i.e. checking it's siblings is pointless.\r
378 if ( el.equals( this.editable ) ) {\r
379 continue;\r
380 }\r
381\r
1794320d 382 // On IE8 element.getElementsByTagName returns comments... sic! (http://dev.ckeditor.com/ticket/13176)\r
c63493c8
IB
383 if ( el.type != CKEDITOR.NODE_ELEMENT ) {\r
384 continue;\r
385 }\r
386\r
387 // Don't visit non-editable internals, for example widget's\r
388 // guts (above wrapper, below nested). Still check editable limits,\r
389 // as they are siblings with editable contents.\r
390 if ( !el.hasAttribute( 'contenteditable' ) && el.isReadOnly() ) {\r
391 continue;\r
392 }\r
393\r
394 if ( isStatic( el ) && el.isVisible() ) {\r
395 // Collect all addresses yielded by lookups for that element.\r
396 for ( l in this.lookups ) {\r
397 if ( ( type = this.lookups[ l ]( el ) ) ) {\r
398 this.store( el, type );\r
399 }\r
400 }\r
401 }\r
402 }\r
403\r
404 return this.relations;\r
405 }\r
406\r
407 /**\r
408 * Relations express elements in DOM that match user-defined {@link #lookups}.\r
409 * Every relation has its own `type` that determines whether\r
410 * it refers to the space before, after or inside the `element`.\r
411 * This object stores relations found by {@link #traverseSearch} or {@link #greedySearch}, structured\r
412 * in the following way:\r
413 *\r
414 * relations: {\r
415 * // Unique identifier of the element.\r
416 * Number: {\r
417 * // Element of this relation.\r
418 * element: {@link CKEDITOR.dom.element}\r
419 * // Conjunction of CKEDITOR.LINEUTILS_BEFORE, CKEDITOR.LINEUTILS_AFTER and CKEDITOR.LINEUTILS_INSIDE.\r
420 * type: Number\r
421 * },\r
422 * ...\r
423 * }\r
424 *\r
425 * @property {Object} relations\r
426 * @readonly\r
427 */\r
428\r
429 /**\r
430 * A set of user-defined functions used by Finder to check if an element\r
431 * is a valid relation, belonging to {@link #relations}.\r
432 * When the criterion is met, lookup returns a logical conjunction of `CKEDITOR.LINEUTILS_BEFORE`,\r
433 * `CKEDITOR.LINEUTILS_AFTER` or `CKEDITOR.LINEUTILS_INSIDE`.\r
434 *\r
435 * Lookups are passed along with Finder's definition.\r
436 *\r
437 * lookups: {\r
438 * 'some lookup': function( el ) {\r
439 * if ( someCondition )\r
440 * return CKEDITOR.LINEUTILS_BEFORE;\r
441 * },\r
442 * ...\r
443 * }\r
444 *\r
445 * @property {Object} lookups\r
446 */\r
447 };\r
448\r
449\r
450 /**\r
451 * A utility that analyses relations found by\r
452 * CKEDITOR.plugins.lineutils.finder and locates them\r
453 * in the viewport as horizontal lines of specific coordinates.\r
454 *\r
455 * @private\r
456 * @class CKEDITOR.plugins.lineutils.locator\r
457 * @constructor Creates a Locator class instance.\r
458 * @param {CKEDITOR.editor} editor Editor instance that Locator belongs to.\r
459 * @since 4.3\r
460 */\r
461 function Locator( editor, def ) {\r
462 CKEDITOR.tools.extend( this, def, {\r
463 editor: editor\r
464 }, true );\r
465 }\r
466\r
467 Locator.prototype = {\r
468 /**\r
469 * Locates the Y coordinate for all types of every single relation and stores\r
470 * them in an object.\r
471 *\r
472 * @param {Object} relations {@link CKEDITOR.plugins.lineutils.finder#relations}.\r
473 * @returns {Object} {@link #locations}.\r
474 */\r
475 locate: ( function() {\r
476 function locateSibling( rel, type ) {\r
477 var sib = rel.element[ type === CKEDITOR.LINEUTILS_BEFORE ? 'getPrevious' : 'getNext' ]();\r
478\r
479 // Return the middle point between siblings.\r
480 if ( sib && isStatic( sib ) ) {\r
481 rel.siblingRect = sib.getClientRect();\r
482\r
483 if ( type == CKEDITOR.LINEUTILS_BEFORE ) {\r
484 return ( rel.siblingRect.bottom + rel.elementRect.top ) / 2;\r
485 } else {\r
486 return ( rel.elementRect.bottom + rel.siblingRect.top ) / 2;\r
487 }\r
488 }\r
489\r
490 // If there's no sibling, use the edge of an element.\r
491 else {\r
492 if ( type == CKEDITOR.LINEUTILS_BEFORE ) {\r
493 return rel.elementRect.top;\r
494 } else {\r
495 return rel.elementRect.bottom;\r
496 }\r
497 }\r
498 }\r
499\r
500 return function( relations ) {\r
501 var rel;\r
502\r
503 this.locations = {};\r
504\r
505 for ( var uid in relations ) {\r
506 rel = relations[ uid ];\r
507 rel.elementRect = rel.element.getClientRect();\r
508\r
509 if ( is( rel.type, CKEDITOR.LINEUTILS_BEFORE ) ) {\r
510 this.store( uid, CKEDITOR.LINEUTILS_BEFORE, locateSibling( rel, CKEDITOR.LINEUTILS_BEFORE ) );\r
511 }\r
512\r
513 if ( is( rel.type, CKEDITOR.LINEUTILS_AFTER ) ) {\r
514 this.store( uid, CKEDITOR.LINEUTILS_AFTER, locateSibling( rel, CKEDITOR.LINEUTILS_AFTER ) );\r
515 }\r
516\r
517 // The middle point of the element.\r
518 if ( is( rel.type, CKEDITOR.LINEUTILS_INSIDE ) ) {\r
519 this.store( uid, CKEDITOR.LINEUTILS_INSIDE, ( rel.elementRect.top + rel.elementRect.bottom ) / 2 );\r
520 }\r
521 }\r
522\r
523 return this.locations;\r
524 };\r
525 } )(),\r
526\r
527 /**\r
528 * Calculates distances from every location to given vertical coordinate\r
529 * and sorts locations according to that distance.\r
530 *\r
531 * @param {Number} y The vertical coordinate used for sorting, used as a reference.\r
532 * @param {Number} [howMany] Determines the number of "closest locations" to be returned.\r
533 * @returns {Array} Sorted, array representation of {@link #locations}.\r
534 */\r
535 sort: ( function() {\r
536 var locations, sorted,\r
537 dist, i;\r
538\r
539 function distance( y, uid, type ) {\r
540 return Math.abs( y - locations[ uid ][ type ] );\r
541 }\r
542\r
543 return function( y, howMany ) {\r
544 locations = this.locations;\r
545 sorted = [];\r
546\r
547 for ( var uid in locations ) {\r
548 for ( var type in locations[ uid ] ) {\r
549 dist = distance( y, uid, type );\r
550\r
551 // An array is empty.\r
552 if ( !sorted.length ) {\r
553 sorted.push( { uid: +uid, type: type, dist: dist } );\r
554 } else {\r
555 // Sort the array on fly when it's populated.\r
556 for ( i = 0; i < sorted.length; i++ ) {\r
557 if ( dist < sorted[ i ].dist ) {\r
558 sorted.splice( i, 0, { uid: +uid, type: type, dist: dist } );\r
559 break;\r
560 }\r
561 }\r
562\r
563 // Nothing was inserted, so the distance is bigger than\r
564 // any of already calculated: push to the end.\r
565 if ( i == sorted.length ) {\r
566 sorted.push( { uid: +uid, type: type, dist: dist } );\r
567 }\r
568 }\r
569 }\r
570 }\r
571\r
572 if ( typeof howMany != 'undefined' ) {\r
573 return sorted.slice( 0, howMany );\r
574 } else {\r
575 return sorted;\r
576 }\r
577 };\r
578 } )(),\r
579\r
580 /**\r
581 * Stores the location in a collection.\r
582 *\r
583 * @param {Number} uid Unique identifier of the relation.\r
584 * @param {Number} type One of `CKEDITOR.LINEUTILS_BEFORE`, `CKEDITOR.LINEUTILS_AFTER` and `CKEDITOR.LINEUTILS_INSIDE`.\r
585 * @param {Number} y Vertical position of the relation.\r
586 */\r
587 store: function( uid, type, y ) {\r
588 if ( !this.locations[ uid ] ) {\r
589 this.locations[ uid ] = {};\r
590 }\r
591\r
592 this.locations[ uid ][ type ] = y;\r
593 }\r
594\r
595 /**\r
596 * @readonly\r
597 * @property {Object} locations\r
598 */\r
599 };\r
600\r
601 var tipCss = {\r
602 display: 'block',\r
603 width: '0px',\r
604 height: '0px',\r
605 'border-color': 'transparent',\r
606 'border-style': 'solid',\r
607 position: 'absolute',\r
608 top: '-6px'\r
609 },\r
610\r
611 lineStyle = {\r
612 height: '0px',\r
613 'border-top': '1px dashed red',\r
614 position: 'absolute',\r
615 'z-index': 9999\r
616 },\r
617\r
618 lineTpl =\r
619 '<div data-cke-lineutils-line="1" class="cke_reset_all" style="{lineStyle}">' +\r
620 '<span style="{tipLeftStyle}">&nbsp;</span>' +\r
621 '<span style="{tipRightStyle}">&nbsp;</span>' +\r
622 '</div>';\r
623\r
624 /**\r
625 * A utility that draws horizontal lines in DOM according to locations\r
626 * returned by CKEDITOR.plugins.lineutils.locator.\r
627 *\r
628 * @private\r
629 * @class CKEDITOR.plugins.lineutils.liner\r
630 * @constructor Creates a Liner class instance.\r
631 * @param {CKEDITOR.editor} editor Editor instance that Liner belongs to.\r
632 * @param {Object} def Liner's definition.\r
633 * @since 4.3\r
634 */\r
635 function Liner( editor, def ) {\r
636 var editable = editor.editable();\r
637\r
638 CKEDITOR.tools.extend( this, {\r
639 editor: editor,\r
640 editable: editable,\r
641 inline: editable.isInline(),\r
642 doc: editor.document,\r
643 win: editor.window,\r
644 container: CKEDITOR.document.getBody(),\r
645 winTop: CKEDITOR.document.getWindow()\r
646 }, def, true );\r
647\r
648 this.hidden = {};\r
649 this.visible = {};\r
650\r
651 if ( !this.inline ) {\r
652 this.frame = this.win.getFrame();\r
653 }\r
654\r
655 this.queryViewport();\r
656\r
657 // Callbacks must be wrapped. Otherwise they're not attached\r
658 // to global DOM objects (i.e. topmost window) for every editor\r
659 // because they're treated as duplicates. They belong to the\r
660 // same prototype shared among Liner instances.\r
661 var queryViewport = CKEDITOR.tools.bind( this.queryViewport, this ),\r
662 hideVisible = CKEDITOR.tools.bind( this.hideVisible, this ),\r
663 removeAll = CKEDITOR.tools.bind( this.removeAll, this );\r
664\r
665 editable.attachListener( this.winTop, 'resize', queryViewport );\r
666 editable.attachListener( this.winTop, 'scroll', queryViewport );\r
667\r
668 editable.attachListener( this.winTop, 'resize', hideVisible );\r
669 editable.attachListener( this.win, 'scroll', hideVisible );\r
670\r
671 editable.attachListener( this.inline ? editable : this.frame, 'mouseout', function( evt ) {\r
672 var x = evt.data.$.clientX,\r
673 y = evt.data.$.clientY;\r
674\r
675 this.queryViewport();\r
676\r
677 // Check if mouse is out of the element (iframe/editable).\r
678 if ( x <= this.rect.left || x >= this.rect.right || y <= this.rect.top || y >= this.rect.bottom ) {\r
679 this.hideVisible();\r
680 }\r
681\r
682 // Check if mouse is out of the top-window vieport.\r
683 if ( x <= 0 || x >= this.winTopPane.width || y <= 0 || y >= this.winTopPane.height ) {\r
684 this.hideVisible();\r
685 }\r
686 }, this );\r
687\r
688 editable.attachListener( editor, 'resize', queryViewport );\r
689 editable.attachListener( editor, 'mode', removeAll );\r
690 editor.on( 'destroy', removeAll );\r
691\r
692 this.lineTpl = new CKEDITOR.template( lineTpl ).output( {\r
693 lineStyle: CKEDITOR.tools.writeCssText(\r
694 CKEDITOR.tools.extend( {}, lineStyle, this.lineStyle, true )\r
695 ),\r
696 tipLeftStyle: CKEDITOR.tools.writeCssText(\r
697 CKEDITOR.tools.extend( {}, tipCss, {\r
698 left: '0px',\r
699 'border-left-color': 'red',\r
700 'border-width': '6px 0 6px 6px'\r
701 }, this.tipCss, this.tipLeftStyle, true )\r
702 ),\r
703 tipRightStyle: CKEDITOR.tools.writeCssText(\r
704 CKEDITOR.tools.extend( {}, tipCss, {\r
705 right: '0px',\r
706 'border-right-color': 'red',\r
707 'border-width': '6px 6px 6px 0'\r
708 }, this.tipCss, this.tipRightStyle, true )\r
709 )\r
710 } );\r
711 }\r
712\r
713 Liner.prototype = {\r
714 /**\r
715 * Permanently removes all lines (both hidden and visible) from DOM.\r
716 */\r
717 removeAll: function() {\r
718 var l;\r
719\r
720 for ( l in this.hidden ) {\r
721 this.hidden[ l ].remove();\r
722 delete this.hidden[ l ];\r
723 }\r
724\r
725 for ( l in this.visible ) {\r
726 this.visible[ l ].remove();\r
727 delete this.visible[ l ];\r
728 }\r
729 },\r
730\r
731 /**\r
732 * Hides a given line.\r
733 *\r
734 * @param {CKEDITOR.dom.element} line The line to be hidden.\r
735 */\r
736 hideLine: function( line ) {\r
737 var uid = line.getUniqueId();\r
738\r
739 line.hide();\r
740\r
741 this.hidden[ uid ] = line;\r
742 delete this.visible[ uid ];\r
743 },\r
744\r
745 /**\r
746 * Shows a given line.\r
747 *\r
748 * @param {CKEDITOR.dom.element} line The line to be shown.\r
749 */\r
750 showLine: function( line ) {\r
751 var uid = line.getUniqueId();\r
752\r
753 line.show();\r
754\r
755 this.visible[ uid ] = line;\r
756 delete this.hidden[ uid ];\r
757 },\r
758\r
759 /**\r
760 * Hides all visible lines.\r
761 */\r
762 hideVisible: function() {\r
763 for ( var l in this.visible ) {\r
764 this.hideLine( this.visible[ l ] );\r
765 }\r
766 },\r
767\r
768 /**\r
769 * Shows a line at given location.\r
770 *\r
771 * @param {Object} location Location object containing the unique identifier of the relation\r
772 * and its type. Usually returned by {@link CKEDITOR.plugins.lineutils.locator#sort}.\r
773 * @param {Function} [callback] A callback to be called once the line is shown.\r
774 */\r
775 placeLine: function( location, callback ) {\r
776 var styles, line, l;\r
777\r
778 // No style means that line would be out of viewport.\r
779 if ( !( styles = this.getStyle( location.uid, location.type ) ) ) {\r
780 return;\r
781 }\r
782\r
783 // Search for any visible line of a different hash first.\r
784 // It's faster to re-position visible line than to show it.\r
785 for ( l in this.visible ) {\r
786 if ( this.visible[ l ].getCustomData( 'hash' ) !== this.hash ) {\r
787 line = this.visible[ l ];\r
788 break;\r
789 }\r
790 }\r
791\r
792 // Search for any hidden line of a different hash.\r
793 if ( !line ) {\r
794 for ( l in this.hidden ) {\r
795 if ( this.hidden[ l ].getCustomData( 'hash' ) !== this.hash ) {\r
796 this.showLine( ( line = this.hidden[ l ] ) );\r
797 break;\r
798 }\r
799 }\r
800 }\r
801\r
802 // If no line available, add the new one.\r
803 if ( !line ) {\r
804 this.showLine( ( line = this.addLine() ) );\r
805 }\r
806\r
807 // Mark the line with current hash.\r
808 line.setCustomData( 'hash', this.hash );\r
809\r
810 // Mark the line as visible.\r
811 this.visible[ line.getUniqueId() ] = line;\r
812\r
813 line.setStyles( styles );\r
814\r
815 callback && callback( line );\r
816 },\r
817\r
818 /**\r
819 * Creates a style set to be used by the line, representing a particular\r
820 * relation (location).\r
821 *\r
822 * @param {Number} uid Unique identifier of the relation.\r
823 * @param {Number} type Type of the relation.\r
824 * @returns {Object} An object containing styles.\r
825 */\r
826 getStyle: function( uid, type ) {\r
827 var rel = this.relations[ uid ],\r
828 loc = this.locations[ uid ][ type ],\r
829 styles = {},\r
830 hdiff;\r
831\r
832 // Line should be between two elements.\r
833 if ( rel.siblingRect ) {\r
834 styles.width = Math.max( rel.siblingRect.width, rel.elementRect.width );\r
835 }\r
836 // Line is relative to a single element.\r
837 else {\r
838 styles.width = rel.elementRect.width;\r
839 }\r
840\r
841 // Let's calculate the vertical position of the line.\r
842 if ( this.inline ) {\r
1794320d 843 // (http://dev.ckeditor.com/ticket/13155)\r
c63493c8
IB
844 styles.top = loc + this.winTopScroll.y - this.rect.relativeY;\r
845 } else {\r
846 styles.top = this.rect.top + this.winTopScroll.y + loc;\r
847 }\r
848\r
849 // Check if line would be vertically out of the viewport.\r
850 if ( styles.top - this.winTopScroll.y < this.rect.top || styles.top - this.winTopScroll.y > this.rect.bottom ) {\r
851 return false;\r
852 }\r
853\r
854 // Now let's calculate the horizontal alignment (left and width).\r
855 if ( this.inline ) {\r
1794320d 856 // (http://dev.ckeditor.com/ticket/13155)\r
c63493c8
IB
857 styles.left = rel.elementRect.left - this.rect.relativeX;\r
858 } else {\r
859 if ( rel.elementRect.left > 0 )\r
860 styles.left = this.rect.left + rel.elementRect.left;\r
861\r
862 // H-scroll case. Left edge of element may be out of viewport.\r
863 else {\r
864 styles.width += rel.elementRect.left;\r
865 styles.left = this.rect.left;\r
866 }\r
867\r
868 // H-scroll case. Right edge of element may be out of viewport.\r
869 if ( ( hdiff = styles.left + styles.width - ( this.rect.left + this.winPane.width ) ) > 0 ) {\r
870 styles.width -= hdiff;\r
871 }\r
872 }\r
873\r
874 // Finally include horizontal scroll of the global window.\r
875 styles.left += this.winTopScroll.x;\r
876\r
877 // Append 'px' to style values.\r
878 for ( var style in styles ) {\r
879 styles[ style ] = CKEDITOR.tools.cssLength( styles[ style ] );\r
880 }\r
881\r
882 return styles;\r
883 },\r
884\r
885 /**\r
886 * Adds a new line to DOM.\r
887 *\r
888 * @returns {CKEDITOR.dom.element} A brand-new line.\r
889 */\r
890 addLine: function() {\r
891 var line = CKEDITOR.dom.element.createFromHtml( this.lineTpl );\r
892\r
893 line.appendTo( this.container );\r
894\r
895 return line;\r
896 },\r
897\r
898 /**\r
899 * Assigns a unique hash to the instance that is later used\r
900 * to tell unwanted lines from new ones. This method **must** be called\r
901 * before a new set of relations is to be visualized so {@link #cleanup}\r
902 * eventually hides obsolete lines. This is because lines\r
903 * are re-used between {@link #placeLine} calls and the number of\r
904 * necessary ones may vary depending on the number of relations.\r
905 *\r
906 * @param {Object} relations {@link CKEDITOR.plugins.lineutils.finder#relations}.\r
907 * @param {Object} locations {@link CKEDITOR.plugins.lineutils.locator#locations}.\r
908 */\r
909 prepare: function( relations, locations ) {\r
910 this.relations = relations;\r
911 this.locations = locations;\r
912 this.hash = Math.random();\r
913 },\r
914\r
915 /**\r
916 * Hides all visible lines that do not belong to current hash\r
917 * and no longer represent relations (locations).\r
918 *\r
919 * See also: {@link #prepare}.\r
920 */\r
921 cleanup: function() {\r
922 var line;\r
923\r
924 for ( var l in this.visible ) {\r
925 line = this.visible[ l ];\r
926\r
927 if ( line.getCustomData( 'hash' ) !== this.hash ) {\r
928 this.hideLine( line );\r
929 }\r
930 }\r
931 },\r
932\r
933 /**\r
934 * Queries dimensions of the viewport, editable, frame etc.\r
935 * that are used for correct positioning of the line.\r
936 */\r
937 queryViewport: function() {\r
938 this.winPane = this.win.getViewPaneSize();\r
939 this.winTopScroll = this.winTop.getScrollPosition();\r
940 this.winTopPane = this.winTop.getViewPaneSize();\r
941\r
1794320d 942 // (http://dev.ckeditor.com/ticket/13155)\r
c63493c8
IB
943 this.rect = this.getClientRect( this.inline ? this.editable : this.frame );\r
944 },\r
945\r
946 /**\r
947 * Returns `boundingClientRect` of an element, shifted by the position\r
1794320d 948 * of `container` when the container is not `static` (http://dev.ckeditor.com/ticket/13155).\r
c63493c8
IB
949 *\r
950 * See also: {@link CKEDITOR.dom.element#getClientRect}.\r
951 *\r
952 * @param {CKEDITOR.dom.element} el A DOM element.\r
953 * @returns {Object} A shifted rect, extended by `relativeY` and `relativeX` properties.\r
954 */\r
955 getClientRect: function( el ) {\r
956 var rect = el.getClientRect(),\r
957 relativeContainerDocPosition = this.container.getDocumentPosition(),\r
958 relativeContainerComputedPosition = this.container.getComputedStyle( 'position' );\r
959\r
960 // Static or not, those values are used to offset the position of the line so they cannot be undefined.\r
961 rect.relativeX = rect.relativeY = 0;\r
962\r
963 if ( relativeContainerComputedPosition != 'static' ) {\r
964 // Remember the offset used to shift the clientRect.\r
965 rect.relativeY = relativeContainerDocPosition.y;\r
966 rect.relativeX = relativeContainerDocPosition.x;\r
967\r
968 rect.top -= rect.relativeY;\r
969 rect.bottom -= rect.relativeY;\r
970 rect.left -= rect.relativeX;\r
971 rect.right -= rect.relativeX;\r
972 }\r
973\r
974 return rect;\r
975 }\r
976 };\r
977\r
978 function is( type, flag ) {\r
979 return type & flag;\r
980 }\r
981\r
982 var floats = { left: 1, right: 1, center: 1 },\r
983 positions = { absolute: 1, fixed: 1 };\r
984\r
985 function isElement( node ) {\r
986 return node && node.type == CKEDITOR.NODE_ELEMENT;\r
987 }\r
988\r
989 function isFloated( el ) {\r
990 return !!( floats[ el.getComputedStyle( 'float' ) ] || floats[ el.getAttribute( 'align' ) ] );\r
991 }\r
992\r
993 function isPositioned( el ) {\r
994 return !!positions[ el.getComputedStyle( 'position' ) ];\r
995 }\r
996\r
997 function isLimit( node ) {\r
998 return isElement( node ) && node.getAttribute( 'contenteditable' ) == 'true';\r
999 }\r
1000\r
1001 function isStatic( node ) {\r
1002 return isElement( node ) && !isFloated( node ) && !isPositioned( node );\r
1003 }\r
1004\r
1005 /**\r
1006 * Global namespace storing definitions and global helpers for the Line Utilities plugin.\r
1007 *\r
1008 * @private\r
1009 * @class\r
1010 * @singleton\r
1011 * @since 4.3\r
1012 */\r
1013 CKEDITOR.plugins.lineutils = {\r
1014 finder: Finder,\r
1015 locator: Locator,\r
1016 liner: Liner\r
1017 };\r
1018} )();\r