mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
241bff3df9
git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@92538 467b73ca-7a2a-4603-9d3b-597d59a354a9
453 lines
14 KiB
JavaScript
453 lines
14 KiB
JavaScript
var console;
|
|
|
|
(function($) {
|
|
|
|
/** What to call to run a function 'soon'. Normally setTimeout, but for syncronous mode we override so soon === now */
|
|
var runSoon = window.setTimeout;
|
|
|
|
/** Stores a count of definitions, so that we can sort identical selectors by definition order */
|
|
var rulecount = 0;
|
|
|
|
/** Utility to optionally display warning messages depending on level */
|
|
var warn = function(message, level) {
|
|
if (level <= $.concrete.warningLevel && console && console.log) console.log(message);
|
|
}
|
|
|
|
/** A property definition */
|
|
$.property = function(options) {
|
|
if (this instanceof $.property) this.options = options;
|
|
else return new $.property(options);
|
|
}
|
|
$.extend($.property, {
|
|
/**
|
|
* Strings for how to cast a value to a specific type. Used in some nasty meta-programming stuff below to try and
|
|
* keep property access as fast as possible
|
|
*/
|
|
casters: {
|
|
'int': 'Math.round(parseFloat(v));',
|
|
'float': 'parseFloat(v);',
|
|
'string': '""+v;'
|
|
},
|
|
|
|
getter: function(options) {
|
|
options = options || {};
|
|
|
|
if (options.initial === undefined) return function(){ return this.d()[arguments.callee.pname] };
|
|
|
|
var getter = function(){
|
|
var d = this.d(); var k = arguments.callee.pname;
|
|
return d.hasOwnProperty(k) ? d[k] : (d[k] = arguments.callee.initial);
|
|
};
|
|
var v = options.initial;
|
|
getter.initial = options.restrict ? eval($.property.casters[options.restrict]) : v;
|
|
|
|
return getter;
|
|
},
|
|
|
|
setter: function(options){
|
|
options = options || {};
|
|
if (options.restrict) {
|
|
var restrict = options.restrict;
|
|
return new Function('v', 'return this.d()[arguments.callee.pname] = ' + $.property.casters[options.restrict]);
|
|
}
|
|
|
|
return function(v){ return this.d()[arguments.callee.pname] = v; }
|
|
}
|
|
});
|
|
$.extend($.property.prototype, {
|
|
getter: function(){
|
|
return $.property.getter(this.options);
|
|
},
|
|
setter: function(){
|
|
return $.property.setter(this.options);
|
|
}
|
|
});
|
|
|
|
var Rule = Base.extend({
|
|
init: function(selector, name) {
|
|
this.selector = selector;
|
|
this.specifity = selector.specifity();
|
|
this.important = 0;
|
|
this.name = name;
|
|
this.rulecount = rulecount++;
|
|
}
|
|
});
|
|
|
|
Rule.compare = function(a, b) {
|
|
var as = a.specifity, bs = b.specifity;
|
|
|
|
return (a.important - b.important) ||
|
|
(as[0] - bs[0]) ||
|
|
(as[1] - bs[1]) ||
|
|
(as[2] - bs[2]) ||
|
|
(a.rulecount - b.rulecount) ;
|
|
}
|
|
|
|
$.fn._super = function(){
|
|
var rv, i = this.length;
|
|
while (i--) {
|
|
var el = this[0];
|
|
rv = el.f(el, arguments, el.i);
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
var namespaces = {};
|
|
|
|
var Namespace = Base.extend({
|
|
init: function(name){
|
|
if (name && !name.match(/^[A-Za-z0-9.]+$/)) warn('Concrete namespace '+name+' is not formatted as period seperated identifiers', $.concrete.WARN_LEVEL_BESTPRACTISE);
|
|
name = name || '__base';
|
|
|
|
this.name = name;
|
|
this.proxies = {};
|
|
this.store = {};
|
|
|
|
namespaces[name] = this;
|
|
|
|
var self = this;
|
|
this.$ = function() {
|
|
var jq = $.apply(window, arguments);
|
|
jq.namespace = self;
|
|
return jq;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Returns a function that does selector matching against the function list for a function name
|
|
* Used by proxy for all calls, and by ctorProxy to handle _super calls
|
|
* @param {String} name - name of the function as passed in the construction object
|
|
* @param {String} funcprop - the property on the Rule object that gives the actual function to call
|
|
*/
|
|
one: function(name, funcprop) {
|
|
var namespace = this;
|
|
var funcs = this.store[name];
|
|
|
|
var one = function(el, args, i){
|
|
if (i === undefined) i = funcs.length;
|
|
while (i--) {
|
|
if (funcs[i].selector.matches(el)) {
|
|
var ret, tmp_i = el.i, tmp_f = el.f;
|
|
el.i = i; el.f = one;
|
|
try { ret = funcs[i][funcprop].apply(namespace.$(el), args); }
|
|
finally { el.i = tmp_i; el.f = tmp_f; }
|
|
return ret;
|
|
}
|
|
}
|
|
}
|
|
|
|
return one;
|
|
},
|
|
|
|
/**
|
|
* A proxy is a function attached to a callable object (either the base jQuery.fn or a subspace object) which handles
|
|
* finding and calling the correct function for each member of the current jQuery context
|
|
* @param {String} name - name of the function as passed in the construction object
|
|
*/
|
|
build_proxy: function(name) {
|
|
var one = this.one(name, 'func');
|
|
|
|
var prxy = function() {
|
|
var rv, ctx = $(this.__context || this);
|
|
|
|
var i = ctx.length;
|
|
while (i--) rv = one(ctx[i], arguments);
|
|
return rv;
|
|
};
|
|
|
|
return prxy;
|
|
},
|
|
|
|
build_jquery_injection: function(name) {
|
|
if (!$.fn[name]) {
|
|
$.fn[name] = function() {
|
|
// Try bound namespace
|
|
var namespace = this.namespace;
|
|
// If that doesn't exist, or doesn't have function, try root namespace
|
|
if (!namespace || !namespace.proxies[name]) namespace = namespaces.__base;
|
|
// If that doesn't exist, throw error
|
|
if (!namespace.proxies[name]) {
|
|
throw new ReferenceError('Concrete function '+name+' not found in ' + (this.namespace ? ('namespace '+this.namespace.name+' or root namespace') : 'root namespace'));
|
|
}
|
|
|
|
namespace.__context = null;
|
|
return namespace.proxies[name].apply(this, arguments);
|
|
}
|
|
$.fn[name].concrete = true;
|
|
}
|
|
|
|
if (!$.fn[name].concrete) {
|
|
warn('Warning: Concrete function '+name+' clashes with regular jQuery function - concrete function will not be callable directly on jQuery object', $.concrete.WARN_LEVEL_IMPORTANT);
|
|
}
|
|
},
|
|
|
|
bind_proxy: function(selector, name, func) {
|
|
var funcs = this.store[name] || (this.store[name] = []) ;
|
|
|
|
var rule = funcs[funcs.length] = Rule(selector, name); rule.func = func;
|
|
funcs.sort(Rule.compare);
|
|
|
|
if (!this.proxies[name]) this.proxies[name] = this.build_proxy(name);
|
|
this.build_jquery_injection(name);
|
|
},
|
|
|
|
bind_event: function(selector, name, func) {
|
|
var funcs = this.store[name] || (this.store[name] = []) ;
|
|
|
|
var rule = funcs[funcs.length] = Rule(selector, name); rule.func = func;
|
|
funcs.sort(Rule.compare);
|
|
|
|
if (!funcs.proxy) {
|
|
funcs.proxy = this.build_proxy(name);
|
|
$(selector.selector).live(match[1], funcs.proxy);
|
|
}
|
|
},
|
|
|
|
bind_condesc: function(selector, name, func) {
|
|
var ctors = this.store.ctors || (this.store.ctors = []) ;
|
|
|
|
var rule;
|
|
for (var i = 0 ; i < ctors.length; i++) {
|
|
if (ctors[i].selector.selector == selector.selector) {
|
|
rule = ctors[i]; break;
|
|
}
|
|
}
|
|
if (!rule) {
|
|
rule = ctors[ctors.length] = Rule(selector, 'ctors');
|
|
ctors.sort(Rule.compare);
|
|
}
|
|
|
|
rule[name] = func;
|
|
|
|
if (!ctors[name+'proxy']) {
|
|
var one = this.one('ctors', name);
|
|
var namespace = this;
|
|
|
|
var proxy = function(els, i, func) {
|
|
var j = els.length;
|
|
while (j--) {
|
|
var el = els[j];
|
|
|
|
var tmp_i = el.i, tmp_f = el.f;
|
|
el.i = i; el.f = one;
|
|
try { func.call(namespace.$(el)); }
|
|
catch(e) { el.i = tmp_i; el.f = tmp_f; }
|
|
}
|
|
}
|
|
|
|
ctors[name+'proxy'] = proxy;
|
|
}
|
|
},
|
|
|
|
add: function(selector, data) {
|
|
for (var k in data) {
|
|
var v = data[k];
|
|
|
|
if ($.isFunction(v)) {
|
|
if (k == 'onmatch' || k == 'onunmatch') {
|
|
this.bind_condesc(selector, k, v);
|
|
}
|
|
else if (match = k.match(/^on(.*)/)) {
|
|
this.bind_event(selector, k, v);
|
|
}
|
|
else {
|
|
this.bind_proxy(selector, k, v);
|
|
}
|
|
}
|
|
else {
|
|
var g, s;
|
|
|
|
if (k.charAt(0) != k.charAt(0).toUpperCase()) warn('Concrete property '+k+' does not start with a capital letter', $.concrete.WARN_LEVEL_BESTPRACTISE);
|
|
|
|
if (v == $.property || v instanceof $.property) {
|
|
g = v.getter(); s = v.setter();
|
|
}
|
|
else {
|
|
var p = $.property({initial: v}); g = p.getter(); s = p.setter();
|
|
}
|
|
|
|
g.pname = s.pname = k;
|
|
this.bind_proxy(selector, k, g);
|
|
this.bind_proxy(selector, 'set'+k, s);
|
|
}
|
|
}
|
|
},
|
|
|
|
has: function(ctx, name) {
|
|
var rulelist = this.store[name];
|
|
if (!rulelist) return false;
|
|
|
|
/* We go forward this time, since low specifity is likely to knock out a bunch of elements quickly */
|
|
for (var i = 0 ; i < rulelist.length; i++) {
|
|
ctx = ctx.not(rulelist[i].selector);
|
|
if (!ctx.length) return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Main concrete function. Used for new definitions, calling into a namespace (or forcing the base namespace) and entering a using block
|
|
*
|
|
*/
|
|
$.fn.concrete = function() {
|
|
var i = 0;
|
|
var selector = $.selector(this.selector);
|
|
|
|
var namespace = namespaces.__base || Namespace();
|
|
if (typeof arguments[i] == 'string') {
|
|
namespace = namespaces[arguments[i]] || Namespace(arguments[i]);
|
|
i++;
|
|
}
|
|
|
|
while (i < arguments.length) {
|
|
var res = arguments[i];
|
|
// If it's a function, call it - either it's a using block or it's a concrete definition builder
|
|
if ($.isFunction(res)) {
|
|
if (res.length != 1) warn('Function block inside concrete definition does not take $ argument properly', $.concrete.WARN_LEVEL_IMPORTANT);
|
|
res = res.call(this, namespace.$);
|
|
}
|
|
else if (namespace.name != '__base') warn('Raw object inside namespaced ('+namespace.name+') concrete definition - namespace lookup will not work properly', $.concrete.WARN_LEVEL_IMPORTANT);
|
|
|
|
// Now if we still have a concrete definition object, inject it into namespace
|
|
if (res) namespace.add(selector, res);
|
|
i++
|
|
}
|
|
|
|
namespace.proxies.__context = this;
|
|
return namespace.proxies;
|
|
}
|
|
|
|
/**
|
|
* A couple of utility functions for accessing the store outside of this closure, and for making things
|
|
* operate in a little more easy-to-test manner
|
|
*/
|
|
$.concrete = {
|
|
|
|
/**
|
|
* Get all the namespaces. Usefull for introspection? Internal interface of Namespace not guaranteed consistant
|
|
*/
|
|
namespaces: function() { return namespaces; },
|
|
|
|
/**
|
|
* Remove all concrete rules
|
|
*/
|
|
clear_all_rules: function() {
|
|
// Remove proxy functions
|
|
for (var k in $.fn) { if ($.fn[k].concrete) delete $.fn[k] ; }
|
|
// Remove namespaces, and start over again
|
|
namespaces = [];
|
|
},
|
|
|
|
/**
|
|
* Make onmatch and onunmatch work in synchronous mode - that is, new elements will be detected immediately after
|
|
* the DOM manipulation that made them match. This is only really useful for during testing, since it's pretty slow
|
|
* (otherwise we'd make it the default).
|
|
*/
|
|
synchronous_mode: function() {
|
|
if (check_id) clearTimeout(check_id); check_id = null;
|
|
runSoon = function(func, delay){ func.call(this); return null; }
|
|
},
|
|
|
|
/**
|
|
* Trigger onmatch and onunmatch now - usefull for after DOM manipulation by methods other than through jQuery.
|
|
* Called automatically on document.ready
|
|
*/
|
|
triggerMatching: function() {
|
|
matching();
|
|
},
|
|
|
|
WARN_LEVEL_NONE: 0,
|
|
WARN_LEVEL_IMPORTANT: 1,
|
|
WARN_LEVEL_BESTPRACTISE: 2,
|
|
|
|
/**
|
|
* Warning level. Set to a higher level to get warnings dumped to console.
|
|
*/
|
|
warningLevel: 0
|
|
}
|
|
|
|
var check_id = null;
|
|
|
|
/**
|
|
* Finds all the elements that now match a different rule (or have been removed) and call onmatch on onunmatch as appropriate
|
|
*
|
|
* Because this has to scan the DOM, and is therefore fairly slow, this is normally triggered off a short timeout, so that
|
|
* a series of DOM manipulations will only trigger this once.
|
|
*
|
|
* The downside of this is that things like:
|
|
* $('#foo').addClass('tabs'); $('#foo').tabFunctionBar();
|
|
* won't work.
|
|
*/
|
|
function matching() {
|
|
// For every namespace
|
|
for (var k in namespaces) {
|
|
// That has constructors or destructors
|
|
var ctors = namespaces[k].store.ctors;
|
|
if (ctors) {
|
|
|
|
// Keep a record of elements that have matched already
|
|
var matched = $([]), match, add, rem;
|
|
// Stepping through each selector from most to least specific
|
|
var j = ctors.length;
|
|
while (j--) {
|
|
// Build some quick-acccess variables
|
|
var sel = ctors[j].selector.selector, ctor = ctors[j].onmatch; dtor = ctors[j].onunmatch;
|
|
// Get the list of elements that match this selector, that haven't yet matched a more specific selector
|
|
res = add = $(sel).not(matched);
|
|
|
|
// If this selector has a list of elements it matched against last time
|
|
if (ctors[j].cache) {
|
|
// Find the ones that are extra this time
|
|
add = res.not(ctors[j].cache);
|
|
// Find the ones that are gone this time
|
|
rem = ctors[j].cache.not(res);
|
|
// And call the desctructor on them
|
|
if (rem.length && dtor) ctors.onunmatchproxy(rem, j, dtor);
|
|
}
|
|
|
|
// Call the constructor on the newly matched ones
|
|
if (add.length && ctor) ctors.onmatchproxy(add, j, ctor);
|
|
|
|
// Add these matched ones to the list tracking all elements matched so far
|
|
matched = matched.add(res);
|
|
// And remember this list of matching elements again this selector, so next matching we can find the unmatched ones
|
|
ctors[j].cache = res;
|
|
}
|
|
}
|
|
}
|
|
|
|
check_id = null;
|
|
}
|
|
|
|
function registerMutateFunction() {
|
|
$.each(arguments, function(i,func){
|
|
var old = $.fn[func];
|
|
$.fn[func] = function() {
|
|
var rv = old.apply(this, arguments);
|
|
if (!check_id) check_id = runSoon(matching, 100);
|
|
return rv;
|
|
}
|
|
})
|
|
}
|
|
|
|
function registerSetterGetterFunction() {
|
|
$.each(arguments, function(i,func){
|
|
var old = $.fn[func];
|
|
$.fn[func] = function(a, b) {
|
|
var rv = old.apply(this, arguments);
|
|
if (!check_id && (b !== undefined || typeof a != 'string')) check_id = runSoon(matching, 100);
|
|
return rv;
|
|
}
|
|
})
|
|
}
|
|
|
|
// Register core DOM manipulation methods
|
|
registerMutateFunction('append', 'prepend', 'after', 'before', 'wrap', 'removeAttr', 'addClass', 'removeClass', 'toggleClass', 'empty', 'remove');
|
|
registerSetterGetterFunction('attr');
|
|
|
|
// And on DOM ready, trigger matching once
|
|
$(function(){ matching(); })
|
|
|
|
})(jQuery);
|