]> git.immae.eu Git - perso/Immae/Projets/packagist/piedsjaloux-ckeditor-component.git/blobdiff - sources/plugins/lineutils/plugin.js
Add oembed
[perso/Immae/Projets/packagist/piedsjaloux-ckeditor-component.git] / sources / plugins / lineutils / plugin.js
diff --git a/sources/plugins/lineutils/plugin.js b/sources/plugins/lineutils/plugin.js
new file mode 100644 (file)
index 0000000..d84c5f3
--- /dev/null
@@ -0,0 +1,1018 @@
+/**\r
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.\r
+ * For licensing, see LICENSE.md or http://ckeditor.com/license\r
+ */\r
+\r
+ /**\r
+ * @fileOverview A set of utilities to find and create horizontal spaces in edited content.\r
+ */\r
+\r
+'use strict';\r
+\r
+( function() {\r
+\r
+       CKEDITOR.plugins.add( 'lineutils' );\r
+\r
+       /**\r
+        * Determines a position relative to an element in DOM (before).\r
+        *\r
+        * @readonly\r
+        * @property {Number} [=0]\r
+        * @member CKEDITOR\r
+        */\r
+       CKEDITOR.LINEUTILS_BEFORE = 1;\r
+\r
+       /**\r
+        * Determines a position relative to an element in DOM (after).\r
+        *\r
+        * @readonly\r
+        * @property {Number} [=2]\r
+        * @member CKEDITOR\r
+        */\r
+       CKEDITOR.LINEUTILS_AFTER = 2;\r
+\r
+       /**\r
+        * Determines a position relative to an element in DOM (inside).\r
+        *\r
+        * @readonly\r
+        * @property {Number} [=4]\r
+        * @member CKEDITOR\r
+        */\r
+       CKEDITOR.LINEUTILS_INSIDE = 4;\r
+\r
+       /**\r
+        * A utility that traverses the DOM tree and discovers elements\r
+        * (relations) matching user-defined lookups.\r
+        *\r
+        * @private\r
+        * @class CKEDITOR.plugins.lineutils.finder\r
+        * @constructor Creates a Finder class instance.\r
+        * @param {CKEDITOR.editor} editor Editor instance that the Finder belongs to.\r
+        * @param {Object} def Finder's definition.\r
+        * @since 4.3\r
+        */\r
+       function Finder( editor, def ) {\r
+               CKEDITOR.tools.extend( this, {\r
+                       editor: editor,\r
+                       editable: editor.editable(),\r
+                       doc: editor.document,\r
+                       win: editor.window\r
+               }, def, true );\r
+\r
+               this.inline = this.editable.isInline();\r
+\r
+               if ( !this.inline ) {\r
+                       this.frame = this.win.getFrame();\r
+               }\r
+\r
+               this.target = this[ this.inline ? 'editable' : 'doc' ];\r
+       }\r
+\r
+       Finder.prototype = {\r
+               /**\r
+                * Initializes searching for elements with every mousemove event fired.\r
+                * To stop searching use {@link #stop}.\r
+                *\r
+                * @param {Function} [callback] Function executed on every iteration.\r
+                */\r
+               start: function( callback ) {\r
+                       var that = this,\r
+                               editor = this.editor,\r
+                               doc = this.doc,\r
+                               el, elfp, x, y;\r
+\r
+                       var moveBuffer = CKEDITOR.tools.eventsBuffer( 50, function() {\r
+                                       if ( editor.readOnly || editor.mode != 'wysiwyg' )\r
+                                               return;\r
+\r
+                                       that.relations = {};\r
+\r
+                                       // Sometimes it happens that elementFromPoint returns null (especially on IE).\r
+                                       // Any further traversal makes no sense if there's no start point. Abort.\r
+                                       // Note: In IE8 elementFromPoint may return zombie nodes of undefined nodeType,\r
+                                       // so rejecting those as well.\r
+                                       if ( !( elfp = doc.$.elementFromPoint( x, y ) ) || !elfp.nodeType ) {\r
+                                               return;\r
+                                       }\r
+\r
+                                       el = new CKEDITOR.dom.element( elfp );\r
+\r
+                                       that.traverseSearch( el );\r
+\r
+                                       if ( !isNaN( x + y ) ) {\r
+                                               that.pixelSearch( el, x, y );\r
+                                       }\r
+\r
+                                       callback && callback( that.relations, x, y );\r
+                               } );\r
+\r
+                       // Searching starting from element from point on mousemove.\r
+                       this.listener = this.editable.attachListener( this.target, 'mousemove', function( evt ) {\r
+                               x = evt.data.$.clientX;\r
+                               y = evt.data.$.clientY;\r
+\r
+                               moveBuffer.input();\r
+                       } );\r
+\r
+                       this.editable.attachListener( this.inline ? this.editable : this.frame, 'mouseout', function() {\r
+                               moveBuffer.reset();\r
+                       } );\r
+               },\r
+\r
+               /**\r
+                * Stops observing mouse events attached by {@link #start}.\r
+                */\r
+               stop: function() {\r
+                       if ( this.listener ) {\r
+                               this.listener.removeListener();\r
+                       }\r
+               },\r
+\r
+               /**\r
+                * Returns a range representing the relation, according to its element\r
+                * and type.\r
+                *\r
+                * @param {Object} location Location containing a unique identifier and type.\r
+                * @returns {CKEDITOR.dom.range} Range representing the relation.\r
+                */\r
+               getRange: ( function() {\r
+                       var where = {};\r
+\r
+                       where[ CKEDITOR.LINEUTILS_BEFORE ] = CKEDITOR.POSITION_BEFORE_START;\r
+                       where[ CKEDITOR.LINEUTILS_AFTER ] = CKEDITOR.POSITION_AFTER_END;\r
+                       where[ CKEDITOR.LINEUTILS_INSIDE ] = CKEDITOR.POSITION_AFTER_START;\r
+\r
+                       return function( location ) {\r
+                               var range = this.editor.createRange();\r
+\r
+                               range.moveToPosition( this.relations[ location.uid ].element, where[ location.type ] );\r
+\r
+                               return range;\r
+                       };\r
+               } )(),\r
+\r
+               /**\r
+                * Stores given relation in a {@link #relations} object. Processes the relation\r
+                * to normalize and avoid duplicates.\r
+                *\r
+                * @param {CKEDITOR.dom.element} el Element of the relation.\r
+                * @param {Number} type Relation, one of `CKEDITOR.LINEUTILS_AFTER`, `CKEDITOR.LINEUTILS_BEFORE`, `CKEDITOR.LINEUTILS_INSIDE`.\r
+                */\r
+               store: ( function() {\r
+                       function merge( el, type, relations ) {\r
+                               var uid = el.getUniqueId();\r
+\r
+                               if ( uid in relations ) {\r
+                                       relations[ uid ].type |= type;\r
+                               } else {\r
+                                       relations[ uid ] = { element: el, type: type };\r
+                               }\r
+                       }\r
+\r
+                       return function( el, type ) {\r
+                               var alt;\r
+\r
+                               // Normalization to avoid duplicates:\r
+                               // CKEDITOR.LINEUTILS_AFTER becomes CKEDITOR.LINEUTILS_BEFORE of el.getNext().\r
+                               if ( is( type, CKEDITOR.LINEUTILS_AFTER ) && isStatic( alt = el.getNext() ) && alt.isVisible() ) {\r
+                                       merge( alt, CKEDITOR.LINEUTILS_BEFORE, this.relations );\r
+                                       type ^= CKEDITOR.LINEUTILS_AFTER;\r
+                               }\r
+\r
+                               // Normalization to avoid duplicates:\r
+                               // CKEDITOR.LINEUTILS_INSIDE becomes CKEDITOR.LINEUTILS_BEFORE of el.getFirst().\r
+                               if ( is( type, CKEDITOR.LINEUTILS_INSIDE ) && isStatic( alt = el.getFirst() ) && alt.isVisible() ) {\r
+                                       merge( alt, CKEDITOR.LINEUTILS_BEFORE, this.relations );\r
+                                       type ^= CKEDITOR.LINEUTILS_INSIDE;\r
+                               }\r
+\r
+                               merge( el, type, this.relations );\r
+                       };\r
+               } )(),\r
+\r
+               /**\r
+                * Traverses the DOM tree towards root, checking all ancestors\r
+                * with lookup rules, avoiding duplicates. Stores positive relations\r
+                * in the {@link #relations} object.\r
+                *\r
+                * @param {CKEDITOR.dom.element} el Element which is the starting point.\r
+                */\r
+               traverseSearch: function( el ) {\r
+                       var l, type, uid;\r
+\r
+                       // Go down DOM towards root (or limit).\r
+                       do {\r
+                               uid = el.$[ 'data-cke-expando' ];\r
+\r
+                               // This element was already visited and checked.\r
+                               if ( uid && uid in this.relations ) {\r
+                                       continue;\r
+                               }\r
+\r
+                               if ( el.equals( this.editable ) ) {\r
+                                       return;\r
+                               }\r
+\r
+                               if ( isStatic( el ) ) {\r
+                                       // Collect all addresses yielded by lookups for that element.\r
+                                       for ( l in this.lookups ) {\r
+\r
+                                               if ( ( type = this.lookups[ l ]( el ) ) ) {\r
+                                                       this.store( el, type );\r
+                                               }\r
+                                       }\r
+                               }\r
+                       } while ( !isLimit( el ) && ( el = el.getParent() ) );\r
+               },\r
+\r
+               /**\r
+                * Iterates vertically pixel-by-pixel within a given element starting\r
+                * from given coordinates, searching for elements in the neighborhood.\r
+                * Once an element is found it is processed by {@link #traverseSearch}.\r
+                *\r
+                * @param {CKEDITOR.dom.element} el Element which is the starting point.\r
+                * @param {Number} [x] Horizontal mouse coordinate relative to the viewport.\r
+                * @param {Number} [y] Vertical mouse coordinate relative to the viewport.\r
+                */\r
+               pixelSearch: ( function() {\r
+                       var contains = CKEDITOR.env.ie || CKEDITOR.env.webkit ?\r
+                               function( el, found ) {\r
+                                       return el.contains( found );\r
+                               } : function( el, found ) {\r
+                                       return !!( el.compareDocumentPosition( found ) & 16 );\r
+                               };\r
+\r
+                       // Iterates pixel-by-pixel from starting coordinates, moving by defined\r
+                       // step and getting elementFromPoint in every iteration. Iteration stops when:\r
+                       //  * A valid element is found.\r
+                       //  * Condition function returns `false` (i.e. reached boundaries of viewport).\r
+                       //  * No element is found (i.e. coordinates out of viewport).\r
+                       //  * Element found is ascendant of starting element.\r
+                       //\r
+                       // @param {Object} doc Native DOM document.\r
+                       // @param {Object} el Native DOM element.\r
+                       // @param {Number} xStart Horizontal starting coordinate to use.\r
+                       // @param {Number} yStart Vertical starting coordinate to use.\r
+                       // @param {Number} step Step of the algorithm.\r
+                       // @param {Function} condition A condition relative to current vertical coordinate.\r
+                       function iterate( el, xStart, yStart, step, condition ) {\r
+                               var y = yStart,\r
+                                       tryouts = 0,\r
+                                       found;\r
+\r
+                               while ( condition( y ) ) {\r
+                                       y += step;\r
+\r
+                                       // If we try and we try, and still nothing's found, let's end\r
+                                       // that party.\r
+                                       if ( ++tryouts == 25 ) {\r
+                                               return;\r
+                                       }\r
+\r
+                                       found = this.doc.$.elementFromPoint( xStart, y );\r
+\r
+                                       // Nothing found. This is crazy... but...\r
+                                       // It might be that a line, which is in different document,\r
+                                       // covers that pixel (elementFromPoint is doc-sensitive).\r
+                                       // Better let's have another try.\r
+                                       if ( !found ) {\r
+                                               continue;\r
+                                       }\r
+\r
+                                       // Still in the same element.\r
+                                       else if ( found == el ) {\r
+                                               tryouts = 0;\r
+                                               continue;\r
+                                       }\r
+\r
+                                       // Reached the edge of an element and found an ancestor or...\r
+                                       // A line, that covers that pixel. Better let's have another try.\r
+                                       else if ( !contains( el, found ) ) {\r
+                                               continue;\r
+                                       }\r
+\r
+                                       tryouts = 0;\r
+\r
+                                       // Found a valid element. Stop iterating.\r
+                                       if ( isStatic( ( found = new CKEDITOR.dom.element( found ) ) ) ) {\r
+                                               return found;\r
+                                       }\r
+                               }\r
+                       }\r
+\r
+                       return function( el, x, y ) {\r
+                               var paneHeight = this.win.getViewPaneSize().height,\r
+\r
+                                       // Try to find an element iterating *up* from the starting point.\r
+                                       neg = iterate.call( this, el.$, x, y, -1, function( y ) {\r
+                                               return y > 0;\r
+                                       } ),\r
+\r
+                                       // Try to find an element iterating *down* from the starting point.\r
+                                       pos = iterate.call( this, el.$, x, y, 1, function( y ) {\r
+                                               return y < paneHeight;\r
+                                       } );\r
+\r
+                               if ( neg ) {\r
+                                       this.traverseSearch( neg );\r
+\r
+                                       // Iterate towards DOM root until neg is a direct child of el.\r
+                                       while ( !neg.getParent().equals( el ) ) {\r
+                                               neg = neg.getParent();\r
+                                       }\r
+                               }\r
+\r
+                               if ( pos ) {\r
+                                       this.traverseSearch( pos );\r
+\r
+                                       // Iterate towards DOM root until pos is a direct child of el.\r
+                                       while ( !pos.getParent().equals( el ) ) {\r
+                                               pos = pos.getParent();\r
+                                       }\r
+                               }\r
+\r
+                               // Iterate forwards starting from neg and backwards from\r
+                               // pos to harvest all children of el between those elements.\r
+                               // Stop when neg and pos meet each other or there's none of them.\r
+                               // TODO (?) reduce number of hops forwards/backwards.\r
+                               while ( neg || pos ) {\r
+                                       if ( neg ) {\r
+                                               neg = neg.getNext( isStatic );\r
+                                       }\r
+\r
+                                       if ( !neg || neg.equals( pos ) ) {\r
+                                               break;\r
+                                       }\r
+\r
+                                       this.traverseSearch( neg );\r
+\r
+                                       if ( pos ) {\r
+                                               pos = pos.getPrevious( isStatic );\r
+                                       }\r
+\r
+                                       if ( !pos || pos.equals( neg ) ) {\r
+                                               break;\r
+                                       }\r
+\r
+                                       this.traverseSearch( pos );\r
+                               }\r
+                       };\r
+               } )(),\r
+\r
+               /**\r
+                * Unlike {@link #traverseSearch}, it collects **all** elements from editable's DOM tree\r
+                * and runs lookups for every one of them, collecting relations.\r
+                *\r
+                * @returns {Object} {@link #relations}.\r
+                */\r
+               greedySearch: function() {\r
+                       this.relations = {};\r
+\r
+                       var all = this.editable.getElementsByTag( '*' ),\r
+                               i = 0,\r
+                               el, type, l;\r
+\r
+                       while ( ( el = all.getItem( i++ ) ) ) {\r
+                               // Don't consider editable, as it might be inline,\r
+                               // and i.e. checking it's siblings is pointless.\r
+                               if ( el.equals( this.editable ) ) {\r
+                                       continue;\r
+                               }\r
+\r
+                               // On IE8 element.getElementsByTagName returns comments... sic! (http://dev.ckeditor.com/ticket/13176)\r
+                               if ( el.type != CKEDITOR.NODE_ELEMENT ) {\r
+                                       continue;\r
+                               }\r
+\r
+                               // Don't visit non-editable internals, for example widget's\r
+                               // guts (above wrapper, below nested). Still check editable limits,\r
+                               // as they are siblings with editable contents.\r
+                               if ( !el.hasAttribute( 'contenteditable' ) && el.isReadOnly() ) {\r
+                                       continue;\r
+                               }\r
+\r
+                               if ( isStatic( el ) && el.isVisible() ) {\r
+                                       // Collect all addresses yielded by lookups for that element.\r
+                                       for ( l in this.lookups ) {\r
+                                               if ( ( type = this.lookups[ l ]( el ) ) ) {\r
+                                                       this.store( el, type );\r
+                                               }\r
+                                       }\r
+                               }\r
+                       }\r
+\r
+                       return this.relations;\r
+               }\r
+\r
+               /**\r
+                * Relations express elements in DOM that match user-defined {@link #lookups}.\r
+                * Every relation has its own `type` that determines whether\r
+                * it refers to the space before, after or inside the `element`.\r
+                * This object stores relations found by {@link #traverseSearch} or {@link #greedySearch}, structured\r
+                * in the following way:\r
+                *\r
+                *              relations: {\r
+                *                      // Unique identifier of the element.\r
+                *                      Number: {\r
+                *                              // Element of this relation.\r
+                *                              element: {@link CKEDITOR.dom.element}\r
+                *                              // Conjunction of CKEDITOR.LINEUTILS_BEFORE, CKEDITOR.LINEUTILS_AFTER and CKEDITOR.LINEUTILS_INSIDE.\r
+                *                              type: Number\r
+                *                      },\r
+                *                      ...\r
+                *              }\r
+                *\r
+                * @property {Object} relations\r
+                * @readonly\r
+                */\r
+\r
+               /**\r
+                * A set of user-defined functions used by Finder to check if an element\r
+                * is a valid relation, belonging to {@link #relations}.\r
+                * When the criterion is met, lookup returns a logical conjunction of `CKEDITOR.LINEUTILS_BEFORE`,\r
+                * `CKEDITOR.LINEUTILS_AFTER` or `CKEDITOR.LINEUTILS_INSIDE`.\r
+                *\r
+                * Lookups are passed along with Finder's definition.\r
+                *\r
+                *              lookups: {\r
+                *                      'some lookup': function( el ) {\r
+                *                              if ( someCondition )\r
+                *                                      return CKEDITOR.LINEUTILS_BEFORE;\r
+                *                      },\r
+                *                      ...\r
+                *              }\r
+                *\r
+                * @property {Object} lookups\r
+                */\r
+       };\r
+\r
+\r
+       /**\r
+        * A utility that analyses relations found by\r
+        * CKEDITOR.plugins.lineutils.finder and locates them\r
+        * in the viewport as horizontal lines of specific coordinates.\r
+        *\r
+        * @private\r
+        * @class CKEDITOR.plugins.lineutils.locator\r
+        * @constructor Creates a Locator class instance.\r
+        * @param {CKEDITOR.editor} editor Editor instance that Locator belongs to.\r
+        * @since 4.3\r
+        */\r
+       function Locator( editor, def ) {\r
+               CKEDITOR.tools.extend( this, def, {\r
+                       editor: editor\r
+               }, true );\r
+       }\r
+\r
+       Locator.prototype = {\r
+               /**\r
+                * Locates the Y coordinate for all types of every single relation and stores\r
+                * them in an object.\r
+                *\r
+                * @param {Object} relations {@link CKEDITOR.plugins.lineutils.finder#relations}.\r
+                * @returns {Object} {@link #locations}.\r
+                */\r
+               locate: ( function() {\r
+                       function locateSibling( rel, type ) {\r
+                               var sib = rel.element[ type === CKEDITOR.LINEUTILS_BEFORE ? 'getPrevious' : 'getNext' ]();\r
+\r
+                               // Return the middle point between siblings.\r
+                               if ( sib && isStatic( sib ) ) {\r
+                                       rel.siblingRect = sib.getClientRect();\r
+\r
+                                       if ( type == CKEDITOR.LINEUTILS_BEFORE ) {\r
+                                               return ( rel.siblingRect.bottom + rel.elementRect.top ) / 2;\r
+                                       } else {\r
+                                               return ( rel.elementRect.bottom + rel.siblingRect.top ) / 2;\r
+                                       }\r
+                               }\r
+\r
+                               // If there's no sibling, use the edge of an element.\r
+                               else {\r
+                                       if ( type == CKEDITOR.LINEUTILS_BEFORE ) {\r
+                                               return rel.elementRect.top;\r
+                                       } else {\r
+                                               return rel.elementRect.bottom;\r
+                                       }\r
+                               }\r
+                       }\r
+\r
+                       return function( relations ) {\r
+                               var rel;\r
+\r
+                               this.locations = {};\r
+\r
+                               for ( var uid in relations ) {\r
+                                       rel = relations[ uid ];\r
+                                       rel.elementRect = rel.element.getClientRect();\r
+\r
+                                       if ( is( rel.type, CKEDITOR.LINEUTILS_BEFORE ) ) {\r
+                                               this.store( uid, CKEDITOR.LINEUTILS_BEFORE, locateSibling( rel, CKEDITOR.LINEUTILS_BEFORE ) );\r
+                                       }\r
+\r
+                                       if ( is( rel.type, CKEDITOR.LINEUTILS_AFTER ) ) {\r
+                                               this.store( uid, CKEDITOR.LINEUTILS_AFTER, locateSibling( rel, CKEDITOR.LINEUTILS_AFTER ) );\r
+                                       }\r
+\r
+                                       // The middle point of the element.\r
+                                       if ( is( rel.type, CKEDITOR.LINEUTILS_INSIDE ) ) {\r
+                                               this.store( uid, CKEDITOR.LINEUTILS_INSIDE, ( rel.elementRect.top + rel.elementRect.bottom ) / 2 );\r
+                                       }\r
+                               }\r
+\r
+                               return this.locations;\r
+                       };\r
+               } )(),\r
+\r
+               /**\r
+                * Calculates distances from every location to given vertical coordinate\r
+                * and sorts locations according to that distance.\r
+                *\r
+                * @param {Number} y The vertical coordinate used for sorting, used as a reference.\r
+                * @param {Number} [howMany] Determines the number of "closest locations" to be returned.\r
+                * @returns {Array} Sorted, array representation of {@link #locations}.\r
+                */\r
+               sort: ( function() {\r
+                       var locations, sorted,\r
+                               dist, i;\r
+\r
+                       function distance( y, uid, type ) {\r
+                               return Math.abs( y - locations[ uid ][ type ] );\r
+                       }\r
+\r
+                       return function( y, howMany ) {\r
+                               locations = this.locations;\r
+                               sorted = [];\r
+\r
+                               for ( var uid in locations ) {\r
+                                       for ( var type in locations[ uid ] ) {\r
+                                               dist = distance( y, uid, type );\r
+\r
+                                               // An array is empty.\r
+                                               if ( !sorted.length ) {\r
+                                                       sorted.push( { uid: +uid, type: type, dist: dist } );\r
+                                               } else {\r
+                                                       // Sort the array on fly when it's populated.\r
+                                                       for ( i = 0; i < sorted.length; i++ ) {\r
+                                                               if ( dist < sorted[ i ].dist ) {\r
+                                                                       sorted.splice( i, 0, { uid: +uid, type: type, dist: dist } );\r
+                                                                       break;\r
+                                                               }\r
+                                                       }\r
+\r
+                                                       // Nothing was inserted, so the distance is bigger than\r
+                                                       // any of already calculated: push to the end.\r
+                                                       if ( i == sorted.length ) {\r
+                                                               sorted.push( { uid: +uid, type: type, dist: dist } );\r
+                                                       }\r
+                                               }\r
+                                       }\r
+                               }\r
+\r
+                               if ( typeof howMany != 'undefined' ) {\r
+                                       return sorted.slice( 0, howMany );\r
+                               } else {\r
+                                       return sorted;\r
+                               }\r
+                       };\r
+               } )(),\r
+\r
+               /**\r
+                * Stores the location in a collection.\r
+                *\r
+                * @param {Number} uid Unique identifier of the relation.\r
+                * @param {Number} type One of `CKEDITOR.LINEUTILS_BEFORE`, `CKEDITOR.LINEUTILS_AFTER` and `CKEDITOR.LINEUTILS_INSIDE`.\r
+                * @param {Number} y Vertical position of the relation.\r
+                */\r
+               store: function( uid, type, y ) {\r
+                       if ( !this.locations[ uid ] ) {\r
+                               this.locations[ uid ] = {};\r
+                       }\r
+\r
+                       this.locations[ uid ][ type ] = y;\r
+               }\r
+\r
+               /**\r
+                * @readonly\r
+                * @property {Object} locations\r
+                */\r
+       };\r
+\r
+       var tipCss = {\r
+                       display: 'block',\r
+                       width: '0px',\r
+                       height: '0px',\r
+                       'border-color': 'transparent',\r
+                       'border-style': 'solid',\r
+                       position: 'absolute',\r
+                       top: '-6px'\r
+               },\r
+\r
+               lineStyle = {\r
+                       height: '0px',\r
+                       'border-top': '1px dashed red',\r
+                       position: 'absolute',\r
+                       'z-index': 9999\r
+               },\r
+\r
+               lineTpl =\r
+                       '<div data-cke-lineutils-line="1" class="cke_reset_all" style="{lineStyle}">' +\r
+                               '<span style="{tipLeftStyle}">&nbsp;</span>' +\r
+                               '<span style="{tipRightStyle}">&nbsp;</span>' +\r
+                       '</div>';\r
+\r
+       /**\r
+        * A utility that draws horizontal lines in DOM according to locations\r
+        * returned by CKEDITOR.plugins.lineutils.locator.\r
+        *\r
+        * @private\r
+        * @class CKEDITOR.plugins.lineutils.liner\r
+        * @constructor Creates a Liner class instance.\r
+        * @param {CKEDITOR.editor} editor Editor instance that Liner belongs to.\r
+        * @param {Object} def Liner's definition.\r
+        * @since 4.3\r
+        */\r
+       function Liner( editor, def ) {\r
+               var editable = editor.editable();\r
+\r
+               CKEDITOR.tools.extend( this, {\r
+                       editor: editor,\r
+                       editable: editable,\r
+                       inline: editable.isInline(),\r
+                       doc: editor.document,\r
+                       win: editor.window,\r
+                       container: CKEDITOR.document.getBody(),\r
+                       winTop: CKEDITOR.document.getWindow()\r
+               }, def, true );\r
+\r
+               this.hidden = {};\r
+               this.visible = {};\r
+\r
+               if ( !this.inline ) {\r
+                       this.frame = this.win.getFrame();\r
+               }\r
+\r
+               this.queryViewport();\r
+\r
+               // Callbacks must be wrapped. Otherwise they're not attached\r
+               // to global DOM objects (i.e. topmost window) for every editor\r
+               // because they're treated as duplicates. They belong to the\r
+               // same prototype shared among Liner instances.\r
+               var queryViewport = CKEDITOR.tools.bind( this.queryViewport, this ),\r
+                       hideVisible = CKEDITOR.tools.bind( this.hideVisible, this ),\r
+                       removeAll = CKEDITOR.tools.bind( this.removeAll, this );\r
+\r
+               editable.attachListener( this.winTop, 'resize', queryViewport );\r
+               editable.attachListener( this.winTop, 'scroll', queryViewport );\r
+\r
+               editable.attachListener( this.winTop, 'resize', hideVisible );\r
+               editable.attachListener( this.win, 'scroll', hideVisible );\r
+\r
+               editable.attachListener( this.inline ? editable : this.frame, 'mouseout', function( evt ) {\r
+                       var x = evt.data.$.clientX,\r
+                               y = evt.data.$.clientY;\r
+\r
+                       this.queryViewport();\r
+\r
+                       // Check if mouse is out of the element (iframe/editable).\r
+                       if ( x <= this.rect.left || x >= this.rect.right || y <= this.rect.top || y >= this.rect.bottom ) {\r
+                               this.hideVisible();\r
+                       }\r
+\r
+                       // Check if mouse is out of the top-window vieport.\r
+                       if ( x <= 0 || x >= this.winTopPane.width || y <= 0 || y >= this.winTopPane.height ) {\r
+                               this.hideVisible();\r
+                       }\r
+               }, this );\r
+\r
+               editable.attachListener( editor, 'resize', queryViewport );\r
+               editable.attachListener( editor, 'mode', removeAll );\r
+               editor.on( 'destroy', removeAll );\r
+\r
+               this.lineTpl = new CKEDITOR.template( lineTpl ).output( {\r
+                       lineStyle: CKEDITOR.tools.writeCssText(\r
+                               CKEDITOR.tools.extend( {}, lineStyle, this.lineStyle, true )\r
+                       ),\r
+                       tipLeftStyle: CKEDITOR.tools.writeCssText(\r
+                               CKEDITOR.tools.extend( {}, tipCss, {\r
+                                       left: '0px',\r
+                                       'border-left-color': 'red',\r
+                                       'border-width': '6px 0 6px 6px'\r
+                               }, this.tipCss, this.tipLeftStyle, true )\r
+                       ),\r
+                       tipRightStyle: CKEDITOR.tools.writeCssText(\r
+                               CKEDITOR.tools.extend( {}, tipCss, {\r
+                                       right: '0px',\r
+                                       'border-right-color': 'red',\r
+                                       'border-width': '6px 6px 6px 0'\r
+                               }, this.tipCss, this.tipRightStyle, true )\r
+                       )\r
+               } );\r
+       }\r
+\r
+       Liner.prototype = {\r
+               /**\r
+                * Permanently removes all lines (both hidden and visible) from DOM.\r
+                */\r
+               removeAll: function() {\r
+                       var l;\r
+\r
+                       for ( l in this.hidden ) {\r
+                               this.hidden[ l ].remove();\r
+                               delete this.hidden[ l ];\r
+                       }\r
+\r
+                       for ( l in this.visible ) {\r
+                               this.visible[ l ].remove();\r
+                               delete this.visible[ l ];\r
+                       }\r
+               },\r
+\r
+               /**\r
+                * Hides a given line.\r
+                *\r
+                * @param {CKEDITOR.dom.element} line The line to be hidden.\r
+                */\r
+               hideLine: function( line ) {\r
+                       var uid = line.getUniqueId();\r
+\r
+                       line.hide();\r
+\r
+                       this.hidden[ uid ] = line;\r
+                       delete this.visible[ uid ];\r
+               },\r
+\r
+               /**\r
+                * Shows a given line.\r
+                *\r
+                * @param {CKEDITOR.dom.element} line The line to be shown.\r
+                */\r
+               showLine: function( line ) {\r
+                       var uid = line.getUniqueId();\r
+\r
+                       line.show();\r
+\r
+                       this.visible[ uid ] = line;\r
+                       delete this.hidden[ uid ];\r
+               },\r
+\r
+               /**\r
+                * Hides all visible lines.\r
+                */\r
+               hideVisible: function() {\r
+                       for ( var l in this.visible ) {\r
+                               this.hideLine( this.visible[ l ] );\r
+                       }\r
+               },\r
+\r
+               /**\r
+                * Shows a line at given location.\r
+                *\r
+                * @param {Object} location Location object containing the unique identifier of the relation\r
+                * and its type. Usually returned by {@link CKEDITOR.plugins.lineutils.locator#sort}.\r
+                * @param {Function} [callback] A callback to be called once the line is shown.\r
+                */\r
+               placeLine: function( location, callback ) {\r
+                       var styles, line, l;\r
+\r
+                       // No style means that line would be out of viewport.\r
+                       if ( !( styles = this.getStyle( location.uid, location.type ) ) ) {\r
+                               return;\r
+                       }\r
+\r
+                       // Search for any visible line of a different hash first.\r
+                       // It's faster to re-position visible line than to show it.\r
+                       for ( l in this.visible ) {\r
+                               if ( this.visible[ l ].getCustomData( 'hash' ) !== this.hash ) {\r
+                                       line = this.visible[ l ];\r
+                                       break;\r
+                               }\r
+                       }\r
+\r
+                       // Search for any hidden line of a different hash.\r
+                       if ( !line ) {\r
+                               for ( l in this.hidden ) {\r
+                                       if ( this.hidden[ l ].getCustomData( 'hash' ) !== this.hash ) {\r
+                                               this.showLine( ( line = this.hidden[ l ] ) );\r
+                                               break;\r
+                                       }\r
+                               }\r
+                       }\r
+\r
+                       // If no line available, add the new one.\r
+                       if ( !line ) {\r
+                               this.showLine( ( line = this.addLine() ) );\r
+                       }\r
+\r
+                       // Mark the line with current hash.\r
+                       line.setCustomData( 'hash', this.hash );\r
+\r
+                       // Mark the line as visible.\r
+                       this.visible[ line.getUniqueId() ] = line;\r
+\r
+                       line.setStyles( styles );\r
+\r
+                       callback && callback( line );\r
+               },\r
+\r
+               /**\r
+                * Creates a style set to be used by the line, representing a particular\r
+                * relation (location).\r
+                *\r
+                * @param {Number} uid Unique identifier of the relation.\r
+                * @param {Number} type Type of the relation.\r
+                * @returns {Object} An object containing styles.\r
+                */\r
+               getStyle: function( uid, type ) {\r
+                       var rel = this.relations[ uid ],\r
+                               loc = this.locations[ uid ][ type ],\r
+                               styles = {},\r
+                               hdiff;\r
+\r
+                       // Line should be between two elements.\r
+                       if ( rel.siblingRect ) {\r
+                               styles.width = Math.max( rel.siblingRect.width, rel.elementRect.width );\r
+                       }\r
+                       // Line is relative to a single element.\r
+                       else {\r
+                               styles.width = rel.elementRect.width;\r
+                       }\r
+\r
+                       // Let's calculate the vertical position of the line.\r
+                       if ( this.inline ) {\r
+                               // (http://dev.ckeditor.com/ticket/13155)\r
+                               styles.top = loc + this.winTopScroll.y - this.rect.relativeY;\r
+                       } else {\r
+                               styles.top = this.rect.top + this.winTopScroll.y + loc;\r
+                       }\r
+\r
+                       // Check if line would be vertically out of the viewport.\r
+                       if ( styles.top - this.winTopScroll.y < this.rect.top || styles.top - this.winTopScroll.y > this.rect.bottom ) {\r
+                               return false;\r
+                       }\r
+\r
+                       // Now let's calculate the horizontal alignment (left and width).\r
+                       if ( this.inline ) {\r
+                               // (http://dev.ckeditor.com/ticket/13155)\r
+                               styles.left = rel.elementRect.left - this.rect.relativeX;\r
+                       } else {\r
+                               if ( rel.elementRect.left > 0 )\r
+                                       styles.left = this.rect.left + rel.elementRect.left;\r
+\r
+                               // H-scroll case. Left edge of element may be out of viewport.\r
+                               else {\r
+                                       styles.width += rel.elementRect.left;\r
+                                       styles.left = this.rect.left;\r
+                               }\r
+\r
+                               // H-scroll case. Right edge of element may be out of viewport.\r
+                               if ( ( hdiff = styles.left + styles.width - ( this.rect.left + this.winPane.width ) ) > 0 ) {\r
+                                       styles.width -= hdiff;\r
+                               }\r
+                       }\r
+\r
+                       // Finally include horizontal scroll of the global window.\r
+                       styles.left += this.winTopScroll.x;\r
+\r
+                       // Append 'px' to style values.\r
+                       for ( var style in styles ) {\r
+                               styles[ style ] = CKEDITOR.tools.cssLength( styles[ style ] );\r
+                       }\r
+\r
+                       return styles;\r
+               },\r
+\r
+               /**\r
+                * Adds a new line to DOM.\r
+                *\r
+                * @returns {CKEDITOR.dom.element} A brand-new line.\r
+                */\r
+               addLine: function() {\r
+                       var line = CKEDITOR.dom.element.createFromHtml( this.lineTpl );\r
+\r
+                       line.appendTo( this.container );\r
+\r
+                       return line;\r
+               },\r
+\r
+               /**\r
+                * Assigns a unique hash to the instance that is later used\r
+                * to tell unwanted lines from new ones. This method **must** be called\r
+                * before a new set of relations is to be visualized so {@link #cleanup}\r
+                * eventually hides obsolete lines. This is because lines\r
+                * are re-used between {@link #placeLine} calls and the number of\r
+                * necessary ones may vary depending on the number of relations.\r
+                *\r
+                * @param {Object} relations {@link CKEDITOR.plugins.lineutils.finder#relations}.\r
+                * @param {Object} locations {@link CKEDITOR.plugins.lineutils.locator#locations}.\r
+                */\r
+               prepare: function( relations, locations ) {\r
+                       this.relations = relations;\r
+                       this.locations = locations;\r
+                       this.hash = Math.random();\r
+               },\r
+\r
+               /**\r
+                * Hides all visible lines that do not belong to current hash\r
+                * and no longer represent relations (locations).\r
+                *\r
+                * See also: {@link #prepare}.\r
+                */\r
+               cleanup: function() {\r
+                       var line;\r
+\r
+                       for ( var l in this.visible ) {\r
+                               line = this.visible[ l ];\r
+\r
+                               if ( line.getCustomData( 'hash' ) !== this.hash ) {\r
+                                       this.hideLine( line );\r
+                               }\r
+                       }\r
+               },\r
+\r
+               /**\r
+                * Queries dimensions of the viewport, editable, frame etc.\r
+                * that are used for correct positioning of the line.\r
+                */\r
+               queryViewport: function() {\r
+                       this.winPane = this.win.getViewPaneSize();\r
+                       this.winTopScroll = this.winTop.getScrollPosition();\r
+                       this.winTopPane = this.winTop.getViewPaneSize();\r
+\r
+                       // (http://dev.ckeditor.com/ticket/13155)\r
+                       this.rect = this.getClientRect( this.inline ? this.editable : this.frame );\r
+               },\r
+\r
+               /**\r
+                * Returns `boundingClientRect` of an element, shifted by the position\r
+                * of `container` when the container is not `static` (http://dev.ckeditor.com/ticket/13155).\r
+                *\r
+                * See also: {@link CKEDITOR.dom.element#getClientRect}.\r
+                *\r
+                * @param {CKEDITOR.dom.element} el A DOM element.\r
+                * @returns {Object} A shifted rect, extended by `relativeY` and `relativeX` properties.\r
+                */\r
+               getClientRect: function( el ) {\r
+                       var rect = el.getClientRect(),\r
+                               relativeContainerDocPosition = this.container.getDocumentPosition(),\r
+                               relativeContainerComputedPosition = this.container.getComputedStyle( 'position' );\r
+\r
+                       // Static or not, those values are used to offset the position of the line so they cannot be undefined.\r
+                       rect.relativeX = rect.relativeY = 0;\r
+\r
+                       if ( relativeContainerComputedPosition != 'static' ) {\r
+                               // Remember the offset used to shift the clientRect.\r
+                               rect.relativeY = relativeContainerDocPosition.y;\r
+                               rect.relativeX = relativeContainerDocPosition.x;\r
+\r
+                               rect.top -= rect.relativeY;\r
+                               rect.bottom -= rect.relativeY;\r
+                               rect.left -= rect.relativeX;\r
+                               rect.right -= rect.relativeX;\r
+                       }\r
+\r
+                       return rect;\r
+               }\r
+       };\r
+\r
+       function is( type, flag ) {\r
+               return type & flag;\r
+       }\r
+\r
+       var floats = { left: 1, right: 1, center: 1 },\r
+               positions = { absolute: 1, fixed: 1 };\r
+\r
+       function isElement( node ) {\r
+               return node && node.type == CKEDITOR.NODE_ELEMENT;\r
+       }\r
+\r
+       function isFloated( el ) {\r
+               return !!( floats[ el.getComputedStyle( 'float' ) ] || floats[ el.getAttribute( 'align' ) ] );\r
+       }\r
+\r
+       function isPositioned( el ) {\r
+               return !!positions[ el.getComputedStyle( 'position' ) ];\r
+       }\r
+\r
+       function isLimit( node ) {\r
+               return isElement( node ) && node.getAttribute( 'contenteditable' ) == 'true';\r
+       }\r
+\r
+       function isStatic( node ) {\r
+               return isElement( node ) && !isFloated( node ) && !isPositioned( node );\r
+       }\r
+\r
+       /**\r
+        * Global namespace storing definitions and global helpers for the Line Utilities plugin.\r
+        *\r
+        * @private\r
+        * @class\r
+        * @singleton\r
+        * @since 4.3\r
+        */\r
+       CKEDITOR.plugins.lineutils = {\r
+               finder: Finder,\r
+               locator: Locator,\r
+               liner: Liner\r
+       };\r
+} )();\r