]>
Commit | Line | Data |
---|---|---|
1 | /** | |
2 | * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | |
4 | */ | |
5 | ||
6 | /** | |
7 | * @fileOverview Defines the {@link CKEDITOR.dom.node} class which is the base | |
8 | * class for classes that represent DOM nodes. | |
9 | */ | |
10 | ||
11 | /** | |
12 | * Base class for classes representing DOM nodes. This constructor may return | |
13 | * an instance of a class that inherits from this class, like | |
14 | * {@link CKEDITOR.dom.element} or {@link CKEDITOR.dom.text}. | |
15 | * | |
16 | * @class | |
17 | * @extends CKEDITOR.dom.domObject | |
18 | * @constructor Creates a node class instance. | |
19 | * @param {Object} domNode A native DOM node. | |
20 | * @see CKEDITOR.dom.element | |
21 | * @see CKEDITOR.dom.text | |
22 | */ | |
23 | CKEDITOR.dom.node = function( domNode ) { | |
24 | if ( domNode ) { | |
25 | var type = | |
26 | domNode.nodeType == CKEDITOR.NODE_DOCUMENT ? 'document' : | |
27 | domNode.nodeType == CKEDITOR.NODE_ELEMENT ? 'element' : | |
28 | domNode.nodeType == CKEDITOR.NODE_TEXT ? 'text' : | |
29 | domNode.nodeType == CKEDITOR.NODE_COMMENT ? 'comment' : | |
30 | domNode.nodeType == CKEDITOR.NODE_DOCUMENT_FRAGMENT ? 'documentFragment' : | |
31 | 'domObject'; // Call the base constructor otherwise. | |
32 | ||
33 | return new CKEDITOR.dom[ type ]( domNode ); | |
34 | } | |
35 | ||
36 | return this; | |
37 | }; | |
38 | ||
39 | CKEDITOR.dom.node.prototype = new CKEDITOR.dom.domObject(); | |
40 | ||
41 | /** | |
42 | * Element node type. | |
43 | * | |
44 | * @readonly | |
45 | * @property {Number} [=1] | |
46 | * @member CKEDITOR | |
47 | */ | |
48 | CKEDITOR.NODE_ELEMENT = 1; | |
49 | ||
50 | /** | |
51 | * Document node type. | |
52 | * | |
53 | * @readonly | |
54 | * @property {Number} [=9] | |
55 | * @member CKEDITOR | |
56 | */ | |
57 | CKEDITOR.NODE_DOCUMENT = 9; | |
58 | ||
59 | /** | |
60 | * Text node type. | |
61 | * | |
62 | * @readonly | |
63 | * @property {Number} [=3] | |
64 | * @member CKEDITOR | |
65 | */ | |
66 | CKEDITOR.NODE_TEXT = 3; | |
67 | ||
68 | /** | |
69 | * Comment node type. | |
70 | * | |
71 | * @readonly | |
72 | * @property {Number} [=8] | |
73 | * @member CKEDITOR | |
74 | */ | |
75 | CKEDITOR.NODE_COMMENT = 8; | |
76 | ||
77 | /** | |
78 | * Document fragment node type. | |
79 | * | |
80 | * @readonly | |
81 | * @property {Number} [=11] | |
82 | * @member CKEDITOR | |
83 | */ | |
84 | CKEDITOR.NODE_DOCUMENT_FRAGMENT = 11; | |
85 | ||
86 | /** | |
87 | * Indicates that positions of both nodes are identical (this is the same node). See {@link CKEDITOR.dom.node#getPosition}. | |
88 | * | |
89 | * @readonly | |
90 | * @property {Number} [=0] | |
91 | * @member CKEDITOR | |
92 | */ | |
93 | CKEDITOR.POSITION_IDENTICAL = 0; | |
94 | ||
95 | /** | |
96 | * Indicates that nodes are in different (detached) trees. See {@link CKEDITOR.dom.node#getPosition}. | |
97 | * | |
98 | * @readonly | |
99 | * @property {Number} [=1] | |
100 | * @member CKEDITOR | |
101 | */ | |
102 | CKEDITOR.POSITION_DISCONNECTED = 1; | |
103 | ||
104 | /** | |
105 | * Indicates that the context node follows the other node. See {@link CKEDITOR.dom.node#getPosition}. | |
106 | * | |
107 | * @readonly | |
108 | * @property {Number} [=2] | |
109 | * @member CKEDITOR | |
110 | */ | |
111 | CKEDITOR.POSITION_FOLLOWING = 2; | |
112 | ||
113 | /** | |
114 | * Indicates that the context node precedes the other node. See {@link CKEDITOR.dom.node#getPosition}. | |
115 | * | |
116 | * @readonly | |
117 | * @property {Number} [=4] | |
118 | * @member CKEDITOR | |
119 | */ | |
120 | CKEDITOR.POSITION_PRECEDING = 4; | |
121 | ||
122 | /** | |
123 | * Indicates that the context node is a descendant of the other node. See {@link CKEDITOR.dom.node#getPosition}. | |
124 | * | |
125 | * @readonly | |
126 | * @property {Number} [=8] | |
127 | * @member CKEDITOR | |
128 | */ | |
129 | CKEDITOR.POSITION_IS_CONTAINED = 8; | |
130 | ||
131 | /** | |
132 | * Indicates that the context node contains the other node. See {@link CKEDITOR.dom.node#getPosition}. | |
133 | * | |
134 | * @readonly | |
135 | * @property {Number} [=16] | |
136 | * @member CKEDITOR | |
137 | */ | |
138 | CKEDITOR.POSITION_CONTAINS = 16; | |
139 | ||
140 | CKEDITOR.tools.extend( CKEDITOR.dom.node.prototype, { | |
141 | /** | |
142 | * Makes this node a child of another element. | |
143 | * | |
144 | * var p = new CKEDITOR.dom.element( 'p' ); | |
145 | * var strong = new CKEDITOR.dom.element( 'strong' ); | |
146 | * strong.appendTo( p ); | |
147 | * | |
148 | * // Result: '<p><strong></strong></p>'. | |
149 | * | |
150 | * @param {CKEDITOR.dom.element} element The target element to which this node will be appended. | |
151 | * @returns {CKEDITOR.dom.element} The target element. | |
152 | */ | |
153 | appendTo: function( element, toStart ) { | |
154 | element.append( this, toStart ); | |
155 | return element; | |
156 | }, | |
157 | ||
158 | /** | |
159 | * Clones this node. | |
160 | * | |
161 | * **Note**: Values set by {#setCustomData} will not be available in the clone. | |
162 | * | |
163 | * @param {Boolean} [includeChildren=false] If `true` then all node's | |
164 | * children will be cloned recursively. | |
165 | * @param {Boolean} [cloneId=false] Whether ID attributes should be cloned, too. | |
166 | * @returns {CKEDITOR.dom.node} Clone of this node. | |
167 | */ | |
168 | clone: function( includeChildren, cloneId ) { | |
169 | var $clone = this.$.cloneNode( includeChildren ); | |
170 | ||
171 | // The "id" attribute should never be cloned to avoid duplication. | |
172 | removeIds( $clone ); | |
173 | ||
174 | var node = new CKEDITOR.dom.node( $clone ); | |
175 | ||
176 | // On IE8 we need to fixed HTML5 node name, see details below. | |
177 | if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 && | |
178 | ( this.type == CKEDITOR.NODE_ELEMENT || this.type == CKEDITOR.NODE_DOCUMENT_FRAGMENT ) ) { | |
179 | renameNodes( node ); | |
180 | } | |
181 | ||
182 | return node; | |
183 | ||
184 | function removeIds( node ) { | |
185 | // Reset data-cke-expando only when has been cloned (IE and only for some types of objects). | |
186 | if ( node[ 'data-cke-expando' ] ) | |
187 | node[ 'data-cke-expando' ] = false; | |
188 | ||
189 | if ( node.nodeType != CKEDITOR.NODE_ELEMENT && node.nodeType != CKEDITOR.NODE_DOCUMENT_FRAGMENT ) | |
190 | return; | |
191 | ||
192 | if ( !cloneId && node.nodeType == CKEDITOR.NODE_ELEMENT ) | |
193 | node.removeAttribute( 'id', false ); | |
194 | ||
195 | if ( includeChildren ) { | |
196 | var childs = node.childNodes; | |
197 | for ( var i = 0; i < childs.length; i++ ) | |
198 | removeIds( childs[ i ] ); | |
199 | } | |
200 | } | |
201 | ||
202 | // IE8 rename HTML5 nodes by adding `:` at the begging of the tag name when the node is cloned, | |
203 | // so `<figure>` will be `<:figure>` after 'cloneNode'. We need to fix it (http://dev.ckeditor.com/ticket/13101). | |
204 | function renameNodes( node ) { | |
205 | if ( node.type != CKEDITOR.NODE_ELEMENT && node.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT ) | |
206 | return; | |
207 | ||
208 | if ( node.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT ) { | |
209 | var name = node.getName(); | |
210 | if ( name[ 0 ] == ':' ) { | |
211 | node.renameNode( name.substring( 1 ) ); | |
212 | } | |
213 | } | |
214 | ||
215 | if ( includeChildren ) { | |
216 | for ( var i = 0; i < node.getChildCount(); i++ ) | |
217 | renameNodes( node.getChild( i ) ); | |
218 | } | |
219 | } | |
220 | }, | |
221 | ||
222 | /** | |
223 | * Checks if the node is preceded by any sibling. | |
224 | * | |
225 | * @returns {Boolean} | |
226 | */ | |
227 | hasPrevious: function() { | |
228 | return !!this.$.previousSibling; | |
229 | }, | |
230 | ||
231 | /** | |
232 | * Checks if the node is succeeded by any sibling. | |
233 | * | |
234 | * @returns {Boolean} | |
235 | */ | |
236 | hasNext: function() { | |
237 | return !!this.$.nextSibling; | |
238 | }, | |
239 | ||
240 | /** | |
241 | * Inserts this element after a node. | |
242 | * | |
243 | * var em = new CKEDITOR.dom.element( 'em' ); | |
244 | * var strong = new CKEDITOR.dom.element( 'strong' ); | |
245 | * strong.insertAfter( em ); | |
246 | * | |
247 | * // Result: '<em></em><strong></strong>' | |
248 | * | |
249 | * @param {CKEDITOR.dom.node} node The node that will precede this element. | |
250 | * @returns {CKEDITOR.dom.node} The node preceding this one after insertion. | |
251 | */ | |
252 | insertAfter: function( node ) { | |
253 | node.$.parentNode.insertBefore( this.$, node.$.nextSibling ); | |
254 | return node; | |
255 | }, | |
256 | ||
257 | /** | |
258 | * Inserts this element before a node. | |
259 | * | |
260 | * var em = new CKEDITOR.dom.element( 'em' ); | |
261 | * var strong = new CKEDITOR.dom.element( 'strong' ); | |
262 | * strong.insertBefore( em ); | |
263 | * | |
264 | * // result: '<strong></strong><em></em>' | |
265 | * | |
266 | * @param {CKEDITOR.dom.node} node The node that will succeed this element. | |
267 | * @returns {CKEDITOR.dom.node} The node being inserted. | |
268 | */ | |
269 | insertBefore: function( node ) { | |
270 | node.$.parentNode.insertBefore( this.$, node.$ ); | |
271 | return node; | |
272 | }, | |
273 | ||
274 | /** | |
275 | * Inserts a node before this node. | |
276 | * | |
277 | * var em = new CKEDITOR.dom.element( 'em' ); | |
278 | * var strong = new CKEDITOR.dom.element( 'strong' ); | |
279 | * strong.insertBeforeMe( em ); | |
280 | * | |
281 | * // result: '<em></em><strong></strong>' | |
282 | * | |
283 | * @param {CKEDITOR.dom.node} node The node that will preceed this element. | |
284 | * @returns {CKEDITOR.dom.node} The node being inserted. | |
285 | */ | |
286 | insertBeforeMe: function( node ) { | |
287 | this.$.parentNode.insertBefore( node.$, this.$ ); | |
288 | return node; | |
289 | }, | |
290 | ||
291 | /** | |
292 | * Retrieves a uniquely identifiable tree address for this node. | |
293 | * The tree address returned is an array of integers, with each integer | |
294 | * indicating a child index of a DOM node, starting from | |
295 | * `document.documentElement`. | |
296 | * | |
297 | * For example, assuming `<body>` is the second child | |
298 | * of `<html>` (`<head>` being the first), | |
299 | * and we would like to address the third child under the | |
300 | * fourth child of `<body>`, the tree address returned would be: | |
301 | * `[1, 3, 2]`. | |
302 | * | |
303 | * The tree address cannot be used for finding back the DOM tree node once | |
304 | * the DOM tree structure has been modified. | |
305 | * | |
306 | * @param {Boolean} [normalized=false] See {@link #getIndex}. | |
307 | * @returns {Array} The address. | |
308 | */ | |
309 | getAddress: function( normalized ) { | |
310 | var address = []; | |
311 | var $documentElement = this.getDocument().$.documentElement; | |
312 | var node = this.$; | |
313 | ||
314 | while ( node && node != $documentElement ) { | |
315 | var parentNode = node.parentNode; | |
316 | ||
317 | if ( parentNode ) { | |
318 | // Get the node index. For performance, call getIndex | |
319 | // directly, instead of creating a new node object. | |
320 | address.unshift( this.getIndex.call( { $: node }, normalized ) ); | |
321 | } | |
322 | ||
323 | node = parentNode; | |
324 | } | |
325 | ||
326 | return address; | |
327 | }, | |
328 | ||
329 | /** | |
330 | * Gets the document containing this element. | |
331 | * | |
332 | * var element = CKEDITOR.document.getById( 'example' ); | |
333 | * alert( element.getDocument().equals( CKEDITOR.document ) ); // true | |
334 | * | |
335 | * @returns {CKEDITOR.dom.document} The document. | |
336 | */ | |
337 | getDocument: function() { | |
338 | return new CKEDITOR.dom.document( this.$.ownerDocument || this.$.parentNode.ownerDocument ); | |
339 | }, | |
340 | ||
341 | /** | |
342 | * Gets the index of a node in an array of its `parent.childNodes`. | |
343 | * Returns `-1` if a node does not have a parent or when the `normalized` argument is set to `true` | |
344 | * and the text node is empty and will be removed during the normalization. | |
345 | * | |
346 | * Let us assume having the following `childNodes` array: | |
347 | * | |
348 | * [ emptyText, element1, text, text, element2, emptyText2 ] | |
349 | * | |
350 | * emptyText.getIndex() // 0 | |
351 | * emptyText.getIndex( true ) // -1 | |
352 | * element1.getIndex(); // 1 | |
353 | * element1.getIndex( true ); // 0 | |
354 | * element2.getIndex(); // 4 | |
355 | * element2.getIndex( true ); // 2 | |
356 | * emptyText2.getIndex(); // 5 | |
357 | * emptyText2.getIndex( true ); // -1 | |
358 | * | |
359 | * @param {Boolean} normalized When `true`, adjacent text nodes are merged and empty text nodes are removed. | |
360 | * @returns {Number} Index of a node or `-1` if a node does not have a parent or is removed during the normalization. | |
361 | */ | |
362 | getIndex: function( normalized ) { | |
363 | // Attention: getAddress depends on this.$ | |
364 | // getIndex is called on a plain object: { $ : node } | |
365 | ||
366 | var current = this.$, | |
367 | index = -1, | |
368 | isNormalizing; | |
369 | ||
370 | if ( !this.$.parentNode ) | |
371 | return -1; | |
372 | ||
373 | // The idea is - all empty text nodes will be virtually merged into their adjacent text nodes. | |
374 | // If an empty text node does not have an adjacent non-empty text node we can return -1 straight away, | |
375 | // because it and all its sibling text nodes will be merged into an empty text node and then totally ignored. | |
376 | if ( normalized && current.nodeType == CKEDITOR.NODE_TEXT && isEmpty( current ) ) { | |
377 | var adjacent = getAdjacentNonEmptyTextNode( current ) || getAdjacentNonEmptyTextNode( current, true ); | |
378 | ||
379 | if ( !adjacent ) | |
380 | return -1; | |
381 | } | |
382 | ||
383 | do { | |
384 | // Bypass blank node and adjacent text nodes. | |
385 | if ( normalized && current != this.$ && current.nodeType == CKEDITOR.NODE_TEXT && ( isNormalizing || isEmpty( current ) ) ) | |
386 | continue; | |
387 | ||
388 | index++; | |
389 | isNormalizing = current.nodeType == CKEDITOR.NODE_TEXT; | |
390 | } | |
391 | while ( ( current = current.previousSibling ) ); | |
392 | ||
393 | return index; | |
394 | ||
395 | function getAdjacentNonEmptyTextNode( node, lookForward ) { | |
396 | var sibling = lookForward ? node.nextSibling : node.previousSibling; | |
397 | ||
398 | if ( !sibling || sibling.nodeType != CKEDITOR.NODE_TEXT ) { | |
399 | return null; | |
400 | } | |
401 | ||
402 | // If found a non-empty text node, then return it. | |
403 | // If not, then continue search. | |
404 | return isEmpty( sibling ) ? getAdjacentNonEmptyTextNode( sibling, lookForward ) : sibling; | |
405 | } | |
406 | ||
407 | // Checks whether a text node is empty or is FCSeq string (which will be totally removed when normalizing). | |
408 | function isEmpty( textNode ) { | |
409 | return !textNode.nodeValue || textNode.nodeValue == CKEDITOR.dom.selection.FILLING_CHAR_SEQUENCE; | |
410 | } | |
411 | }, | |
412 | ||
413 | /** | |
414 | * @todo | |
415 | */ | |
416 | getNextSourceNode: function( startFromSibling, nodeType, guard ) { | |
417 | // If "guard" is a node, transform it in a function. | |
418 | if ( guard && !guard.call ) { | |
419 | var guardNode = guard; | |
420 | guard = function( node ) { | |
421 | return !node.equals( guardNode ); | |
422 | }; | |
423 | } | |
424 | ||
425 | var node = ( !startFromSibling && this.getFirst && this.getFirst() ), | |
426 | parent; | |
427 | ||
428 | // Guarding when we're skipping the current element( no children or 'startFromSibling' ). | |
429 | // send the 'moving out' signal even we don't actually dive into. | |
430 | if ( !node ) { | |
431 | if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false ) | |
432 | return null; | |
433 | node = this.getNext(); | |
434 | } | |
435 | ||
436 | while ( !node && ( parent = ( parent || this ).getParent() ) ) { | |
437 | // The guard check sends the "true" paramenter to indicate that | |
438 | // we are moving "out" of the element. | |
439 | if ( guard && guard( parent, true ) === false ) | |
440 | return null; | |
441 | ||
442 | node = parent.getNext(); | |
443 | } | |
444 | ||
445 | if ( !node ) | |
446 | return null; | |
447 | ||
448 | if ( guard && guard( node ) === false ) | |
449 | return null; | |
450 | ||
451 | if ( nodeType && nodeType != node.type ) | |
452 | return node.getNextSourceNode( false, nodeType, guard ); | |
453 | ||
454 | return node; | |
455 | }, | |
456 | ||
457 | /** | |
458 | * @todo | |
459 | */ | |
460 | getPreviousSourceNode: function( startFromSibling, nodeType, guard ) { | |
461 | if ( guard && !guard.call ) { | |
462 | var guardNode = guard; | |
463 | guard = function( node ) { | |
464 | return !node.equals( guardNode ); | |
465 | }; | |
466 | } | |
467 | ||
468 | var node = ( !startFromSibling && this.getLast && this.getLast() ), | |
469 | parent; | |
470 | ||
471 | // Guarding when we're skipping the current element( no children or 'startFromSibling' ). | |
472 | // send the 'moving out' signal even we don't actually dive into. | |
473 | if ( !node ) { | |
474 | if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false ) | |
475 | return null; | |
476 | node = this.getPrevious(); | |
477 | } | |
478 | ||
479 | while ( !node && ( parent = ( parent || this ).getParent() ) ) { | |
480 | // The guard check sends the "true" paramenter to indicate that | |
481 | // we are moving "out" of the element. | |
482 | if ( guard && guard( parent, true ) === false ) | |
483 | return null; | |
484 | ||
485 | node = parent.getPrevious(); | |
486 | } | |
487 | ||
488 | if ( !node ) | |
489 | return null; | |
490 | ||
491 | if ( guard && guard( node ) === false ) | |
492 | return null; | |
493 | ||
494 | if ( nodeType && node.type != nodeType ) | |
495 | return node.getPreviousSourceNode( false, nodeType, guard ); | |
496 | ||
497 | return node; | |
498 | }, | |
499 | ||
500 | /** | |
501 | * Gets the node that preceeds this element in its parent's child list. | |
502 | * | |
503 | * var element = CKEDITOR.dom.element.createFromHtml( '<div><i>prev</i><b>Example</b></div>' ); | |
504 | * var first = element.getLast().getPrev(); | |
505 | * alert( first.getName() ); // 'i' | |
506 | * | |
507 | * @param {Function} [evaluator] Filtering the result node. | |
508 | * @returns {CKEDITOR.dom.node} The previous node or null if not available. | |
509 | */ | |
510 | getPrevious: function( evaluator ) { | |
511 | var previous = this.$, | |
512 | retval; | |
513 | do { | |
514 | previous = previous.previousSibling; | |
515 | ||
516 | // Avoid returning the doc type node. | |
517 | // http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-412266927 | |
518 | retval = previous && previous.nodeType != 10 && new CKEDITOR.dom.node( previous ); | |
519 | } | |
520 | while ( retval && evaluator && !evaluator( retval ) ); | |
521 | return retval; | |
522 | }, | |
523 | ||
524 | /** | |
525 | * Gets the node that follows this element in its parent's child list. | |
526 | * | |
527 | * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b><i>next</i></div>' ); | |
528 | * var last = element.getFirst().getNext(); | |
529 | * alert( last.getName() ); // 'i' | |
530 | * | |
531 | * @param {Function} [evaluator] Filtering the result node. | |
532 | * @returns {CKEDITOR.dom.node} The next node or null if not available. | |
533 | */ | |
534 | getNext: function( evaluator ) { | |
535 | var next = this.$, | |
536 | retval; | |
537 | do { | |
538 | next = next.nextSibling; | |
539 | retval = next && new CKEDITOR.dom.node( next ); | |
540 | } | |
541 | while ( retval && evaluator && !evaluator( retval ) ); | |
542 | return retval; | |
543 | }, | |
544 | ||
545 | /** | |
546 | * Gets the parent element for this node. | |
547 | * | |
548 | * var node = editor.document.getBody().getFirst(); | |
549 | * var parent = node.getParent(); | |
550 | * alert( parent.getName() ); // 'body' | |
551 | * | |
552 | * @param {Boolean} [allowFragmentParent=false] Consider also parent node that is of | |
553 | * fragment type {@link CKEDITOR#NODE_DOCUMENT_FRAGMENT}. | |
554 | * @returns {CKEDITOR.dom.element} The parent element. | |
555 | */ | |
556 | getParent: function( allowFragmentParent ) { | |
557 | var parent = this.$.parentNode; | |
558 | return ( parent && ( parent.nodeType == CKEDITOR.NODE_ELEMENT || allowFragmentParent && parent.nodeType == CKEDITOR.NODE_DOCUMENT_FRAGMENT ) ) ? new CKEDITOR.dom.node( parent ) : null; | |
559 | }, | |
560 | ||
561 | /** | |
562 | * Returns an array containing node parents and the node itself. By default nodes are in _descending_ order. | |
563 | * | |
564 | * // Assuming that body has paragraph as the first child. | |
565 | * var node = editor.document.getBody().getFirst(); | |
566 | * var parents = node.getParents(); | |
567 | * alert( parents[ 0 ].getName() + ',' + parents[ 2 ].getName() ); // 'html,p' | |
568 | * | |
569 | * @param {Boolean} [closerFirst=false] Determines the order of returned nodes. | |
570 | * @returns {Array} Returns an array of {@link CKEDITOR.dom.node}. | |
571 | */ | |
572 | getParents: function( closerFirst ) { | |
573 | var node = this; | |
574 | var parents = []; | |
575 | ||
576 | do { | |
577 | parents[ closerFirst ? 'push' : 'unshift' ]( node ); | |
578 | } | |
579 | while ( ( node = node.getParent() ) ); | |
580 | ||
581 | return parents; | |
582 | }, | |
583 | ||
584 | /** | |
585 | * @todo | |
586 | */ | |
587 | getCommonAncestor: function( node ) { | |
588 | if ( node.equals( this ) ) | |
589 | return this; | |
590 | ||
591 | if ( node.contains && node.contains( this ) ) | |
592 | return node; | |
593 | ||
594 | var start = this.contains ? this : this.getParent(); | |
595 | ||
596 | do { | |
597 | if ( start.contains( node ) ) return start; | |
598 | } | |
599 | while ( ( start = start.getParent() ) ); | |
600 | ||
601 | return null; | |
602 | }, | |
603 | ||
604 | /** | |
605 | * Determines the position relation between this node and the given {@link CKEDITOR.dom.node} in the document. | |
606 | * This node can be preceding ({@link CKEDITOR#POSITION_PRECEDING}) or following ({@link CKEDITOR#POSITION_FOLLOWING}) | |
607 | * the given node. This node can also contain ({@link CKEDITOR#POSITION_CONTAINS}) or be contained by | |
608 | * ({@link CKEDITOR#POSITION_IS_CONTAINED}) the given node. The function returns a bitmask of constants | |
609 | * listed above or {@link CKEDITOR#POSITION_IDENTICAL} if the given node is the same as this node. | |
610 | * | |
611 | * @param {CKEDITOR.dom.node} otherNode A node to check relation with. | |
612 | * @returns {Number} Position relation between this node and given node. | |
613 | */ | |
614 | getPosition: function( otherNode ) { | |
615 | var $ = this.$; | |
616 | var $other = otherNode.$; | |
617 | ||
618 | if ( $.compareDocumentPosition ) | |
619 | return $.compareDocumentPosition( $other ); | |
620 | ||
621 | // IE and Safari have no support for compareDocumentPosition. | |
622 | ||
623 | if ( $ == $other ) | |
624 | return CKEDITOR.POSITION_IDENTICAL; | |
625 | ||
626 | // Only element nodes support contains and sourceIndex. | |
627 | if ( this.type == CKEDITOR.NODE_ELEMENT && otherNode.type == CKEDITOR.NODE_ELEMENT ) { | |
628 | if ( $.contains ) { | |
629 | if ( $.contains( $other ) ) | |
630 | return CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING; | |
631 | ||
632 | if ( $other.contains( $ ) ) | |
633 | return CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING; | |
634 | } | |
635 | ||
636 | if ( 'sourceIndex' in $ ) | |
637 | return ( $.sourceIndex < 0 || $other.sourceIndex < 0 ) ? CKEDITOR.POSITION_DISCONNECTED : ( $.sourceIndex < $other.sourceIndex ) ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING; | |
638 | ||
639 | } | |
640 | ||
641 | // For nodes that don't support compareDocumentPosition, contains | |
642 | // or sourceIndex, their "address" is compared. | |
643 | ||
644 | var addressOfThis = this.getAddress(), | |
645 | addressOfOther = otherNode.getAddress(), | |
646 | minLevel = Math.min( addressOfThis.length, addressOfOther.length ); | |
647 | ||
648 | // Determinate preceding/following relationship. | |
649 | for ( var i = 0; i < minLevel; i++ ) { | |
650 | if ( addressOfThis[ i ] != addressOfOther[ i ] ) { | |
651 | return addressOfThis[ i ] < addressOfOther[ i ] ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING; | |
652 | } | |
653 | } | |
654 | ||
655 | // Determinate contains/contained relationship. | |
656 | return ( addressOfThis.length < addressOfOther.length ) ? CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING; | |
657 | }, | |
658 | ||
659 | /** | |
660 | * Gets the closest ancestor node of this node, specified by its name or using an evaluator function. | |
661 | * | |
662 | * // Suppose we have the following HTML structure: | |
663 | * // <div id="outer"><div id="inner"><p><b>Some text</b></p></div></div> | |
664 | * // If node == <b> | |
665 | * ascendant = node.getAscendant( 'div' ); // ascendant == <div id="inner"> | |
666 | * ascendant = node.getAscendant( 'b' ); // ascendant == null | |
667 | * ascendant = node.getAscendant( 'b', true ); // ascendant == <b> | |
668 | * ascendant = node.getAscendant( { div:1, p:1 } ); // Searches for the first 'div' or 'p': ascendant == <div id="inner"> | |
669 | * | |
670 | * // Using custom evaluator: | |
671 | * ascendant = node.getAscendant( function( el ) { | |
672 | * return el.getId() == 'inner'; | |
673 | * } ); | |
674 | * // ascendant == <div id="inner"> | |
675 | * | |
676 | * @since 3.6.1 | |
677 | * @param {String/Function/Object} query The name of the ancestor node to search or | |
678 | * an object with the node names to search for or an evaluator function. | |
679 | * @param {Boolean} [includeSelf] Whether to include the current | |
680 | * node in the search. | |
681 | * @returns {CKEDITOR.dom.node} The located ancestor node or `null` if not found. | |
682 | */ | |
683 | getAscendant: function( query, includeSelf ) { | |
684 | var $ = this.$, | |
685 | evaluator, | |
686 | isCustomEvaluator; | |
687 | ||
688 | if ( !includeSelf ) { | |
689 | $ = $.parentNode; | |
690 | } | |
691 | ||
692 | // Custom checker provided in an argument. | |
693 | if ( typeof query == 'function' ) { | |
694 | isCustomEvaluator = true; | |
695 | evaluator = query; | |
696 | } else { | |
697 | // Predefined tag name checker. | |
698 | isCustomEvaluator = false; | |
699 | evaluator = function( $ ) { | |
700 | var name = ( typeof $.nodeName == 'string' ? $.nodeName.toLowerCase() : '' ); | |
701 | ||
702 | return ( typeof query == 'string' ? name == query : name in query ); | |
703 | }; | |
704 | } | |
705 | ||
706 | while ( $ ) { | |
707 | // For user provided checker we use CKEDITOR.dom.node. | |
708 | if ( evaluator( isCustomEvaluator ? new CKEDITOR.dom.node( $ ) : $ ) ) { | |
709 | return new CKEDITOR.dom.node( $ ); | |
710 | } | |
711 | ||
712 | try { | |
713 | $ = $.parentNode; | |
714 | } catch ( e ) { | |
715 | $ = null; | |
716 | } | |
717 | } | |
718 | ||
719 | return null; | |
720 | }, | |
721 | ||
722 | /** | |
723 | * @todo | |
724 | */ | |
725 | hasAscendant: function( name, includeSelf ) { | |
726 | var $ = this.$; | |
727 | ||
728 | if ( !includeSelf ) | |
729 | $ = $.parentNode; | |
730 | ||
731 | while ( $ ) { | |
732 | if ( $.nodeName && $.nodeName.toLowerCase() == name ) | |
733 | return true; | |
734 | ||
735 | $ = $.parentNode; | |
736 | } | |
737 | return false; | |
738 | }, | |
739 | ||
740 | /** | |
741 | * @todo | |
742 | */ | |
743 | move: function( target, toStart ) { | |
744 | target.append( this.remove(), toStart ); | |
745 | }, | |
746 | ||
747 | /** | |
748 | * Removes this node from the document DOM. | |
749 | * | |
750 | * var element = CKEDITOR.document.getById( 'MyElement' ); | |
751 | * element.remove(); | |
752 | * | |
753 | * @param {Boolean} [preserveChildren=false] Indicates that the children | |
754 | * elements must remain in the document, removing only the outer tags. | |
755 | */ | |
756 | remove: function( preserveChildren ) { | |
757 | var $ = this.$; | |
758 | var parent = $.parentNode; | |
759 | ||
760 | if ( parent ) { | |
761 | if ( preserveChildren ) { | |
762 | // Move all children before the node. | |
763 | for ( var child; | |
764 | ( child = $.firstChild ); ) { | |
765 | parent.insertBefore( $.removeChild( child ), $ ); | |
766 | } | |
767 | } | |
768 | ||
769 | parent.removeChild( $ ); | |
770 | } | |
771 | ||
772 | return this; | |
773 | }, | |
774 | ||
775 | /** | |
776 | * @todo | |
777 | */ | |
778 | replace: function( nodeToReplace ) { | |
779 | this.insertBefore( nodeToReplace ); | |
780 | nodeToReplace.remove(); | |
781 | }, | |
782 | ||
783 | /** | |
784 | * @todo | |
785 | */ | |
786 | trim: function() { | |
787 | this.ltrim(); | |
788 | this.rtrim(); | |
789 | }, | |
790 | ||
791 | /** | |
792 | * @todo | |
793 | */ | |
794 | ltrim: function() { | |
795 | var child; | |
796 | while ( this.getFirst && ( child = this.getFirst() ) ) { | |
797 | if ( child.type == CKEDITOR.NODE_TEXT ) { | |
798 | var trimmed = CKEDITOR.tools.ltrim( child.getText() ), | |
799 | originalLength = child.getLength(); | |
800 | ||
801 | if ( !trimmed ) { | |
802 | child.remove(); | |
803 | continue; | |
804 | } else if ( trimmed.length < originalLength ) { | |
805 | child.split( originalLength - trimmed.length ); | |
806 | ||
807 | // IE BUG: child.remove() may raise JavaScript errors here. (http://dev.ckeditor.com/ticket/81) | |
808 | this.$.removeChild( this.$.firstChild ); | |
809 | } | |
810 | } | |
811 | break; | |
812 | } | |
813 | }, | |
814 | ||
815 | /** | |
816 | * @todo | |
817 | */ | |
818 | rtrim: function() { | |
819 | var child; | |
820 | while ( this.getLast && ( child = this.getLast() ) ) { | |
821 | if ( child.type == CKEDITOR.NODE_TEXT ) { | |
822 | var trimmed = CKEDITOR.tools.rtrim( child.getText() ), | |
823 | originalLength = child.getLength(); | |
824 | ||
825 | if ( !trimmed ) { | |
826 | child.remove(); | |
827 | continue; | |
828 | } else if ( trimmed.length < originalLength ) { | |
829 | child.split( trimmed.length ); | |
830 | ||
831 | // IE BUG: child.getNext().remove() may raise JavaScript errors here. | |
832 | // (http://dev.ckeditor.com/ticket/81) | |
833 | this.$.lastChild.parentNode.removeChild( this.$.lastChild ); | |
834 | } | |
835 | } | |
836 | break; | |
837 | } | |
838 | ||
839 | if ( CKEDITOR.env.needsBrFiller ) { | |
840 | child = this.$.lastChild; | |
841 | ||
842 | if ( child && child.type == 1 && child.nodeName.toLowerCase() == 'br' ) { | |
843 | // Use "eChildNode.parentNode" instead of "node" to avoid IE bug (http://dev.ckeditor.com/ticket/324). | |
844 | child.parentNode.removeChild( child ); | |
845 | } | |
846 | } | |
847 | }, | |
848 | ||
849 | /** | |
850 | * Checks if this node is read-only (should not be changed). | |
851 | * | |
852 | * // For the following HTML: | |
853 | * // <b>foo</b><div contenteditable="false"><i>bar</i></div> | |
854 | * | |
855 | * elB.isReadOnly(); // -> false | |
856 | * foo.isReadOnly(); // -> false | |
857 | * elDiv.isReadOnly(); // -> true | |
858 | * elI.isReadOnly(); // -> true | |
859 | * | |
860 | * This method works in two modes depending on browser support for the `element.isContentEditable` property and | |
861 | * the value of the `checkOnlyAttributes` parameter. The `element.isContentEditable` check is faster, but it is known | |
862 | * to malfunction in hidden or detached nodes. Additionally, when processing some detached DOM tree you may want to imitate | |
863 | * that this happens inside an editable container (like it would happen inside the {@link CKEDITOR.editable}). To do so, | |
864 | * you can temporarily attach this tree to an element with the `data-cke-editable` attribute and use the | |
865 | * `checkOnlyAttributes` mode. | |
866 | * | |
867 | * @since 3.5 | |
868 | * @param {Boolean} [checkOnlyAttributes=false] If `true`, only attributes will be checked, native methods will not | |
869 | * be used. This parameter needs to be `true` to check hidden or detached elements. Introduced in 4.5. | |
870 | * @returns {Boolean} | |
871 | */ | |
872 | isReadOnly: function( checkOnlyAttributes ) { | |
873 | var element = this; | |
874 | if ( this.type != CKEDITOR.NODE_ELEMENT ) | |
875 | element = this.getParent(); | |
876 | ||
877 | // Prevent Edge crash (http://dev.ckeditor.com/ticket/13609, http://dev.ckeditor.com/ticket/13919). | |
878 | if ( CKEDITOR.env.edge && element && element.is( 'textarea', 'input' ) ) { | |
879 | checkOnlyAttributes = true; | |
880 | } | |
881 | ||
882 | if ( !checkOnlyAttributes && element && typeof element.$.isContentEditable != 'undefined' ) { | |
883 | return !( element.$.isContentEditable || element.data( 'cke-editable' ) ); | |
884 | } | |
885 | else { | |
886 | // Degrade for old browsers which don't support "isContentEditable", e.g. FF3 | |
887 | ||
888 | while ( element ) { | |
889 | if ( element.data( 'cke-editable' ) ) { | |
890 | return false; | |
891 | } else if ( element.hasAttribute( 'contenteditable' ) ) { | |
892 | return element.getAttribute( 'contenteditable' ) == 'false'; | |
893 | } | |
894 | ||
895 | element = element.getParent(); | |
896 | } | |
897 | ||
898 | // Reached the root of DOM tree, no editable found. | |
899 | return true; | |
900 | } | |
901 | } | |
902 | } ); |