363 lines
8.3 KiB
JavaScript
Executable File
363 lines
8.3 KiB
JavaScript
Executable File
/**
|
|
* Selector.js
|
|
*
|
|
* Copyright, Moxiecode Systems AB
|
|
* Released under LGPL License.
|
|
*
|
|
* License: http://www.tinymce.com/license
|
|
* Contributing: http://www.tinymce.com/contributing
|
|
*/
|
|
|
|
/**
|
|
* Selector engine, enables you to select controls by using CSS like expressions.
|
|
* We currently only support basic CSS expressions to reduce the size of the core
|
|
* and the ones we support should be enough for most cases.
|
|
*
|
|
* @example
|
|
* Supported expressions:
|
|
* element
|
|
* element#name
|
|
* element.class
|
|
* element[attr]
|
|
* element[attr*=value]
|
|
* element[attr~=value]
|
|
* element[attr!=value]
|
|
* element[attr^=value]
|
|
* element[attr$=value]
|
|
* element:<state>
|
|
* element:not(<expession>)
|
|
* element:first
|
|
* element:last
|
|
* element:odd
|
|
* element:even
|
|
* element element
|
|
* element > element
|
|
*
|
|
* @class tinymce.ui.Selector
|
|
*/
|
|
define("tinymce/ui/Selector", [
|
|
"tinymce/util/Class",
|
|
"tinymce/util/Tools"
|
|
], function(Class, Tools) {
|
|
"use strict";
|
|
|
|
/**
|
|
* Produces an array with a unique set of objects. It will not compare the values
|
|
* but the references of the objects.
|
|
*
|
|
* @private
|
|
* @method unqiue
|
|
* @param {Array} array Array to make into an array with unique items.
|
|
* @return {Array} Array with unique items.
|
|
*/
|
|
function unique(array) {
|
|
var uniqueItems = [], i = array.length, item;
|
|
|
|
while (i--) {
|
|
item = array[i];
|
|
|
|
if (!item.__checked) {
|
|
uniqueItems.push(item);
|
|
item.__checked = 1;
|
|
}
|
|
}
|
|
|
|
i = uniqueItems.length;
|
|
while (i--) {
|
|
delete uniqueItems[i].__checked;
|
|
}
|
|
|
|
return uniqueItems;
|
|
}
|
|
|
|
var expression = /^([\w\\*]+)?(?:#([\w\\]+))?(?:\.([\w\\\.]+))?(?:\[\@?([\w\\]+)([\^\$\*!~]?=)([\w\\]+)\])?(?:\:(.+))?/i;
|
|
|
|
/*jshint maxlen:255 */
|
|
var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
|
|
whiteSpace = /^\s*|\s*$/g,
|
|
Collection;
|
|
|
|
var Selector = Class.extend({
|
|
/**
|
|
* Constructs a new Selector instance.
|
|
*
|
|
* @constructor
|
|
* @method init
|
|
* @param {String} selector CSS like selector expression.
|
|
*/
|
|
init: function(selector) {
|
|
var match = this.match;
|
|
|
|
function compileNameFilter(name) {
|
|
if (name) {
|
|
name = name.toLowerCase();
|
|
|
|
return function(item) {
|
|
return name === '*' || item.type === name;
|
|
};
|
|
}
|
|
}
|
|
|
|
function compileIdFilter(id) {
|
|
if (id) {
|
|
return function(item) {
|
|
return item._name === id;
|
|
};
|
|
}
|
|
}
|
|
|
|
function compileClassesFilter(classes) {
|
|
if (classes) {
|
|
classes = classes.split('.');
|
|
|
|
return function(item) {
|
|
var i = classes.length;
|
|
|
|
while (i--) {
|
|
if (!item.hasClass(classes[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
}
|
|
}
|
|
|
|
function compileAttrFilter(name, cmp, check) {
|
|
if (name) {
|
|
return function(item) {
|
|
var value = item[name] ? item[name]() : '';
|
|
|
|
return !cmp ? !!check :
|
|
cmp === "=" ? value === check :
|
|
cmp === "*=" ? value.indexOf(check) >= 0 :
|
|
cmp === "~=" ? (" " + value + " ").indexOf(" " + check + " ") >= 0 :
|
|
cmp === "!=" ? value != check :
|
|
cmp === "^=" ? value.indexOf(check) === 0 :
|
|
cmp === "$=" ? value.substr(value.length - check.length) === check :
|
|
false;
|
|
};
|
|
}
|
|
}
|
|
|
|
function compilePsuedoFilter(name) {
|
|
var notSelectors;
|
|
|
|
if (name) {
|
|
name = /(?:not\((.+)\))|(.+)/i.exec(name);
|
|
|
|
if (!name[1]) {
|
|
name = name[2];
|
|
|
|
return function(item, index, length) {
|
|
return name === 'first' ? index === 0 :
|
|
name === 'last' ? index === length - 1 :
|
|
name === 'even' ? index % 2 === 0 :
|
|
name === 'odd' ? index % 2 === 1 :
|
|
item[name] ? item[name]() :
|
|
false;
|
|
};
|
|
} else {
|
|
// Compile not expression
|
|
notSelectors = parseChunks(name[1], []);
|
|
|
|
return function(item) {
|
|
return !match(item, notSelectors);
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
function compile(selector, filters, direct) {
|
|
var parts;
|
|
|
|
function add(filter) {
|
|
if (filter) {
|
|
filters.push(filter);
|
|
}
|
|
}
|
|
|
|
// Parse expression into parts
|
|
parts = expression.exec(selector.replace(whiteSpace, ''));
|
|
|
|
add(compileNameFilter(parts[1]));
|
|
add(compileIdFilter(parts[2]));
|
|
add(compileClassesFilter(parts[3]));
|
|
add(compileAttrFilter(parts[4], parts[5], parts[6]));
|
|
add(compilePsuedoFilter(parts[7]));
|
|
|
|
// Mark the filter with psuedo for performance
|
|
filters.psuedo = !!parts[7];
|
|
filters.direct = direct;
|
|
|
|
return filters;
|
|
}
|
|
|
|
// Parser logic based on Sizzle by John Resig
|
|
function parseChunks(selector, selectors) {
|
|
var parts = [], extra, matches, i;
|
|
|
|
do {
|
|
chunker.exec("");
|
|
matches = chunker.exec(selector);
|
|
|
|
if (matches) {
|
|
selector = matches[3];
|
|
parts.push(matches[1]);
|
|
|
|
if (matches[2]) {
|
|
extra = matches[3];
|
|
break;
|
|
}
|
|
}
|
|
} while (matches);
|
|
|
|
if (extra) {
|
|
parseChunks(extra, selectors);
|
|
}
|
|
|
|
selector = [];
|
|
for (i = 0; i < parts.length; i++) {
|
|
if (parts[i] != '>') {
|
|
selector.push(compile(parts[i], [], parts[i - 1] === '>'));
|
|
}
|
|
}
|
|
|
|
selectors.push(selector);
|
|
|
|
return selectors;
|
|
}
|
|
|
|
this._selectors = parseChunks(selector, []);
|
|
},
|
|
|
|
/**
|
|
* Returns true/false if the selector matches the specified control.
|
|
*
|
|
* @method match
|
|
* @param {tinymce.ui.Control} control Control to match agains the selector.
|
|
* @param {Array} selectors Optional array of selectors, mostly used internally.
|
|
* @return {Boolean} true/false state if the control matches or not.
|
|
*/
|
|
match: function(control, selectors) {
|
|
var i, l, si, sl, selector, fi, fl, filters, index, length, siblings, count, item;
|
|
|
|
selectors = selectors || this._selectors;
|
|
for (i = 0, l = selectors.length; i < l; i++) {
|
|
selector = selectors[i];
|
|
sl = selector.length;
|
|
item = control;
|
|
count = 0;
|
|
|
|
for (si = sl - 1; si >= 0; si--) {
|
|
filters = selector[si];
|
|
|
|
while (item) {
|
|
// Find the index and length since a psuedo filter like :first needs it
|
|
if (filters.psuedo) {
|
|
siblings = item.parent().items();
|
|
index = Tools.inArray(item, siblings);
|
|
length = siblings.length;
|
|
}
|
|
|
|
for (fi = 0, fl = filters.length; fi < fl; fi++) {
|
|
if (!filters[fi](item, index, length)) {
|
|
fi = fl + 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (fi === fl) {
|
|
count++;
|
|
break;
|
|
} else {
|
|
// If it didn't match the right most expression then
|
|
// break since it's no point looking at the parents
|
|
if (si === sl - 1) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
item = item.parent();
|
|
}
|
|
}
|
|
|
|
// If we found all selectors then return true otherwise continue looking
|
|
if (count === sl) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Returns a tinymce.ui.Collection with matches of the specified selector inside the specified container.
|
|
*
|
|
* @method find
|
|
* @param {tinymce.ui.Control} container Container to look for items in.
|
|
* @return {tinymce.ui.Collection} Collection with matched elements.
|
|
*/
|
|
find: function(container) {
|
|
var matches = [], i, l, selectors = this._selectors;
|
|
|
|
function collect(items, selector, index) {
|
|
var i, l, fi, fl, item, filters = selector[index];
|
|
|
|
for (i = 0, l = items.length; i < l; i++) {
|
|
item = items[i];
|
|
|
|
// Run each filter agains the item
|
|
for (fi = 0, fl = filters.length; fi < fl; fi++) {
|
|
if (!filters[fi](item, i, l)) {
|
|
fi = fl + 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// All filters matched the item
|
|
if (fi === fl) {
|
|
// Matched item is on the last expession like: panel toolbar [button]
|
|
if (index == selector.length - 1) {
|
|
matches.push(item);
|
|
} else {
|
|
// Collect next expession type
|
|
if (item.items) {
|
|
collect(item.items(), selector, index + 1);
|
|
}
|
|
}
|
|
} else if (filters.direct) {
|
|
return;
|
|
}
|
|
|
|
// Collect child items
|
|
if (item.items) {
|
|
collect(item.items(), selector, index);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (container.items) {
|
|
for (i = 0, l = selectors.length; i < l; i++) {
|
|
collect(container.items(), selectors[i], 0);
|
|
}
|
|
|
|
// Unique the matches if needed
|
|
if (l > 1) {
|
|
matches = unique(matches);
|
|
}
|
|
}
|
|
|
|
// Fix for circular reference
|
|
if (!Collection) {
|
|
// TODO: Fix me!
|
|
Collection = Selector.Collection;
|
|
}
|
|
|
|
return new Collection(matches);
|
|
}
|
|
});
|
|
|
|
return Selector;
|
|
}); |