summaryrefslogtreecommitdiff
path: root/core/dom/elementpath.js
blob: 533011bf527482d566cdae7e558b98a6b3804a12 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
/**
 * @license Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.
 * For licensing, see LICENSE.html or http://ckeditor.com/license
 */

(function() {
	// Elements that are considered the "Block limit" in an element path.
	var pathBlockLimitElements = {};
	for ( var tag in CKEDITOR.dtd.$blockLimit ) {
		// Exclude from list roots.
		if ( !( tag in CKEDITOR.dtd.$list ) )
			pathBlockLimitElements[ tag ] = 1;
	}

	// Elements that are considered the "End level Block" in an element path.
	var pathBlockElements = {};

	for ( tag in CKEDITOR.dtd.$block ) {
		// Exclude block limits, and empty block element, e.g. hr.
		if ( !( tag in CKEDITOR.dtd.$blockLimit || tag in CKEDITOR.dtd.$empty ) )
			pathBlockElements[ tag ] = 1;
	}

	// Check if an element contains any block element.
	var checkHasBlock = function( element ) {
			var childNodes = element.getChildren();

			for ( var i = 0, count = childNodes.count(); i < count; i++ ) {
				var child = childNodes.getItem( i );

				if ( child.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$block[ child.getName() ] )
					return true;
			}

			return false;
		};

	/**
	 * Retrieve the list of nodes walked from the start node up to the editable element of the editor.
	 *
	 * @class
	 * @constructor Creates a element path class instance.
	 * @param {CKEDITOR.dom.element} startNode From which the path should start.
	 * @param {CKEDITOR.dom.element} root To which element the path should stop, default to the body element.
	 */
	CKEDITOR.dom.elementPath = function( startNode, root ) {
		var block = null;
		var blockLimit = null;
		var elements = [];

		// Backward compact.
		root = root || startNode.getDocument().getBody();

		var e = startNode;

		do {
			if ( e.type == CKEDITOR.NODE_ELEMENT ) {
				elements.push( e );

				if ( !this.lastElement ) {
					this.lastElement = e;

					// If a table is fully selected at the end of the element path,
					// it must not become the block limit.
					if ( e.is( CKEDITOR.dtd.$object ) )
						continue;
				}

				var elementName = e.getName();

				if ( !blockLimit ) {
					if ( !block && pathBlockElements[ elementName ] )
						block = e;

					if ( pathBlockLimitElements[ elementName ] ) {
						// End level DIV is considered as the block, if no block is available. (#525)
						// But it must NOT be as the root element.
						if ( !block && elementName == 'div' && !checkHasBlock( e ) && !e.equals( root ) ) {
							block = e;
						} else
							blockLimit = e;
					}
				}

				if ( e.equals( root ) )
					break;
			}
		}
		while ( ( e = e.getParent() ) );

		/**
		 * @property {CKEDITOR.dom.element}
		 * @todo
		 */
		this.block = block;

		/**
		 * @property {CKEDITOR.dom.element}
		 * @todo
		 */
		this.blockLimit = blockLimit;

		/**
		 * The root of the elements path - `startNode` argument passed to class constructor or body element.
		 *
		 * @property {CKEDITOR.dom.element}
		 * @todo
		 */
		this.root = root;

		/**
		 * @property {CKEDITOR.dom.element[]}
		 * @todo
		 */
		this.elements = elements;
	};

})();

CKEDITOR.dom.elementPath.prototype = {
	/**
	 * Compares this element path with another one.
	 *
	 * @param {CKEDITOR.dom.elementPath} otherPath The elementPath object to be
	 * compared with this one.
	 * @returns {Boolean} `true` if the paths are equal, containing the same
	 * number of elements and the same elements in the same order.
	 */
	compare: function( otherPath ) {
		var thisElements = this.elements;
		var otherElements = otherPath && otherPath.elements;

		if ( !otherElements || thisElements.length != otherElements.length )
			return false;

		for ( var i = 0; i < thisElements.length; i++ ) {
			if ( !thisElements[ i ].equals( otherElements[ i ] ) )
				return false;
		}

		return true;
	},

	/**
	 * Search the path elements that meets the specified criteria.
	 *
	 * @param {String/Array/Function/Object/CKEDITOR.dom.element} query The criteria that can be
	 * either a tag name, list (array and object) of tag names, element or an node evaluator function.
	 * @param {Boolean} excludeRoot Not taking path root element into consideration.
	 * @param {Boolean} fromTop Search start from the topmost element instead of bottom.
	 * @returns {CKEDITOR.dom.element} The first matched dom element or `null`.
	 */
	contains: function( query, excludeRoot, fromTop ) {
		var evaluator;
		if ( typeof query == 'string' )
			evaluator = function( node ) {
				return node.getName() == query;
			};
		if ( query instanceof CKEDITOR.dom.element )
			evaluator = function( node ) {
				return node.equals( query );
			};
		else if ( CKEDITOR.tools.isArray( query ) )
			evaluator = function( node ) {
				return CKEDITOR.tools.indexOf( query, node.getName() ) > -1;
			};
		else if ( typeof query == 'function' )
			evaluator = query;
		else if ( typeof query == 'object' )
			evaluator = function( node ) {
				return node.getName() in query;
			};

		var elements = this.elements,
			length = elements.length;
		excludeRoot && length--;

		if ( fromTop ) {
			elements = Array.prototype.slice.call( elements, 0 );
			elements.reverse();
		}

		for ( var i = 0; i < length; i++ ) {
			if ( evaluator( elements[ i ] ) )
				return elements[ i ];
		}

		return null;
	},

	/**
	 * Check whether the elements path is the proper context for the specified
	 * tag name in the DTD.
	 *
	 * @param {String} tag The tag name.
	 * @returns {Boolean}
	 */
	isContextFor: function( tag ) {
		var holder;

		// Check for block context.
		if ( tag in CKEDITOR.dtd.$block ) {
			// Indeterminate elements which are not subjected to be splitted or surrounded must be checked first.
			var inter = this.contains( CKEDITOR.dtd.$intermediate );
			holder = inter || ( this.root.equals( this.block ) && this.block ) || this.blockLimit;
			return !!holder.getDtd()[ tag ];
		}

		return true;

	},

	/**
	 * Retrieve the text direction for this elements path.
	 *
	 * @returns {'ltr'/'rtl'}
	 */
	direction: function() {
		var directionNode = this.block || this.blockLimit || this.root;
		return directionNode.getDirection( 1 );
	}
};