|
1 /** |
|
2 * Selector.js |
|
3 * |
|
4 * Copyright, Moxiecode Systems AB |
|
5 * Released under LGPL License. |
|
6 * |
|
7 * License: http://www.tinymce.com/license |
|
8 * Contributing: http://www.tinymce.com/contributing |
|
9 */ |
|
10 |
|
11 /*eslint no-nested-ternary:0 */ |
|
12 |
|
13 /** |
|
14 * Selector engine, enables you to select controls by using CSS like expressions. |
|
15 * We currently only support basic CSS expressions to reduce the size of the core |
|
16 * and the ones we support should be enough for most cases. |
|
17 * |
|
18 * @example |
|
19 * Supported expressions: |
|
20 * element |
|
21 * element#name |
|
22 * element.class |
|
23 * element[attr] |
|
24 * element[attr*=value] |
|
25 * element[attr~=value] |
|
26 * element[attr!=value] |
|
27 * element[attr^=value] |
|
28 * element[attr$=value] |
|
29 * element:<state> |
|
30 * element:not(<expression>) |
|
31 * element:first |
|
32 * element:last |
|
33 * element:odd |
|
34 * element:even |
|
35 * element element |
|
36 * element > element |
|
37 * |
|
38 * @class tinymce.ui.Selector |
|
39 */ |
|
40 define("tinymce/ui/Selector", [ |
|
41 "tinymce/util/Class" |
|
42 ], function(Class) { |
|
43 "use strict"; |
|
44 |
|
45 /** |
|
46 * Produces an array with a unique set of objects. It will not compare the values |
|
47 * but the references of the objects. |
|
48 * |
|
49 * @private |
|
50 * @method unqiue |
|
51 * @param {Array} array Array to make into an array with unique items. |
|
52 * @return {Array} Array with unique items. |
|
53 */ |
|
54 function unique(array) { |
|
55 var uniqueItems = [], i = array.length, item; |
|
56 |
|
57 while (i--) { |
|
58 item = array[i]; |
|
59 |
|
60 if (!item.__checked) { |
|
61 uniqueItems.push(item); |
|
62 item.__checked = 1; |
|
63 } |
|
64 } |
|
65 |
|
66 i = uniqueItems.length; |
|
67 while (i--) { |
|
68 delete uniqueItems[i].__checked; |
|
69 } |
|
70 |
|
71 return uniqueItems; |
|
72 } |
|
73 |
|
74 var expression = /^([\w\\*]+)?(?:#([\w\\]+))?(?:\.([\w\\\.]+))?(?:\[\@?([\w\\]+)([\^\$\*!~]?=)([\w\\]+)\])?(?:\:(.+))?/i; |
|
75 |
|
76 /*jshint maxlen:255 */ |
|
77 /*eslint max-len:0 */ |
|
78 var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, |
|
79 whiteSpace = /^\s*|\s*$/g, |
|
80 Collection; |
|
81 |
|
82 var Selector = Class.extend({ |
|
83 /** |
|
84 * Constructs a new Selector instance. |
|
85 * |
|
86 * @constructor |
|
87 * @method init |
|
88 * @param {String} selector CSS like selector expression. |
|
89 */ |
|
90 init: function(selector) { |
|
91 var match = this.match; |
|
92 |
|
93 function compileNameFilter(name) { |
|
94 if (name) { |
|
95 name = name.toLowerCase(); |
|
96 |
|
97 return function(item) { |
|
98 return name === '*' || item.type === name; |
|
99 }; |
|
100 } |
|
101 } |
|
102 |
|
103 function compileIdFilter(id) { |
|
104 if (id) { |
|
105 return function(item) { |
|
106 return item._name === id; |
|
107 }; |
|
108 } |
|
109 } |
|
110 |
|
111 function compileClassesFilter(classes) { |
|
112 if (classes) { |
|
113 classes = classes.split('.'); |
|
114 |
|
115 return function(item) { |
|
116 var i = classes.length; |
|
117 |
|
118 while (i--) { |
|
119 if (!item.hasClass(classes[i])) { |
|
120 return false; |
|
121 } |
|
122 } |
|
123 |
|
124 return true; |
|
125 }; |
|
126 } |
|
127 } |
|
128 |
|
129 function compileAttrFilter(name, cmp, check) { |
|
130 if (name) { |
|
131 return function(item) { |
|
132 var value = item[name] ? item[name]() : ''; |
|
133 |
|
134 return !cmp ? !!check : |
|
135 cmp === "=" ? value === check : |
|
136 cmp === "*=" ? value.indexOf(check) >= 0 : |
|
137 cmp === "~=" ? (" " + value + " ").indexOf(" " + check + " ") >= 0 : |
|
138 cmp === "!=" ? value != check : |
|
139 cmp === "^=" ? value.indexOf(check) === 0 : |
|
140 cmp === "$=" ? value.substr(value.length - check.length) === check : |
|
141 false; |
|
142 }; |
|
143 } |
|
144 } |
|
145 |
|
146 function compilePsuedoFilter(name) { |
|
147 var notSelectors; |
|
148 |
|
149 if (name) { |
|
150 name = /(?:not\((.+)\))|(.+)/i.exec(name); |
|
151 |
|
152 if (!name[1]) { |
|
153 name = name[2]; |
|
154 |
|
155 return function(item, index, length) { |
|
156 return name === 'first' ? index === 0 : |
|
157 name === 'last' ? index === length - 1 : |
|
158 name === 'even' ? index % 2 === 0 : |
|
159 name === 'odd' ? index % 2 === 1 : |
|
160 item[name] ? item[name]() : |
|
161 false; |
|
162 }; |
|
163 } else { |
|
164 // Compile not expression |
|
165 notSelectors = parseChunks(name[1], []); |
|
166 |
|
167 return function(item) { |
|
168 return !match(item, notSelectors); |
|
169 }; |
|
170 } |
|
171 } |
|
172 } |
|
173 |
|
174 function compile(selector, filters, direct) { |
|
175 var parts; |
|
176 |
|
177 function add(filter) { |
|
178 if (filter) { |
|
179 filters.push(filter); |
|
180 } |
|
181 } |
|
182 |
|
183 // Parse expression into parts |
|
184 parts = expression.exec(selector.replace(whiteSpace, '')); |
|
185 |
|
186 add(compileNameFilter(parts[1])); |
|
187 add(compileIdFilter(parts[2])); |
|
188 add(compileClassesFilter(parts[3])); |
|
189 add(compileAttrFilter(parts[4], parts[5], parts[6])); |
|
190 add(compilePsuedoFilter(parts[7])); |
|
191 |
|
192 // Mark the filter with psuedo for performance |
|
193 filters.psuedo = !!parts[7]; |
|
194 filters.direct = direct; |
|
195 |
|
196 return filters; |
|
197 } |
|
198 |
|
199 // Parser logic based on Sizzle by John Resig |
|
200 function parseChunks(selector, selectors) { |
|
201 var parts = [], extra, matches, i; |
|
202 |
|
203 do { |
|
204 chunker.exec(""); |
|
205 matches = chunker.exec(selector); |
|
206 |
|
207 if (matches) { |
|
208 selector = matches[3]; |
|
209 parts.push(matches[1]); |
|
210 |
|
211 if (matches[2]) { |
|
212 extra = matches[3]; |
|
213 break; |
|
214 } |
|
215 } |
|
216 } while (matches); |
|
217 |
|
218 if (extra) { |
|
219 parseChunks(extra, selectors); |
|
220 } |
|
221 |
|
222 selector = []; |
|
223 for (i = 0; i < parts.length; i++) { |
|
224 if (parts[i] != '>') { |
|
225 selector.push(compile(parts[i], [], parts[i - 1] === '>')); |
|
226 } |
|
227 } |
|
228 |
|
229 selectors.push(selector); |
|
230 |
|
231 return selectors; |
|
232 } |
|
233 |
|
234 this._selectors = parseChunks(selector, []); |
|
235 }, |
|
236 |
|
237 /** |
|
238 * Returns true/false if the selector matches the specified control. |
|
239 * |
|
240 * @method match |
|
241 * @param {tinymce.ui.Control} control Control to match agains the selector. |
|
242 * @param {Array} selectors Optional array of selectors, mostly used internally. |
|
243 * @return {Boolean} true/false state if the control matches or not. |
|
244 */ |
|
245 match: function(control, selectors) { |
|
246 var i, l, si, sl, selector, fi, fl, filters, index, length, siblings, count, item; |
|
247 |
|
248 selectors = selectors || this._selectors; |
|
249 for (i = 0, l = selectors.length; i < l; i++) { |
|
250 selector = selectors[i]; |
|
251 sl = selector.length; |
|
252 item = control; |
|
253 count = 0; |
|
254 |
|
255 for (si = sl - 1; si >= 0; si--) { |
|
256 filters = selector[si]; |
|
257 |
|
258 while (item) { |
|
259 // Find the index and length since a psuedo filter like :first needs it |
|
260 if (filters.psuedo) { |
|
261 siblings = item.parent().items(); |
|
262 index = length = siblings.length; |
|
263 while (index--) { |
|
264 if (siblings[index] === item) { |
|
265 break; |
|
266 } |
|
267 } |
|
268 } |
|
269 |
|
270 for (fi = 0, fl = filters.length; fi < fl; fi++) { |
|
271 if (!filters[fi](item, index, length)) { |
|
272 fi = fl + 1; |
|
273 break; |
|
274 } |
|
275 } |
|
276 |
|
277 if (fi === fl) { |
|
278 count++; |
|
279 break; |
|
280 } else { |
|
281 // If it didn't match the right most expression then |
|
282 // break since it's no point looking at the parents |
|
283 if (si === sl - 1) { |
|
284 break; |
|
285 } |
|
286 } |
|
287 |
|
288 item = item.parent(); |
|
289 } |
|
290 } |
|
291 |
|
292 // If we found all selectors then return true otherwise continue looking |
|
293 if (count === sl) { |
|
294 return true; |
|
295 } |
|
296 } |
|
297 |
|
298 return false; |
|
299 }, |
|
300 |
|
301 /** |
|
302 * Returns a tinymce.ui.Collection with matches of the specified selector inside the specified container. |
|
303 * |
|
304 * @method find |
|
305 * @param {tinymce.ui.Control} container Container to look for items in. |
|
306 * @return {tinymce.ui.Collection} Collection with matched elements. |
|
307 */ |
|
308 find: function(container) { |
|
309 var matches = [], i, l, selectors = this._selectors; |
|
310 |
|
311 function collect(items, selector, index) { |
|
312 var i, l, fi, fl, item, filters = selector[index]; |
|
313 |
|
314 for (i = 0, l = items.length; i < l; i++) { |
|
315 item = items[i]; |
|
316 |
|
317 // Run each filter agains the item |
|
318 for (fi = 0, fl = filters.length; fi < fl; fi++) { |
|
319 if (!filters[fi](item, i, l)) { |
|
320 fi = fl + 1; |
|
321 break; |
|
322 } |
|
323 } |
|
324 |
|
325 // All filters matched the item |
|
326 if (fi === fl) { |
|
327 // Matched item is on the last expression like: panel toolbar [button] |
|
328 if (index == selector.length - 1) { |
|
329 matches.push(item); |
|
330 } else { |
|
331 // Collect next expression type |
|
332 if (item.items) { |
|
333 collect(item.items(), selector, index + 1); |
|
334 } |
|
335 } |
|
336 } else if (filters.direct) { |
|
337 return; |
|
338 } |
|
339 |
|
340 // Collect child items |
|
341 if (item.items) { |
|
342 collect(item.items(), selector, index); |
|
343 } |
|
344 } |
|
345 } |
|
346 |
|
347 if (container.items) { |
|
348 for (i = 0, l = selectors.length; i < l; i++) { |
|
349 collect(container.items(), selectors[i], 0); |
|
350 } |
|
351 |
|
352 // Unique the matches if needed |
|
353 if (l > 1) { |
|
354 matches = unique(matches); |
|
355 } |
|
356 } |
|
357 |
|
358 // Fix for circular reference |
|
359 if (!Collection) { |
|
360 // TODO: Fix me! |
|
361 Collection = Selector.Collection; |
|
362 } |
|
363 |
|
364 return new Collection(matches); |
|
365 } |
|
366 }); |
|
367 |
|
368 return Selector; |
|
369 }); |