]>
Commit | Line | Data |
---|---|---|
7adcb81e IB |
1 | /** |
2 | * @license Copyright (c) 2003-2015, 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 (#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 && !current.nodeValue ) { | |
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 || !current.nodeValue ) ) | |
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 sibling.nodeValue ? sibling : getAdjacentNonEmptyTextNode( sibling, lookForward ); | |
405 | } | |
406 | }, | |
407 | ||
408 | /** | |
409 | * @todo | |
410 | */ | |
411 | getNextSourceNode: function( startFromSibling, nodeType, guard ) { | |
412 | // If "guard" is a node, transform it in a function. | |
413 | if ( guard && !guard.call ) { | |
414 | var guardNode = guard; | |
415 | guard = function( node ) { | |
416 | return !node.equals( guardNode ); | |
417 | }; | |
418 | } | |
419 | ||
420 | var node = ( !startFromSibling && this.getFirst && this.getFirst() ), | |
421 | parent; | |
422 | ||
423 | // Guarding when we're skipping the current element( no children or 'startFromSibling' ). | |
424 | // send the 'moving out' signal even we don't actually dive into. | |
425 | if ( !node ) { | |
426 | if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false ) | |
427 | return null; | |
428 | node = this.getNext(); | |
429 | } | |
430 | ||
431 | while ( !node && ( parent = ( parent || this ).getParent() ) ) { | |
432 | // The guard check sends the "true" paramenter to indicate that | |
433 | // we are moving "out" of the element. | |
434 | if ( guard && guard( parent, true ) === false ) | |
435 | return null; | |
436 | ||
437 | node = parent.getNext(); | |
438 | } | |
439 | ||
440 | if ( !node ) | |
441 | return null; | |
442 | ||
443 | if ( guard && guard( node ) === false ) | |
444 | return null; | |
445 | ||
446 | if ( nodeType && nodeType != node.type ) | |
447 | return node.getNextSourceNode( false, nodeType, guard ); | |
448 | ||
449 | return node; | |
450 | }, | |
451 | ||
452 | /** | |
453 | * @todo | |
454 | */ | |
455 | getPreviousSourceNode: function( startFromSibling, nodeType, guard ) { | |
456 | if ( guard && !guard.call ) { | |
457 | var guardNode = guard; | |
458 | guard = function( node ) { | |
459 | return !node.equals( guardNode ); | |
460 | }; | |
461 | } | |
462 | ||
463 | var node = ( !startFromSibling && this.getLast && this.getLast() ), | |
464 | parent; | |
465 | ||
466 | // Guarding when we're skipping the current element( no children or 'startFromSibling' ). | |
467 | // send the 'moving out' signal even we don't actually dive into. | |
468 | if ( !node ) { | |
469 | if ( this.type == CKEDITOR.NODE_ELEMENT && guard && guard( this, true ) === false ) | |
470 | return null; | |
471 | node = this.getPrevious(); | |
472 | } | |
473 | ||
474 | while ( !node && ( parent = ( parent || this ).getParent() ) ) { | |
475 | // The guard check sends the "true" paramenter to indicate that | |
476 | // we are moving "out" of the element. | |
477 | if ( guard && guard( parent, true ) === false ) | |
478 | return null; | |
479 | ||
480 | node = parent.getPrevious(); | |
481 | } | |
482 | ||
483 | if ( !node ) | |
484 | return null; | |
485 | ||
486 | if ( guard && guard( node ) === false ) | |
487 | return null; | |
488 | ||
489 | if ( nodeType && node.type != nodeType ) | |
490 | return node.getPreviousSourceNode( false, nodeType, guard ); | |
491 | ||
492 | return node; | |
493 | }, | |
494 | ||
495 | /** | |
496 | * Gets the node that preceeds this element in its parent's child list. | |
497 | * | |
498 | * var element = CKEDITOR.dom.element.createFromHtml( '<div><i>prev</i><b>Example</b></div>' ); | |
499 | * var first = element.getLast().getPrev(); | |
500 | * alert( first.getName() ); // 'i' | |
501 | * | |
502 | * @param {Function} [evaluator] Filtering the result node. | |
503 | * @returns {CKEDITOR.dom.node} The previous node or null if not available. | |
504 | */ | |
505 | getPrevious: function( evaluator ) { | |
506 | var previous = this.$, | |
507 | retval; | |
508 | do { | |
509 | previous = previous.previousSibling; | |
510 | ||
511 | // Avoid returning the doc type node. | |
512 | // http://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-412266927 | |
513 | retval = previous && previous.nodeType != 10 && new CKEDITOR.dom.node( previous ); | |
514 | } | |
515 | while ( retval && evaluator && !evaluator( retval ) ); | |
516 | return retval; | |
517 | }, | |
518 | ||
519 | /** | |
520 | * Gets the node that follows this element in its parent's child list. | |
521 | * | |
522 | * var element = CKEDITOR.dom.element.createFromHtml( '<div><b>Example</b><i>next</i></div>' ); | |
523 | * var last = element.getFirst().getNext(); | |
524 | * alert( last.getName() ); // 'i' | |
525 | * | |
526 | * @param {Function} [evaluator] Filtering the result node. | |
527 | * @returns {CKEDITOR.dom.node} The next node or null if not available. | |
528 | */ | |
529 | getNext: function( evaluator ) { | |
530 | var next = this.$, | |
531 | retval; | |
532 | do { | |
533 | next = next.nextSibling; | |
534 | retval = next && new CKEDITOR.dom.node( next ); | |
535 | } | |
536 | while ( retval && evaluator && !evaluator( retval ) ); | |
537 | return retval; | |
538 | }, | |
539 | ||
540 | /** | |
541 | * Gets the parent element for this node. | |
542 | * | |
543 | * var node = editor.document.getBody().getFirst(); | |
544 | * var parent = node.getParent(); | |
545 | * alert( parent.getName() ); // 'body' | |
546 | * | |
547 | * @param {Boolean} [allowFragmentParent=false] Consider also parent node that is of | |
548 | * fragment type {@link CKEDITOR#NODE_DOCUMENT_FRAGMENT}. | |
549 | * @returns {CKEDITOR.dom.element} The parent element. | |
550 | */ | |
551 | getParent: function( allowFragmentParent ) { | |
552 | var parent = this.$.parentNode; | |
553 | return ( parent && ( parent.nodeType == CKEDITOR.NODE_ELEMENT || allowFragmentParent && parent.nodeType == CKEDITOR.NODE_DOCUMENT_FRAGMENT ) ) ? new CKEDITOR.dom.node( parent ) : null; | |
554 | }, | |
555 | ||
556 | /** | |
557 | * Returns an array containing node parents and the node itself. By default nodes are in _descending_ order. | |
558 | * | |
559 | * // Assuming that body has paragraph as the first child. | |
560 | * var node = editor.document.getBody().getFirst(); | |
561 | * var parents = node.getParents(); | |
562 | * alert( parents[ 0 ].getName() + ',' + parents[ 2 ].getName() ); // 'html,p' | |
563 | * | |
564 | * @param {Boolean} [closerFirst=false] Determines the order of returned nodes. | |
565 | * @returns {Array} Returns an array of {@link CKEDITOR.dom.node}. | |
566 | */ | |
567 | getParents: function( closerFirst ) { | |
568 | var node = this; | |
569 | var parents = []; | |
570 | ||
571 | do { | |
572 | parents[ closerFirst ? 'push' : 'unshift' ]( node ); | |
573 | } | |
574 | while ( ( node = node.getParent() ) ); | |
575 | ||
576 | return parents; | |
577 | }, | |
578 | ||
579 | /** | |
580 | * @todo | |
581 | */ | |
582 | getCommonAncestor: function( node ) { | |
583 | if ( node.equals( this ) ) | |
584 | return this; | |
585 | ||
586 | if ( node.contains && node.contains( this ) ) | |
587 | return node; | |
588 | ||
589 | var start = this.contains ? this : this.getParent(); | |
590 | ||
591 | do { | |
592 | if ( start.contains( node ) ) return start; | |
593 | } | |
594 | while ( ( start = start.getParent() ) ); | |
595 | ||
596 | return null; | |
597 | }, | |
598 | ||
599 | /** | |
600 | * Determines the position relation between this node and the given {@link CKEDITOR.dom.node} in the document. | |
601 | * This node can be preceding ({@link CKEDITOR#POSITION_PRECEDING}) or following ({@link CKEDITOR#POSITION_FOLLOWING}) | |
602 | * the given node. This node can also contain ({@link CKEDITOR#POSITION_CONTAINS}) or be contained by | |
603 | * ({@link CKEDITOR#POSITION_IS_CONTAINED}) the given node. The function returns a bitmask of constants | |
604 | * listed above or {@link CKEDITOR#POSITION_IDENTICAL} if the given node is the same as this node. | |
605 | * | |
606 | * @param {CKEDITOR.dom.node} otherNode A node to check relation with. | |
607 | * @returns {Number} Position relation between this node and given node. | |
608 | */ | |
609 | getPosition: function( otherNode ) { | |
610 | var $ = this.$; | |
611 | var $other = otherNode.$; | |
612 | ||
613 | if ( $.compareDocumentPosition ) | |
614 | return $.compareDocumentPosition( $other ); | |
615 | ||
616 | // IE and Safari have no support for compareDocumentPosition. | |
617 | ||
618 | if ( $ == $other ) | |
619 | return CKEDITOR.POSITION_IDENTICAL; | |
620 | ||
621 | // Only element nodes support contains and sourceIndex. | |
622 | if ( this.type == CKEDITOR.NODE_ELEMENT && otherNode.type == CKEDITOR.NODE_ELEMENT ) { | |
623 | if ( $.contains ) { | |
624 | if ( $.contains( $other ) ) | |
625 | return CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING; | |
626 | ||
627 | if ( $other.contains( $ ) ) | |
628 | return CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING; | |
629 | } | |
630 | ||
631 | if ( 'sourceIndex' in $ ) | |
632 | return ( $.sourceIndex < 0 || $other.sourceIndex < 0 ) ? CKEDITOR.POSITION_DISCONNECTED : ( $.sourceIndex < $other.sourceIndex ) ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING; | |
633 | ||
634 | } | |
635 | ||
636 | // For nodes that don't support compareDocumentPosition, contains | |
637 | // or sourceIndex, their "address" is compared. | |
638 | ||
639 | var addressOfThis = this.getAddress(), | |
640 | addressOfOther = otherNode.getAddress(), | |
641 | minLevel = Math.min( addressOfThis.length, addressOfOther.length ); | |
642 | ||
643 | // Determinate preceding/following relationship. | |
644 | for ( var i = 0; i < minLevel; i++ ) { | |
645 | if ( addressOfThis[ i ] != addressOfOther[ i ] ) { | |
646 | return addressOfThis[ i ] < addressOfOther[ i ] ? CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_FOLLOWING; | |
647 | } | |
648 | } | |
649 | ||
650 | // Determinate contains/contained relationship. | |
651 | return ( addressOfThis.length < addressOfOther.length ) ? CKEDITOR.POSITION_CONTAINS + CKEDITOR.POSITION_PRECEDING : CKEDITOR.POSITION_IS_CONTAINED + CKEDITOR.POSITION_FOLLOWING; | |
652 | }, | |
653 | ||
654 | /** | |
655 | * Gets the closest ancestor node of this node, specified by its name or using an evaluator function. | |
656 | * | |
657 | * // Suppose we have the following HTML structure: | |
658 | * // <div id="outer"><div id="inner"><p><b>Some text</b></p></div></div> | |
659 | * // If node == <b> | |
660 | * ascendant = node.getAscendant( 'div' ); // ascendant == <div id="inner"> | |
661 | * ascendant = node.getAscendant( 'b' ); // ascendant == null | |
662 | * ascendant = node.getAscendant( 'b', true ); // ascendant == <b> | |
663 | * ascendant = node.getAscendant( { div:1, p:1 } ); // Searches for the first 'div' or 'p': ascendant == <div id="inner"> | |
664 | * | |
665 | * // Using custom evaluator: | |
666 | * ascendant = node.getAscendant( function( el ) { | |
667 | * return el.getId() == 'inner'; | |
668 | * } ); | |
669 | * // ascendant == <div id="inner"> | |
670 | * | |
671 | * @since 3.6.1 | |
672 | * @param {String/Function/Object} query The name of the ancestor node to search or | |
673 | * an object with the node names to search for or an evaluator function. | |
674 | * @param {Boolean} [includeSelf] Whether to include the current | |
675 | * node in the search. | |
676 | * @returns {CKEDITOR.dom.node} The located ancestor node or `null` if not found. | |
677 | */ | |
678 | getAscendant: function( query, includeSelf ) { | |
679 | var $ = this.$, | |
680 | evaluator, | |
681 | isCustomEvaluator; | |
682 | ||
683 | if ( !includeSelf ) { | |
684 | $ = $.parentNode; | |
685 | } | |
686 | ||
687 | // Custom checker provided in an argument. | |
688 | if ( typeof query == 'function' ) { | |
689 | isCustomEvaluator = true; | |
690 | evaluator = query; | |
691 | } else { | |
692 | // Predefined tag name checker. | |
693 | isCustomEvaluator = false; | |
694 | evaluator = function( $ ) { | |
695 | var name = ( typeof $.nodeName == 'string' ? $.nodeName.toLowerCase() : '' ); | |
696 | ||
697 | return ( typeof query == 'string' ? name == query : name in query ); | |
698 | }; | |
699 | } | |
700 | ||
701 | while ( $ ) { | |
702 | // For user provided checker we use CKEDITOR.dom.node. | |
703 | if ( evaluator( isCustomEvaluator ? new CKEDITOR.dom.node( $ ) : $ ) ) { | |
704 | return new CKEDITOR.dom.node( $ ); | |
705 | } | |
706 | ||
707 | try { | |
708 | $ = $.parentNode; | |
709 | } catch ( e ) { | |
710 | $ = null; | |
711 | } | |
712 | } | |
713 | ||
714 | return null; | |
715 | }, | |
716 | ||
717 | /** | |
718 | * @todo | |
719 | */ | |
720 | hasAscendant: function( name, includeSelf ) { | |
721 | var $ = this.$; | |
722 | ||
723 | if ( !includeSelf ) | |
724 | $ = $.parentNode; | |
725 | ||
726 | while ( $ ) { | |
727 | if ( $.nodeName && $.nodeName.toLowerCase() == name ) | |
728 | return true; | |
729 | ||
730 | $ = $.parentNode; | |
731 | } | |
732 | return false; | |
733 | }, | |
734 | ||
735 | /** | |
736 | * @todo | |
737 | */ | |
738 | move: function( target, toStart ) { | |
739 | target.append( this.remove(), toStart ); | |
740 | }, | |
741 | ||
742 | /** | |
743 | * Removes this node from the document DOM. | |
744 | * | |
745 | * var element = CKEDITOR.document.getById( 'MyElement' ); | |
746 | * element.remove(); | |
747 | * | |
748 | * @param {Boolean} [preserveChildren=false] Indicates that the children | |
749 | * elements must remain in the document, removing only the outer tags. | |
750 | */ | |
751 | remove: function( preserveChildren ) { | |
752 | var $ = this.$; | |
753 | var parent = $.parentNode; | |
754 | ||
755 | if ( parent ) { | |
756 | if ( preserveChildren ) { | |
757 | // Move all children before the node. | |
758 | for ( var child; | |
759 | ( child = $.firstChild ); ) { | |
760 | parent.insertBefore( $.removeChild( child ), $ ); | |
761 | } | |
762 | } | |
763 | ||
764 | parent.removeChild( $ ); | |
765 | } | |
766 | ||
767 | return this; | |
768 | }, | |
769 | ||
770 | /** | |
771 | * @todo | |
772 | */ | |
773 | replace: function( nodeToReplace ) { | |
774 | this.insertBefore( nodeToReplace ); | |
775 | nodeToReplace.remove(); | |
776 | }, | |
777 | ||
778 | /** | |
779 | * @todo | |
780 | */ | |
781 | trim: function() { | |
782 | this.ltrim(); | |
783 | this.rtrim(); | |
784 | }, | |
785 | ||
786 | /** | |
787 | * @todo | |
788 | */ | |
789 | ltrim: function() { | |
790 | var child; | |
791 | while ( this.getFirst && ( child = this.getFirst() ) ) { | |
792 | if ( child.type == CKEDITOR.NODE_TEXT ) { | |
793 | var trimmed = CKEDITOR.tools.ltrim( child.getText() ), | |
794 | originalLength = child.getLength(); | |
795 | ||
796 | if ( !trimmed ) { | |
797 | child.remove(); | |
798 | continue; | |
799 | } else if ( trimmed.length < originalLength ) { | |
800 | child.split( originalLength - trimmed.length ); | |
801 | ||
802 | // IE BUG: child.remove() may raise JavaScript errors here. (#81) | |
803 | this.$.removeChild( this.$.firstChild ); | |
804 | } | |
805 | } | |
806 | break; | |
807 | } | |
808 | }, | |
809 | ||
810 | /** | |
811 | * @todo | |
812 | */ | |
813 | rtrim: function() { | |
814 | var child; | |
815 | while ( this.getLast && ( child = this.getLast() ) ) { | |
816 | if ( child.type == CKEDITOR.NODE_TEXT ) { | |
817 | var trimmed = CKEDITOR.tools.rtrim( child.getText() ), | |
818 | originalLength = child.getLength(); | |
819 | ||
820 | if ( !trimmed ) { | |
821 | child.remove(); | |
822 | continue; | |
823 | } else if ( trimmed.length < originalLength ) { | |
824 | child.split( trimmed.length ); | |
825 | ||
826 | // IE BUG: child.getNext().remove() may raise JavaScript errors here. | |
827 | // (#81) | |
828 | this.$.lastChild.parentNode.removeChild( this.$.lastChild ); | |
829 | } | |
830 | } | |
831 | break; | |
832 | } | |
833 | ||
834 | if ( CKEDITOR.env.needsBrFiller ) { | |
835 | child = this.$.lastChild; | |
836 | ||
837 | if ( child && child.type == 1 && child.nodeName.toLowerCase() == 'br' ) { | |
838 | // Use "eChildNode.parentNode" instead of "node" to avoid IE bug (#324). | |
839 | child.parentNode.removeChild( child ); | |
840 | } | |
841 | } | |
842 | }, | |
843 | ||
844 | /** | |
845 | * Checks if this node is read-only (should not be changed). | |
846 | * | |
847 | * // For the following HTML: | |
848 | * // <b>foo</b><div contenteditable="false"><i>bar</i></div> | |
849 | * | |
850 | * elB.isReadOnly(); // -> false | |
851 | * foo.isReadOnly(); // -> false | |
852 | * elDiv.isReadOnly(); // -> true | |
853 | * elI.isReadOnly(); // -> true | |
854 | * | |
855 | * This method works in two modes depending on browser support for the `element.isContentEditable` property and | |
856 | * the value of the `checkOnlyAttributes` parameter. The `element.isContentEditable` check is faster, but it is known | |
857 | * to malfunction in hidden or detached nodes. Additionally, when processing some detached DOM tree you may want to imitate | |
858 | * that this happens inside an editable container (like it would happen inside the {@link CKEDITOR.editable}). To do so, | |
859 | * you can temporarily attach this tree to an element with the `data-cke-editable` attribute and use the | |
860 | * `checkOnlyAttributes` mode. | |
861 | * | |
862 | * @since 3.5 | |
863 | * @param {Boolean} [checkOnlyAttributes=false] If `true`, only attributes will be checked, native methods will not | |
864 | * be used. This parameter needs to be `true` to check hidden or detached elements. Introduced in 4.5. | |
865 | * @returns {Boolean} | |
866 | */ | |
867 | isReadOnly: function( checkOnlyAttributes ) { | |
868 | var element = this; | |
869 | if ( this.type != CKEDITOR.NODE_ELEMENT ) | |
870 | element = this.getParent(); | |
871 | ||
872 | // Prevent Edge crash (#13609, #13919). | |
873 | if ( CKEDITOR.env.edge && element && element.is( 'textarea', 'input' ) ) { | |
874 | checkOnlyAttributes = true; | |
875 | } | |
876 | ||
877 | if ( !checkOnlyAttributes && element && typeof element.$.isContentEditable != 'undefined' ) { | |
878 | return !( element.$.isContentEditable || element.data( 'cke-editable' ) ); | |
879 | } | |
880 | else { | |
881 | // Degrade for old browsers which don't support "isContentEditable", e.g. FF3 | |
882 | ||
883 | while ( element ) { | |
884 | if ( element.data( 'cke-editable' ) ) { | |
885 | return false; | |
886 | } else if ( element.hasAttribute( 'contenteditable' ) ) { | |
887 | return element.getAttribute( 'contenteditable' ) == 'false'; | |
888 | } | |
889 | ||
890 | element = element.getParent(); | |
891 | } | |
892 | ||
893 | // Reached the root of DOM tree, no editable found. | |
894 | return true; | |
895 | } | |
896 | } | |
897 | } ); |