]>
Commit | Line | Data |
---|---|---|
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 | |
382 | // On IE8 element.getElementsByTagName returns comments... sic! (#13176)\r | |
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}"> </span>' +\r | |
621 | '<span style="{tipRightStyle}"> </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 | |
843 | // (#13155)\r | |
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 | |
856 | // (#13155)\r | |
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 | |
942 | // (#13155)\r | |
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 | |
948 | * of `container` when the container is not `static` (#13155).\r | |
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 |