diff options
Diffstat (limited to 'sources/plugins/lineutils/plugin.js')
-rw-r--r-- | sources/plugins/lineutils/plugin.js | 1018 |
1 files changed, 1018 insertions, 0 deletions
diff --git a/sources/plugins/lineutils/plugin.js b/sources/plugins/lineutils/plugin.js new file mode 100644 index 00000000..bb54fa07 --- /dev/null +++ b/sources/plugins/lineutils/plugin.js | |||
@@ -0,0 +1,1018 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * @fileOverview A set of utilities to find and create horizontal spaces in edited content. | ||
8 | */ | ||
9 | |||
10 | 'use strict'; | ||
11 | |||
12 | ( function() { | ||
13 | |||
14 | CKEDITOR.plugins.add( 'lineutils' ); | ||
15 | |||
16 | /** | ||
17 | * Determines a position relative to an element in DOM (before). | ||
18 | * | ||
19 | * @readonly | ||
20 | * @property {Number} [=0] | ||
21 | * @member CKEDITOR | ||
22 | */ | ||
23 | CKEDITOR.LINEUTILS_BEFORE = 1; | ||
24 | |||
25 | /** | ||
26 | * Determines a position relative to an element in DOM (after). | ||
27 | * | ||
28 | * @readonly | ||
29 | * @property {Number} [=2] | ||
30 | * @member CKEDITOR | ||
31 | */ | ||
32 | CKEDITOR.LINEUTILS_AFTER = 2; | ||
33 | |||
34 | /** | ||
35 | * Determines a position relative to an element in DOM (inside). | ||
36 | * | ||
37 | * @readonly | ||
38 | * @property {Number} [=4] | ||
39 | * @member CKEDITOR | ||
40 | */ | ||
41 | CKEDITOR.LINEUTILS_INSIDE = 4; | ||
42 | |||
43 | /** | ||
44 | * A utility that traverses the DOM tree and discovers elements | ||
45 | * (relations) matching user-defined lookups. | ||
46 | * | ||
47 | * @private | ||
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. | ||
52 | * @since 4.3 | ||
53 | */ | ||
54 | function Finder( editor, def ) { | ||
55 | CKEDITOR.tools.extend( this, { | ||
56 | editor: editor, | ||
57 | editable: editor.editable(), | ||
58 | doc: editor.document, | ||
59 | win: editor.window | ||
60 | }, def, true ); | ||
61 | |||
62 | this.inline = this.editable.isInline(); | ||
63 | |||
64 | if ( !this.inline ) { | ||
65 | this.frame = this.win.getFrame(); | ||
66 | } | ||
67 | |||
68 | this.target = this[ this.inline ? 'editable' : 'doc' ]; | ||
69 | } | ||
70 | |||
71 | Finder.prototype = { | ||
72 | /** | ||
73 | * Initializes searching for elements with every mousemove event fired. | ||
74 | * To stop searching use {@link #stop}. | ||
75 | * | ||
76 | * @param {Function} [callback] Function executed on every iteration. | ||
77 | */ | ||
78 | start: function( callback ) { | ||
79 | var that = this, | ||
80 | editor = this.editor, | ||
81 | doc = this.doc, | ||
82 | el, elfp, x, y; | ||
83 | |||
84 | var moveBuffer = CKEDITOR.tools.eventsBuffer( 50, function() { | ||
85 | if ( editor.readOnly || editor.mode != 'wysiwyg' ) | ||
86 | return; | ||
87 | |||
88 | that.relations = {}; | ||
89 | |||
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 ) { | ||
95 | return; | ||
96 | } | ||
97 | |||
98 | el = new CKEDITOR.dom.element( elfp ); | ||
99 | |||
100 | that.traverseSearch( el ); | ||
101 | |||
102 | if ( !isNaN( x + y ) ) { | ||
103 | that.pixelSearch( el, x, y ); | ||
104 | } | ||
105 | |||
106 | callback && callback( that.relations, x, y ); | ||
107 | } ); | ||
108 | |||
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; | ||
113 | |||
114 | moveBuffer.input(); | ||
115 | } ); | ||
116 | |||
117 | this.editable.attachListener( this.inline ? this.editable : this.frame, 'mouseout', function() { | ||
118 | moveBuffer.reset(); | ||
119 | } ); | ||
120 | }, | ||
121 | |||
122 | /** | ||
123 | * Stops observing mouse events attached by {@link #start}. | ||
124 | */ | ||
125 | stop: function() { | ||
126 | if ( this.listener ) { | ||
127 | this.listener.removeListener(); | ||
128 | } | ||
129 | }, | ||
130 | |||
131 | /** | ||
132 | * Returns a range representing the relation, according to its element | ||
133 | * and type. | ||
134 | * | ||
135 | * @param {Object} location Location containing a unique identifier and type. | ||
136 | * @returns {CKEDITOR.dom.range} Range representing the relation. | ||
137 | */ | ||
138 | getRange: ( function() { | ||
139 | var where = {}; | ||
140 | |||
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; | ||
144 | |||
145 | return function( location ) { | ||
146 | var range = this.editor.createRange(); | ||
147 | |||
148 | range.moveToPosition( this.relations[ location.uid ].element, where[ location.type ] ); | ||
149 | |||
150 | return range; | ||
151 | }; | ||
152 | } )(), | ||
153 | |||
154 | /** | ||
155 | * Stores given relation in a {@link #relations} object. Processes the relation | ||
156 | * to normalize and avoid duplicates. | ||
157 | * | ||
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`. | ||
160 | */ | ||
161 | store: ( function() { | ||
162 | function merge( el, type, relations ) { | ||
163 | var uid = el.getUniqueId(); | ||
164 | |||
165 | if ( uid in relations ) { | ||
166 | relations[ uid ].type |= type; | ||
167 | } else { | ||
168 | relations[ uid ] = { element: el, type: type }; | ||
169 | } | ||
170 | } | ||
171 | |||
172 | return function( el, type ) { | ||
173 | var alt; | ||
174 | |||
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; | ||
180 | } | ||
181 | |||
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; | ||
187 | } | ||
188 | |||
189 | merge( el, type, this.relations ); | ||
190 | }; | ||
191 | } )(), | ||
192 | |||
193 | /** | ||
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. | ||
197 | * | ||
198 | * @param {CKEDITOR.dom.element} el Element which is the starting point. | ||
199 | */ | ||
200 | traverseSearch: function( el ) { | ||
201 | var l, type, uid; | ||
202 | |||
203 | // Go down DOM towards root (or limit). | ||
204 | do { | ||
205 | uid = el.$[ 'data-cke-expando' ]; | ||
206 | |||
207 | // This element was already visited and checked. | ||
208 | if ( uid && uid in this.relations ) { | ||
209 | continue; | ||
210 | } | ||
211 | |||
212 | if ( el.equals( this.editable ) ) { | ||
213 | return; | ||
214 | } | ||
215 | |||
216 | if ( isStatic( el ) ) { | ||
217 | // Collect all addresses yielded by lookups for that element. | ||
218 | for ( l in this.lookups ) { | ||
219 | |||
220 | if ( ( type = this.lookups[ l ]( el ) ) ) { | ||
221 | this.store( el, type ); | ||
222 | } | ||
223 | } | ||
224 | } | ||
225 | } while ( !isLimit( el ) && ( el = el.getParent() ) ); | ||
226 | }, | ||
227 | |||
228 | /** | ||
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}. | ||
232 | * | ||
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. | ||
236 | */ | ||
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 ); | ||
243 | }; | ||
244 | |||
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. | ||
251 | // | ||
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 ) { | ||
259 | var y = yStart, | ||
260 | tryouts = 0, | ||
261 | found; | ||
262 | |||
263 | while ( condition( y ) ) { | ||
264 | y += step; | ||
265 | |||
266 | // If we try and we try, and still nothing's found, let's end | ||
267 | // that party. | ||
268 | if ( ++tryouts == 25 ) { | ||
269 | return; | ||
270 | } | ||
271 | |||
272 | found = this.doc.$.elementFromPoint( xStart, y ); | ||
273 | |||
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. | ||
278 | if ( !found ) { | ||
279 | continue; | ||
280 | } | ||
281 | |||
282 | // Still in the same element. | ||
283 | else if ( found == el ) { | ||
284 | tryouts = 0; | ||
285 | continue; | ||
286 | } | ||
287 | |||
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 ) ) { | ||
291 | continue; | ||
292 | } | ||
293 | |||
294 | tryouts = 0; | ||
295 | |||
296 | // Found a valid element. Stop iterating. | ||
297 | if ( isStatic( ( found = new CKEDITOR.dom.element( found ) ) ) ) { | ||
298 | return found; | ||
299 | } | ||
300 | } | ||
301 | } | ||
302 | |||
303 | return function( el, x, y ) { | ||
304 | var paneHeight = this.win.getViewPaneSize().height, | ||
305 | |||
306 | // Try to find an element iterating *up* from the starting point. | ||
307 | neg = iterate.call( this, el.$, x, y, -1, function( y ) { | ||
308 | return y > 0; | ||
309 | } ), | ||
310 | |||
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; | ||
314 | } ); | ||
315 | |||
316 | if ( neg ) { | ||
317 | this.traverseSearch( neg ); | ||
318 | |||
319 | // Iterate towards DOM root until neg is a direct child of el. | ||
320 | while ( !neg.getParent().equals( el ) ) { | ||
321 | neg = neg.getParent(); | ||
322 | } | ||
323 | } | ||
324 | |||
325 | if ( pos ) { | ||
326 | this.traverseSearch( pos ); | ||
327 | |||
328 | // Iterate towards DOM root until pos is a direct child of el. | ||
329 | while ( !pos.getParent().equals( el ) ) { | ||
330 | pos = pos.getParent(); | ||
331 | } | ||
332 | } | ||
333 | |||
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 ) { | ||
339 | if ( neg ) { | ||
340 | neg = neg.getNext( isStatic ); | ||
341 | } | ||
342 | |||
343 | if ( !neg || neg.equals( pos ) ) { | ||
344 | break; | ||
345 | } | ||
346 | |||
347 | this.traverseSearch( neg ); | ||
348 | |||
349 | if ( pos ) { | ||
350 | pos = pos.getPrevious( isStatic ); | ||
351 | } | ||
352 | |||
353 | if ( !pos || pos.equals( neg ) ) { | ||
354 | break; | ||
355 | } | ||
356 | |||
357 | this.traverseSearch( pos ); | ||
358 | } | ||
359 | }; | ||
360 | } )(), | ||
361 | |||
362 | /** | ||
363 | * Unlike {@link #traverseSearch}, it collects **all** elements from editable's DOM tree | ||
364 | * and runs lookups for every one of them, collecting relations. | ||
365 | * | ||
366 | * @returns {Object} {@link #relations}. | ||
367 | */ | ||
368 | greedySearch: function() { | ||
369 | this.relations = {}; | ||
370 | |||
371 | var all = this.editable.getElementsByTag( '*' ), | ||
372 | i = 0, | ||
373 | el, type, l; | ||
374 | |||
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 ) ) { | ||
379 | continue; | ||
380 | } | ||
381 | |||
382 | // On IE8 element.getElementsByTagName returns comments... sic! (#13176) | ||
383 | if ( el.type != CKEDITOR.NODE_ELEMENT ) { | ||
384 | continue; | ||
385 | } | ||
386 | |||
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() ) { | ||
391 | continue; | ||
392 | } | ||
393 | |||
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 ); | ||
399 | } | ||
400 | } | ||
401 | } | ||
402 | } | ||
403 | |||
404 | return this.relations; | ||
405 | } | ||
406 | |||
407 | /** | ||
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: | ||
413 | * | ||
414 | * relations: { | ||
415 | * // Unique identifier of the element. | ||
416 | * Number: { | ||
417 | * // Element of this relation. | ||
418 | * element: {@link CKEDITOR.dom.element} | ||
419 | * // Conjunction of CKEDITOR.LINEUTILS_BEFORE, CKEDITOR.LINEUTILS_AFTER and CKEDITOR.LINEUTILS_INSIDE. | ||
420 | * type: Number | ||
421 | * }, | ||
422 | * ... | ||
423 | * } | ||
424 | * | ||
425 | * @property {Object} relations | ||
426 | * @readonly | ||
427 | */ | ||
428 | |||
429 | /** | ||
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`. | ||
434 | * | ||
435 | * Lookups are passed along with Finder's definition. | ||
436 | * | ||
437 | * lookups: { | ||
438 | * 'some lookup': function( el ) { | ||
439 | * if ( someCondition ) | ||
440 | * return CKEDITOR.LINEUTILS_BEFORE; | ||
441 | * }, | ||
442 | * ... | ||
443 | * } | ||
444 | * | ||
445 | * @property {Object} lookups | ||
446 | */ | ||
447 | }; | ||
448 | |||
449 | |||
450 | /** | ||
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. | ||
454 | * | ||
455 | * @private | ||
456 | * @class CKEDITOR.plugins.lineutils.locator | ||
457 | * @constructor Creates a Locator class instance. | ||
458 | * @param {CKEDITOR.editor} editor Editor instance that Locator belongs to. | ||
459 | * @since 4.3 | ||
460 | */ | ||
461 | function Locator( editor, def ) { | ||
462 | CKEDITOR.tools.extend( this, def, { | ||
463 | editor: editor | ||
464 | }, true ); | ||
465 | } | ||
466 | |||
467 | Locator.prototype = { | ||
468 | /** | ||
469 | * Locates the Y coordinate for all types of every single relation and stores | ||
470 | * them in an object. | ||
471 | * | ||
472 | * @param {Object} relations {@link CKEDITOR.plugins.lineutils.finder#relations}. | ||
473 | * @returns {Object} {@link #locations}. | ||
474 | */ | ||
475 | locate: ( function() { | ||
476 | function locateSibling( rel, type ) { | ||
477 | var sib = rel.element[ type === CKEDITOR.LINEUTILS_BEFORE ? 'getPrevious' : 'getNext' ](); | ||
478 | |||
479 | // Return the middle point between siblings. | ||
480 | if ( sib && isStatic( sib ) ) { | ||
481 | rel.siblingRect = sib.getClientRect(); | ||
482 | |||
483 | if ( type == CKEDITOR.LINEUTILS_BEFORE ) { | ||
484 | return ( rel.siblingRect.bottom + rel.elementRect.top ) / 2; | ||
485 | } else { | ||
486 | return ( rel.elementRect.bottom + rel.siblingRect.top ) / 2; | ||
487 | } | ||
488 | } | ||
489 | |||
490 | // If there's no sibling, use the edge of an element. | ||
491 | else { | ||
492 | if ( type == CKEDITOR.LINEUTILS_BEFORE ) { | ||
493 | return rel.elementRect.top; | ||
494 | } else { | ||
495 | return rel.elementRect.bottom; | ||
496 | } | ||
497 | } | ||
498 | } | ||
499 | |||
500 | return function( relations ) { | ||
501 | var rel; | ||
502 | |||
503 | this.locations = {}; | ||
504 | |||
505 | for ( var uid in relations ) { | ||
506 | rel = relations[ uid ]; | ||
507 | rel.elementRect = rel.element.getClientRect(); | ||
508 | |||
509 | if ( is( rel.type, CKEDITOR.LINEUTILS_BEFORE ) ) { | ||
510 | this.store( uid, CKEDITOR.LINEUTILS_BEFORE, locateSibling( rel, CKEDITOR.LINEUTILS_BEFORE ) ); | ||
511 | } | ||
512 | |||
513 | if ( is( rel.type, CKEDITOR.LINEUTILS_AFTER ) ) { | ||
514 | this.store( uid, CKEDITOR.LINEUTILS_AFTER, locateSibling( rel, CKEDITOR.LINEUTILS_AFTER ) ); | ||
515 | } | ||
516 | |||
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 ); | ||
520 | } | ||
521 | } | ||
522 | |||
523 | return this.locations; | ||
524 | }; | ||
525 | } )(), | ||
526 | |||
527 | /** | ||
528 | * Calculates distances from every location to given vertical coordinate | ||
529 | * and sorts locations according to that distance. | ||
530 | * | ||
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}. | ||
534 | */ | ||
535 | sort: ( function() { | ||
536 | var locations, sorted, | ||
537 | dist, i; | ||
538 | |||
539 | function distance( y, uid, type ) { | ||
540 | return Math.abs( y - locations[ uid ][ type ] ); | ||
541 | } | ||
542 | |||
543 | return function( y, howMany ) { | ||
544 | locations = this.locations; | ||
545 | sorted = []; | ||
546 | |||
547 | for ( var uid in locations ) { | ||
548 | for ( var type in locations[ uid ] ) { | ||
549 | dist = distance( y, uid, type ); | ||
550 | |||
551 | // An array is empty. | ||
552 | if ( !sorted.length ) { | ||
553 | sorted.push( { uid: +uid, type: type, dist: dist } ); | ||
554 | } else { | ||
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 } ); | ||
559 | break; | ||
560 | } | ||
561 | } | ||
562 | |||
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 } ); | ||
567 | } | ||
568 | } | ||
569 | } | ||
570 | } | ||
571 | |||
572 | if ( typeof howMany != 'undefined' ) { | ||
573 | return sorted.slice( 0, howMany ); | ||
574 | } else { | ||
575 | return sorted; | ||
576 | } | ||
577 | }; | ||
578 | } )(), | ||
579 | |||
580 | /** | ||
581 | * Stores the location in a collection. | ||
582 | * | ||
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. | ||
586 | */ | ||
587 | store: function( uid, type, y ) { | ||
588 | if ( !this.locations[ uid ] ) { | ||
589 | this.locations[ uid ] = {}; | ||
590 | } | ||
591 | |||
592 | this.locations[ uid ][ type ] = y; | ||
593 | } | ||
594 | |||
595 | /** | ||
596 | * @readonly | ||
597 | * @property {Object} locations | ||
598 | */ | ||
599 | }; | ||
600 | |||
601 | var tipCss = { | ||
602 | display: 'block', | ||
603 | width: '0px', | ||
604 | height: '0px', | ||
605 | 'border-color': 'transparent', | ||
606 | 'border-style': 'solid', | ||
607 | position: 'absolute', | ||
608 | top: '-6px' | ||
609 | }, | ||
610 | |||
611 | lineStyle = { | ||
612 | height: '0px', | ||
613 | 'border-top': '1px dashed red', | ||
614 | position: 'absolute', | ||
615 | 'z-index': 9999 | ||
616 | }, | ||
617 | |||
618 | lineTpl = | ||
619 | '<div data-cke-lineutils-line="1" class="cke_reset_all" style="{lineStyle}">' + | ||
620 | '<span style="{tipLeftStyle}"> </span>' + | ||
621 | '<span style="{tipRightStyle}"> </span>' + | ||
622 | '</div>'; | ||
623 | |||
624 | /** | ||
625 | * A utility that draws horizontal lines in DOM according to locations | ||
626 | * returned by CKEDITOR.plugins.lineutils.locator. | ||
627 | * | ||
628 | * @private | ||
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. | ||
633 | * @since 4.3 | ||
634 | */ | ||
635 | function Liner( editor, def ) { | ||
636 | var editable = editor.editable(); | ||
637 | |||
638 | CKEDITOR.tools.extend( this, { | ||
639 | editor: editor, | ||
640 | editable: editable, | ||
641 | inline: editable.isInline(), | ||
642 | doc: editor.document, | ||
643 | win: editor.window, | ||
644 | container: CKEDITOR.document.getBody(), | ||
645 | winTop: CKEDITOR.document.getWindow() | ||
646 | }, def, true ); | ||
647 | |||
648 | this.hidden = {}; | ||
649 | this.visible = {}; | ||
650 | |||
651 | if ( !this.inline ) { | ||
652 | this.frame = this.win.getFrame(); | ||
653 | } | ||
654 | |||
655 | this.queryViewport(); | ||
656 | |||
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 ); | ||
664 | |||
665 | editable.attachListener( this.winTop, 'resize', queryViewport ); | ||
666 | editable.attachListener( this.winTop, 'scroll', queryViewport ); | ||
667 | |||
668 | editable.attachListener( this.winTop, 'resize', hideVisible ); | ||
669 | editable.attachListener( this.win, 'scroll', hideVisible ); | ||
670 | |||
671 | editable.attachListener( this.inline ? editable : this.frame, 'mouseout', function( evt ) { | ||
672 | var x = evt.data.$.clientX, | ||
673 | y = evt.data.$.clientY; | ||
674 | |||
675 | this.queryViewport(); | ||
676 | |||
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 ) { | ||
679 | this.hideVisible(); | ||
680 | } | ||
681 | |||
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 ) { | ||
684 | this.hideVisible(); | ||
685 | } | ||
686 | }, this ); | ||
687 | |||
688 | editable.attachListener( editor, 'resize', queryViewport ); | ||
689 | editable.attachListener( editor, 'mode', removeAll ); | ||
690 | editor.on( 'destroy', removeAll ); | ||
691 | |||
692 | this.lineTpl = new CKEDITOR.template( lineTpl ).output( { | ||
693 | lineStyle: CKEDITOR.tools.writeCssText( | ||
694 | CKEDITOR.tools.extend( {}, lineStyle, this.lineStyle, true ) | ||
695 | ), | ||
696 | tipLeftStyle: CKEDITOR.tools.writeCssText( | ||
697 | CKEDITOR.tools.extend( {}, tipCss, { | ||
698 | left: '0px', | ||
699 | 'border-left-color': 'red', | ||
700 | 'border-width': '6px 0 6px 6px' | ||
701 | }, this.tipCss, this.tipLeftStyle, true ) | ||
702 | ), | ||
703 | tipRightStyle: CKEDITOR.tools.writeCssText( | ||
704 | CKEDITOR.tools.extend( {}, tipCss, { | ||
705 | right: '0px', | ||
706 | 'border-right-color': 'red', | ||
707 | 'border-width': '6px 6px 6px 0' | ||
708 | }, this.tipCss, this.tipRightStyle, true ) | ||
709 | ) | ||
710 | } ); | ||
711 | } | ||
712 | |||
713 | Liner.prototype = { | ||
714 | /** | ||
715 | * Permanently removes all lines (both hidden and visible) from DOM. | ||
716 | */ | ||
717 | removeAll: function() { | ||
718 | var l; | ||
719 | |||
720 | for ( l in this.hidden ) { | ||
721 | this.hidden[ l ].remove(); | ||
722 | delete this.hidden[ l ]; | ||
723 | } | ||
724 | |||
725 | for ( l in this.visible ) { | ||
726 | this.visible[ l ].remove(); | ||
727 | delete this.visible[ l ]; | ||
728 | } | ||
729 | }, | ||
730 | |||
731 | /** | ||
732 | * Hides a given line. | ||
733 | * | ||
734 | * @param {CKEDITOR.dom.element} line The line to be hidden. | ||
735 | */ | ||
736 | hideLine: function( line ) { | ||
737 | var uid = line.getUniqueId(); | ||
738 | |||
739 | line.hide(); | ||
740 | |||
741 | this.hidden[ uid ] = line; | ||
742 | delete this.visible[ uid ]; | ||
743 | }, | ||
744 | |||
745 | /** | ||
746 | * Shows a given line. | ||
747 | * | ||
748 | * @param {CKEDITOR.dom.element} line The line to be shown. | ||
749 | */ | ||
750 | showLine: function( line ) { | ||
751 | var uid = line.getUniqueId(); | ||
752 | |||
753 | line.show(); | ||
754 | |||
755 | this.visible[ uid ] = line; | ||
756 | delete this.hidden[ uid ]; | ||
757 | }, | ||
758 | |||
759 | /** | ||
760 | * Hides all visible lines. | ||
761 | */ | ||
762 | hideVisible: function() { | ||
763 | for ( var l in this.visible ) { | ||
764 | this.hideLine( this.visible[ l ] ); | ||
765 | } | ||
766 | }, | ||
767 | |||
768 | /** | ||
769 | * Shows a line at given location. | ||
770 | * | ||
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. | ||
774 | */ | ||
775 | placeLine: function( location, callback ) { | ||
776 | var styles, line, l; | ||
777 | |||
778 | // No style means that line would be out of viewport. | ||
779 | if ( !( styles = this.getStyle( location.uid, location.type ) ) ) { | ||
780 | return; | ||
781 | } | ||
782 | |||
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 ]; | ||
788 | break; | ||
789 | } | ||
790 | } | ||
791 | |||
792 | // Search for any hidden line of a different hash. | ||
793 | if ( !line ) { | ||
794 | for ( l in this.hidden ) { | ||
795 | if ( this.hidden[ l ].getCustomData( 'hash' ) !== this.hash ) { | ||
796 | this.showLine( ( line = this.hidden[ l ] ) ); | ||
797 | break; | ||
798 | } | ||
799 | } | ||
800 | } | ||
801 | |||
802 | // If no line available, add the new one. | ||
803 | if ( !line ) { | ||
804 | this.showLine( ( line = this.addLine() ) ); | ||
805 | } | ||
806 | |||
807 | // Mark the line with current hash. | ||
808 | line.setCustomData( 'hash', this.hash ); | ||
809 | |||
810 | // Mark the line as visible. | ||
811 | this.visible[ line.getUniqueId() ] = line; | ||
812 | |||
813 | line.setStyles( styles ); | ||
814 | |||
815 | callback && callback( line ); | ||
816 | }, | ||
817 | |||
818 | /** | ||
819 | * Creates a style set to be used by the line, representing a particular | ||
820 | * relation (location). | ||
821 | * | ||
822 | * @param {Number} uid Unique identifier of the relation. | ||
823 | * @param {Number} type Type of the relation. | ||
824 | * @returns {Object} An object containing styles. | ||
825 | */ | ||
826 | getStyle: function( uid, type ) { | ||
827 | var rel = this.relations[ uid ], | ||
828 | loc = this.locations[ uid ][ type ], | ||
829 | styles = {}, | ||
830 | hdiff; | ||
831 | |||
832 | // Line should be between two elements. | ||
833 | if ( rel.siblingRect ) { | ||
834 | styles.width = Math.max( rel.siblingRect.width, rel.elementRect.width ); | ||
835 | } | ||
836 | // Line is relative to a single element. | ||
837 | else { | ||
838 | styles.width = rel.elementRect.width; | ||
839 | } | ||
840 | |||
841 | // Let's calculate the vertical position of the line. | ||
842 | if ( this.inline ) { | ||
843 | // (#13155) | ||
844 | styles.top = loc + this.winTopScroll.y - this.rect.relativeY; | ||
845 | } else { | ||
846 | styles.top = this.rect.top + this.winTopScroll.y + loc; | ||
847 | } | ||
848 | |||
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 ) { | ||
851 | return false; | ||
852 | } | ||
853 | |||
854 | // Now let's calculate the horizontal alignment (left and width). | ||
855 | if ( this.inline ) { | ||
856 | // (#13155) | ||
857 | styles.left = rel.elementRect.left - this.rect.relativeX; | ||
858 | } else { | ||
859 | if ( rel.elementRect.left > 0 ) | ||
860 | styles.left = this.rect.left + rel.elementRect.left; | ||
861 | |||
862 | // H-scroll case. Left edge of element may be out of viewport. | ||
863 | else { | ||
864 | styles.width += rel.elementRect.left; | ||
865 | styles.left = this.rect.left; | ||
866 | } | ||
867 | |||
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; | ||
871 | } | ||
872 | } | ||
873 | |||
874 | // Finally include horizontal scroll of the global window. | ||
875 | styles.left += this.winTopScroll.x; | ||
876 | |||
877 | // Append 'px' to style values. | ||
878 | for ( var style in styles ) { | ||
879 | styles[ style ] = CKEDITOR.tools.cssLength( styles[ style ] ); | ||
880 | } | ||
881 | |||
882 | return styles; | ||
883 | }, | ||
884 | |||
885 | /** | ||
886 | * Adds a new line to DOM. | ||
887 | * | ||
888 | * @returns {CKEDITOR.dom.element} A brand-new line. | ||
889 | */ | ||
890 | addLine: function() { | ||
891 | var line = CKEDITOR.dom.element.createFromHtml( this.lineTpl ); | ||
892 | |||
893 | line.appendTo( this.container ); | ||
894 | |||
895 | return line; | ||
896 | }, | ||
897 | |||
898 | /** | ||
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. | ||
905 | * | ||
906 | * @param {Object} relations {@link CKEDITOR.plugins.lineutils.finder#relations}. | ||
907 | * @param {Object} locations {@link CKEDITOR.plugins.lineutils.locator#locations}. | ||
908 | */ | ||
909 | prepare: function( relations, locations ) { | ||
910 | this.relations = relations; | ||
911 | this.locations = locations; | ||
912 | this.hash = Math.random(); | ||
913 | }, | ||
914 | |||
915 | /** | ||
916 | * Hides all visible lines that do not belong to current hash | ||
917 | * and no longer represent relations (locations). | ||
918 | * | ||
919 | * See also: {@link #prepare}. | ||
920 | */ | ||
921 | cleanup: function() { | ||
922 | var line; | ||
923 | |||
924 | for ( var l in this.visible ) { | ||
925 | line = this.visible[ l ]; | ||
926 | |||
927 | if ( line.getCustomData( 'hash' ) !== this.hash ) { | ||
928 | this.hideLine( line ); | ||
929 | } | ||
930 | } | ||
931 | }, | ||
932 | |||
933 | /** | ||
934 | * Queries dimensions of the viewport, editable, frame etc. | ||
935 | * that are used for correct positioning of the line. | ||
936 | */ | ||
937 | queryViewport: function() { | ||
938 | this.winPane = this.win.getViewPaneSize(); | ||
939 | this.winTopScroll = this.winTop.getScrollPosition(); | ||
940 | this.winTopPane = this.winTop.getViewPaneSize(); | ||
941 | |||
942 | // (#13155) | ||
943 | this.rect = this.getClientRect( this.inline ? this.editable : this.frame ); | ||
944 | }, | ||
945 | |||
946 | /** | ||
947 | * Returns `boundingClientRect` of an element, shifted by the position | ||
948 | * of `container` when the container is not `static` (#13155). | ||
949 | * | ||
950 | * See also: {@link CKEDITOR.dom.element#getClientRect}. | ||
951 | * | ||
952 | * @param {CKEDITOR.dom.element} el A DOM element. | ||
953 | * @returns {Object} A shifted rect, extended by `relativeY` and `relativeX` properties. | ||
954 | */ | ||
955 | getClientRect: function( el ) { | ||
956 | var rect = el.getClientRect(), | ||
957 | relativeContainerDocPosition = this.container.getDocumentPosition(), | ||
958 | relativeContainerComputedPosition = this.container.getComputedStyle( 'position' ); | ||
959 | |||
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; | ||
962 | |||
963 | if ( relativeContainerComputedPosition != 'static' ) { | ||
964 | // Remember the offset used to shift the clientRect. | ||
965 | rect.relativeY = relativeContainerDocPosition.y; | ||
966 | rect.relativeX = relativeContainerDocPosition.x; | ||
967 | |||
968 | rect.top -= rect.relativeY; | ||
969 | rect.bottom -= rect.relativeY; | ||
970 | rect.left -= rect.relativeX; | ||
971 | rect.right -= rect.relativeX; | ||
972 | } | ||
973 | |||
974 | return rect; | ||
975 | } | ||
976 | }; | ||
977 | |||
978 | function is( type, flag ) { | ||
979 | return type & flag; | ||
980 | } | ||
981 | |||
982 | var floats = { left: 1, right: 1, center: 1 }, | ||
983 | positions = { absolute: 1, fixed: 1 }; | ||
984 | |||
985 | function isElement( node ) { | ||
986 | return node && node.type == CKEDITOR.NODE_ELEMENT; | ||
987 | } | ||
988 | |||
989 | function isFloated( el ) { | ||
990 | return !!( floats[ el.getComputedStyle( 'float' ) ] || floats[ el.getAttribute( 'align' ) ] ); | ||
991 | } | ||
992 | |||
993 | function isPositioned( el ) { | ||
994 | return !!positions[ el.getComputedStyle( 'position' ) ]; | ||
995 | } | ||
996 | |||
997 | function isLimit( node ) { | ||
998 | return isElement( node ) && node.getAttribute( 'contenteditable' ) == 'true'; | ||
999 | } | ||
1000 | |||
1001 | function isStatic( node ) { | ||
1002 | return isElement( node ) && !isFloated( node ) && !isPositioned( node ); | ||
1003 | } | ||
1004 | |||
1005 | /** | ||
1006 | * Global namespace storing definitions and global helpers for the Line Utilities plugin. | ||
1007 | * | ||
1008 | * @private | ||
1009 | * @class | ||
1010 | * @singleton | ||
1011 | * @since 4.3 | ||
1012 | */ | ||
1013 | CKEDITOR.plugins.lineutils = { | ||
1014 | finder: Finder, | ||
1015 | locator: Locator, | ||
1016 | liner: Liner | ||
1017 | }; | ||
1018 | } )(); | ||