From cbb10922c5da0ff8c6183670fa7e11217f9e65e7 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Tue, 8 Feb 2011 13:14:52 +0100 Subject: [PATCH] Include the full Bootstrap library for now to support mouse events. --- src/Bootstrap.js | 5038 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 5032 insertions(+), 6 deletions(-) diff --git a/src/Bootstrap.js b/src/Bootstrap.js index 1d25262f..0f375eb1 100644 --- a/src/Bootstrap.js +++ b/src/Bootstrap.js @@ -1,30 +1,91 @@ -new function() { +/** + * Bootstrap JavaScript Library + * (c) 2006 - 2010 Juerg Lehni, http://scratchdisk.com/ + * + * Bootstrap is released under the MIT license + * http://bootstrap-js.net/ + * + * Inspirations: + * http://dean.edwards.name/weblog/2006/03/base/ + * http://dev.helma.org/Wiki/JavaScript+Inheritance+Sugar/ + * http://prototypejs.org/ + * http://mootools.net/ + * + * Some code in this file is based on Mootools.net and adapted to the + * architecture of Bootstrap, with added changes in design and architecture + * where deemeded necessary. + * See http://www.bootstrap-js.net/wiki/MootoolsDifferences + */ + +//////////////////////////////////////////////////////////////////////////////// +// Base + +new function() { // Bootstrap scope + // Fix __proto__ for browsers where it is not implemented (IE and Opera). + // Do this before anything else, for "var i in" to work without filtering. var fix = !this.__proto__ && [Function, Number, Boolean, String, Array, Date, RegExp]; if (fix) for (var i in fix) fix[i].prototype.__proto__ = fix[i].prototype; + /** + * Private function that checks if an object contains a given property. + * Naming it 'has' causes problems on Opera when defining + * Object.prototype.has, as the local version then seems to be overriden + * by that. Giving it a idfferent name fixes it. + */ + // Check if environment supports hasOwnProperty, and use a differnt version + // of has if ti does, for higher performance as checking on each has() call. + // All Browsers that need (IE and Opera) have hasOwnProperty, so + // the version without hasOwnProperty does not need to check for __proto__ var has = {}.hasOwnProperty ? function(obj, name) { return (!fix || name != '__proto__') && obj.hasOwnProperty(name); } : function(obj, name) { + // We need to filter out what does not belong to the object itself. + // This is done by comparing the value with the value of the same + // name in the prototype. If the value is equal it's defined in one + // of the prototypes, not the object itself. + // Object.prototype is untouched, so we cannot assume __proto__ to + // always be defined on legacy browsers. return obj[name] !== (obj.__proto__ || Object.prototype)[name]; }; + /** + * Private function that injects functions from src into dest, overriding + * (and inherinting from) base. if allowProto is set, the name "prototype" + * is inherited too. This is false for static fields, as prototype there + * points to the classes' prototype. + */ function inject(dest, src, enumerable, base, generics) { + /** + * Private function that injects one field with given name + */ function field(name, dontCheck, generics) { var val = src[name], func = typeof val == 'function', res = val, prev = dest[name]; + // Make generics first, as we might jump out bellow in the + // val !== (src.__proto__ || Object.prototype)[name] check, + // e.g. when explicitely reinjecting Array.prototype methods + // to produce generics of them. if (generics && func && (!src.preserve || !generics[name])) generics[name] = function(bind) { + // Do not call Array.slice generic here, as on Safari, + // this seems to confuse scopes (calling another + // generic from generic-producing code). return bind && dest[name].apply(bind, Array.prototype.slice.call(arguments, 1)); } + // TODO: On proper JS implementation, dontCheck is always set + // Add this with a compile switch here! if ((dontCheck || val !== undefined && has(src, name)) && (!prev || !src.preserve)) { if (func) { if (prev && /\bthis\.base\b/.test(val)) { var fromBase = base && base[name] == prev; res = (function() { + // Look up the base function each time if we can, + // to reflect changes to the base class after + // inheritance. var tmp = this.base; this.base = fromBase ? base[name] : prev; try { return val.apply(this, arguments); } @@ -35,48 +96,102 @@ new function() { dest[name] = res; } } + // Iterate through all definitions in src with an iteator function + // that checks if the field is a function that needs to be wrapped for + // calls of base. This is only needed if the function in base is + // different from the one in src, and if the one in src is actually + // calling base through base. the string of the function is parsed + // for base to detect calls. + // dest[name] then is set to either src[name] or the wrapped function. if (src) { for (var name in src) if (has(src, name) && !/^(statics|generics|preserve|prototype|constructor|__proto__|toString|valueOf)$/.test(name)) field(name, true, generics); + // IE (and some other browsers?) never enumerate these, even + // if they are simply set on an object. Force their creation. + // Do not create generics for these, and check them for not + // being defined (by passing undefined for dontCheck). field('toString'); field('valueOf'); } } + /** + * Private function that creates a constructor to extend the given object. + * When this constructor is called through new, a new object is craeted + * that inherits all from obj. + */ function extend(obj) { + // Create the constructor for the new prototype that calls initialize + // if it is defined. function ctor(dont) { + // Fix __proto__ if (fix) this.__proto__ = obj; + // Call the constructor function, if defined and we're not inheriting + // in which case ctor.dont would be set, see further bellow. if (this.initialize && dont !== ctor.dont) return this.initialize.apply(this, arguments); } ctor.prototype = obj; + // Add a toString function that delegates to initialize if possible ctor.toString = function() { return (this.prototype.initialize || function() {}).toString(); } return ctor; } + // Now we can use the private inject to add methods to the Function.prototype inject(Function.prototype, { - inject: function(src) { + inject: function(src/*, ... */) { if (src) { var proto = this.prototype, base = proto.__proto__ && proto.__proto__.constructor; + // When called from extend, a third argument is passed, pointing + // to the base class (the constructor). + // this variable is needed for inheriting static fields and proper + // lookups of base on each call (see bellow) inject(proto, src, false, base && base.prototype, src.generics && this); + // Define new static fields as enumerable, and inherit from base. + // enumerable is necessary so they can be copied over from base, + // and it does not disturb to be enumerable in the constructor. inject(this, src.statics, true, base); } + // If there are more than one argument, loop through them and call + // inject again. Do not simple inline the above code in one loop, + // since each of the passed objects might override this.inject. for (var i = 1, l = arguments.length; i < l; i++) this.inject(arguments[i]); return this; }, - extend: function(src) { + extend: function(src/* , ... */) { + // The new prototype extends the constructor on which extend is called. + // Fix constructor + // TODO: Consider using Object.create instead of using this.dont if + // available? var proto = new this(this.dont), ctor = proto.constructor = extend(proto); + // An object to be passed as the first parameter in constructors + // when initialize should not be called. This needs to be a property + // of the created constructor, so that if .extend is called on native + // constructors or constructors not created through .extend, + // this.dont will be undefined and no value will be passed to the + // constructor that would not know what to do with it. ctor.dont = {}; + // Copy over static fields, as prototype-like inheritance + // is not possible for static fields. Mark them as enumerable + // so they can be copied over again. + // TODO: This needs fixing for versioning on the server! inject(ctor, this, true); + // Inject all the definitions in src + // Use the new inject instead of the one in ctor, in case it was + // overriden. + // Needed when overriding static inject as in HtmlElements.js. + // Only inject if there's something to actually inject. return arguments.length ? this.inject.apply(ctor, arguments) : ctor; }, pretend: function(fn) { + // Redirect toString to the one from the original function + // to "hide" the wrapper function this.toString = function() { return fn.toString(); } @@ -92,7 +207,14 @@ new function() { ? Array : Hash).prototype.each.call(obj, iter, bind) : bind; } + // Let's not touch Object.prototype Base = Object.extend({ + /** + * Returns true if the object contains a property with the given name, + * false otherwise. + * Just like in .each, objects only contained in the prototype(s) are + * filtered. + */ has: function(name) { return has(this, name); }, @@ -101,27 +223,47 @@ new function() { return each(this, iter, bind); }, - inject: function() { + /** + * Injects the fields from the given object, adding base functionality + */ + inject: function(/* src, ... */) { for (var i = 0, l = arguments.length; i < l; i++) inject(this, arguments[i]); return this; }, - extend: function() { + /** + * Returns a new object that inherits all properties from "this", through + * proper JS inheritance, not copying. + * Optionally, src and hide parameters can be passed to fill in the + * newly created object just like in inject(), to copy the behavior + * of Function.prototype.extend. + */ + extend: function(/* src, ... */) { + // Notice the "new" here: the private extend returns a constructor + // as it's used for Function.prototype.extend as well. But when + // extending objects, we want to return a new object that inherits + // from "this". In that case, the constructor is never used again, + // its just created to create a new object with the proper + // inheritance set and is garbage collected right after. var res = new (extend(this)); return res.inject.apply(res, arguments); }, statics: { + // Expose some local privates as Base generics. has: has, each: each, type: function(obj) { + // Handle elements, as needed by DomNode.js return (obj || obj === 0) && ( obj._type || obj.nodeName && ( obj.nodeType == 1 && 'element' || obj.nodeType == 3 && 'textnode' || obj.nodeType == 9 && 'document') + // TODO: Find better way to identify windows and use + // the same cod ein DomNode$getConstructor || obj.location && obj.frames && obj.history && 'window' || typeof obj) || null; }, @@ -130,6 +272,11 @@ new function() { return !!(obj || obj === 0); }, + /** + * Returns the first argument that is defined. + * Null is counted as defined too, since !== undefined is used for + * comparisons. In this it differs from Mootools! + */ pick: function() { for (var i = 0, l = arguments.length; i < l; i++) if (arguments[i] !== undefined) @@ -137,14 +284,38 @@ new function() { return null; }, + /** + * Converts the argument to an iterator function. If none is + * specified, the identity function is returned. + * This supports normal functions, which are returned unmodified, + * and values to compare to. Wherever this function is used in the + * Enumerable functions, a value, a Function or null may be passed. + */ iterator: function(iter) { return !iter ? function(val) { return val } : typeof iter != 'function' ? function(val) { return val == iter } : iter; + /* + // For RegExp support, used this: + else switch (Base.type(iter)) { + case 'function': return iter; + case 'regexp': return function(val) { return iter.test(val) }; + default: return function(val) { return val == iter }; + } + */ }, + /** + * A special constant, to be thrown by closures passed to each() + * + * $continue / Base.next is not implemented, as the same + * functionality can achieved by using return in the closure. + * In prototype, the implementation of $continue also leads to a huge + * speed decrease, as the closure is wrapped in another closure that + * does nothing else than handling $continue. + */ stop: {} } }, { @@ -153,8 +324,25 @@ new function() { debug: function() { return /^(string|number|function|regexp)$/.test(Base.type(this)) ? this : Base.each(this, function(val, key) { this.push(key + ': ' + val); }, []).join(', '); + /* + switch (Base.type(this)) { + case 'string': case 'number': case 'regexp': + return this; + case 'function': + return 'function ' + (this.name || ''); + } + var buf = []; + for (var key in this) + if (Base.has(this, key)) + buf.push(key + ': ' + Base.debug(this[key])); + return buf.join(', '); + */ }, + /** + * Creates a new object of the same type and copies over all + * name / value pairs from this object. + */ clone: function() { return Base.each(this, function(val, i) { this[i] = val; @@ -167,4 +355,4842 @@ new function() { }, []).join('&'); } }); -} \ No newline at end of file +} + +$each = Base.each; +$type = Base.type; +$check = Base.check; +$pick = Base.pick; +$stop = $break = Base.stop; + +//////////////////////////////////////////////////////////////////////////////// +// Enumerable + +/** + * The Enumerable interface. To add enumerable functionality to any prototype, + * just use Constructor.inject(Enumerable); + * This adds the function .each() that can handle both arrays (detected through + * .length) and dictionaries (if it's not an array, enumerating with for-in). + */ + +// TODO: Base.each is used mostly so functions can be generalized. +// But that's not enough, since find and others are still called +// on this. +Enumerable = { + generics: true, + // Make sure it's not overriding native functions when injecting into Array + preserve: true, + + /** + * Searches the list for the first element where the passed iterator + * does not return null and returns an object containing key, value and + * iterator result for the given entry. This is used in find and remove. + * If no iterator is passed, the value is used directly. + */ + findEntry: function(iter, bind) { + var that = this, iter = Base.iterator(iter), ret = null; + Base.each(this, function(val, key) { + var res = iter.call(bind, val, key, that); + if (res) { + ret = { key: key, value: val, result: res }; + throw Base.stop; + } + }); + return ret; + }, + + /** + * Calls the passed iterator for each element and returns the first + * result of the iterator calls that is not null. + * If no iterator is passed, the value is used directly. + */ + find: function(iter, bind) { + var entry = this.findEntry(iter, bind); + return entry && entry.result; + }, + + contains: function(iter) { + return !!this.findEntry(iter); + }, + + remove: function(iter, bind) { + var entry = this.findEntry(iter, bind); + if (entry) { + delete this[entry.key]; + return entry.value; + } + }, + + /** + * Collects all elements for which the condition of the passed iterator + * or regular expression is true. + * This is compatible with JS 1.5's Array#filter + */ + filter: function(iter, bind) { + var that = this; + return Base.each(this, function(val, i) { + if (iter.call(bind, val, i, that)) + this[this.length] = val; + }, []); + }, + + /** + * Maps the result of the given iterator applied to each of the + * elements to an array and returns it. + * If no iterator is passed, the value is used directly. + * This is compatible with JS 1.5's Array#map + */ + map: function(iter, bind) { + var that = this; + return Base.each(this, function(val, i) { + this[this.length] = iter.call(bind, val, i, that); + }, []); + }, + + /** + * Returns true if the condition defined by the passed iterator is true + * for all elements, false otherwise. + * If no iterator is passed, the value is used directly. + * This is compatible with JS 1.5's Array#every + */ + every: function(iter, bind) { + var that = this; + return this.find(function(val, i) { + // as "this" is not used for anything else, use it for bind, + // so that lookups on the object are faster (according to + // benchmarking) + return !iter.call(this, val, i, that); + }, bind || null) == null; + // See #some for explanation of || null + }, + + /** + * Returns true if the condition defined by the passed iterator is true + * for one or more of the elements, false otherwise. + * If no iterator is passed, the value is used directly. + * This is compatible with JS 1.5's Array#some + */ + some: function(iter, bind) { + // Passing null instead of undefined causes bind not to be set to + // this, as we want the same behavior here as the native Array#some. + return this.find(iter, bind || null) != null; + }, + + /** + * Collects the result of the given iterator applied to each of the + * elements to an array and returns it. + * The difference to map is that it does not add null / undefined values. + */ + collect: function(iter, bind) { + var that = this, iter = Base.iterator(iter); + return Base.each(this, function(val, i) { + val = iter.call(bind, val, i, that); + if (val != null) + this[this.length] = val; + }, []); + }, + + /** + * Returns the maximum value of the result of the passed iterator + * applied to each element. + * If no iterator is passed, the value is used directly. + */ + max: function(iter, bind) { + var that = this, iter = Base.iterator(iter); + return Base.each(this, function(val, i) { + val = iter.call(bind, val, i, that); + if (val >= (this.max || val)) this.max = val; + }, {}).max; + }, + + /** + * Returns the minimum value of the result of the passed iterator + * applied to each element. + * If no iterator is passed, the value is used directly. + */ + min: function(iter, bind) { + var that = this, iter = Base.iterator(iter); + return Base.each(this, function(val, i) { + val = iter.call(bind, val, i, that); + if (val <= (this.min || val)) this.min = val; + }, {}).min; + }, + + /** + * Collects the values of the given property of each of the elements + * in an array and returns it. + */ + pluck: function(prop) { + return this.map(function(val) { + return val[prop]; + }); + }, + + /** + * Sorts the elements depending on the outcome of the passed iterator + * and returns the sorted list in an array. + * Inspired by Prototype.js + */ + sortBy: function(iter, bind) { + var that = this, iter = Base.iterator(iter); + // TODO: Does not work as generics + return this.map(function(val, i) { + return { value: val, compare: iter.call(bind, val, i, that) }; + }, bind).sort(function(left, right) { + var a = left.compare, b = right.compare; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + /** + * Converts the Enumerable to a normal array. + */ + toArray: function() { + return this.map(function(value) { + return value; + }); + } +}; + +//////////////////////////////////////////////////////////////////////////////// +// Hash + +/** + * As Object only defines each and two other basic functions to avoid name + * clashes in all other prototypes, define a second prototype called Hash, + * which basically does the same but fully implements Enumberable. + * Also note the difference to Prototype.js, where Hash does not iterate + * in the same way. Instead of creating a new key / value pair object for + * each element and passing the numerical index of it in the iteration as a + * second argument, use the key as the index, and the value as the first + * element. This is much simpler and faster, and I have not yet found out the + * advantage of how Prototype handles it. + */ +Hash = Base.extend(Enumerable, { + generics: true, + + /** + * Constructs a new Hash. The constructor takes a variable amount of + * argument objects of which the fields are all merged into the hash. + */ + initialize: function(arg) { + // If the first argument is a string, assume pairs of key/value arguments, + // to be set on the hash. + if (typeof arg == 'string') { + for (var i = 0, l = arguments.length; i < l; i += 2) + this[arguments[i]] = arguments[i + 1]; + } else { + this.append.apply(this, arguments); + } + // Explicitly return object as it is used in Hash.create's return statement + return this; + }, + + each: function(iter, bind) { + // Do not use Object.keys for iteration as iterators might modify + // the object we're iterating over, making the hasOwnProperty still + // necessary. + // Rely on Base.has instead of hasOwnProperty directly. + var bind = bind || this, iter = Base.iterator(iter), has = Base.has; + try { + for (var i in this) + if (has(this, i)) + iter.call(bind, this[i], i, this); + } catch (e) { + if (e !== Base.stop) throw e; + } + return bind; + }, + + /** + * append is faster and more low level than merge, completely based on + * for-in and Base.has, and not relying on any .each function, so can + * be used early in the bootstrapping process. + */ + append: function() { + for (var i = 0, l = arguments.length; i < l; i++) { + var obj = arguments[i]; + for (var key in obj) + if (Base.has(obj, key)) + this[key] = obj[key]; + } + return this; + }, + + /** + * Deep merges with the given enumerable object and returns the modifed hash. + * Recursively calls merge or clone on value pairs if they are dictionaries. + */ + merge: function() { + // Allways use Base.each() as we don't know wether the passed object + // really inherits from Base. + // Do not rely on .each / .forEach, so merge can be used in low level + // operations such as insertion of such functions as well. Just use + // the Base.has generic to filter out parent values. + return Array.each(arguments, function(obj) { + Base.each(obj, function(val, key) { + this[key] = Base.type(this[key]) == 'object' + ? Hash.prototype.merge.call(this[key], val) + : Base.type(val) == 'object' ? Base.clone(val) : val; + }, this); + }, this); + }, + + /** + * Returns the keys of all elements in an array. + */ + getKeys: function() { + return Hash.getKeys(this); + }, + + /** + * Does the same as toArray(), but renamed to go together with getKeys() + */ + getValues: Enumerable.toArray, + + getSize: function() { + return this.each(function() { + this.size++; + }, { size: 0 }).size; + }, + + statics: { + /** + * Converts the passed object to a hash. + * Warning: Does not create a new instance if it is a hash already! + */ + create: function(obj) { + return arguments.length == 1 && obj.constructor == Hash + ? obj : Hash.prototype.initialize.apply(new Hash(), arguments); + }, + + /** + * Returns the keys of all elements in an array. + * Uses the native Object.keys if available. + */ + getKeys: Object.keys || function(obj) { + return Hash.map(function(val, key) { + return key; + }); + } + } +}); + +// Short-cut to Hash.create +$H = Hash.create; + +//////////////////////////////////////////////////////////////////////////////// +// Array + +// Define standard methods that might not be present and only get injected +// if they don't exist because of preserve: true +Array.inject({ + generics: true, + preserve: true, + // tell Base.type what to return for arrays. + _type: 'array', + + forEach: function(iter, bind) { + for (var i = 0, l = this.length; i < l; i++) + iter.call(bind, this[i], i, this); + }, + + indexOf: function(obj, i) { + i = i || 0; + if (i < 0) i = Math.max(0, this.length + i); + for (var l = this.length; i < l; i++) + if (this[i] == obj) return i; + return -1; + }, + + lastIndexOf: function(obj, i) { + i = i != null ? i : this.length - 1; + if (i < 0) i = Math.max(0, this.length + i); + for (; i >= 0; i--) + if (this[i] == obj) return i; + return -1; + }, + + filter: function(iter, bind) { + var res = []; + for (var i = 0, l = this.length; i < l; i++) { + var val = this[i]; + if (iter.call(bind, val, i, this)) + res[res.length] = val; + } + return res; + }, + + map: function(iter, bind) { + var res = new Array(this.length); + for (var i = 0, l = this.length; i < l; i++) + res[i] = iter.call(bind, this[i], i, this); + return res; + }, + + every: function(iter, bind) { + for (var i = 0, l = this.length; i < l; i++) + if (!iter.call(bind, this[i], i, this)) + return false; + return true; + }, + + some: function(iter, bind) { + for (var i = 0, l = this.length; i < l; i++) + if (iter.call(bind, this[i], i, this)) + return true; + return false; + }, + + reduce: function(fn, value) { + var i = 0; + if (arguments.length < 2 && this.length) value = this[i++]; + for (var l = this.length; i < l; i++) + value = fn.call(null, value, this[i], i, this); + return value; + } +}, Enumerable, { + // TODO: this.each / this.findEntry / this.indexOf breaks many generics! + generics: true, + + each: function(iter, bind) { + try { + Array.prototype.forEach.call(this, Base.iterator(iter), bind = bind || this); + } catch (e) { + if (e !== Base.stop) throw e; + } + return bind; + }, + + collect: function(iter, bind) { + var that = this; + return this.each(function(val, i) { + if ((val = iter.call(bind, val, i, that)) != null) + this[this.length] = val; + }, []); + }, + + findEntry: function(iter, bind) { + // Use the faster indexOf in case we're not using iterator functions. + if (typeof iter != 'function') { + var i = this.indexOf(iter); + // Return the same result as if Enumerable.findEntry was used + // and the iter object was converter to an iterator. + return i == -1 ? null : { key: i, value: iter, result: iter }; + } + // Do not use this.base, as we might call this on non-arrays + return Enumerable.findEntry.call(this, iter, bind); + }, + + remove: function(iter, bind) { + var entry = this.findEntry(iter, bind); + if (entry) { + this.splice(entry.key, 1); + return entry.value; + } + }, + + /** + * Overrides the definition in Enumerable.toArray with a more efficient + * version. + */ + toArray: function() { + return Array.prototype.slice.call(this); + }, + + /** + * Clones the array. + */ + clone: function() { + return this.toArray(); + }, + + /** + * Clears the array. + */ + clear: function() { + this.length = 0; + }, + + /** + * Returns a compacted version of the array containing only + * elements that are not null. + */ + compact: function() { + return this.filter(function(value) { + return value != null; + }); + }, + + /** + * Appends the items of the passed array to this array. + */ + append: function(items) { + // It would be nice if calling push with the items of the array + // as arguments would work, but it does not for non-arrays: + // this.push.apply(this, items); + // this.length is explicitely altered, so non-array sub-prototypes + // can use it too. + for (var i = 0, l = items.length; i < l; i++) + this[this.length++] = items[i]; + return this; + }, + + /** + * Creates a hash object containing the array's values associated to the + * given keys as defined by obj. + * This is based on mootools' associate, but extended by the possibility + * to not pass an obj, or pass a function: + * - If obj is an array, its values are the new keys. + * - If obj is a hash object, mootools behavior is assumed. + * - If obj is not defined, it is set to the array itself, resulting in + * a hash with key and value set to the same (the initial array entry). + * - If obj is a function, it's passed to this.map(), and the resulting + * array is used for the key values. + */ + associate: function(obj) { + if (!obj) + obj = this; + else if (typeof obj == 'function') + obj = this.map(obj); + if (obj.length != null) { + var that = this; + return Base.each(obj, function(name, index) { + this[name] = that[index]; + if (index == that.length) + throw Base.stop; + }, {}); + } else { + // Produce a new bare object since we're deleting from it. + obj = Hash.append({}, obj); + // Use Base.each since this is also used for generics + return Array.each(this, function(val) { + var type = Base.type(val); + // Use Base.each since it's a bare object for speed reasons + // on the browser. + Base.each(obj, function(hint, name) { + if (hint == 'any' || type == hint) { + this[name] = val; + delete obj[name]; + throw Base.stop; + } + }, this); + }, {}); + } + }, + + /** + * adds all elements in the passed array, if they are not contained + * in the array already. + */ + /* TODO: needed? Call unite instead? or union? + include: function(obj) { + return Base.each(obj, function(val) { + if (this.indexOf(val) == -1) this.push(val); + }, this); + }, + */ + + /** + * Flattens multi-dimensional array structures by breaking down each + * sub-array into the main array. + */ + flatten: function() { + // Make it generics friendly through Array.each + return Array.each(this, function(val) { + if (val != null && val.flatten) this.append(val.flatten()); + else this.push(val); + }, []); + }, + + /** + * Swaps two elements of the object at the given indices, and returns + * the value that is placed at the first index. + */ + swap: function(i, j) { + var tmp = this[j]; + this[j] = this[i]; + this[i] = tmp; + return tmp; + }, + + /** + * Returns a copy of the array containing the elements in + * shuffled sequence. + */ + shuffle: function() { + var res = this.clone(); + var i = this.length; + while (i--) res.swap(i, Math.rand(i + 1)); + return res; + }, + + pick: function() { + return this[Math.rand(this.length)]; + }, + + /** + * Returns the first element of the array. + */ + getFirst: function() { + return this[0]; + }, + + /** + * Returns the last element of the array. + */ + getLast: function() { + return this[this.length - 1]; + } +}, new function() { + // Merge sutract / combine in one function through a producer: + function combine(subtract) { + return function(items) { + var res = new this.constructor(); + for (var i = this.length - 1; i >= 0; i--) + if (subtract == !Array.find(items, this[i])) + res.push(this[i]); + return res; + } + } + + return { + /** + * Returns a new array containing all objects from this array that are + * not contained in items. + */ + subtract: combine(true), + + /** + * Returns a new array containing all objects contained in both arrays. + */ + intersect: combine(false) + } +}); + +// Now add code that makes Array.extend() a possibilitiy: +Array.inject(new function() { + // Fields that are hidden in Array.prototype are explicitely copied over, + // so that they can be inherited in extend() below, and generics are created + // for them too. + var proto = Array.prototype, fields = ['push','pop','shift','unshift','sort', + 'reverse','join','slice','splice','forEach','indexOf','lastIndexOf', + 'filter','map','every','some','reduce','concat'].each(function(name) { + this[name] = proto[name]; + }, { generics: true, preserve: true }); + + // Make sure there are generics for all of them. Again this is not dangerous + // because we'rew using preserve: true + Array.inject(fields); + + // Now add the fields to be injected into sub-prototypes from Array. + // See Array.extend for more explanation. + Hash.append(fields, proto, { + /** + * Clears the array. + * For non-array sub-prototypes, setting this.length = 0 does not clear + * the array. Exlicit delete is needed. For sub-prototypes. + */ + clear: function() { + for (var i = 0, l = this.length; i < l; i++) + delete this[i]; + this.length = 0; + }, + + // Safari breaks native concat on sub classes of arrays. Simulate it here. + // TODO: Test if newer versions are find with this. + concat: function(list) { + return Browser.WEBKIT + ? new Array(this.length + list.length).append(this).append(list) + : Array.concat(this, list); + }, + + // The native toString does not work for classes inheriting from Array. + // but luckily join does the same and works. + toString: proto.join, + + // length is set so instances of array have it set to 0 to begin with. + // (any length modifying operation on them like #push will then + // define / modify the length field in the insance). + length: 0 + }); + + return { + statics: { + /** + * Creates an array from the past object. + */ + create: function(obj) { + if (obj == null) + return []; + if (obj.toArray) + return obj.toArray(); + if (typeof obj.length == 'number') + return Array.prototype.slice.call(obj); + return [obj]; + }, + + /** + * Makes sure the passed object is an array and converts it to one + * if not through Array.create. + */ + convert: function(obj) { + return Base.type(obj) == 'array' ? obj : Array.create(obj); + }, + + extend: function(src) { + // On IE browsers, we cannot directly inherit from Array + // by setting ctor.prototype = new Array(), as setting of #length + // on such instances is ignored. + // Simulate extending of Array, by actually extending Base and + // injecting the Array fields, which explicitely contain the + // native functions too (see bellow). + // Notice: since fields as the preserve flag set, the + // Array#clone() will not override the Base#clone method, + // so derived arrays will successfully clone themselves. + var ret = Base.extend(fields, src); + // The subclass can use the normal extend again: + ret.extend = Function.extend; + return ret; + } + } + }; +}); + +// Short-cut to Array.create +$A = Array.create; + +//////////////////////////////////////////////////////////////////////////////// +// Function + +Function.inject(new function() { + + function timer(set) { + return function(delay, bind, args) { + // It's a bit of a shame we can't use the ES5 bind() here easily: + var func = this.wrap(bind, args); + // If delay is not defined, execute right away and return the result + // of the function. This is used in fireEvent. + if (delay === undefined) + return func(); + var timer = set(func, delay); + func.clear = function() { + clearTimeout(timer); + clearInterval(timer); + }; + return func; + }; + } + + return { + generics: true, + preserve: true, + + delay: timer(setTimeout), + periodic: timer(setInterval), + + bind: function(bind) { + var that = this, slice = Array.prototype.slice, + args = arguments.length > 1 ? slice.call(arguments, 1) : null; + return function() { + return that.apply(bind, args ? arguments.length > 0 + ? args.concat(slice.call(arguments)) : args : arguments); + } + }, + + wrap: function(bind, args) { + var that = this; + return function() { + return that.apply(bind, args || arguments); + } + } + } +}); + +//////////////////////////////////////////////////////////////////////////////// +// Number + +Number.inject({ + // tell Base.type that number objects are numbers too. + _type: 'number', + + limit: function(min, max) { + return Math.min(max, Math.max(min, this)); + }, + + times: function(func, bind) { + for (var i = 0; i < this; i++) + func.call(bind, i); + return bind || this; + }, + + toInt: function(base) { + return parseInt(this, base || 10); + }, + + toFloat: function() { + return parseFloat(this); + }, + + toPaddedString: function(length, base, prefix) { + var str = this.toString(base || 10); + return (prefix || '0').times(length - str.length) + str; + } +}); + +//////////////////////////////////////////////////////////////////////////////// +// String + +String.inject({ + _type: 'string', + + test: function(exp, param) { + return new RegExp(exp, param || '').test(this); + }, + + /** + * Splits the string into an array of words. This can also be used on any + * String through $A as defined in Array.js, to work similarly to $w in Ruby + */ + toArray: function() { + return this ? this.split(/\s+/) : []; + }, + + toInt: Number.prototype.toInt, + + toFloat: Number.prototype.toFloat, + + camelize: function(separator) { + return this.replace(separator ? new RegExp('[' + separator + '](\\w)', 'g') : /-(\w)/g, function(all, chr) { + return chr.toUpperCase(); + }); + }, + + uncamelize: function(separator) { + separator = separator || ' '; + return this.replace(/[a-z][A-Z0-9]|[0-9][a-zA-Z]|[A-Z]{2}[a-z]/g, function(match) { + return match.charAt(0) + separator + match.substring(1); + }); + }, + + hyphenate: function(separator) { + return this.uncamelize(separator || '-').toLowerCase(); + }, + + capitalize: function() { + return this.replace(/\b[a-z]/g, function(match) { + return match.toUpperCase(); + }); + }, + + // TODO: Is this a good name? Does it need to be improved? (lighter.js relies on it). + escapeRegExp: function() { + return this.replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1'); + }, + + /** + * Trims away characters matching a given regular expression at the beginning + * and end of the strnig. If no expression is given, \s is used to match white + * space. + */ + trim: function(exp) { + exp = exp ? '[' + exp + ']' : '\\s'; + return this.replace(new RegExp('^' + exp + '+|' + exp + '+$', 'g'), ''); + }, + + clean: function() { + return this.replace(/\s{2,}/g, ' ').trim(); + }, + + contains: function(string, sep) { + return (sep ? (sep + this + sep).indexOf(sep + string + sep) : this.indexOf(string)) != -1; + }, + + times: function(count) { + // Nice trick from Prototype: + return count < 1 ? '' : new Array(count + 1).join(this); + }, + + isHtml: function() { + // From jQuery: + return /^[^<]*(<(.|\s)+>)[^>]*$/.test(this); + } +}); + +//////////////////////////////////////////////////////////////////////////////// +// RegExp + +RegExp.inject({ + // Tell Base.type what to return for regexps. + _type: 'regexp' +}); + +//////////////////////////////////////////////////////////////////////////////// +// Date + +Date.inject({ + statics: { + SECOND: 1000, + MINUTE: 60000, + HOUR: 3600000, + DAY: 86400000, + WEEK: 604800000, // 7 * DAY + MONTH: 2592000000, // 30 * DAY + YEAR: 31536000000, // 365 * DAY + + now: Date.now || function() { + return +new Date(); + } + } +}); + +//////////////////////////////////////////////////////////////////////////////// +// Math + +/** + * Returns a random integer number first <= x < second, if two arguments are + * passed or 0 <= x < first, if there is donly one. + */ +Math.rand = function(first, second) { + return second == undefined + ? Math.rand(0, first) + : Math.floor(Math.random() * (second - first) + first); +} + +//////////////////////////////////////////////////////////////////////////////// +// Color + +Array.inject({ + hexToRgb: function(toArray) { + if (this.length >= 3) { + var rgb = []; + for (var i = 0; i < 3; i++) + rgb.push((this[i].length == 1 ? this[i] + this[i] : this[i]).toInt(16)); + return toArray ? rgb : 'rgb(' + rgb.join(',') + ')'; + } + }, + + rgbToHex: function(toArray) { + if (this.length >= 3) { + if (this.length == 4 && this[3] == 0 && !toArray) return 'transparent'; + var hex = []; + for (var i = 0; i < 3; i++) { + var bit = (this[i] - 0).toString(16); + hex.push(bit.length == 1 ? '0' + bit : bit); + } + return toArray ? hex : '#' + hex.join(''); + } + }, + + rgbToHsb: function() { + var r = this[0], g = this[1], b = this[2]; + var hue, saturation, brightness; + var max = Math.max(r, g, b), min = Math.min(r, g, b); + var delta = max - min; + brightness = max / 255; + saturation = (max != 0) ? delta / max : 0; + if (saturation == 0) { + hue = 0; + } else { + var rr = (max - r) / delta; + var gr = (max - g) / delta; + var br = (max - b) / delta; + if (r == max) hue = br - gr; + else if (g == max) hue = 2 + rr - br; + else hue = 4 + gr - rr; + hue /= 6; + if (hue < 0) hue++; + } + return [Math.round(hue * 360), Math.round(saturation * 100), Math.round(brightness * 100)]; + }, + + hsbToRgb: function() { + var br = Math.round(this[2] / 100 * 255); + if (this[1] == 0) { + return [br, br, br]; + } else { + var hue = this[0] % 360; + var f = hue % 60; + var p = Math.round((this[2] * (100 - this[1])) / 10000 * 255); + var q = Math.round((this[2] * (6000 - this[1] * f)) / 600000 * 255); + var t = Math.round((this[2] * (6000 - this[1] * (60 - f))) / 600000 * 255); + switch (Math.floor(hue / 60)) { + case 0: return [br, t, p]; + case 1: return [q, br, p]; + case 2: return [p, br, t]; + case 3: return [p, q, br]; + case 4: return [t, p, br]; + case 5: return [br, p, q]; + } + } + } +}); + +String.inject({ + hexToRgb: function(toArray) { + var hex = this.match(/^#?(\w{1,2})(\w{1,2})(\w{1,2})$/); + return hex && hex.slice(1).hexToRgb(toArray); + }, + + rgbToHex: function(toArray) { + var rgb = this.match(/\d{1,3}/g); + return rgb && rgb.rgbToHex(toArray); + } +}); + +//////////////////////////////////////////////////////////////////////////////// +// Json + +Json = function(JSON) { + var special = { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', "'" : "\\'", '\\': '\\\\' }; + // Support the native Json object if it is there, fall back on JS version. + return { + encode: JSON + ? function(obj, properties) { + // Unfortunately IE does not natively support __proto__, so + // we need to filter it out from Json + return JSON.stringify(obj, properties || Browser.TRIDENT && function(key, value) { + return key == '__proto__' ? undefined : value; + }); + } + : function(obj, properties) { + if (Base.type(properties) == 'array') { + // Convert properties to a lookup table: + properties = properties.each(function(val) { + this[val] = true; + }, {}); + } + switch (Base.type(obj)) { + case 'string': + return '"' + obj.replace(/[\x00-\x1f\\"]/g, function(chr) { + return special[chr] || '\\u' + chr.charCodeAt(0).toPaddedString(4, 16); + }) + '"'; + case 'array': + return '[' + obj.collect(function(val) { + return Json.encode(val, properties); + }) + ']'; + case 'object': + // Treat hash just like object + case 'hash': + return '{' + Hash.collect(obj, function(val, key) { + if (!properties || properties[key]) { + val = Json.encode(val, properties); + if (val !== undefined) + return Json.encode(key) + ':' + val; + } + }) + '}'; + // Filter out functions, they are not part of JSON + case 'function': + return undefined; + default: + return obj + ''; + } + return null; + }, + + decode: JSON + ? function(str, secure) { + try { + // No need for security checks when using native JSON. + return JSON.parse(str); + } catch (e) { + return null; + } + } + : function(str, secure) { + try { + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + // Make sure leading/trailing whitespace is removed (IE can't handle it) + return Base.type(str) == 'string' && (str = str.trim()) && + (!secure || /^[\],:{}\s]*$/.test( + str.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, "@") + .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, "]") + .replace(/(?:^|:|,)(?:\s*\[)+/g, ""))) + ? (new Function('return ' + str))() : null; + } catch (e) { + return null; + } + } + }; +}(this.JSON); + +//////////////////////////////////////////////////////////////////////////////// +// Browser + +Browser = new function() { + var name = window.orientation != undefined ? 'ipod' + : (navigator.platform.match(/mac|win|linux|nix/i) || ['other'])[0].toLowerCase(); + var fields = { + PLATFORM: name, + XPATH: !!document.evaluate, + QUERY: !!document.querySelector + }; + // Add platform name directly in uppercase too + fields[name.toUpperCase()] = true; + + function getVersion(prefix, min, max) { + var ver = (new RegExp(prefix + '([\\d.]+)', 'i').exec(navigator.userAgent) || [0, '0'])[1].split('.'); + return (ver.slice(0, min).join('') + '.' + ver.slice(min, max || ver.length).join('')).toFloat(); + } + + var engines = { + presto: function() { + // Opera < v.10 does not report Presto versions, so use Opera versions + // there instead. As presto starts at 22.15 the range of the value + // does not clash and we can compare, e.g. Browser.VERSION < 10. + // Also, Opera 8 reports "...Opera 8...", while 9 reports "...Opera/9..." + return !window.opera ? false : getVersion('Presto/', 2) || getVersion('Opera[/ ]', 1); + }, + + trident: function() { + return !window.ActiveXObject ? false : getVersion('MSIE ', 1); + }, + + webkit: function() { + return navigator.taintEnabled ? false : getVersion('WebKit/', 1, 2); + }, + + gecko: function() { + return !document.getBoxObjectFor && window.mozInnerScreenX == null ? false : getVersion('rv:', 2); + } + }; + + for (var engine in engines) { + var version = engines[engine](); + if (version) { + fields.ENGINE = engine; + fields.VERSION = version; + engine = engine.toUpperCase(); + fields[engine] = true; + fields[(engine + version).replace(/\./g, '')] = true; + break; + } + } + + // Add console loggin on most browsers as good as we can. + fields.log = function() { + // IE does not seem to join with ' ' and has problems with apply + if (!Browser.TRIDENT && window.console && console.log) + console.log.apply(console, arguments); + else + (window.console && console.log + || window.opera && opera.postError + || alert)(Array.join(arguments, ' ')); + } + + return fields; +}; + +// DomElement + +//////////////////////////////////////////////////////////////////////////////// +// DomNodes + +/** + * DomNodes extends inject so that each of the functions + * defined bellow are executed on the whole set of nodes, and the + * returned values are collected in an array and converted to a DomNodes + * array again, if it contains only nodes. + */ +DomNodes = Array.extend(new function() { + var unique = 0; + return { + initialize: function(nodes) { + // Define this collections's unique ID. Nodes that are added to it + // get that field set too, in order to detect multiple additions of + // nodes in one go. Notice that this does not work when nodes + // are added to another collection, then again to this one. + // But for Dom Query functions, this is enough. + this._unique = unique++; + // Do not use nodes.push to detect arrays, as we're passing pseudo + // arrays here too (e.g. childNodes). But the native Option defines + // .length too, so rule that out by checking nodeType as well. + this.append(nodes && nodes.length != null && !nodes.nodeType + ? nodes : arguments); + }, + + /** + * Only #push wraps the added node in a DomNode. splice and unshift + * are NOT overridden to do the same. + */ + push: function() { + this.append(arguments); + return this.length; + }, + + append: function(items) { + for (var i = 0, l = items.length; i < l; i++) { + var el = items[i]; + // Try _wrapper first, for faster performance + if ((el = el && (el._wrapper || DomNode.wrap(el))) && el._unique != this._unique) { + el._unique = this._unique; + this[this.length++] = el; + } + } + return this; + }, + + toNode: function() { + // return the DomNodes array itself. See inserters comments further bellow + return this; + }, + + statics: { + inject: function(src/*, ... */) { + // For each function that is injected into DomNodes, create a + // new function that iterates that calls the function on each of + // the collection's nodes. + // src can either be a function to be called, or a object literal. + var proto = this.prototype; + this.base(Base.each(src || {}, function(val, key) { + if (typeof val == 'function') { + var func = val, prev = proto[key]; + var count = func.length, prevCount = prev && prev.length; + val = function() { + var args = arguments, values; + // If there was a previous implementation under this name + // and the arguments match better, use that one instead. + // The strategy is very basic: If the same amount of arguments + // are provided as the previous one accepts, or if more arguments + // are provided than the new function can handle and the previous + // implementation expects more, use the previous one instead. + if (prev && args.length == prevCount + || (args.length > count && args.length <= prevCount)) + return prev.apply(this, args); + this.each(function(obj) { + // Try to use original method if it's there, in order + // to support base, as this will be the wrapper that + // sets it + var ret = (obj[key] || func).apply(obj, args); + // Only collect return values if defined and not + // returning 'this'. + if (ret !== undefined && ret != obj) { + values = values || (DomNode.isNode(ret) + ? new obj._collection() : []); + values.push(ret); + } + }); + return values || this; + } + } + this[key] = val; + }, {})); + for (var i = 1, l = arguments.length; i < l; i++) + this.inject(arguments[i]); + return this; + } + } + }; +}); + +//////////////////////////////////////////////////////////////////////////////// +// DomNode + +DomNode = Base.extend(new function() { + var nodes = []; + // LUTs for tags and class based constructors. Bootstrap can automatically + // use sub-prototype of DomNode for any given wrapped node based on + // its className Attribute. The sub-prototype only needs to define _class + var tags = {}, classes = {}, classCheck, unique = 0; + + // Garbage collection - uncache nodes/purge listeners on orphaned nodes + // so we don't hold a reference and cause the browser to retain them. + function dispose(force) { + for (var i = nodes.length - 1; i >= 0; i--) { + var el = nodes[i]; + if (force || (!el || el != window && el != document && + (!el.parentNode || !el.offsetParent))) { + if (el) { + var obj = el._wrapper; + if (obj && obj.finalize) obj.finalize(); + el._wrapper = el._unique = null; + } + if (!force) nodes.splice(i, 1); + } + } + } + // TODO: this seems to cause problems. Turn off for now. + // dispose.periodic(30000); + + // Private inject function for DomNode. It adds support for + // _methods and _properties declarations, which forward calls and define + // getter / setters for fields of the native DOM node. + function inject(src) { + // Forward method calls. Returns result if any, otherwise reference + // to this. + src = src || {}; + (src._methods || []).each(function(name) { + src[name] = function(arg) { + // .apply seems to not be present on native dom functions on + // Safari. Just pass on the first argument and call directly. + var ret = this.$[name] && this.$[name](arg); + return ret === undefined ? this : ret; + } + }); + // Define getter / setters + (src._properties || []).each(function(name) { + // get/setProperty() expects lowercase property name. + var part = name.capitalize(), prop = name.toLowerCase(); + src['get' + part] = function() { + return this.getProperty(prop); + } + src['set' + part] = function(value) { + return this.setProperty(prop, value); + } + }); + delete src._methods; + delete src._properties; + return Function.inject.call(this, src); + } + + function getConstructor(el) { + // Use DomNode as as the default, HtmlElement for anything with + // className !== undefined and special constructors based on tag names. + // tags stores both upper-case and lower-case references for higher + // speed. + // classCheck only exists if HtmlElement was extended with prototypes + // defining _class. In this case, classCheck is a regular expression that + // checks className for the occurence of any of the prototype mapped + // classes, and returns the first occurence, which is then used to + // decide for constructor. This allows using e.g. "window hidden" for + // an element that should map to a window prototype. + var match; + // Check classCheck first, since it can override the _tag setting + return classCheck && el.className && (match = el.className.match(classCheck)) && match[2] && classes[match[2]] || + // Check _tag settings for extended HtmlElement prototypes bound to tagNames, e.g. HtmlForm, etc. + el.tagName && tags[el.tagName] || + // Html elements + el.className !== undefined && HtmlElement || + // Elements + el.nodeType == 1 && DomElement || + // TextNodes + el.nodeType == 3 && DomTextNode || + // Documents + el.nodeType == 9 && (el.documentElement.nodeName.toLowerCase() == 'html' && HtmlDocument || DomDocument) || + // Windows + el.location && el.frames && el.history && DomWindow || + // Everything else + DomNode; + } + + var dont = {}; + + return { + // Tells Base.type the type to return when encountering an node. + _type: 'node', + _collection: DomNodes, + // Tell extend to automatically call this.base in overridden initialize + // methods of DomNodes + // See extend bellow for more information about this. + _initialize: true, + + initialize: function(el, props, doc) { + if (!el) return null; + // Support node creating constructors on subclasses of DomNode + // that define prototype._tag and can take one argument, which + // defines the properties to be set: + if (this._tag && Base.type(el) == 'object') { + props = el; + el = this._tag; + } + // doc is only used when producing an node from a string. + if (typeof(el) == 'string') { + // Call the internal element creation helper. This does not fully + // set props, only the one needed for the IE workaround. + // Set(props) is called further bellow. + el = DomElement.create(el, props, doc); + } else if (el._wrapper) { + // Does the DomNode wrapper for this node already exist? + return el._wrapper; + } + if (props === dont) { + props = null; + } else { + // Check if we're using the right constructor, if not, construct + // with the right one: + var ctor = getConstructor(el); + if (ctor != this.constructor) + return new ctor(el, props); + } + // Store a reference to the native node. + this.$ = el; + // Store a reference in the native node to the wrapper. + // Needs to be cleaned up by garbage collection. See above. + // Not all nodes allow setting of values. E.g. on IE, textnodes don't + // For now we just ingore them and do not store the wrapper. + try { + el._wrapper = this; + nodes[nodes.length] = el; + } catch (e) {} // Ignore error + if (props) this.set(props); + }, + + statics: { + inject: function(src/*, ... */) { + if (src) { + // Produce generic-versions for each of the injected + // non-static methods, so that they function on native + // methods instead of wrapped ones. This means + // DomNode.getProperty(el, name) can be called on non + // wrapped nodes. + var proto = this.prototype, that = this; + src.statics = Base.each(src, function(val, name) { + if (typeof val == 'function' && !this[name] && !that[name]) { + // We need to be fast, so assume a maximum of two + // params instead of using Function#apply. + this[name] = function(el, param1, param2) { + if (el) try { + // Use the ugly but fast trick of setting + // $ on the prototype and call throught + // that, then erase again. + proto.$ = el.$ || el; + return proto[name](param1, param2); + } finally { + delete proto.$; + } + } + } + }, src.statics || {}); + inject.call(this, src); + // Remove toString, as we do not want it to be multiplied in + // _collection (it would not return a string but an array then). + delete src.toString; + // Now, after src was processed in #inject, inject not only + // into this, but also into DomNodes where the functions + // are "multiplied" for each of the nodes of the collection. + proto._collection.inject(src); + } + for (var i = 1, l = arguments.length; i < l; i++) + this.inject(arguments[i]); + return this; + }, + + extend: function(src) { + // Do not pass src to base, as we weed to fix #inject first. + var ret = this.base(); + // If initialize is defined, explicitely calls this.base(el, props) + // here. This is a specialy DomNode extension that does not + // require the user to call this.base(), since it is used for _class + // stuff often. + var init = src.initialize; + if (init) src.initialize = function(el, props) { + var ret = this._initialize && this.base(el, props); + if (ret) return ret; + init.apply(this, arguments); + } + inject.call(ret, src); + // Undo overriding of the inject method above for subclasses that + // do not define a different _collection value, as only injecting + // into DomNode (not subclasses) shall also inject into DomNodes! + // Reseting before does not work, as it would be overridden + // during static inheritance again. + if (ret.prototype._collection == this.prototype._collection) + ret.inject = inject; + // When extending DomNode with a tag name field specified, this + // prototype will be used when wrapping nodes of that type. + // If this is a prototype for a certain tag name, store it in the LUT. + if (src) { + // tags stores both upper-case and lower-case references + // for higher speed in getConstructor, since tagName can + // be used for direct lookup, regardless of its case. + if (src._tag) + tags[src._tag.toLowerCase()] = tags[src._tag.toUpperCase()] = ret; + // classCheck is null until a sub-prototype defines _class + if (src._class) { + classes[src._class] = ret; + // Create a regular expression that allows detection of + // the first prototype mapped className. This needs to + // contain all defined classes. See getConstructor + // e.g.: /(^|\s)(post-it|window)(\s|$)/ + classCheck = new RegExp('(^|\\s)(' + Base.each(classes, function(val, name) { + this.push(name); + }, []).join('|') + ')(\\s|$)'); + // If the prototype defines an initialize method, and it + // does not want to be lazily loaded, force wrapping of + // these nodes on domready, so that initialize will be + // directly called and further dom manipulation can be done. + if (!src._lazy && src.initialize) Browser.document.addEvent('domready', function() { + this.getElements('.' + src._class); + }); + } + } + return ret; + }, + + /** + * Wraps the passed node in a DomNode wrapper. + * It returns existing wrappers through el._wrapper, if defined. + */ + wrap: function(el) { + return el ? typeof el == 'string' // selector? + ? DomElement.get(el) + // Make sure we're using the right constructor. + : el._wrapper || el._collection && el || new (getConstructor(el))(el, dont) + : null; + }, + + /** + * Unwraps a wrapped node and returns its native dom node, or + * the node itself if it is already native. + */ + unwrap: function(el) { + return el && el.$ || el; + }, + + unique: function(el) { + if (!el._unique) { + nodes.push(el); + el._unique = ++unique; + } + }, + + isNode: function(obj) { + return /^(element|node|textnode|document)$/.test( + typeof obj == 'string' ? obj : Base.type(obj)); + }, + + dispose: function() { + dispose(true); + } + } + } +}); + +// Use the modified inject function from above which injects both into DomNode +// and DomNodes. +DomNode.inject(new function() { + // Dom / Html to JS property mappings, as used by getProperty, setProperty + // and removeProperty. + var bools = ['compact', 'nowrap', 'ismap', 'declare', 'noshade', 'checked', + 'disabled', 'readonly', 'multiple', 'selected', 'noresize', 'defer' + ].associate(); + var properties = Hash.append({ // props + text: Browser.TRIDENT || Browser.WEBKIT && Browser.VERSION < 420 || Browser.PRESTO && Browser.VERSION < 9 + ? function(node) { + return node.$.innerText !== undefined ? 'innerText' : 'nodeValue' + } : 'textContent', + // Make sure that setting both class and className uses this.$.className instead of setAttribute + html: 'innerHTML', 'class': 'className', className: 'className', 'for': 'htmlFor' + }, [ // camels and other values that need to be accessed directly, not through getAttribute + 'value', 'accessKey', 'cellPadding', 'cellSpacing', 'colSpan', + 'frameBorder', 'maxLength', 'readOnly', 'rowSpan', 'tabIndex', + 'selectedIndex', 'useMap', 'width', 'height' + ].associate(function(name) { + return name.toLowerCase(); + }), bools); + + // Values to manually copy over when cloning with content + var clones = { input: 'checked', option: 'selected', textarea: Browser.WEBKIT && Browser.VERSION < 420 ? 'innerHTML' : 'value' }; + + // handle() handles both get and set calls for any given property name. + // prefix is either set or get, and is used for lookup of getter / setter + // methods. get/setProperty is used as a fallback. + // See DomElement#get/set + function handle(that, prefix, name, value) { + var ctor = that.__proto__.constructor; + // handle caches getter and setter functions for given property names. + // Store the handlers in the constructor of each prototype, so caching + // between different sub-prototypes that might redefine getter/setters + // does not get mixed up: + var handlers = ctor.handlers = ctor.handlers || { get: {}, set: {} }; + var list = handlers[prefix]; + // First see if there is a getter / setter for the given property + var fn = name == 'events' && prefix == 'set' ? that.addEvents : list[name]; + if (fn === undefined) + fn = list[name] = that[prefix + name.capitalize()] || null; + // If the passed value is an array, use it as the argument + // list for the call. + return fn + ? fn[Base.type(value) == 'array' ? 'apply' : 'call'](that, value) + : that[prefix + 'Property'](name, value); + } + + // A helper for calling toNode and returning results. + function toNodes(elements) { + // Support passing things as argument lists, without the first wrapping array + // Do not reset elements, since this causes a circular reference on Opera + // where arguments inherits from array and therefore is returned umodified + // by Array.create, and where setting elements to a new value modifies + // this arguments list directly. + var els = Base.type(elements) == 'array' ? elements : Array.create(arguments); + // Find out if elements are created, or if they were already passed. + // The convention is to return the newly created elements if they are not + // elements already, otherwise return this. + var created = els.find(function(el) { + return !DomNode.isNode(el); + }); + // toNode can either return a single DomElement or a DomElements array. + var result = els.toNode(this.getDocument()); + return { + // Make sure we always return an array of the resulted elements as well, + // for simpler handling in inserters below + array: result ? (Base.type(result) == 'array' ? result : [result]) : [], + // Result might be a single element or an array, depending on what the + // user passed. This is to be returned back. Only define it if the elements + // were created. + result: created && result + }; + } + + var fields = { + _properties: ['text'], + + set: function(name, value) { + switch (Base.type(name)) { + case 'string': + return handle(this, 'set', name, value); + case 'object': + return Base.each(name, function(value, key) { + handle(this, 'set', key, value); + }, this); + } + return this; + }, + + get: function(name) { + return handle(this, 'get', name); + }, + + getDocument: function() { + return DomNode.wrap(this.$.ownerDocument); + }, + + getWindow: function() { + return this.getDocument().getWindow(); + }, + + getPreviousNode: function() { + return DomNode.wrap(this.$.previousSibling); + }, + + getNextNode: function() { + return DomNode.wrap(this.$.nextSibling); + }, + + getFirstNode: function() { + return DomNode.wrap(this.$.firstChild); + }, + + getLastNode: function() { + return DomNode.wrap(this.$.lastChild); + }, + + getParentNode: function() { + return DomNode.wrap(this.$.parentNode); + }, + + // Returns all the Element's children including text nodes + getChildNodes: function() { + return new DomNodes(this.$.childNodes); + }, + + hasChildNodes: function() { + return this.$.hasChildNodes(); + }, + + appendChild: function(el) { + if (el = DomNode.wrap(el)) { + // Fix a bug on Mac IE when inserting Option elements to Select + // elements, where the text on these objects is lost after insertion + // -> inserters.before does the same. + // This appears to still be needed on IE7. + var text = Browser.TRIDENT && el.$.text; + if (text) el.$.text = ''; + this.$.appendChild(el.$); + if (text) el.$.text = text; + } + return this; + }, + + appendChildren: function() { + return Array.flatten(arguments).each(function(el) { + this.appendChild($(DomNode.wrap(el))); + }, this); + }, + + appendText: function(text) { + return this.injectBottom(this.getDocument().createTextNode(text)); + }, + + prependText: function(text) { + return this.injectTop(this.getDocument().createTextNode(text)); + }, + + remove: function() { + if (this.$.parentNode) + this.$.parentNode.removeChild(this.$); + return this; + }, + + removeChild: function(el) { + el = DomNode.wrap(el); + this.$.removeChild(el.$); + return el; + }, + + removeChildren: function() { + var nodes = this.getChildNodes(); + nodes.remove(); + return nodes; + }, + + replaceWith: function(el) { + if (this.$.parentNode) { + // Use toNodes to support on the fly creation of one or more + // elements to replace with: + el = toNodes.apply(this, arguments); + var els = el.array; + // Replace the first item of the array and insert all the others + // afterwards, if there are more than one: + if (els.length > 0) + this.$.parentNode.replaceChild(els[0].$, this.$); + for (var i = els.length - 1; i > 0; i--) + els[i].insertAfter(els[0]); + return el.result; + } + return null; + }, + + /** + * Wraps the passed elements around the current one. + * Elements are converted through toNodes + * + * Inspired by: jQuery + */ + wrap: function() { + var el = this.injectBefore.apply(this, arguments), last; + do { + last = el; + el = el.getFirst(); + } while(el); + last.appendChild(this); + return last; + }, + + clone: function(contents) { + var clone = this.$.cloneNode(!!contents); + function clean(left, right) { + if (Browser.TRIDENT) { + left.clearAttributes(); + left.mergeAttributes(right); + left.removeAttribute('_wrapper'); + left.removeAttribute('_unique'); + if (left.options) + for (var l = left.options, r = right.options, i = l.length; i--;) + l[i].selected = r[i].selected; + } + var name = clones[right.tagName.toLowerCase()]; + if (name && right[name]) + left[name] = right[name]; + if (contents) + for (var l = left.childNodes, r = right.childNodes, i = l.length; i--;) + clean(l[i], r[i]); + } + clean(clone, this.$); + return DomNode.wrap(clone); + }, + + hasProperty: function(name) { + var key = properties[name]; + // Support key branching through functions, as needed by 'text' on IE + key = key && typeof key == 'function' ? key(this) : key; + return key ? this.$[key] !== undefined : this.$.hasAttribute(name); + }, + + getProperty: function(name) { + var key = properties[name], value; + // Support key branching through functions, as needed by 'text' on IE + key = key && typeof key == 'function' ? key(this) : key; + var value = key ? this.$[key] : this.$.getAttribute(name); + return bools[name] ? !!value : value; + }, + + setProperty: function(name, value) { + var key = properties[name], defined = value !== undefined; + key = key && typeof key == 'function' ? key(this) : key; + if (key && bools[name]) value = value || !defined ? true : false; + else if (!defined) return this.removeProperty(name); + key ? this.$[key] = value : this.$.setAttribute(name, value); + return this; + }, + + removeProperty: function(name) { + var key = properties[name], bool = key && bools[name]; + key = key && typeof key == 'function' ? key(this) : key; + key ? this.$[key] = bool ? false : '' : this.$.removeAttribute(name); + return this; + }, + + getProperties: function() { + var props = {}; + for (var i = 0; i < arguments.length; i++) + props[arguments[i]] = this.getProperty(arguments[i]); + return props; + }, + + setProperties: function(src) { + return Base.each(src, function(value, name) { + this.setProperty(name, value); + }, this); + }, + + removeProperties: function() { + return Array.each(arguments, this.removeProperty, this); + } + }; + + // Inserters are only used internally and can assume the source and dest + // elements to be wrapped elements. + var inserters = { + /** + * Inserts the source element before the dest element in the DOM. + */ + before: function(source, dest) { + if (source && dest && dest.$.parentNode) { + // Fix a bug on Mac IE when inserting Option elements to Select + // elements, where the text on these objects is lost after insertion. + // -> DomNode#appendChild does the same. + var text = Browser.TRIDENT && dest.$.text; + if (text) dest.$.text = ''; + dest.$.parentNode.insertBefore(source.$, dest.$); + if (text) dest.$.text = text; + } + }, + + /** + * Inserts the source element after the dest element in the DOM. + */ + after: function(source, dest) { + if (source && dest && dest.$.parentNode) { + var next = dest.$.nextSibling; + // Do not use the native methods since these do not include the + // workaround for legacy browsers above. Once that part is + // deprecated, we can change strategy here. Might be bit faster. + if (next) source.insertBefore(next); + else dest.getParent().appendChild(source); + } + }, + + /** + * Inserts the source element at the bottom of the dest element's children. + */ + bottom: function(source, dest) { + if (source && dest) + dest.appendChild(source); + }, + + /** + * Inserts the source element at the top of the dest element's children. + */ + top: function(source, dest) { + if (source && dest) { + var first = dest.$.firstChild; + if (first) source.insertBefore(first); + else dest.appendChild(source); + } + } + }; + + inserters.inside = inserters.bottom; + + // Now add the inserters + // Important: The inseters return this if the object passed is already an + // element. But if it is a string or an array that is converted to an element, + // the newly created element is returned instead. + + Base.each(inserters, function(inserter, name) { + var part = name.capitalize(); + // #insert* acts like the dom #insert* functions, inserting this element + // into the passed element(s). + fields['insert' + part] = function(el) { + el = toNodes.apply(this, arguments); + // Clone the object for every index other than the first + // as we're inserting into multiple times. + for (var i = 0, list = el.array, l = list.length; i < l; i++) + inserter(i == 0 ? this : this.clone(true), list[i]); + return el.result || this; + } + + // #inject* does the reverse of #insert*, it injects the passed element(s) + // into this element. + fields['inject' + part] = function(el) { + el = toNodes.apply(this, arguments); + for (var i = 0, list = el.array, l = list.length; i < l; i++) + inserter(list[i], this); + return el.result || this; + } + }); + + return fields; +}); + +//////////////////////////////////////////////////////////////////////////////// +// DomElements + +DomElements = DomNodes.extend(); + +//////////////////////////////////////////////////////////////////////////////// +// DomElement + +DomElement = DomNode.extend({ + // Tells Base.type the type to return when encountering an element. + _type: 'element', + _collection: DomElements, + + statics: { + /** + * Returns the first element matching the given selector, within root + * or Browser.document, if root is not specified. + */ + get: function(selector, root) { + // Do not use this for DomElement since $ is a link to DomElement.get + return (root && DomNode.wrap(root) || Browser.document).getElement(selector); + }, + + /** + * Returns all elements matching the given selector, within root + * or Browser.document, if root is not specified. + */ + getAll: function(selector, root) { + // Do not use this for DomElement since $$ is a link to DomElement.getAll + return (root && DomNode.wrap(root) || Browser.document).getElements(selector); + }, + + /** + * This is only a helper method that's used both in DomDocument and DomElement. + * It does not fully set props, only the values needed for a IE workaround. + * It also returns an unwrapped object, that needs to further initalization + * and setting of props. + * This is needed to avoid production of two objects to match the proper + * prototype when using new HtmlElement(name, props). + */ + create: function(tag, props, doc) { + if (Browser.TRIDENT && props) { + ['name', 'type', 'checked'].each(function(key) { + if (props[key]) { + tag += ' ' + key + '="' + props[key] + '"'; + if (key != 'checked') + delete props[key]; + } + }); + tag = '<' + tag + '>'; + } + return (DomElement.unwrap(doc) || document).createElement(tag); + }, + + isAncestor: function(el, parent) { + // Handle el.ownerDocumet == parent specially for efficiency and + // also since documents don't define neither contains nor + // compareDocumentPosition + return !el ? false : el.ownerDocument == parent ? true + : Browser.WEBKIT && Browser.VERSION < 420 + ? Array.contains(parent.getElementsByTagName(el.tagName), el) + : parent.contains + ? parent != el && parent.contains(el) + : !!(parent.compareDocumentPosition(el) & 16) + } + } +}); + +DomElement.inject(new function() { + // A helper for walking the DOM, skipping text nodes + function walk(el, walk, start, match, all) { + var elements = all && new el._collection(); + el = el.$[start || walk]; + while (el) { + if (el.nodeType == 1 && (!match || DomElement.match(el, match))) { + if (!all) return DomNode.wrap(el); + elements.push(el); + } + el = el[walk]; + } + return elements; + } + + return { + _properties: ['id'], + + getTag: function() { + return (this.$.tagName || '').toLowerCase(); + }, + + getPrevious: function(match) { + return walk(this, 'previousSibling', null, match); + }, + + getAllPrevious: function(match) { + return walk(this, 'previousSibling', null, match, true); + }, + + getNext: function(match) { + return walk(this, 'nextSibling', null, match); + }, + + getAllNext: function(match) { + return walk(this, 'nextSibling', null, match, true); + }, + + getFirst: function(match) { + return walk(this, 'nextSibling', 'firstChild', match); + }, + + getLast: function(match) { + return walk(this, 'previousSibling', 'lastChild', match); + }, + + hasChild: function(match) { + return DomNode.isNode(match) + ? DomElement.isAncestor(DomElement.unwrap(match), this.$) + : !!this.getFirst(match); + }, + + getParent: function(match) { + return walk(this, 'parentNode', null, match); + }, + + getParents: function(match) { + return walk(this, 'parentNode', null, match, true); + }, + + hasParent: function(match) { + return DomNode.isNode(match) + ? DomElement.isAncestor(this.$, DomElement.unwrap(match)) + : !!this.getParent(match); + }, + + // Returns all the Element's children excluding text nodes + getChildren: function(match) { + return walk(this, 'nextSibling', 'firstChild', match, true); + }, + + hasChildren: function(match) { + return !!this.getChildren(match).length; + }, + + toString: function() { + return (this.$.tagName || this._type).toLowerCase() + + (this.$.id ? '#' + this.$.id : ''); + }, + + toNode: function() { + return this; + } + }; +}); + +$ = DomElement.get; +$$ = DomElement.getAll; + +//////////////////////////////////////////////////////////////////////////////// +// DomTextNode + +DomTextNode = DomNode.extend({ + _type: 'textnode' +}); + +//////////////////////////////////////////////////////////////////////////////// +// DomDocument + +DomDocument = DomElement.extend({ + _type: 'document', + + initialize: function() { + if(Browser.TRIDENT && Browser.VERSION < 7) + try { + // Fix background flickering on IE. + this.$.execCommand('BackgroundImageCache', false, true); + } catch (e) {} + }, + + createElement: function(tag, props) { + // Call DomElement.create, the internal creation helper. This does not + // fully set props, only the one needed for the IE workaround. + // set(props) is called after for all the others. + return DomNode.wrap(DomElement.create(tag, props, this.$)).set(props); + }, + + createTextNode: function(text) { + return $(this.$.createTextNode(text)); + }, + + getDocument: function() { + return this; + }, + + getWindow: function() { + return DomNode.wrap(this.$.defaultView || this.$.parentWindow); + }, + + open: function() { + this.$.open(); + }, + + close: function() { + this.$.close(); + }, + + write: function(markup) { + this.$.write(markup); + }, + + writeln: function(markup) { + this.$.writeln(markup); + } +}); + +//////////////////////////////////////////////////////////////////////////////// +// DomWindow + +// Let Window point to DomWindow for now, so new Window(...) can be called. +// This makese for nicer code, but might have to change in the future. +Window = DomWindow = DomElement.extend({ + _type: 'window', + // Don't automatically call this.base in overridden initialize methods + _initialize: false, + _methods: ['close', 'alert', 'prompt', 'confirm', 'blur', 'focus', 'reload'], + + getDocument: function() { + return DomNode.wrap(this.$.document); + }, + + getWindow: function() { + return this; + }, + + // TODO: add get/setStatus, get/setLocation, and find way to deal with + // referencing of native fields at the end of initialize. + + /** + * A constructor for DomWindow that is based on window.open and extends it + * to allow more options in the third parameter. + * + * If param is a string, the standard window.open is executed. + * If param is an object, additional parameters maybe be defined, such as + * param.confirm, param.focus, etc. Also, if param.width & height are + * defined, The window is centered on screen. + */ + initialize: function(param) { + var win; + // Are we wrapping a window? + if (param.location && param.frames && param.history) { + // Do not return yet as we need to add some properties further down + win = this.base(param) || this; + } else { + // If param a string, convert to param object, using its value for url. + if (typeof param == 'string') + param = { url: param }; + // Convert boolean values to 0 / 1: + (['toolbar','menubar','location','status','resizable','scrollbars']).each(function(key) { + param[key] = param[key] ? 1 : 0; + }); + // Center window if left / top is not defined, but dimensions are: + if (param.width && param.height) { + if (param.left == null) param.left = Math.round( + Math.max(0, (screen.width - param.width) / 2)); + if (param.top == null) param.top = Math.round( + Math.max(0, (screen.height - param.height) / 2 - 40)); + } + // Now convert paramets to string. + var str = Base.each(param, function(val, key) { + // Filter out non-standard param names and convert boolean values to 0 / 1 simply by adding 0 to it + if (!/^(focus|confirm|url|name)$/.test(key)) + this.push(key + '=' + (val + 0)); + }, []).join(); + win = this.base(window.open(param.url, param.name.replace(/\s+|\.+|-+/gi, ''), str)) || this; + if (win && param.focus) + win.focus(); + } + // Copy over default windows properties before returning + return ['location', 'frames', 'history'].each(function(key) { + this[key] = this.$[key]; + }, win); + } +}); + +//////////////////////////////////////////////////////////////////////////////// +// Dimension + +// TODO: Consider splitting this into Position and Dimension, or naming it +// Measure instead +DomElement.inject(new function() { + function cumulate(name, parent, iter) { + var left = name + 'Left', top = name + 'Top'; + return function(that) { + var cur, next = that, x = 0, y = 0; + do { + cur = next; + x += cur.$[left] || 0; + y += cur.$[top] || 0; + } while((next = DomNode.wrap(cur.$[parent])) && (!iter || iter(cur, next))) + return { x: x, y: y }; + } + } + + function bounds(fields, offset) { + // Pass one of these: + // (left, top, width, height, clip) + // ([left, top, width, height, clip]) + // ({ left: , top: , width: , height: , clip: }) + // Do not set bounds, as arguments would then be modified, which we're + // referencing here: + return function(values) { + var vals = /^(object|array)$/.test(Base.type(values)) ? values : arguments; + if (offset) { + if (vals.x) vals.left = vals.x; + if (vals.y) vals.top = vals.y; + } + var i = 0; + return fields.each(function(name) { + var val = vals.length ? vals[i++] : vals[name]; + if (val != null) this.setStyle(name, val); + }, this); + } + } + + function body(that) { + return that.getTag() == 'body'; + } + + var getAbsolute = cumulate('offset', 'offsetParent', Browser.WEBKIT ? function(cur, next) { + // Safari returns margins on body which is incorrect if the + // child is absolutely positioned. + return next.$ != document.body || cur.getStyle('position') != 'absolute'; + } : null, true); + + var getPositioned = cumulate('offset', 'offsetParent', function(cur, next) { + return next.$ != document.body && !/^(relative|absolute)$/.test(next.getStyle('position')); + }); + + var getScrollOffset = cumulate('scroll', 'parentNode'); + + var fields = { + + getSize: function() { + return body(this) + ? this.getWindow().getSize() + : { width: this.$.offsetWidth, height: this.$.offsetHeight }; + }, + + /** + * relative can either be a boolean value, indicating positioned (true) + * or absolute (false) offsets, or it can be an element in relation to + * which the offset is returned. + */ + getOffset: function(relative) { + if (body(this)) + return this.getWindow().getOffset(); + if (relative && !DomNode.isNode(relative)) + return getPositioned(this); + var off = getAbsolute(this); + if (relative) { + var rel = getAbsolute(DomNode.wrap(relative)); + off = { x: off.x - rel.x, y: off.y - rel.y }; + } + return off; + }, + + getScrollOffset: function() { + return body(this) + ? this.getWindow().getScrollOffset() + : getScrollOffset(this); + }, + + getScrollSize: function() { + return body(this) + ? this.getWindow().getScrollSize() + : { width: this.$.scrollWidth, height: this.$.scrollHeight }; + }, + + getBounds: function(relative) { + if (body(this)) + return this.getWindow().getBounds(); + var off = this.getOffset(relative), el = this.$; + return { + left: off.x, + top: off.y, + right: off.x + el.offsetWidth, + bottom: off.y + el.offsetHeight, + width: el.offsetWidth, + height: el.offsetHeight + }; + }, + + setBounds: bounds(['left', 'top', 'width', 'height', 'clip'], true), + + setOffset: bounds(['left', 'top'], true), + + setSize: bounds(['width', 'height', 'clip']), + + setScrollOffset: function(x, y) { + if (body(this)) { + this.getWindow().setScrollOffset(x, y); + } else { + // Convert { x: y: } to x / y + var off = typeof x == 'object' ? x : { x: x, y: y }; + this.$.scrollLeft = off.x; + this.$.scrollTop = off.y; + } + return this; + }, + + scrollTo: function(x, y) { + // Redirect to setScrollOffset, wich is there for symetry with getScrolloffset + // Do not simply point to the same function, since setScrollOffset is overridden + // for DomDocument and DomWindow. + return this.setScrollOffset(x, y); + }, + + contains: function(pos) { + var bounds = this.getBounds(); + return pos.x >= bounds.left && pos.x < bounds.right && + pos.y >= bounds.top && pos.y < bounds.bottom; + }, + + /** + * Tests wether element is within the window bounds and thus visible. + * Also returns false if display style is set to none. + * @fully specifies wether to test for full or partial visibility. + */ + isVisible: function(fully) { + var win = this.getWindow(), top = win.getScrollOffset().y, + bottom = top + win.getSize().height, bounds = this.getBounds(); + return (bounds.height > 0 || bounds.width > 0) // visible + && (bounds.top >= top && bounds.bottom <= bottom // fully + || (fully && bounds.top <= top && bounds.bottom >= bottom) // fully & bigger than screen + || !fully && (bounds.top <= top && bounds.bottom >= top // partly top + || bounds.top <= bottom && bounds.bottom >= bottom)); // partly bottom + } + }; + + // Dimension getters and setters: + ['left', 'top', 'right', 'bottom', 'width', 'height'].each(function(name) { + var part = name.capitalize(); + fields['get' + part] = function() { + return this.$['offset' + part]; + }; + fields['set' + part] = function(value) { + // Check for isNaN since it might be values like 'auto' too: + this.$.style[name] = isNaN(value) ? value : value + 'px'; + }; + }); + + return fields; +}); + +// Inject dimension methods into both DomDocument and Window. +// Use the bind object in each to do so: +[DomDocument, DomWindow].each(function(ctor) { + ctor.inject(this); +}, { + + getSize: function() { + if (Browser.PRESTO || Browser.WEBKIT) { + var win = this.getWindow().$; + return { width: win.innerWidth, height: win.innerHeight }; + } + var doc = this.getCompatElement(); + return { width: doc.clientWidth, height: doc.clientHeight }; + }, + + getScrollOffset: function() { + var win = this.getWindow().$, doc = this.getCompatElement(); + return { x: win.pageXOffset || doc.scrollLeft, y: win.pageYOffset || doc.scrollTop }; + }, + + getScrollSize: function() { + var doc = this.getCompatElement(), min = this.getSize(); + return { width: Math.max(doc.scrollWidth, min.width), height: Math.max(doc.scrollHeight, min.height) }; + }, + + getOffset: function() { + return { x: 0, y: 0 }; + }, + + getBounds: function() { + var size = this.getSize(); + return { + left: 0, top: 0, + right: size.width, bottom: size.height, + width: size.width, height: size.height + }; + }, + + setScrollOffset: function(x, y) { + // Convert { x: y: } to x / y + var off = typeof x == 'object' ? x : { x: x, y: y }; + this.getWindow().$.scrollTo(off.x, off.y); + return this; + }, + + getElementAt: function(pos, exclude) { + var el = this.getDocument().getElement('body'); + while (true) { + var max = -1; + var ch = el.getFirst(); + while (ch) { + if (ch.contains(pos) && ch != exclude) { + var z = ch.$.style.zIndex.toInt() || 0; + if (z >= max) { + el = ch; + max = z; + } + } + ch = ch.getNext(); + } + if (max < 0) break; + } + return el; + }, + + getCompatElement: function() { + var doc = this.getDocument(); + return doc.getElement(!doc.$.compatMode + || doc.$.compatMode == 'CSS1Compat' ? 'html' : 'body').$; + } +}); + +//////////////////////////////////////////////////////////////////////////////// +// DomEvent + +// Name it DomEvent instead of Event, as Event is a native prototype. +DomEvent = Base.extend(new function() { + // MACIE does not accept numbers for keys, so use strings: + var keys = { + '8': 'backspace', + '13': 'enter', + '27': 'escape', + '32': 'space', + '37': 'left', + '38': 'up', + '39': 'right', + '40': 'down', + '46': 'delete' + }; + + function hover(name, type) { + return { + type: type, + listener: function(event) { + if (event.relatedTarget != this && !this.hasChild(event.relatedTarget)) + this.fireEvent(name, [event]); + } + } + } + + return { + initialize: function(event) { + this.event = event = event || window.event; + this.type = event.type; + this.target = DomNode.wrap(event.target || event.srcElement); + if (this.target && this.target.$.nodeType == 3) + this.target = this.target.getParentNode(); // Safari + this.shift = event.shiftKey; + this.control = event.ctrlKey; + this.alt = event.altKey; + this.meta = event.metaKey; + if (/^(mousewheel|DOMMouseScroll)$/.test(this.type)) { + this.wheel = event.wheelDelta ? + event.wheelDelta / (window.opera ? -120 : 120) : + - (event.detail || 0) / 3; + } else if (/^key/.test(this.type)) { + this.code = event.which || event.keyCode; + this.key = keys[this.code] || String.fromCharCode(this.code).toLowerCase(); + } else if (/^mouse|^click$/.test(this.type)) { + this.page = { + x: event.pageX || event.clientX + document.documentElement.scrollLeft, + y: event.pageY || event.clientY + document.documentElement.scrollTop + }; + this.client = { + x: event.pageX ? event.pageX - window.pageXOffset : event.clientX, + y: event.pageY ? event.pageY - window.pageYOffset : event.clientY + }; + // TODO: Calculate only if Dimension.js is defined! Add conditional macro? + var offset = this.target.getOffset(); + this.offset = { + x: this.page.x - offset.x, + y: this.page.y - offset.y + } + this.rightClick = event.which == 3 || event.button == 2; + if (/^mouse(over|out)$/.test(this.type)) + this.relatedTarget = DomNode.wrap(event.relatedTarget || + this.type == 'mouseout' ? event.toElement : event.fromElement); + } + }, + + stop: function() { + this.stopPropagation(); + this.preventDefault(); + return this; + }, + + stopPropagation: function() { + if (this.event.stopPropagation) this.event.stopPropagation(); + else this.event.cancelBubble = true; + // Needed for dragging + this.stopped = true; + return this; + }, + + preventDefault: function() { + if (this.event.preventDefault) this.event.preventDefault(); + else this.event.returnValue = false; + return this; + }, + + statics: { + events: new Hash({ + mouseenter: hover('mouseenter', 'mouseover'), + + mouseleave: hover('mouseleave', 'mouseout'), + + mousewheel: { type: Browser.GECKO ? 'DOMMouseScroll' : 'mousewheel' }, + + domready: function(func) { + var win = this.getWindow(), doc = this.getDocument(); + if (Browser.loaded) { + func.call(this); + } else if (!doc.onDomReady) { + // Only install it once, since fireEvent calls all the + // handlers. + doc.onDomReady = function() { + if (!Browser.loaded) { + Browser.loaded = true; + doc.fireEvent('domready'); + win.fireEvent('domready'); + } + } + if (Browser.TRIDENT) { + // From: http://www.hedgerwow.com/360/dhtml/ie-dom-ondocumentready.html + var temp = doc.createElement('div'); + // Do not call immediatelly. Call it right after the event handler + // is actually installed, through delay 0. + (function() { + try { + // This throws an error when the dom is not ready, except for framesets, + // where the second line is needed and will throw an error when not ready. + temp.$.doScroll('left'); + temp.insertBottom(DomElement.get('body')).setHtml('temp').remove(); + doc.onDomReady(); + } catch (e) { + arguments.callee.delay(50); + } + }).delay(0); + } else if (Browser.WEBKIT && Browser.VERSION < 525) { + (function() { + /^(loaded|complete)$/.test(doc.$.readyState) + ? doc.onDomReady() : arguments.callee.delay(50); + })(); + } else { + win.addEvent('load', doc.onDomReady); + doc.addEvent('DOMContentLoaded', doc.onDomReady); + } + } + } + }), + + add: function(events) { + this.events.append(events); + } + } + }; +}); + +DomElement.inject(new function() { + // Function that fires / triggers an event. The difference is taht + // to trigger fake events, one need to call the 'bound' object, whereas + // to fire the response, one calls 'func'. Bootstrap supports calling both + // and callEvent produces the closure for each case. + // In most cases, this calls the same function, but e.g. to initiate + // a dragstart, we need to trigger it. + function callEvent(fire) { + return function(type, args, delay) { + var entries = (this.events || {})[type]; + if (entries) { + // Make sure we pass already wrapped events through + var event = args && args[0]; + if (event) + args[0] = event.event ? event : new DomEvent(event); + entries.each(function(entry) { + entry[fire ? 'func' : 'bound'].delay(delay, this, args); + }, this); + } + // Return true if event was fired, false otherwise + return !!entries; + } + } + + return { + addEvent: function(type, func) { + this.events = this.events || {}; + var entries = this.events[type] = this.events[type] || []; + if (func && !entries.find(function(entry) { return entry.func == func })) { + // See if we have a pseudo event here. + var listener = func, name = type, pseudo = DomEvent.events[type]; + if (pseudo) { + if (typeof pseudo == 'function') pseudo = pseudo.call(this, func); + listener = pseudo && pseudo.listener || listener; + // name should contain the name of the native handler that should + // be remove in removeEvent. It's ok for this to be empty. + name = pseudo && pseudo.type; + } + // Wrap the event handler in another function that checks if an event + // object was passed or globally set. The DomEvent contstructor + // handles window.event as well. + var that = this, bound = function(event) { + if (event || window.event) + event = event && event.event ? event : new DomEvent(event); + if (listener.call(that, event) === false && event) + event.stop(); + }; + if (name) { + if (this.$.addEventListener) { + this.$.addEventListener(name, bound, false); + } else if (this.$.attachEvent) { + this.$.attachEvent('on' + name, bound); + } + } + // Func is the one to be called through fireEvent. see dragstart + // Also store a refrence to name here, as this might have changed too. + entries.push({ func: func, name: name, bound: bound }); + } + return this; + }, + + removeEvent: function(type, func) { + var entries = (this.events || {})[type], entry; + if (func && entries) { + if (entry = entries.remove(function(entry) { return entry.func == func })) { + var name = entry.name, pseudo = DomEvent.events[type]; + if (pseudo && pseudo.remove) pseudo.remove.call(this, func); + if (name) { + if (this.$.removeEventListener) { + this.$.removeEventListener(name, entry.bound, false); + } else if (this.$.detachEvent) { + this.$.detachEvent('on' + name, entry.bound); + } + } + } + } + return this; + }, + + addEvents: function(events) { + return Base.each(events || [], function(fn, type) { + this.addEvent(type, fn); + }, this); + }, + + removeEvents: function(type) { + if (this.events) { + if (type) { + (this.events[type] || []).each(function(fn) { + this.removeEvent(type, fn); + }, this); + delete this.events[type]; + } else { + Base.each(this.events, function(ev, type) { + this.removeEvents(type); + }, this); + this.events = null; + } + } + return this; + }, + + fireEvent: callEvent(true), + + triggerEvent: callEvent(false), + + finalize: function() { + this.removeEvents(); + } + }; +}); + +//////////////////////////////////////////////////////////////////////////////// +// Drag + +// Fake dragstart, drag and dragend events, all in a self contained inject scope. + +DomEvent.add(new function() { + var object, last; + + function dragStart(event) { + if (object != this) { + event.type = 'dragstart'; + last = event.page; + this.fireEvent('dragstart', [event]); + // dragstart might stop the event, check here + if (!event.stopped) { + event.stop(); + var doc = this.getDocument(); + doc.addEvent('mousemove', drag); + doc.addEvent('mouseup', dragEnd); + object = this; + } + } + } + + function drag(event) { + event.type = 'drag'; + event.delta = { + x: event.page.x - last.x, + y: event.page.y - last.y + } + last = event.page; + object.fireEvent('drag', [event]); + event.preventDefault(); + } + + function dragEnd(event) { + if (object) { + event.type = 'dragend'; + object.fireEvent('dragend', [event]); + event.preventDefault(); + var doc = object.getDocument(); + doc.removeEvent('mousemove', drag); + doc.removeEvent('mouseup', dragEnd); + object = null; + } + } + + return { + dragstart: { + type: 'mousedown', + listener: dragStart + }, + + drag: { + type: 'mousedown', + listener: dragStart + }, + + dragend: {} + }; +}); + +// Dom Selectors + +//////////////////////////////////////////////////////////////////////////////// +// Selectors + +DomElement.inject(new function() { + // Method indices: + var XPATH= 0, FILTER = 1; + + var methods = [{ // XPATH + getParam: function(items, separator, context, params) { + var str = context.namespaceURI ? 'xhtml:' + params.tag : params.tag; + if (separator && (separator = DomElement.separators[separator])) + str = separator[XPATH] + str; + for (var i = params.pseudos.length; i--;) { + var pseudo = params.pseudos[i]; + str += pseudo.handler[XPATH](pseudo.argument); + } + if (params.id) str += '[@id="' + params.id + '"]'; + for (var i = params.classes.length; i--;) + str += '[contains(concat(" ", @class, " "), " ' + params.classes[i] + ' ")]'; + for (var i = params.attributes.length; i--;) { + var attribute = params.attributes[i]; + var operator = DomElement.operators[attribute[1]]; + if (operator) str += operator[XPATH](attribute[0], attribute[2]); + else str += '[@' + attribute[0] + ']'; + } + items.push(str); + return items; + }, + + getElements: function(items, elements, context) { + function resolver(prefix) { + return prefix == 'xhtml' ? 'http://www.w3.org/1999/xhtml' : false; + } + var res = (context.ownerDocument || context).evaluate('.//' + items.join(''), context, + resolver, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); + for (var i = 0, l = res.snapshotLength; i < l; i++) + elements.push(res.snapshotItem(i)); + } + }, { // FILTER + getParam: function(items, separator, context, params, data) { + var found = []; + var tag = params.tag; + if (separator && (separator = DomElement.separators[separator])) { + separator = separator[FILTER]; + var uniques = {}; + function add(item) { + if (!item._unique) + DomNode.unique(item); + if (!uniques[item._unique] && match(item, params, data)) { + uniques[item._unique] = true; + found.push(item); + return true; + } + } + for (var i = 0, l = items.length; i < l; i++) + separator(items[i], params, add); + if (params.clearTag) + params.tag = params.clearTag = null; + return found; + } + if (params.id) { + // First try getElementById. If that does not return the right + // object, retrieve tags first and then filter by id. + var el = (context.ownerDocument || context).getElementById(params.id); + // Clear as it is already filtered by getElementById + params.id = null; + return el && DomElement.isAncestor(el, context) + && match(el, params, data) ? [el] : null; + } else { + if (!items.length) { + items = context.getElementsByTagName(tag); + // Clear as it is already filtered by getElementsByTagName + params.tag = null; + } + for (var i = 0, l = items.length; i < l; i++) + if (match(items[i], params, data)) + found.push(items[i]); + } + return found; + }, + + getElements: function(items, elements, context) { + elements.append(items); + } + }]; + + function parse(selector) { + var params = { tag: '*', id: null, classes: [], attributes: [], pseudos: [] }; + selector.replace(/:([^:(]+)*(?:\((["']?)(.*?)\2\))?|\[([\w-]+)(?:([!*^$~|]?=)(["']?)(.*?)\6)?\]|\.[\w-]+|#[\w-]+|\w+|\*/g, function(part) { + switch (part.charAt(0)) { + case '.': params.classes.push(part.slice(1)); break; + case '#': params.id = part.slice(1); break; + case '[': params.attributes.push([arguments[4], arguments[5], arguments[7]]); break; + case ':': + var handler = DomElement.pseudos[arguments[1]]; + if (!handler) { + params.attributes.push([arguments[1], arguments[3] ? '=' : '', arguments[3]]); + break; + } + params.pseudos.push({ + name: arguments[1], + argument: handler && handler.parser + ? (handler.parser.apply ? handler.parser(arguments[3]) : handler.parser) + : arguments[3], + handler: handler.handler || handler + }); + break; + default: params.tag = part; + } + return ''; + }); + return params; + } + + function match(el, params, data) { + if (params.id && params.id != el.id) + return false; + + if (params.tag && params.tag != '*' && params.tag != (el.tagName || '').toLowerCase()) + return false; + + for (var i = params.classes.length; i--;) + if (!el.className || !el.className.contains(params.classes[i], ' ')) + return false; + + var proto = DomElement.prototype; + for (var i = params.attributes.length; i--;) { + var attribute = params.attributes[i]; + // Use a hack to call DomElement.prototype.getProperty on + // unwrapped elements very quickly: Set $ on + // DomElement.prototype, then call getProperty on it. + // This is much faster than the DomElement.getProperty generic. + proto.$ = el; // Point to the native elment for the call + var val = proto.getProperty(attribute[0]); + if (!val) return false; + var operator = DomElement.operators[attribute[1]]; + operator = operator && operator[FILTER]; + if (operator && (!val || !operator(val, attribute[2]))) + return false; + } + + for (var i = params.pseudos.length; i--;) { + var pseudo = params.pseudos[i]; + if (!pseudo.handler[FILTER](el, pseudo.argument, data)) + return false; + } + + return true; + } + + function filter(items, selector, context, elements, data) { + // XPATH does not properly match selected attributes in option elements + // Force filter code when the selectors contain "option[" + // Also, use FILTER when filtering a previously filled list of items, + // as used by getParents() + var method = methods[!Browser.XPATH || items.length || + typeof selector == 'string' && selector.contains('option[') + ? FILTER : XPATH]; + var separators = []; + selector = selector.trim().replace(/\s*([+>~\s])[a-zA-Z#.*\s]/g, function(match) { + if (match.charAt(2)) match = match.trim(); + separators.push(match.charAt(0)); + return ':)' + match.charAt(1); + }).split(':)'); + for (var i = 0, l = selector.length; i < l; i++) { + var params = parse(selector[i]); + if (!params) return elements; // TODO: correct? + var next = method.getParam(items, separators[i - 1], context, params, data); + if (!next) break; + items = next; + } + method.getElements(items, elements, context); + return elements; + } + + return { + + getElements: function(selectors, nowrap) { + var elements = nowrap ? [] : new this._collection(); + selectors = !selectors ? ['*'] : typeof selectors == 'string' + ? selectors.split(',') + : selectors.length != null ? selectors : [selectors]; + for (var i = 0, l = selectors.length; i < l; i++) { + var selector = selectors[i]; + if (Base.type(selector) == 'element') elements.push(selector); + else filter([], selector, this.$, elements, {}); + } + return elements; + }, + + getElement: function(selector) { + var el, type = Base.type(selector), match; + // Try fetching by id first, if no success, assume a real selector. + // Note that '#' is not needed, a string that could be an id (a-zA-Z_-) + // is enough for trying getElementById first. + // So $() can also work like Mootools' $() + if (type == 'window') { + el = selector; + } else { + if (type == 'string' && (match = selector.match(/^#?([\w-]+)$/))) + el = this.getDocument().$.getElementById(match[1]); + // TODO! + else if (DomNode.isNode(type)) + el = DomElement.unwrap(selector); + // If el was fetched by id above, but is not a child of this or is this, + // use the real selector. + if (el && el != this.$ && !DomElement.isAncestor(el, this.$)) + el = null; + // TODO: Is there a way to only fetch the first in getElements, + // with an optional third parameter? + if (!el) + el = this.getElements(selector, true)[0]; + } + return DomNode.wrap(el); + }, + + hasElement: function(selector) { + return !!this.getElement(selector); + }, + + match: function(selector) { + return !selector || match(this.$, parse(selector), {}); + }, + + filter: function(elements, selector) { + return filter(elements, selector, this.$, new this._collection(), {}); + }, + + statics: { + match: function(el, selector) { + return !selector || match(DomElement.unwrap(el), parse(selector), {}); + } + } + }; +}); + +//////////////////////////////////////////////////////////////////////////////// +// Separators + +DomElement.separators = { + '~': [ + // XPATH + '/following-sibling::', + // FILTER + function(item, params, add) { + while (item = item.nextSibling) + if (item.nodeType == 1 && add(item)) + break; + } + ], + + '+': [ + // XPATH + '/following-sibling::*[1]/self::', + // FILTER + function(item, params, add) { + while (item = item.nextSibling) { + if (item.nodeType == 1) { + add(item); + break; + } + } + } + ], + + '>': [ + // XPATH + '/', + // FILTER + function(item, params, add) { + var children = item.childNodes; + for (var i = 0, l = children.length; i < l; i++) + if (children[i].nodeType == 1) + add(children[i]); + } + ], + + ' ': [ + // XPATH + '//', + // FILTER + function(item, params, add) { + var children = item.getElementsByTagName(params.tag); + params.clearTag = true; + for (var i = 0, l = children.length; i < l; i++) + add(children[i]); + } + ] +}; + +//////////////////////////////////////////////////////////////////////////////// +// Operators + +DomElement.operators = new function() { + // Producer for the group of contains based operators: *=, |=, ~=. See bellow. + function contains(sep) { + return [ + // XPATH + function(a, v) { + return '[contains(' + (sep ? 'concat("' + sep + '", @' + a + ', "' + sep + '")' : '@' + a) + ', "' + sep + v + sep + '")]'; + }, + // FILTER + function(a, v) { + return a.contains(v, sep); + } + ] + } + + return { + '=': [ + // XPATH + function(a, v) { + return '[@' + a + '="' + v + '"]'; + }, + // FILTER + function(a, v) { + return a == v; + } + ], + + '^=': [ + // XPATH + function(a, v) { + return '[starts-with(@' + a + ', "' + v + '")]'; + }, + // FILTER + function(a, v) { + return a.substring(0, v.length) == v; + } + ], + + '$=': [ + // XPATH + function(a, v) { + return '[substring(@' + a + ', string-length(@' + a + ') - ' + v.length + ' + 1) = "' + v + '"]'; + }, + // FILTER + function(a, v) { + return a.substring(a.length - v.length) == v; + } + ], + + '!=': [ + // XPATH + function(a, v) { + return '[@' + a + '!="' + v + '"]'; + }, + // FILTER + function(a, v) { + return a != v; + } + ], + + '*=': contains(''), + + '|=': contains('-'), + + '~=': contains(' ') + }; +}; + +//////////////////////////////////////////////////////////////////////////////// +// Pseudos + +DomElement.pseudos = new function() { + // Handler for the nth-child group of pseudo operators. + var nthChild = [ + // XPATH + function(argument) { + switch (argument.special) { + case 'n': return '[count(preceding-sibling::*) mod ' + argument.a + ' = ' + argument.b + ']'; + case 'first': return '[count(preceding-sibling::*) = 0]'; + case 'last': return '[count(following-sibling::*) = 0]'; + case 'only': return '[not(preceding-sibling::* or following-sibling::*)]'; + case 'index': return '[count(preceding-sibling::*) = ' + argument.a + ']'; + } + }, + // FILTER + function(el, argument, data) { + var count = 0; + switch (argument.special) { + case 'n': + data.indices = data.indices || {}; + if (!data.indices[el._unique]) { + var children = el.parentNode.childNodes; + for (var i = 0, l = children.length; i < l; i++) { + var child = children[i]; + if (child.nodeType == 1) { + if (!child._unique) + DomNode.unique(item); + data.indices[child._unique] = count++; + } + } + } + return data.indices[el._unique] % argument.a == argument.b; + case 'first': + while (el = el.previousSibling) + if (el.nodeType == 1) + return false; + return true; + case 'last': + while (el = el.nextSibling) + if (el.nodeType == 1) + return false; + return true; + case 'only': + var prev = el; + while(prev = prev.previousSibling) + if (prev.nodeType == 1) + return false; + var next = el; + while (next = next.nextSibling) + if (next.nodeType == 1) + return false; + return true; + case 'index': + while (el = el.previousSibling) + if (el.nodeType == 1 && ++count > argument.a) + return false; + return true; + } + return false; + } + ]; + + // Producer for both case-sensitive and caseless versions of the contains + // pseudo operator. + function contains(caseless) { + // abc for lowercase translation. + var abc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + return [ + // XPATH + function(argument) { + return '[contains(' + (caseless ? 'translate(text(), "' + abc + + '", "' + abc.toLowerCase() + '")' : 'text()') + ', "' + + (caseless && argument ? argument.toLowerCase() : argument) + '")]'; + }, + // FILTER + function(el, argument) { + if (caseless && argument) argument = argument.toLowerCase(); + var nodes = el.childNodes; + for (var i = nodes.length - 1; i >= 0; i--) { + var child = nodes[i]; + if (child.nodeName && child.nodeType == 3 && + (caseless ? child.nodeValue.toLowerCase() : child.nodeValue).contains(argument)) + return true; + } + return false; + } + ]; + } + + return { + 'nth-child': { + parser: function(argument) { + var match = argument ? argument.match(/^([+-]?\d*)?([a-z]+)?([+-]?\d*)?$/) : [null, 1, 'n', 0]; + if (!match) return null; + var i = parseInt(match[1]), + a = isNaN(i) ? 1 : i, + special = match[2], + b = parseInt(match[3]) || 0; + if (a != 0) { + b--; + while (b < 1) b += a; + while (b >= a) b -= a; + } else { + a = b; + special = 'index'; + } + switch (special) { + case 'n': return { a: a, b: b, special: 'n' }; + case 'odd': return { a: 2, b: 0, special: 'n' }; + case 'even': return { a: 2, b: 1, special: 'n' }; + case 'first': return { special: 'first' }; + case 'last': return { special: 'last' }; + case 'only': return { special: 'only' }; + default: return { a: a - 1, special: 'index' }; + } + }, + handler: nthChild + }, + + // Short-cut to nth-child(even) / nth-child(2n) + 'even': { + parser: { a: 2, b: 1, special: 'n' }, + handler: nthChild + }, + + // Short-cut to nth-child(odd) / nth-child(2n+1) + 'odd': { + parser: { a: 2, b: 0, special: 'n' }, + handler: nthChild + }, + + 'first-child': { + parser: { special: 'first' }, + handler: nthChild + }, + + 'last-child': { + parser: { special: 'last' }, + handler: nthChild + }, + + 'only-child': { + parser: { special: 'only' }, + handler: nthChild + }, + + 'enabled': [ + // XPATH + function() { + return '[not(@disabled)]'; + }, + // FILTER + function(el) { + return !el.disabled; + } + ], + + 'empty': [ + // XPATH + function() { + return '[not(node())]'; + }, + // FILTER + function(el) { + return !(el.innerText || el.textContent || '').length; + } + ], + + 'contains': contains(false), + + // Extension of contains for case insensitive compare. This is very + // helpfull for on-site searches. + 'contains-caseless': contains(true) + }; +}; + +// Html + +//////////////////////////////////////////////////////////////////////////////// +// HtmlElements + +HtmlElements = DomElements.extend(); + +//////////////////////////////////////////////////////////////////////////////// +// HtmlElement + +HtmlElement = DomElement.extend({ + _collection: HtmlElements +}); + +// Use the modified inject function from above which injects both into HtmlElement +// and HtmlElements. +HtmlElement.inject({ + _properties: ['html'], + + getClass: function() { + return this.$.className; + }, + + setClass: function(cls) { + this.$.className = cls; + }, + + modifyClass: function(name, add) { + if (!this.hasClass(name) ^ !add) // xor + this.$.className = (add ? this.$.className + ' ' + name : + this.$.className.replace(name, '')).clean(); + return this; + }, + + addClass: function(name) { + return this.modifyClass(name, true); + }, + + removeClass: function(name) { + return this.modifyClass(name, false); + }, + + toggleClass: function(name) { + return this.modifyClass(name, !this.hasClass(name)); + }, + + hasClass: function(name) { + return this.$.className.contains(name, ' '); + } +}); + +//////////////////////////////////////////////////////////////////////////////// +// toNode conversion for Array and String + +Array.inject({ + toNode: function(doc) { + doc = DomNode.wrap(doc || document); + // ['div', { margin: 10 }, [ // Children + // 'span', { html: 'hello ' }, + // 'world' + // ]] + var elements = new HtmlElements(); + for (var i = 0; i < this.length;) { + var value = this[i++], element = null, type = Base.type(value); + if (type == 'string') { + // If the string is html, convert it through String#toNode. + // Otherwise assume it's a tag name, and look see the following + // value is a properties hash. Use these to create the element: + var props = /^(object|hash)$/.test(Base.type(this[i])) && this[i++]; + element = value.isHtml() + ? value.toNode(doc).set(props) + : doc.createElement(value, props); + // See if it has children defined, and add them through Array#toNode + if (Base.type(this[i]) == 'array') + element.injectBottom(this[i++].toNode(doc)); + } else if (DomNode.isNode(type)) { + // Raw nodes / elements + element = value; + } else if (value && value.toNode) { + // Anything else + element = value.toNode(doc); + } + // Append arrays and push single elements. + if (element) + elements[Base.type(element) == 'array' ? 'append' : 'push'](element); + } + // Unbox if there's only one element in the array + return elements.length == 1 ? elements[0] : elements; + } +}); + +String.inject({ + toNode: function(doc) { + var doc = doc || document, elements; + // See if it contains tags. If so, produce nodes, otherwise execute + // the string as a selector + if (this.isHtml()) { + // Html code. Conversion to HtmlElements ported from jQuery + // Trim whitespace, otherwise indexOf won't work as expected + var str = this.trim().toLowerCase(); + // doc can be native or wrapped: + var div = DomElement.unwrap(doc).createElement('div'); + + var wrap = + // option or optgroup + !str.indexOf('', ''] || + !str.indexOf('', ''] || + (!str.indexOf('', ''] || + !str.indexOf('', ''] || + // matched above + (!str.indexOf('', ''] || + !str.indexOf('', ''] || + [0,'','']; + + // Go to html and back, then peel off extra wrappers + div.innerHTML = wrap[1] + this + wrap[2]; + // Move to the right depth + while (wrap[0]--) + div = div.firstChild; + // Remove IE's autoinserted from table fragments + if (Browser.TRIDENT) { + var els = []; + if (!str.indexOf(', *may* have spurious + els = div.firstChild && div.firstChild.childNodes; + } else if (wrap[1] == '' && str.indexOf(' or + els = div.childNodes; + } + for (var i = els.length - 1; i >= 0 ; --i) { + var el = els[i]; + if (el.nodeName.toLowerCase() == 'tbody' && !el.childNodes.length) + el.parentNode.removeChild(el); + } + } + elements = new HtmlElements(div.childNodes); + } else { + // Simply execute string as dom selector. + // Make sure doc is wrapped. + elements = DomNode.wrap(doc).getElements(this); + } + // Unbox if there's only one element in the array + return elements.length == 1 ? elements[0] : elements; + } +}); + +//////////////////////////////////////////////////////////////////////////////// +// HtmlDocument + +HtmlDocument = DomDocument.extend({ + // Use HtmlElements collection instead of DomElements for HtmlDocuments + _collection: HtmlElements +}); + +//////////////////////////////////////////////////////////////////////////////// +// Style + +HtmlElement.inject(new function() { + var styles = { + all: { + width: '@px', height: '@px', left: '@px', top: '@px', right: '@px', bottom: '@px', + color: 'rgb(@, @, @)', backgroundColor: 'rgb(@, @, @)', backgroundPosition: '@px @px', + fontSize: '@px', letterSpacing: '@px', lineHeight: '@px', textIndent: '@px', + margin: '@px @px @px @px', padding: '@px @px @px @px', + border: '@px @ rgb(@, @, @) @px @ rgb(@, @, @) @px @ rgb(@, @, @) @px @ rgb(@, @, @)', + borderWidth: '@px @px @px @px', borderStyle: '@ @ @ @', + borderColor: 'rgb(@, @, @) rgb(@, @, @) rgb(@, @, @) rgb(@, @, @)', + clip: 'rect(@px, @px, @px, @px)', opacity: '@' + }, + part: { + 'border': {}, 'borderWidth': {}, 'borderStyle': {}, 'borderColor': {}, + 'margin': {}, 'padding': {} + } + }; + + ['Top', 'Right', 'Bottom', 'Left'].each(function(dir) { + ['margin', 'padding'].each(function(style) { + var sd = style + dir; + styles.part[style][sd] = styles.all[sd] = '@px'; + }); + var bd = 'border' + dir; + styles.part.border[bd] = styles.all[bd] = '@px @ rgb(@, @, @)'; + var bdw = bd + 'Width', bds = bd + 'Style', bdc = bd + 'Color'; + styles.part[bd] = {}; + styles.part.borderWidth[bdw] = styles.part[bd][bdw] = '@px'; + styles.part.borderStyle[bds] = styles.part[bd][bds] = '@'; + styles.part.borderColor[bdc] = styles.part[bd][bdc] = 'rgb(@, @, @)'; + }); + + // Now pre-split all style.all settings at ' ', instead of each time + // in setStyles + Base.each(styles.all, function(val, name) { + this[name] = val.split(' '); + }); + + var fields = { + + getComputedStyle: function(name) { + if (this.$.currentStyle) return this.$.currentStyle[name.camelize()]; + var style = this.getWindow().$.getComputedStyle(this.$, null); + return style ? style.getPropertyValue(name.hyphenate()) : null; + }, + + getStyle: function(name) { + if (name === undefined) return this.getStyles(); + if (name == 'opacity') { + var op = this.opacity; + return op || op == 0 ? op : this.getVisibility() ? 1 : 0; + } + var el = this.$; + name = name.camelize(); + var style = el.style[name]; + if (!Base.check(style)) { + if (styles.part[name]) { + style = Hash.map(styles.part[name], function(val, key) { + return this.getStyle(key); + }, this); + return style.every(function(val) { + return val == style[0]; + }) ? style[0] : style.join(' '); + } + style = this.getComputedStyle(name); + } + if (name == 'visibility') + return /^(visible|inherit(|ed))$/.test(style); + var color = style && style.match(/rgb[a]?\([\d\s,]+\)/); + if (color) return style.replace(color[0], color[0].rgbToHex()); + if (Browser.PRESTO || Browser.TRIDENT && isNaN(parseInt(style))) { + // Fix IE / Opera style that falsly include border and padding: + if (/^(width|height)$/.test(name)) { + var size = 0; + (name == 'width' ? ['left', 'right'] : ['top', 'bottom']).each(function(val) { + size += this.getStyle('border-' + val + '-width').toInt() + this.getStyle('padding-' + val).toInt(); + }, this); + // TODO: Should 'scroll' be used instead, as 'offset' also includes the scroll bars? + return this.$['offset' + name.capitalize()] - size + 'px'; + } + if (Browser.PRESTO && /px/.test(style)) return style; + if (/border(.+)[wW]idth|margin|padding/.test(name)) return '0px'; + } + return style; + }, + + setStyle: function(name, value) { + if (value === undefined) return this.setStyles(name); + var el = this.$; + switch (name) { + case 'float': + name = Browser.TRIDENT ? 'styleFloat' : 'cssFloat'; + break; + case 'clip': + // Setting clip to true sets it to the current bounds + // TODO: Calculate only if Dimension.js is defined? add conditional macro? + if (value == true) + value = [0, el.offsetWidth, el.offsetHeight, 0]; + break; + default: + name = name.camelize(); + } + var type = Base.type(value); + if (value != undefined && type != 'string') { + var parts = styles.all[name] || ['@'], index = 0; + // Flatten arrays, e.g. for borderColor where it might be an + // array of four color arrays. + value = (type == 'array' ? value.flatten() : [value]).map(function(val) { + var part = parts[index++]; + if (!part) + throw Base.stop; + return Base.type(val) == 'number' ? part.replace('@', name == 'opacity' ? val : Math.round(val)) : val; + }).join(' '); + } + switch (name) { + case 'visibility': + // Convert 0 to false, 1 to true before converting to visible / hidden + if (!isNaN(value)) value = !!value.toInt() + ''; + // Convert true -> visible, false -> hidden, everything else remains unchanged + value = value == 'true' && 'visible' || value == 'false' && 'hidden' || value; + break; + case 'opacity': + // Set opacity to 1 if it's 0 and set visibility to 0 instead, + // to fix a problem on Firefox on Mac, where antialiasing is affected + // otherwise... TODO: Find better solution? + this.opacity = value = parseFloat(value); + this.setStyle('visibility', !!value); + if (!value) value = 1; + if (!el.currentStyle || !el.currentStyle.hasLayout) el.style.zoom = 1; + if (Browser.TRIDENT) el.style.filter = value > 0 && value < 1 ? 'alpha(opacity=' + value * 100 + ')' : ''; + el.style.opacity = value; + return this; + } + el.style[name] = value; + return this; + }, + + getStyles: function() { + return arguments.length ? Array.each(arguments, function(name) { + this[name] = that.getStyle(name); + }, {}) : this.$.style.cssText; + }, + + setStyles: function(styles) { + switch (Base.type(styles)) { + case 'object': + Base.each(styles, function(style, name) { + // only set styles that have a defined value (null !== undefined) + if (style !== undefined) + this.setStyle(name, style); + }, this); + break; + case 'string': + this.$.style.cssText = styles; + } + return this; + } + }; + + // Create getters and setters for some often used css properties: + // TODO: Add more? + ['opacity', 'color', 'background', 'visibility', 'clip', 'zIndex', + 'border', 'margin', 'padding', 'display'].each(function(name) { + var part = name.capitalize(); + fields['get' + part] = function() { + return this.getStyle(name); + }; + fields['set' + part] = function(value) { + // pass mutliple params as array + return this.setStyle(name, arguments.length > 1 + ? Array.create(arguments) : value); + }; + }); + + return fields; +}); + +//////////////////////////////////////////////////////////////////////////////// +// HtmlForm + +// HtmlForm related functions, but available in all elements: + +HtmlElement.inject({ + + getFormElements: function() { + return this.getElements(['input', 'select', 'textarea']); + }, + + getValue: function(name) { + var el = this.getElement(name); + return el && el.getValue && el.getValue(); + }, + + setValue: function(name, val) { + var el = this.getElement(name); + // On Safari, using injectBottom here causes problems with transmission of + // some of the form values sometimes. Injecting at the top seems to solve + // this. + if (!el) el = this.injectTop('input', { type: 'hidden', id: name, name: name }); + return el.setValue(val); + }, + + getValues: function() { + return this.getFormElements().each(function(el) { + var name = el.getName(), value = el.getValue(); + if (name && value !== undefined && !el.getDisabled()) + this[name] = value; + }, new Hash()); + }, + + setValues: function(values) { + return Base.each(values, function(val, name) { + this.setValue(name, val); + }, this); + }, + + toQueryString: function() { + return Base.toQueryString(this.getValues()); + } +}); + +HtmlForm = HtmlElement.extend({ + _tag: 'form', + _properties: ['action', 'method', 'target'], + _methods: ['submit'], + + blur: function() { + return this.getFormElements().each(function(el) { + el.blur(); + }, this); + }, + + enable: function(enable) { + return this.getFormElements().each(function(el) { + el.enable(enable); + }, this); + } +}); + +HtmlFormElement = HtmlElement.extend({ + _properties: ['name', 'disabled'], + _methods: ['focus', 'blur'], + + enable: function(enable) { + var disabled = !enable && enable !== undefined; + if (disabled) this.$.blur(); + this.$.disabled = disabled; + return this; + } +}); + +HtmlInput = HtmlFormElement.extend({ + _tag: 'input', + _properties: ['type', 'checked', 'defaultChecked', 'readOnly', 'maxLength'], + _methods: ['click'], + + getValue: function() { + if (this.$.checked && /^(checkbox|radio)$/.test(this.$.type) || + /^(hidden|text|password|button|search)$/.test(this.$.type)) + return this.$.value; + }, + + // TODO: Decide if setValue for checkboxes / radios should actually change + // the value or set checked if the values match! Maybe a new function is + // needed that does that, e.g. set / getCurrent + setValue: function(val) { + if (/^(checkbox|radio)$/.test(this.$.type)) this.$.checked = this.$.value == val; + // Fix IE bug where string values set to null appear as 'null' instead of '' + else this.$.value = val != null ? val : ''; + return this; + } +}); + +HtmlTextArea = HtmlFormElement.extend({ + _tag: 'textarea', + _properties: ['value'] +}); + +HtmlSelect = HtmlFormElement.extend({ + _tag: 'select', + _properties: ['type', 'selectedIndex'], + + getOptions: function() { + return this.getElements('option'); + }, + + getSelected: function() { + return this.getElements('option[selected]'); + }, + + setSelected: function(values) { + this.$.selectedIndex = -1; + if (values) { + Array.each(values.length != null ? values : [values], function(val) { + val = DomElement.unwrap(val); + if (val != null) + this.getElements('option[value="' + (val.value || val) + '"]').setProperty('selected', true); + }, this); + } + return this; + }, + + getValue: function() { + return this.getSelected().getProperty('value'); + }, + + setValue: function(values) { + return this.setSelected(values); + } +}); + +HtmlOption = HtmlFormElement.extend({ + _tag: 'option', + _properties: ['text', 'value', 'selected', 'defaultSelected', 'index'] +}); + +//////////////////////////////////////////////////////////////////////////////// +// Selection + +HtmlFormElement.inject({ + setSelection: function(start, end) { + var sel = end == undefined ? start : { start: start, end: end }; + this.focus(); + if(this.$.setSelectionRange) { + this.$.setSelectionRange(sel.start, sel.end); + } else { + var value = this.getValue(); + var len = value.substring(sel.start, sel.end).replace(/\r/g, '').length; + var pos = value.substring(0, sel.start).replace(/\r/g, '').length; + var range = this.$.createTextRange(); + range.collapse(true); + range.moveEnd('character', pos + len); + range.moveStart('character', pos); + range.select(); + } + return this; + }, + + getSelection: function() { + if (this.$.selectionStart !== undefined) { + return { start: this.$.selectionStart, end: this.$.selectionEnd }; + } else { + this.focus(); + var pos = { start: 0, end: 0 }; + var range = this.getDocument().$.selection.createRange(); + var dup = range.duplicate(); + if (this.$.type == 'text') { + pos.start = 0 - dup.moveStart('character', -100000); + pos.end = pos.start + range.text.length; + } else { + var value = this.getValue(); + dup.moveToElementText(this.$); + dup.setEndPoint('StartToEnd', range); + pos.end = value.length - dup.text.length; + dup.setEndPoint('StartToStart', range); + pos.start = value.length - dup.text.length; + } + return pos; + } + }, + + getSelectedText: function() { + var range = this.getSelection(); + return this.getValue().substring(range.start, range.end); + }, + + replaceSelectedText: function(value, select) { + var range = this.getSelection(), current = this.getValue(); + // Fix Firefox scroll bug, see http://userscripts.org/scripts/review/9452, #insertAtCaret() + var top = this.$.scrollTop, height = this.$.scrollHeight; + this.setValue(current.substring(0, range.start) + value + current.substring(range.end, current.length)); + if(top != null) + this.$.scrollTop = top + this.$.scrollHeight - height; + return select || select == undefined + ? this.setSelection(range.start, range.start + value.length) + : this.setCaret(range.start + value.length); + }, + + getCaret: function() { + return this.getSelection().start; + }, + + setCaret: function(pos) { + return this.setSelection(pos, pos); + } +}); + +//////////////////////////////////////////////////////////////////////////////// +// HtmlImage + +HtmlImage = HtmlElement.extend({ + _tag: 'img', + _properties: ['src', 'alt', 'title'] +}); + +// Globals + +//////////////////////////////////////////////////////////////////////////////// +// Globals + +// Sort out garbage collection at the same time +$document = Browser.document = DomNode.wrap(document); +$window = Browser.window = DomNode.wrap(window).addEvent('unload', DomNode.dispose); + +// Remote + +//////////////////////////////////////////////////////////////////////////////// +// Callback + +/** + * Chain interface + */ +Chain = { + chain: function(fn) { + (this._chain = this._chain || []).push(fn); + return this; + }, + + callChain: function() { + if (this._chain && this._chain.length) + this._chain.shift().apply(this, arguments); + return this; + }, + + clearChain: function() { + this._chain = []; + return this; + } +}; + +/** + * Callback interface + */ +Callback = { + addEvent: function(type, fn) { + var ref = this.events = this.events || {}; + ref = ref[type] = ref[type] || []; + // We need to pass an iterator function to find, as otherwise fn + // is used as an iterator. + if (!ref.find(function(val) { return val == fn })) ref.push(fn); + return this; + }, + + addEvents: function(events) { + return Base.each((events || []), function(fn, type) { + this.addEvent(type, fn); + }, this); + }, + + fireEvent: function(type, args, delay) { + return (this.events && this.events[type] || []).each(function(fn) { + fn.delay(delay, this, args); + }, this); + }, + + removeEvent: function(type, fn) { + if (this.events && this.events[type]) + this.events[type].remove(function(val) { return fn == val; }); + return this; + }, + + setOptions: function(opts) { + // Keep copying this.options, as it might be defined in the prototype + return (this.options = Hash.create(this.options, opts)).each(function(val, i) { + if (typeof val == 'function' && (i = i.match(/^on([A-Z]\w*)/))) + this.addEvent(i[1].toLowerCase(), val); + }, this); + }, + + statics: { + inject: function(/* ... */) { + var proto = this.prototype, options = proto.options; + this.base.apply(this, arguments); + if (proto.options != options) + proto.options = Hash.merge({}, options, proto.options); + return this; + } + } +}; + +//////////////////////////////////////////////////////////////////////////////// +// Request + +// options: +// data +// headers +// method +// async +// link +// urlEncoded +// encoding +// evalScripts +// evalResponse +// emulation +// type (json, html, xml) +// secure +// update +// filter + +Request = Base.extend(Chain, Callback, new function() { + var unique = 0; + + function createRequest(that) { + if (!that.transport) + that.transport = window.XMLHttpRequest && new XMLHttpRequest() + || Browser.TRIDENT && new ActiveXObject('Microsoft.XMLHTTP'); + } + + function createFrame(that) { + var id = 'request_' + unique++, load = that.onFrameLoad.bind(that); + // IE Fix: Setting load event on iframes does not work, use onreadystatechange + var div = DomElement.get('body').injectBottom('div', { + styles: { + position: 'absolute', width: 0, height: 0, top: 0, marginLeft: '-10000px' + } + }, [ + 'iframe', { + name: id, id: id, events: { load: load, readystatechange: load } + } + ] + ); + that.frame = { + id: id, div: div, + iframe: window.frames[id] || document.getElementById(id), + element: DomElement.get(id) + }; + // Opera fix: force the iframe to be valid + div.offsetWidth; + } + + return { + options: { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' + }, + method: 'post', + async: true, + urlEncoded: true, + encoding: 'utf-8', + emulation: true, + secure: false + }, + + initialize: function(/* url: 'string', options: 'object', handler: 'function' */) { + var params = Array.associate(arguments, { url: 'string', options: 'object', handler: 'function' }); + this.setOptions(params.options); + // If a handler is passed, it is used to recieve both success and + // failure events. Only the success event will recieve a result + // argument though. + if (params.handler) + this.addEvent('complete', params.handler); + // Always set type to html if updating elements + if (this.options.update) + this.options.type = 'html'; + this.headers = new Hash(this.options.headers); + if (this.options.type == 'json') { + this.setHeader('Accept', 'application/json'); + this.setHeader('X-Request', 'JSON'); + } + if (this.options.urlEncoded && /^(post|put)$/.test(this.options.method)) { + this.setHeader('Content-Type', 'application/x-www-form-urlencoded' + + (this.options.encoding ? '; charset=' + this.options.encoding : '')); + } + this.headers.append(this.options.headers); + }, + + onStateChange: function() { + if (this.transport.readyState == 4 && this.running) { + this.running = false; + this.status = 0; + try { + this.status = this.transport.status; + delete this.transport.onreadystatechange; + } catch (e) {} + if (!this.status || this.status >= 200 && this.status < 300) { + this.success(this.transport.responseText, this.transport.responseXML); + } else { + this.fireEvent('complete').fireEvent('failure'); + } + } + }, + + onFrameLoad: function() { + var frame = this.frame && this.frame.iframe, loc = frame && frame.location, + doc = frame && (frame.contentDocument || frame.contentWindow || frame).document; + if (this.running && frame && loc && (!loc.href || loc.href.indexOf(this.url) != -1) + && /^(loaded|complete|undefined)$/.test(doc.readyState)) { + this.running = false; + // Try fetching value from the first tetarea in the document first, + // since that's the convention to send data with iframes now, just + // like in dojo. + var html = this.options.type == 'html', area = !html + && doc.getElementsByTagName('textarea')[0]; + var text = doc && (area && area.value || doc.body + && (html && doc.body.innerHTML || doc.body.textContent + || doc.body.innerText)) || ''; + // Clear src + this.frame.element.setProperty('src', ''); + // TODO: Add support for xml? + this.success(text); + // We need the iframe to stay around for a little while, + // otherwise it appears to load endlessly. Insert it back in + // and use delay to remove it again. This even works if + // success above changes the whole html and would remove the + // iframe, as it can happen during editing. Since we remove + // it before already, it is untouched by this. + if (!this.options.link) { + var div = this.frame.div; + div.insertBottom(DomElement.get('body')); + div.remove.delay(5000, div); + this.frame = null; + } + } + }, + + success: function(text, xml) { + var args; + switch (this.options.type) { + case 'html': + var match = text.match(/]*>([\u0000-\uffff]*?)<\/body>/i); + var stripped = this.stripScripts(match ? match[1] : text); + if (this.options.update) + DomElement.get(this.options.update).setHtml(stripped.html); + if (this.options.evalScripts) + this.executeScript(stripped.script); + args = [ stripped.html, text ]; + break; + case 'json': + args = [ Json.decode(text, this.options.secure), text ]; + break; + default: // xml? + args = [ this.processScripts(text), xml ] + } + this.fireEvent('complete', args) + .fireEvent('success', args) + .callChain(); + }, + + stripScripts: function(html) { + var script = ''; + html = html.replace(/]*>([\u0000-\uffff]*?)<\/script>/gi, function() { + script += arguments[1] + '\n'; + return ''; + }); + return { html: html, script: script }; + }, + + processScripts: function(text) { + if (this.options.evalResponse || (/(ecma|java)script/).test(this.getHeader('Content-type'))) { + this.executeScript(text); + return text; + } else { + // Strip scripts from text and execute bellow + var stripped = this.stripScripts(text); + if (this.options.evalScripts) + this.executeScript(stripped.script); + return stripped.html; + } + }, + + executeScript: function(script) { + if (window.execScript) { + window.execScript(script); + } else { + DomElement.get('head').injectBottom('script', { + type: 'text/javascript', text: script + }).remove(); + } + }, + + setHeader: function(name, value) { + this.headers[name] = value; + return this; + }, + + getHeader: function(name) { + try { + if (this.transport) + return this.transport.getResponseHeader(name); + } catch (e) {} + return null; + }, + + send: function() { + var params = Array.associate(arguments, { url: 'string', options: 'object', handler: 'function' }); + var opts = params.options ? Hash.merge(params.options, this.options) : this.options; + if (params.handler) + this.addEvent('complete', function() { + params.handler.apply(this, arguments); + this.removeEvent('complete', arguments.callee); + }); + if (this.running) { + switch (opts.link) { + case 'cancel': + this.cancel(); + break; + case 'chain': + this.chain(this.send.wrap(this, arguments)); + default: + return this; + } + } + var data = opts.data || ''; + var url = params.url || opts.url; + switch (Base.type(data)) { + case 'element': + data = DomNode.wrap(data); + // No need to post using forms if there are no files + if (data.getTag() != 'form' || !data.hasElement('input[type=file]')) + data = data.toQueryString(); + break; + case 'object': + data = Base.toQueryString(data); + break; + default: + data = data.toString(); + } + var string = typeof data == 'string', method = opts.method; + if (opts.emulation && /^(put|delete)$/.test(method)) { + if (string) data += '&_method=' + method; + else data.setValue('_method', method); + method = 'post'; + } + if (string && !this.options.iframe) { + createRequest(this); + if (!this.transport) { + if (!this.frame) + createFrame(this); + // No support for POST when using iframes. We could fake + // it through a hidden form that's produced on the fly, + // parse data and url for query values, but that's going a bit + // far for legacy support. + method = 'get'; + } + } else if (!this.frame) { + createFrame(this); + } + if (string && data && method == 'get') { + url += (url.contains('?') ? '&' : '?') + data; + data = null; + } + this.running = true; + this.url = url; + // Check frame first, as this is never reused. + if (this.frame) { + // Are we sending the request by submitting a form or simply + // setting the src? + var form = !string && data; + if (form) { + form.set({ + target: this.frame.id, action: url, method: method, + enctype: /* TODO: opts.urlEncoded || */ method == 'get' + ? 'application/x-www-form-urlencoded' + : 'multipart/form-data', + 'accept-charset': opts.encoding || '' + }).submit(); + } else { + this.frame.element.setProperty('src', url); + } + } else if (this.transport) { + try { + this.transport.open(method.toUpperCase(), url, opts.async); + this.transport.onreadystatechange = this.onStateChange.bind(this); + new Hash(this.headers, opts.headers).each(function(header, name) { + try{ + this.transport.setRequestHeader(name, header); + } catch (e) { + this.fireEvent('exception', [e, name, header]); + } + }, this); + this.fireEvent('request'); + this.transport.send(data); + if (!opts.async) + this.onStateChange(); + } catch (e) { + this.fireEvent('failure', [e]); + } + } + return this; + }, + + cancel: function() { + if (this.running) { + this.running = false; + if (this.transport) { + this.transport.abort(); + this.transport.onreadystatechange = null; + this.transport = null; + } else if (this.frame) { + this.frame.div.remove(); + this.frame = null; + } + this.fireEvent('cancel'); + } + return this; + } + }; +}); + +HtmlForm.inject({ + send: function(url) { + if (!this.sender) + this.sender = new Request({ link: 'cancel' }); + this.sender.send({ + url: url || this.getProperty('action'), + data: this, method: this.getProperty('method') || 'post' + }); + } +}); + +HtmlElement.inject({ + load: function() { + if (!this.loader) + this.loader = new Request({ link: 'cancel', update: this, method: 'get' }); + this.loader.send(Array.associate(arguments, { data: 'object', url: 'string' })); + return this; + } +}); + +//////////////////////////////////////////////////////////////////////////////// +// Asset + +Asset = new function() { + // Clones props and remove all handlers: + function getProperties(props) { + return props ? Hash.create(props).each(function(val, key) { + if (/^on/.test(key)) delete this[key]; + }) : {}; + } + + /* + Trial at syncronized loading for multiple script assets + but how to return the full set of assets, since they are created serially? + function createMultiple(type, sources, options, sync) { + var props = getProperties(options), count = 0; + options = options || {}; + var assets = new HtmlElements(); + function load(src) { + props.onLoad = function() { + if (options.onProgress) + options.onProgress.call(this, src); + if (++count == sources.length && options.onComplete) + options.onComplete.call(this); + } + assets.push(Asset[type](src, props)); + } + if (sync) { + var progress = options.onProgress; + options.onProgress = function(src) { + if (progress) + progress.call(this, src); + var next = sources[count + 1]; + if (next) + load(next); + }; + load(sources[0]); + } else { + sources.each(load); + } + return assets; + } + */ + function createMultiple(type, sources, options) { + var props = getProperties(options), count = 0; + options = options || {}; + return sources.each(function(src) { + props.onLoad = function() { + if (options.onProgress) + options.onProgress(src); + if (++count == sources.length && options.onComplete) + options.onComplete(); + } + this.push(Asset[type](src, props)); + }, new HtmlElements()); + } + + return { + script: function(src, props) { + var script = DomElement.get('head').injectBottom('script', Hash.merge({ + events: { + // props.onLoad can be null + load: props.onLoad && function() { + // We receive this event more than once on Opera, filter + // out here... + if (!this.loaded) { + this.loaded = true; + props.onLoad.call(this); + } + }, + readystatechange: function() { + if (/loaded|complete/.test(this.$.readyState)) + this.fireEvent('load'); + } + }, + src: src + }, getProperties(props))); + // On Safari < 3, execute a Request for the same resource at + // the same time. The resource will only be loaded once, and the + // Request will recieve a notification, while the script does not. + if (Browser.WEBKIT && Browser.VERSION < 420) + new Request({ url: src, method: 'get' }).addEvent('success', function() { + script.fireEvent('load', [], 1); + }).send(); + return script; + }, + + stylesheet: function(src, props) { + return new HtmlElement('link', new Hash({ + rel: 'stylesheet', media: 'screen', type: 'text/css', href: src + }, props)).insertInside(DomElement.get('head')); + }, + + image: function(src, props) { + props = props || {}; + var image = new Image(); + image.src = src; + var element = new HtmlElement('img', { src: src }); + ['load', 'abort', 'error'].each(function(type) { + var name = 'on' + type.capitalize(); + if (props[name]) element.addEvent(type, function() { + this.removeEvent(type, arguments.callee); + props[name].call(this); + }); + }); + if (image.width && image.height) + element.fireEvent('load', [], 1); + return element.setProperties(getProperties(props)); + }, + + scripts: function(sources, options) { + return createMultiple('script', sources, options); + }, + + stylesheets: function(sources, options) { + return createMultiple('stylesheet', sources, options); + }, + + images: function(sources, options) { + return createMultiple('image', sources, options); + } + } +}; + +//////////////////////////////////////////////////////////////////////////////// +// Cookie + +Cookie = { + set: function(name, value, expires, path) { + document.cookie = name + '=' + encodeURIComponent(value) + (expires ? ';expires=' + + expires.toGMTString() : '') + ';path=' + (path || '/'); + }, + get: function(name) { + var res = document.cookie.match('(?:^|;)\\s*' + name + '=([^;]*)'); + if (res) return decodeURIComponent(res[1]); + }, + + remove: function(name) { + this.set(name, '', -1); + } +}; + +// Effects + +//////////////////////////////////////////////////////////////////////////////// +// Fx + +// Mootools uses #setNow to define the current value and #increase to set them +// Bootstrap relies instead on #update that recieves a value to set and #get +// to retrieve the current value. Any class extending Fx needs to define these. + +Fx = Base.extend(Chain, Callback, { + options: { + fps: 50, + unit: false, + duration: 500, + wait: true, + transition: function(p) { + return -(Math.cos(Math.PI * p) - 1) / 2; + } + }, + + initialize: function(element, options) { + this.element = DomElement.get(element); + this.setOptions(options); + }, + + step: function() { + var time = Date.now(); + if (time < this.time + this.options.duration) { + this.delta = this.options.transition((time - this.time) / this.options.duration); + this.update(this.get()); + } else { + this.stop(true); + this.update(this.to); + this.fireEvent('complete', [this.element]).callChain(); + } + }, + + set: function(to) { + this.update(to); + this.fireEvent('set', [this.element]); + return this; + }, + + get: function() { + return this.compute(this.from, this.to); + }, + + compute: function(from, to) { + return (to - from) * this.delta + from; + }, + + start: function(from, to) { + if (!this.options.wait) this.stop(); + else if (this.timer) return this; + this.from = from; + this.to = to; + this.time = Date.now(); + // Fx.Elements allows effects to be run in slave mode. + if (!this.slave) { + this.timer = this.step.periodic(Math.round(1000 / this.options.fps), this); + this.fireEvent('start', [this.element]); + } + // Make the first step now: + this.step(); + return this; + }, + + stop: function(end) { + if (this.timer) { + this.timer = this.timer.clear(); + if (!end) this.fireEvent('cancel', [this.element]).clearChain(); + } + return this; + } +}); + +//////////////////////////////////////////////////////////////////////////////// +// Fx.Scroll + +Fx.Scroll = Fx.extend({ + options: { + offset: { x: 0, y: 0 }, + wheelStops: true + }, + + initialize: function(element, options) { + this.base(element, options); + if (this.options.wheelStops) { + var stop = this.stop.bind(this), stopper = this.element; + this.addEvent('start', function() { + stopper.addEvent('mousewheel', stop); + }, true); + this.addEvent('complete', function() { + stopper.removeEvent('mousewheel', stop); + }, true); + } + }, + + update: function(x, y) { + var now = Array.flatten(arguments); + this.element.setScrollOffset(now[0], now[1]); + }, + + get: function() { + var now = []; + for (var i = 0; i < 2; i++) + now.push(this.compute(this.from[i], this.to[i])); + return now; + }, + + start: function(x, y) { + var offsetSize = this.element.getSize(), + scrollSize = this.element.getScrollSize(), + scroll = this.element.getScrollOffset(), + values = { x: x, y: y }; + var lookup = { x: 'width', y: 'height' }; + for (var i in values) { + var s = lookup[i]; + var max = scrollSize[s] - offsetSize[s]; + if (Base.check(values[i])) + values[i] = Base.type(values[i]) == 'number' + ? values[i].limit(0, max) : max; + else values[i] = scroll[i]; + values[i] += this.options.offset[i]; + } + return this.base([scroll.x, scroll.y], [values.x, values.y]); + }, + + toTop: function() { + return this.start(false, 0); + }, + + toLeft: function() { + return this.start(0, false); + }, + + toRight: function() { + return this.start('right', false); + }, + + toBottom: function() { + return this.start(false, 'bottom'); + }, + + toElement: function(el) { + var offset = DomElement.get(el).getOffset(); + return this.start(offset.x, offset.y); + } +}); + +Fx.SmoothScroll = Fx.Scroll.extend({ + initialize: function(options, context) { + context = DomElement.get(context || document); + var doc = context.getDocument(), win = context.getWindow(); + this.base(doc, options); + var links = this.options.links ? $$(this.options.links) : $$('a', context); + var loc = win.location.href.match(/^[^#]*/)[0] + '#'; + links.each(function(link) { + if (link.$.href.indexOf(loc) != 0) return; + var hash = link.$.href.substring(loc.length); + var anchor = hash && DomElement.get('#' + hash, context); + if (anchor) { + link.addEvent('click', function(event) { + this.toElement(anchor); + var props = anchor.getProperties('name', 'id'); + anchor.removeProperties('name', 'id'); + win.location.hash = hash; + anchor.setProperties(props); + event.stop(); + }.bind(this)); + } + }, this); + } +}); + +//////////////////////////////////////////////////////////////////////////////// +// Fx.CSS + +Fx.CSS = new function() { + + var parsers = new Hash({ + color: { + match: function(value) { + if (value.match(/^#[0-9a-f]{3,6}$/i)) return value.hexToRgb(true); + return ((value = value.match(/(\d+),\s*(\d+),\s*(\d+)/))) ? [value[1], value[2], value[3]] : false; + }, + + compute: function(from, to, fx) { + return from.map(function(value, i) { + return Math.round(fx.compute(value, to[i])); + }); + }, + + get: function(value) { + return value.map(Number); + } + }, + + number: { + match: function(value) { + return parseFloat(value); + }, + + compute: function(from, to, fx) { + return fx.compute(from, to); + }, + + get: function(value, unit) { + return (unit) ? value + unit : value; + } + } + }); + + return { + start: function(element, property, values) { + values = Array.convert(values); + // If only one value is specified, use the current state as the + // starting point. + if (!Base.check(values[1])) + values = [ element.getStyle(property), values[0] ]; + var parsed = values.map(Fx.CSS.set); + return { from: parsed[0], to: parsed[1] }; + }, + + set: function(value) { + // Array.create splits strings at white spaces through String#toArray + return Array.convert(value).map(function(val) { + val = val + ''; + var res = parsers.find(function(parser, key) { + var value = parser.match(val); + if (Base.check(value)) return { value: value, parser: parser }; + }) || { + value: val, + parser: { + compute: function(from, to) { + return to; + } + } + }; + return res; + }); + }, + + compute: function(from, to, fx) { + return from.map(function(obj, i) { + return { + value: obj.parser.compute(obj.value, to[i].value, fx), + parser: obj.parser + }; + }); + }, + + get: function(now, unit) { + return now.reduce(function(prev, cur) { + var get = cur.parser.get; + return prev.concat(get ? get(cur.value, unit) : cur.value); + }, []); + } + } +}; + +//////////////////////////////////////////////////////////////////////////////// +// Fx.Style + +Fx.Style = Fx.extend({ + initialize: function(element, property, options) { + this.base(element, options); + this.property = property; + }, + + hide: function() { + return this.set(0); + }, + + get: function() { + return Fx.CSS.compute(this.from, this.to, this); + }, + + set: function(to) { + return this.base(Fx.CSS.set(to)); + }, + + start: function(from, to) { + if (this.timer && this.options.wait) return this; + var parsed = Fx.CSS.start(this.element, this.property, [from, to]); + return this.base(parsed.from, parsed.to); + }, + + update: function(val) { + this.element.setStyle(this.property, Fx.CSS.get(val, this.options.unit)); + } +}); + +HtmlElement.inject({ + effect: function(prop, opts) { + return new Fx.Style(this, prop, opts); + } +}); + +//////////////////////////////////////////////////////////////////////////////// +// Fx.Style + +Fx.Styles = Fx.extend({ + get: function() { + var that = this; + return Base.each(this.from, function(from, key) { + this[key] = Fx.CSS.compute(from, that.to[key], that); + }, {}); + }, + + set: function(to) { + return this.base(Base.each(to, function(val, key) { + this[key] = Fx.CSS.set(val); + }, {})); + }, + + start: function(obj) { + if (this.timer && this.options.wait) return this; + var from = {}, to = {}; + Base.each(obj, function(val, key) { + var parsed = Fx.CSS.start(this.element, key, val); + from[key] = parsed.from; + to[key] = parsed.to; + }, this); + return this.base(from, to); + }, + + update: function(val) { + Base.each(val, function(val, key) { + this.element.setStyle(key, Fx.CSS.get(val, this.options.unit)); + }, this); + } + +}); + +HtmlElement.inject({ + effects: function(opts) { + return new Fx.Styles(this, opts); + } +}); + +Fx.Elements = Fx.extend({ + initialize: function(elements, options) { + this.base(null, options); + this.elements = DomElement.getAll(elements); + }, + + start: function(obj) { + if (this.timer && this.options.wait) return this; + this.effects = {}; + + function start(that, key, val) { + var fx = that.effects[key] = new Fx.Styles(that.elements[key], that.options); + // Tell Fx we're in slave mode + fx.slave = true; + fx.start(val); + } + + Base.each(obj, function(val, key) { + if (key == '*') { + // Wildcard for effects to be applied to all elements + this.elements.each(function(el, key) { + start(this, key, val); + }, this); + } else if (isNaN(parseInt(key))) { + // A selector, for elements to be added, if they are not there + // already. + var els = DomElement.getAll(key); + this.elements.append(els); + els.each(function(el) { + start(this, this.elements.indexOf(el), val); + }, this); + } else { + // A normal array index in the passed elements array + start(this, key, val); + } + }, this); + return this.base(); + }, + + set: function(to) { + // do nothing, since update() handles slaves + }, + + update: function(to) { + Base.each(this.effects, function(fx) { + fx.step(); + }); + } +}); + +Fx.Transitions = new Base().inject({ + // Override the Fx.Transitions' inject function so that each function that + // is injected recieved #In, #Out and #InOut as additional methods. + inject: function(src) { + // Walk through all passed functions and add the additional functions. + return this.base(Base.each(src, function(func, name) { + func.In = func; + + func.Out = function(pos) { + return 1 - func(1 - pos); + } + + func.InOut = function(pos) { + return pos <= 0.5 ? func(2 * pos) / 2 : (2 - func(2 * (1 - pos))) / 2; + } + })); + }, + + Linear: function(p) { + return p; + } +}); + +Fx.Transitions.inject({ + Pow: function(p, x) { + return Math.pow(p, x[0] || 6); + }, + + Expo: function(p) { + return Math.pow(2, 8 * (p - 1)); + }, + + Circ: function(p) { + return 1 - Math.sin(Math.acos(p)); + }, + + Sine: function(p) { + return 1 - Math.sin((1 - p) * Math.PI / 2); + }, + + Back: function(p, x) { + x = x[0] || 1.618; + return Math.pow(p, 2) * ((x + 1) * p - x); + }, + + Bounce: function(p) { + var value; + for (var a = 0, b = 1; 1; a += b, b /= 2) { + if (p >= (7 - 4 * a) / 11) { + value = - Math.pow((11 - 6 * a - 11 * p) / 4, 2) + b * b; + break; + } + } + return value; + }, + + Elastic: function(p, x) { + return Math.pow(2, 10 * --p) * Math.cos(20 * p * Math.PI * (x[0] || 1) / 3); + } + +}); + +Fx.Transitions.inject(['Quad', 'Cubic', 'Quart', 'Quint'].each(function(name, i) { + this[name] = function(p) { + return Math.pow(p, i + 2); + } +}, {})); \ No newline at end of file