diff options
Diffstat (limited to 'sources/core/dom/elementpath.js')
-rw-r--r-- | sources/core/dom/elementpath.js | 251 |
1 files changed, 251 insertions, 0 deletions
diff --git a/sources/core/dom/elementpath.js b/sources/core/dom/elementpath.js new file mode 100644 index 0000000..1ee551b --- /dev/null +++ b/sources/core/dom/elementpath.js | |||
@@ -0,0 +1,251 @@ | |||
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 | 'use strict'; | ||
7 | |||
8 | ( function() { | ||
9 | |||
10 | var pathBlockLimitElements = {}, | ||
11 | pathBlockElements = {}, | ||
12 | tag; | ||
13 | |||
14 | // Elements that are considered the "Block limit" in an element path. | ||
15 | for ( tag in CKEDITOR.dtd.$blockLimit ) { | ||
16 | // Exclude from list roots. | ||
17 | if ( !( tag in CKEDITOR.dtd.$list ) ) | ||
18 | pathBlockLimitElements[ tag ] = 1; | ||
19 | } | ||
20 | |||
21 | // Elements that are considered the "End level Block" in an element path. | ||
22 | for ( tag in CKEDITOR.dtd.$block ) { | ||
23 | // Exclude block limits, and empty block element, e.g. hr. | ||
24 | if ( !( tag in CKEDITOR.dtd.$blockLimit || tag in CKEDITOR.dtd.$empty ) ) | ||
25 | pathBlockElements[ tag ] = 1; | ||
26 | } | ||
27 | |||
28 | // Check if an element contains any block element. | ||
29 | function checkHasBlock( element ) { | ||
30 | var childNodes = element.getChildren(); | ||
31 | |||
32 | for ( var i = 0, count = childNodes.count(); i < count; i++ ) { | ||
33 | var child = childNodes.getItem( i ); | ||
34 | |||
35 | if ( child.type == CKEDITOR.NODE_ELEMENT && CKEDITOR.dtd.$block[ child.getName() ] ) | ||
36 | return true; | ||
37 | } | ||
38 | |||
39 | return false; | ||
40 | } | ||
41 | |||
42 | /** | ||
43 | * Retrieve the list of nodes walked from the start node up to the editable element of the editor. | ||
44 | * | ||
45 | * @class | ||
46 | * @constructor Creates an element path class instance. | ||
47 | * @param {CKEDITOR.dom.element} startNode From which the path should start. | ||
48 | * @param {CKEDITOR.dom.element} root To which element the path should stop, defaults to the `body` element. | ||
49 | */ | ||
50 | CKEDITOR.dom.elementPath = function( startNode, root ) { | ||
51 | var block = null, | ||
52 | blockLimit = null, | ||
53 | elements = [], | ||
54 | e = startNode, | ||
55 | elementName; | ||
56 | |||
57 | // Backward compact. | ||
58 | root = root || startNode.getDocument().getBody(); | ||
59 | |||
60 | do { | ||
61 | if ( e.type == CKEDITOR.NODE_ELEMENT ) { | ||
62 | elements.push( e ); | ||
63 | |||
64 | if ( !this.lastElement ) { | ||
65 | this.lastElement = e; | ||
66 | |||
67 | // If an object or non-editable element is fully selected at the end of the element path, | ||
68 | // it must not become the block limit. | ||
69 | if ( e.is( CKEDITOR.dtd.$object ) || e.getAttribute( 'contenteditable' ) == 'false' ) | ||
70 | continue; | ||
71 | } | ||
72 | |||
73 | if ( e.equals( root ) ) | ||
74 | break; | ||
75 | |||
76 | if ( !blockLimit ) { | ||
77 | elementName = e.getName(); | ||
78 | |||
79 | // First editable element becomes a block limit, because it cannot be split. | ||
80 | if ( e.getAttribute( 'contenteditable' ) == 'true' ) | ||
81 | blockLimit = e; | ||
82 | // "Else" because element cannot be both - block and block levelimit. | ||
83 | else if ( !block && pathBlockElements[ elementName ] ) | ||
84 | block = e; | ||
85 | |||
86 | if ( pathBlockLimitElements[ elementName ] ) { | ||
87 | // End level DIV is considered as the block, if no block is available. (#525) | ||
88 | // But it must NOT be the root element (checked above). | ||
89 | if ( !block && elementName == 'div' && !checkHasBlock( e ) ) | ||
90 | block = e; | ||
91 | else | ||
92 | blockLimit = e; | ||
93 | } | ||
94 | } | ||
95 | } | ||
96 | } | ||
97 | while ( ( e = e.getParent() ) ); | ||
98 | |||
99 | // Block limit defaults to root. | ||
100 | if ( !blockLimit ) | ||
101 | blockLimit = root; | ||
102 | |||
103 | /** | ||
104 | * First non-empty block element which: | ||
105 | * | ||
106 | * * is not a {@link CKEDITOR.dtd#$blockLimit}, | ||
107 | * * or is a `div` which does not contain block elements and is not a `root`. | ||
108 | * | ||
109 | * This means a first, splittable block in elements path. | ||
110 | * | ||
111 | * @readonly | ||
112 | * @property {CKEDITOR.dom.element} | ||
113 | */ | ||
114 | this.block = block; | ||
115 | |||
116 | /** | ||
117 | * See the {@link CKEDITOR.dtd#$blockLimit} description. | ||
118 | * | ||
119 | * @readonly | ||
120 | * @property {CKEDITOR.dom.element} | ||
121 | */ | ||
122 | this.blockLimit = blockLimit; | ||
123 | |||
124 | /** | ||
125 | * The root of the elements path - `root` argument passed to class constructor or a `body` element. | ||
126 | * | ||
127 | * @readonly | ||
128 | * @property {CKEDITOR.dom.element} | ||
129 | */ | ||
130 | this.root = root; | ||
131 | |||
132 | /** | ||
133 | * An array of elements (from `startNode` to `root`) in the path. | ||
134 | * | ||
135 | * @readonly | ||
136 | * @property {CKEDITOR.dom.element[]} | ||
137 | */ | ||
138 | this.elements = elements; | ||
139 | |||
140 | /** | ||
141 | * The last element of the elements path - `startNode` or its parent. | ||
142 | * | ||
143 | * @readonly | ||
144 | * @property {CKEDITOR.dom.element} lastElement | ||
145 | */ | ||
146 | }; | ||
147 | |||
148 | } )(); | ||
149 | |||
150 | CKEDITOR.dom.elementPath.prototype = { | ||
151 | /** | ||
152 | * Compares this element path with another one. | ||
153 | * | ||
154 | * @param {CKEDITOR.dom.elementPath} otherPath The elementPath object to be | ||
155 | * compared with this one. | ||
156 | * @returns {Boolean} `true` if the paths are equal, containing the same | ||
157 | * number of elements and the same elements in the same order. | ||
158 | */ | ||
159 | compare: function( otherPath ) { | ||
160 | var thisElements = this.elements; | ||
161 | var otherElements = otherPath && otherPath.elements; | ||
162 | |||
163 | if ( !otherElements || thisElements.length != otherElements.length ) | ||
164 | return false; | ||
165 | |||
166 | for ( var i = 0; i < thisElements.length; i++ ) { | ||
167 | if ( !thisElements[ i ].equals( otherElements[ i ] ) ) | ||
168 | return false; | ||
169 | } | ||
170 | |||
171 | return true; | ||
172 | }, | ||
173 | |||
174 | /** | ||
175 | * Search the path elements that meets the specified criteria. | ||
176 | * | ||
177 | * @param {String/Array/Function/Object/CKEDITOR.dom.element} query The criteria that can be | ||
178 | * either a tag name, list (array and object) of tag names, element or an node evaluator function. | ||
179 | * @param {Boolean} [excludeRoot] Not taking path root element into consideration. | ||
180 | * @param {Boolean} [fromTop] Search start from the topmost element instead of bottom. | ||
181 | * @returns {CKEDITOR.dom.element} The first matched dom element or `null`. | ||
182 | */ | ||
183 | contains: function( query, excludeRoot, fromTop ) { | ||
184 | var evaluator; | ||
185 | if ( typeof query == 'string' ) | ||
186 | evaluator = function( node ) { | ||
187 | return node.getName() == query; | ||
188 | }; | ||
189 | if ( query instanceof CKEDITOR.dom.element ) | ||
190 | evaluator = function( node ) { | ||
191 | return node.equals( query ); | ||
192 | }; | ||
193 | else if ( CKEDITOR.tools.isArray( query ) ) | ||
194 | evaluator = function( node ) { | ||
195 | return CKEDITOR.tools.indexOf( query, node.getName() ) > -1; | ||
196 | }; | ||
197 | else if ( typeof query == 'function' ) | ||
198 | evaluator = query; | ||
199 | else if ( typeof query == 'object' ) | ||
200 | evaluator = function( node ) { | ||
201 | return node.getName() in query; | ||
202 | }; | ||
203 | |||
204 | var elements = this.elements, | ||
205 | length = elements.length; | ||
206 | excludeRoot && length--; | ||
207 | |||
208 | if ( fromTop ) { | ||
209 | elements = Array.prototype.slice.call( elements, 0 ); | ||
210 | elements.reverse(); | ||
211 | } | ||
212 | |||
213 | for ( var i = 0; i < length; i++ ) { | ||
214 | if ( evaluator( elements[ i ] ) ) | ||
215 | return elements[ i ]; | ||
216 | } | ||
217 | |||
218 | return null; | ||
219 | }, | ||
220 | |||
221 | /** | ||
222 | * Check whether the elements path is the proper context for the specified | ||
223 | * tag name in the DTD. | ||
224 | * | ||
225 | * @param {String} tag The tag name. | ||
226 | * @returns {Boolean} | ||
227 | */ | ||
228 | isContextFor: function( tag ) { | ||
229 | var holder; | ||
230 | |||
231 | // Check for block context. | ||
232 | if ( tag in CKEDITOR.dtd.$block ) { | ||
233 | // Indeterminate elements which are not subjected to be splitted or surrounded must be checked first. | ||
234 | var inter = this.contains( CKEDITOR.dtd.$intermediate ); | ||
235 | holder = inter || ( this.root.equals( this.block ) && this.block ) || this.blockLimit; | ||
236 | return !!holder.getDtd()[ tag ]; | ||
237 | } | ||
238 | |||
239 | return true; | ||
240 | }, | ||
241 | |||
242 | /** | ||
243 | * Retrieve the text direction for this elements path. | ||
244 | * | ||
245 | * @returns {'ltr'/'rtl'} | ||
246 | */ | ||
247 | direction: function() { | ||
248 | var directionNode = this.block || this.blockLimit || this.root; | ||
249 | return directionNode.getDirection( 1 ); | ||
250 | } | ||
251 | }; | ||