2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
7 * @fileOverview A set of utilities to find and create horizontal spaces in edited content.
14 CKEDITOR
.plugins
.add( 'lineutils' );
17 * Determines a position relative to an element in DOM (before).
20 * @property {Number} [=0]
23 CKEDITOR
.LINEUTILS_BEFORE
= 1;
26 * Determines a position relative to an element in DOM (after).
29 * @property {Number} [=2]
32 CKEDITOR
.LINEUTILS_AFTER
= 2;
35 * Determines a position relative to an element in DOM (inside).
38 * @property {Number} [=4]
41 CKEDITOR
.LINEUTILS_INSIDE
= 4;
44 * A utility that traverses the DOM tree and discovers elements
45 * (relations) matching user-defined lookups.
48 * @class CKEDITOR.plugins.lineutils.finder
49 * @constructor Creates a Finder class instance.
50 * @param {CKEDITOR.editor} editor Editor instance that the Finder belongs to.
51 * @param {Object} def Finder's definition.
54 function Finder( editor
, def
) {
55 CKEDITOR
.tools
.extend( this, {
57 editable: editor
.editable(),
62 this.inline
= this.editable
.isInline();
65 this.frame
= this.win
.getFrame();
68 this.target
= this[ this.inline
? 'editable' : 'doc' ];
73 * Initializes searching for elements with every mousemove event fired.
74 * To stop searching use {@link #stop}.
76 * @param {Function} [callback] Function executed on every iteration.
78 start: function( callback
) {
84 var moveBuffer
= CKEDITOR
.tools
.eventsBuffer( 50, function() {
85 if ( editor
.readOnly
|| editor
.mode
!= 'wysiwyg' )
90 // Sometimes it happens that elementFromPoint returns null (especially on IE).
91 // Any further traversal makes no sense if there's no start point. Abort.
92 // Note: In IE8 elementFromPoint may return zombie nodes of undefined nodeType,
93 // so rejecting those as well.
94 if ( !( elfp
= doc
.$.elementFromPoint( x
, y
) ) || !elfp
.nodeType
) {
98 el
= new CKEDITOR
.dom
.element( elfp
);
100 that
.traverseSearch( el
);
102 if ( !isNaN( x
+ y
) ) {
103 that
.pixelSearch( el
, x
, y
);
106 callback
&& callback( that
.relations
, x
, y
);
109 // Searching starting from element from point on mousemove.
110 this.listener
= this.editable
.attachListener( this.target
, 'mousemove', function( evt
) {
111 x
= evt
.data
.$.clientX
;
112 y
= evt
.data
.$.clientY
;
117 this.editable
.attachListener( this.inline
? this.editable : this.frame
, 'mouseout', function() {
123 * Stops observing mouse events attached by {@link #start}.
126 if ( this.listener
) {
127 this.listener
.removeListener();
132 * Returns a range representing the relation, according to its element
135 * @param {Object} location Location containing a unique identifier and type.
136 * @returns {CKEDITOR.dom.range} Range representing the relation.
138 getRange: ( function() {
141 where
[ CKEDITOR
.LINEUTILS_BEFORE
] = CKEDITOR
.POSITION_BEFORE_START
;
142 where
[ CKEDITOR
.LINEUTILS_AFTER
] = CKEDITOR
.POSITION_AFTER_END
;
143 where
[ CKEDITOR
.LINEUTILS_INSIDE
] = CKEDITOR
.POSITION_AFTER_START
;
145 return function( location
) {
146 var range
= this.editor
.createRange();
148 range
.moveToPosition( this.relations
[ location
.uid
].element
, where
[ location
.type
] );
155 * Stores given relation in a {@link #relations} object. Processes the relation
156 * to normalize and avoid duplicates.
158 * @param {CKEDITOR.dom.element} el Element of the relation.
159 * @param {Number} type Relation, one of `CKEDITOR.LINEUTILS_AFTER`, `CKEDITOR.LINEUTILS_BEFORE`, `CKEDITOR.LINEUTILS_INSIDE`.
161 store: ( function() {
162 function merge( el
, type
, relations
) {
163 var uid
= el
.getUniqueId();
165 if ( uid
in relations
) {
166 relations
[ uid
].type
|= type
;
168 relations
[ uid
] = { element: el
, type: type
};
172 return function( el
, type
) {
175 // Normalization to avoid duplicates:
176 // CKEDITOR.LINEUTILS_AFTER becomes CKEDITOR.LINEUTILS_BEFORE of el.getNext().
177 if ( is( type
, CKEDITOR
.LINEUTILS_AFTER
) && isStatic( alt
= el
.getNext() ) && alt
.isVisible() ) {
178 merge( alt
, CKEDITOR
.LINEUTILS_BEFORE
, this.relations
);
179 type
^= CKEDITOR
.LINEUTILS_AFTER
;
182 // Normalization to avoid duplicates:
183 // CKEDITOR.LINEUTILS_INSIDE becomes CKEDITOR.LINEUTILS_BEFORE of el.getFirst().
184 if ( is( type
, CKEDITOR
.LINEUTILS_INSIDE
) && isStatic( alt
= el
.getFirst() ) && alt
.isVisible() ) {
185 merge( alt
, CKEDITOR
.LINEUTILS_BEFORE
, this.relations
);
186 type
^= CKEDITOR
.LINEUTILS_INSIDE
;
189 merge( el
, type
, this.relations
);
194 * Traverses the DOM tree towards root, checking all ancestors
195 * with lookup rules, avoiding duplicates. Stores positive relations
196 * in the {@link #relations} object.
198 * @param {CKEDITOR.dom.element} el Element which is the starting point.
200 traverseSearch: function( el
) {
203 // Go down DOM towards root (or limit).
205 uid
= el
.$[ 'data-cke-expando' ];
207 // This element was already visited and checked.
208 if ( uid
&& uid
in this.relations
) {
212 if ( el
.equals( this.editable
) ) {
216 if ( isStatic( el
) ) {
217 // Collect all addresses yielded by lookups for that element.
218 for ( l
in this.lookups
) {
220 if ( ( type
= this.lookups
[ l
]( el
) ) ) {
221 this.store( el
, type
);
225 } while ( !isLimit( el
) && ( el
= el
.getParent() ) );
229 * Iterates vertically pixel-by-pixel within a given element starting
230 * from given coordinates, searching for elements in the neighborhood.
231 * Once an element is found it is processed by {@link #traverseSearch}.
233 * @param {CKEDITOR.dom.element} el Element which is the starting point.
234 * @param {Number} [x] Horizontal mouse coordinate relative to the viewport.
235 * @param {Number} [y] Vertical mouse coordinate relative to the viewport.
237 pixelSearch: ( function() {
238 var contains
= CKEDITOR
.env
.ie
|| CKEDITOR
.env
.webkit
?
239 function( el
, found
) {
240 return el
.contains( found
);
241 } : function( el
, found
) {
242 return !!( el
.compareDocumentPosition( found
) & 16 );
245 // Iterates pixel-by-pixel from starting coordinates, moving by defined
246 // step and getting elementFromPoint in every iteration. Iteration stops when:
247 // * A valid element is found.
248 // * Condition function returns `false` (i.e. reached boundaries of viewport).
249 // * No element is found (i.e. coordinates out of viewport).
250 // * Element found is ascendant of starting element.
252 // @param {Object} doc Native DOM document.
253 // @param {Object} el Native DOM element.
254 // @param {Number} xStart Horizontal starting coordinate to use.
255 // @param {Number} yStart Vertical starting coordinate to use.
256 // @param {Number} step Step of the algorithm.
257 // @param {Function} condition A condition relative to current vertical coordinate.
258 function iterate( el
, xStart
, yStart
, step
, condition
) {
263 while ( condition( y
) ) {
266 // If we try and we try, and still nothing's found, let's end
268 if ( ++tryouts
== 25 ) {
272 found
= this.doc
.$.elementFromPoint( xStart
, y
);
274 // Nothing found. This is crazy... but...
275 // It might be that a line, which is in different document,
276 // covers that pixel (elementFromPoint is doc-sensitive).
277 // Better let's have another try.
282 // Still in the same element.
283 else if ( found
== el
) {
288 // Reached the edge of an element and found an ancestor or...
289 // A line, that covers that pixel. Better let's have another try.
290 else if ( !contains( el
, found
) ) {
296 // Found a valid element. Stop iterating.
297 if ( isStatic( ( found
= new CKEDITOR
.dom
.element( found
) ) ) ) {
303 return function( el
, x
, y
) {
304 var paneHeight
= this.win
.getViewPaneSize().height
,
306 // Try to find an element iterating *up* from the starting point.
307 neg
= iterate
.call( this, el
.$, x
, y
, -1, function( y
) {
311 // Try to find an element iterating *down* from the starting point.
312 pos
= iterate
.call( this, el
.$, x
, y
, 1, function( y
) {
313 return y
< paneHeight
;
317 this.traverseSearch( neg
);
319 // Iterate towards DOM root until neg is a direct child of el.
320 while ( !neg
.getParent().equals( el
) ) {
321 neg
= neg
.getParent();
326 this.traverseSearch( pos
);
328 // Iterate towards DOM root until pos is a direct child of el.
329 while ( !pos
.getParent().equals( el
) ) {
330 pos
= pos
.getParent();
334 // Iterate forwards starting from neg and backwards from
335 // pos to harvest all children of el between those elements.
336 // Stop when neg and pos meet each other or there's none of them.
337 // TODO (?) reduce number of hops forwards/backwards.
338 while ( neg
|| pos
) {
340 neg
= neg
.getNext( isStatic
);
343 if ( !neg
|| neg
.equals( pos
) ) {
347 this.traverseSearch( neg
);
350 pos
= pos
.getPrevious( isStatic
);
353 if ( !pos
|| pos
.equals( neg
) ) {
357 this.traverseSearch( pos
);
363 * Unlike {@link #traverseSearch}, it collects **all** elements from editable's DOM tree
364 * and runs lookups for every one of them, collecting relations.
366 * @returns {Object} {@link #relations}.
368 greedySearch: function() {
371 var all
= this.editable
.getElementsByTag( '*' ),
375 while ( ( el
= all
.getItem( i
++ ) ) ) {
376 // Don't consider editable, as it might be inline,
377 // and i.e. checking it's siblings is pointless.
378 if ( el
.equals( this.editable
) ) {
382 // On IE8 element.getElementsByTagName returns comments... sic! (#13176)
383 if ( el
.type
!= CKEDITOR
.NODE_ELEMENT
) {
387 // Don't visit non-editable internals, for example widget's
388 // guts (above wrapper, below nested). Still check editable limits,
389 // as they are siblings with editable contents.
390 if ( !el
.hasAttribute( 'contenteditable' ) && el
.isReadOnly() ) {
394 if ( isStatic( el
) && el
.isVisible() ) {
395 // Collect all addresses yielded by lookups for that element.
396 for ( l
in this.lookups
) {
397 if ( ( type
= this.lookups
[ l
]( el
) ) ) {
398 this.store( el
, type
);
404 return this.relations
;
408 * Relations express elements in DOM that match user-defined {@link #lookups}.
409 * Every relation has its own `type` that determines whether
410 * it refers to the space before, after or inside the `element`.
411 * This object stores relations found by {@link #traverseSearch} or {@link #greedySearch}, structured
412 * in the following way:
415 * // Unique identifier of the element.
417 * // Element of this relation.
418 * element: {@link CKEDITOR.dom.element}
419 * // Conjunction of CKEDITOR.LINEUTILS_BEFORE, CKEDITOR.LINEUTILS_AFTER and CKEDITOR.LINEUTILS_INSIDE.
425 * @property {Object} relations
430 * A set of user-defined functions used by Finder to check if an element
431 * is a valid relation, belonging to {@link #relations}.
432 * When the criterion is met, lookup returns a logical conjunction of `CKEDITOR.LINEUTILS_BEFORE`,
433 * `CKEDITOR.LINEUTILS_AFTER` or `CKEDITOR.LINEUTILS_INSIDE`.
435 * Lookups are passed along with Finder's definition.
438 * 'some lookup': function( el ) {
439 * if ( someCondition )
440 * return CKEDITOR.LINEUTILS_BEFORE;
445 * @property {Object} lookups
451 * A utility that analyses relations found by
452 * CKEDITOR.plugins.lineutils.finder and locates them
453 * in the viewport as horizontal lines of specific coordinates.
456 * @class CKEDITOR.plugins.lineutils.locator
457 * @constructor Creates a Locator class instance.
458 * @param {CKEDITOR.editor} editor Editor instance that Locator belongs to.
461 function Locator( editor
, def
) {
462 CKEDITOR
.tools
.extend( this, def
, {
467 Locator
.prototype = {
469 * Locates the Y coordinate for all types of every single relation and stores
472 * @param {Object} relations {@link CKEDITOR.plugins.lineutils.finder#relations}.
473 * @returns {Object} {@link #locations}.
475 locate: ( function() {
476 function locateSibling( rel
, type
) {
477 var sib
= rel
.element
[ type
=== CKEDITOR
.LINEUTILS_BEFORE
? 'getPrevious' : 'getNext' ]();
479 // Return the middle point between siblings.
480 if ( sib
&& isStatic( sib
) ) {
481 rel
.siblingRect
= sib
.getClientRect();
483 if ( type
== CKEDITOR
.LINEUTILS_BEFORE
) {
484 return ( rel
.siblingRect
.bottom
+ rel
.elementRect
.top
) / 2;
486 return ( rel
.elementRect
.bottom
+ rel
.siblingRect
.top
) / 2;
490 // If there's no sibling, use the edge of an element.
492 if ( type
== CKEDITOR
.LINEUTILS_BEFORE
) {
493 return rel
.elementRect
.top
;
495 return rel
.elementRect
.bottom
;
500 return function( relations
) {
505 for ( var uid
in relations
) {
506 rel
= relations
[ uid
];
507 rel
.elementRect
= rel
.element
.getClientRect();
509 if ( is( rel
.type
, CKEDITOR
.LINEUTILS_BEFORE
) ) {
510 this.store( uid
, CKEDITOR
.LINEUTILS_BEFORE
, locateSibling( rel
, CKEDITOR
.LINEUTILS_BEFORE
) );
513 if ( is( rel
.type
, CKEDITOR
.LINEUTILS_AFTER
) ) {
514 this.store( uid
, CKEDITOR
.LINEUTILS_AFTER
, locateSibling( rel
, CKEDITOR
.LINEUTILS_AFTER
) );
517 // The middle point of the element.
518 if ( is( rel
.type
, CKEDITOR
.LINEUTILS_INSIDE
) ) {
519 this.store( uid
, CKEDITOR
.LINEUTILS_INSIDE
, ( rel
.elementRect
.top
+ rel
.elementRect
.bottom
) / 2 );
523 return this.locations
;
528 * Calculates distances from every location to given vertical coordinate
529 * and sorts locations according to that distance.
531 * @param {Number} y The vertical coordinate used for sorting, used as a reference.
532 * @param {Number} [howMany] Determines the number of "closest locations" to be returned.
533 * @returns {Array} Sorted, array representation of {@link #locations}.
536 var locations
, sorted
,
539 function distance( y
, uid
, type
) {
540 return Math
.abs( y
- locations
[ uid
][ type
] );
543 return function( y
, howMany
) {
544 locations
= this.locations
;
547 for ( var uid
in locations
) {
548 for ( var type
in locations
[ uid
] ) {
549 dist
= distance( y
, uid
, type
);
551 // An array is empty.
552 if ( !sorted
.length
) {
553 sorted
.push( { uid: +uid
, type: type
, dist: dist
} );
555 // Sort the array on fly when it's populated.
556 for ( i
= 0; i
< sorted
.length
; i
++ ) {
557 if ( dist
< sorted
[ i
].dist
) {
558 sorted
.splice( i
, 0, { uid: +uid
, type: type
, dist: dist
} );
563 // Nothing was inserted, so the distance is bigger than
564 // any of already calculated: push to the end.
565 if ( i
== sorted
.length
) {
566 sorted
.push( { uid: +uid
, type: type
, dist: dist
} );
572 if ( typeof howMany
!= 'undefined' ) {
573 return sorted
.slice( 0, howMany
);
581 * Stores the location in a collection.
583 * @param {Number} uid Unique identifier of the relation.
584 * @param {Number} type One of `CKEDITOR.LINEUTILS_BEFORE`, `CKEDITOR.LINEUTILS_AFTER` and `CKEDITOR.LINEUTILS_INSIDE`.
585 * @param {Number} y Vertical position of the relation.
587 store: function( uid
, type
, y
) {
588 if ( !this.locations
[ uid
] ) {
589 this.locations
[ uid
] = {};
592 this.locations
[ uid
][ type
] = y
;
597 * @property {Object} locations
605 'border-color': 'transparent',
606 'border-style': 'solid',
607 position: 'absolute',
613 'border-top': '1px dashed red',
614 position: 'absolute',
619 '<div data-cke-lineutils-line="1" class="cke_reset_all" style="{lineStyle}">' +
620 '<span style="{tipLeftStyle}"> </span>' +
621 '<span style="{tipRightStyle}"> </span>' +
625 * A utility that draws horizontal lines in DOM according to locations
626 * returned by CKEDITOR.plugins.lineutils.locator.
629 * @class CKEDITOR.plugins.lineutils.liner
630 * @constructor Creates a Liner class instance.
631 * @param {CKEDITOR.editor} editor Editor instance that Liner belongs to.
632 * @param {Object} def Liner's definition.
635 function Liner( editor
, def
) {
636 var editable
= editor
.editable();
638 CKEDITOR
.tools
.extend( this, {
641 inline: editable
.isInline(),
642 doc: editor
.document
,
644 container: CKEDITOR
.document
.getBody(),
645 winTop: CKEDITOR
.document
.getWindow()
651 if ( !this.inline
) {
652 this.frame
= this.win
.getFrame();
655 this.queryViewport();
657 // Callbacks must be wrapped. Otherwise they're not attached
658 // to global DOM objects (i.e. topmost window) for every editor
659 // because they're treated as duplicates. They belong to the
660 // same prototype shared among Liner instances.
661 var queryViewport
= CKEDITOR
.tools
.bind( this.queryViewport
, this ),
662 hideVisible
= CKEDITOR
.tools
.bind( this.hideVisible
, this ),
663 removeAll
= CKEDITOR
.tools
.bind( this.removeAll
, this );
665 editable
.attachListener( this.winTop
, 'resize', queryViewport
);
666 editable
.attachListener( this.winTop
, 'scroll', queryViewport
);
668 editable
.attachListener( this.winTop
, 'resize', hideVisible
);
669 editable
.attachListener( this.win
, 'scroll', hideVisible
);
671 editable
.attachListener( this.inline
? editable : this.frame
, 'mouseout', function( evt
) {
672 var x
= evt
.data
.$.clientX
,
673 y
= evt
.data
.$.clientY
;
675 this.queryViewport();
677 // Check if mouse is out of the element (iframe/editable).
678 if ( x
<= this.rect
.left
|| x
>= this.rect
.right
|| y
<= this.rect
.top
|| y
>= this.rect
.bottom
) {
682 // Check if mouse is out of the top-window vieport.
683 if ( x
<= 0 || x
>= this.winTopPane
.width
|| y
<= 0 || y
>= this.winTopPane
.height
) {
688 editable
.attachListener( editor
, 'resize', queryViewport
);
689 editable
.attachListener( editor
, 'mode', removeAll
);
690 editor
.on( 'destroy', removeAll
);
692 this.lineTpl
= new CKEDITOR
.template( lineTpl
).output( {
693 lineStyle: CKEDITOR
.tools
.writeCssText(
694 CKEDITOR
.tools
.extend( {}, lineStyle
, this.lineStyle
, true )
696 tipLeftStyle: CKEDITOR
.tools
.writeCssText(
697 CKEDITOR
.tools
.extend( {}, tipCss
, {
699 'border-left-color': 'red',
700 'border-width': '6px 0 6px 6px'
701 }, this.tipCss
, this.tipLeftStyle
, true )
703 tipRightStyle: CKEDITOR
.tools
.writeCssText(
704 CKEDITOR
.tools
.extend( {}, tipCss
, {
706 'border-right-color': 'red',
707 'border-width': '6px 6px 6px 0'
708 }, this.tipCss
, this.tipRightStyle
, true )
715 * Permanently removes all lines (both hidden and visible) from DOM.
717 removeAll: function() {
720 for ( l
in this.hidden
) {
721 this.hidden
[ l
].remove();
722 delete this.hidden
[ l
];
725 for ( l
in this.visible
) {
726 this.visible
[ l
].remove();
727 delete this.visible
[ l
];
732 * Hides a given line.
734 * @param {CKEDITOR.dom.element} line The line to be hidden.
736 hideLine: function( line
) {
737 var uid
= line
.getUniqueId();
741 this.hidden
[ uid
] = line
;
742 delete this.visible
[ uid
];
746 * Shows a given line.
748 * @param {CKEDITOR.dom.element} line The line to be shown.
750 showLine: function( line
) {
751 var uid
= line
.getUniqueId();
755 this.visible
[ uid
] = line
;
756 delete this.hidden
[ uid
];
760 * Hides all visible lines.
762 hideVisible: function() {
763 for ( var l
in this.visible
) {
764 this.hideLine( this.visible
[ l
] );
769 * Shows a line at given location.
771 * @param {Object} location Location object containing the unique identifier of the relation
772 * and its type. Usually returned by {@link CKEDITOR.plugins.lineutils.locator#sort}.
773 * @param {Function} [callback] A callback to be called once the line is shown.
775 placeLine: function( location
, callback
) {
778 // No style means that line would be out of viewport.
779 if ( !( styles
= this.getStyle( location
.uid
, location
.type
) ) ) {
783 // Search for any visible line of a different hash first.
784 // It's faster to re-position visible line than to show it.
785 for ( l
in this.visible
) {
786 if ( this.visible
[ l
].getCustomData( 'hash' ) !== this.hash
) {
787 line
= this.visible
[ l
];
792 // Search for any hidden line of a different hash.
794 for ( l
in this.hidden
) {
795 if ( this.hidden
[ l
].getCustomData( 'hash' ) !== this.hash
) {
796 this.showLine( ( line
= this.hidden
[ l
] ) );
802 // If no line available, add the new one.
804 this.showLine( ( line
= this.addLine() ) );
807 // Mark the line with current hash.
808 line
.setCustomData( 'hash', this.hash
);
810 // Mark the line as visible.
811 this.visible
[ line
.getUniqueId() ] = line
;
813 line
.setStyles( styles
);
815 callback
&& callback( line
);
819 * Creates a style set to be used by the line, representing a particular
820 * relation (location).
822 * @param {Number} uid Unique identifier of the relation.
823 * @param {Number} type Type of the relation.
824 * @returns {Object} An object containing styles.
826 getStyle: function( uid
, type
) {
827 var rel
= this.relations
[ uid
],
828 loc
= this.locations
[ uid
][ type
],
832 // Line should be between two elements.
833 if ( rel
.siblingRect
) {
834 styles
.width
= Math
.max( rel
.siblingRect
.width
, rel
.elementRect
.width
);
836 // Line is relative to a single element.
838 styles
.width
= rel
.elementRect
.width
;
841 // Let's calculate the vertical position of the line.
844 styles
.top
= loc
+ this.winTopScroll
.y
- this.rect
.relativeY
;
846 styles
.top
= this.rect
.top
+ this.winTopScroll
.y
+ loc
;
849 // Check if line would be vertically out of the viewport.
850 if ( styles
.top
- this.winTopScroll
.y
< this.rect
.top
|| styles
.top
- this.winTopScroll
.y
> this.rect
.bottom
) {
854 // Now let's calculate the horizontal alignment (left and width).
857 styles
.left
= rel
.elementRect
.left
- this.rect
.relativeX
;
859 if ( rel
.elementRect
.left
> 0 )
860 styles
.left
= this.rect
.left
+ rel
.elementRect
.left
;
862 // H-scroll case. Left edge of element may be out of viewport.
864 styles
.width
+= rel
.elementRect
.left
;
865 styles
.left
= this.rect
.left
;
868 // H-scroll case. Right edge of element may be out of viewport.
869 if ( ( hdiff
= styles
.left
+ styles
.width
- ( this.rect
.left
+ this.winPane
.width
) ) > 0 ) {
870 styles
.width
-= hdiff
;
874 // Finally include horizontal scroll of the global window.
875 styles
.left
+= this.winTopScroll
.x
;
877 // Append 'px' to style values.
878 for ( var style
in styles
) {
879 styles
[ style
] = CKEDITOR
.tools
.cssLength( styles
[ style
] );
886 * Adds a new line to DOM.
888 * @returns {CKEDITOR.dom.element} A brand-new line.
890 addLine: function() {
891 var line
= CKEDITOR
.dom
.element
.createFromHtml( this.lineTpl
);
893 line
.appendTo( this.container
);
899 * Assigns a unique hash to the instance that is later used
900 * to tell unwanted lines from new ones. This method **must** be called
901 * before a new set of relations is to be visualized so {@link #cleanup}
902 * eventually hides obsolete lines. This is because lines
903 * are re-used between {@link #placeLine} calls and the number of
904 * necessary ones may vary depending on the number of relations.
906 * @param {Object} relations {@link CKEDITOR.plugins.lineutils.finder#relations}.
907 * @param {Object} locations {@link CKEDITOR.plugins.lineutils.locator#locations}.
909 prepare: function( relations
, locations
) {
910 this.relations
= relations
;
911 this.locations
= locations
;
912 this.hash
= Math
.random();
916 * Hides all visible lines that do not belong to current hash
917 * and no longer represent relations (locations).
919 * See also: {@link #prepare}.
921 cleanup: function() {
924 for ( var l
in this.visible
) {
925 line
= this.visible
[ l
];
927 if ( line
.getCustomData( 'hash' ) !== this.hash
) {
928 this.hideLine( line
);
934 * Queries dimensions of the viewport, editable, frame etc.
935 * that are used for correct positioning of the line.
937 queryViewport: function() {
938 this.winPane
= this.win
.getViewPaneSize();
939 this.winTopScroll
= this.winTop
.getScrollPosition();
940 this.winTopPane
= this.winTop
.getViewPaneSize();
943 this.rect
= this.getClientRect( this.inline
? this.editable : this.frame
);
947 * Returns `boundingClientRect` of an element, shifted by the position
948 * of `container` when the container is not `static` (#13155).
950 * See also: {@link CKEDITOR.dom.element#getClientRect}.
952 * @param {CKEDITOR.dom.element} el A DOM element.
953 * @returns {Object} A shifted rect, extended by `relativeY` and `relativeX` properties.
955 getClientRect: function( el
) {
956 var rect
= el
.getClientRect(),
957 relativeContainerDocPosition
= this.container
.getDocumentPosition(),
958 relativeContainerComputedPosition
= this.container
.getComputedStyle( 'position' );
960 // Static or not, those values are used to offset the position of the line so they cannot be undefined.
961 rect
.relativeX
= rect
.relativeY
= 0;
963 if ( relativeContainerComputedPosition
!= 'static' ) {
964 // Remember the offset used to shift the clientRect.
965 rect
.relativeY
= relativeContainerDocPosition
.y
;
966 rect
.relativeX
= relativeContainerDocPosition
.x
;
968 rect
.top
-= rect
.relativeY
;
969 rect
.bottom
-= rect
.relativeY
;
970 rect
.left
-= rect
.relativeX
;
971 rect
.right
-= rect
.relativeX
;
978 function is( type
, flag
) {
982 var floats
= { left: 1, right: 1, center: 1 },
983 positions
= { absolute: 1, fixed: 1 };
985 function isElement( node
) {
986 return node
&& node
.type
== CKEDITOR
.NODE_ELEMENT
;
989 function isFloated( el
) {
990 return !!( floats
[ el
.getComputedStyle( 'float' ) ] || floats
[ el
.getAttribute( 'align' ) ] );
993 function isPositioned( el
) {
994 return !!positions
[ el
.getComputedStyle( 'position' ) ];
997 function isLimit( node
) {
998 return isElement( node
) && node
.getAttribute( 'contenteditable' ) == 'true';
1001 function isStatic( node
) {
1002 return isElement( node
) && !isFloated( node
) && !isPositioned( node
);
1006 * Global namespace storing definitions and global helpers for the Line Utilities plugin.
1013 CKEDITOR
.plugins
.lineutils
= {