mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-05-11 07:13:09 +00:00
1763 lines
No EOL
62 KiB
JavaScript
1763 lines
No EOL
62 KiB
JavaScript
/*
|
|
Copyright (c) Garrett Smith.
|
|
Licensed under the AFL license.
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview
|
|
* <code>APE</code> provides core features, including namespacing and object creational aspects.
|
|
*
|
|
* <h3>APE JavaScript Library</h3>
|
|
* <p>
|
|
* Released under Academic Free Licence 3.0.
|
|
* </p>
|
|
*
|
|
* @author Garrett Smith
|
|
*/
|
|
|
|
/** @name APE
|
|
* @namespace */
|
|
if(APE !== undefined) throw Error("APE is already defined.");
|
|
var APE = {
|
|
|
|
/**
|
|
* @memberOf APE
|
|
* @description Prototype inheritance.
|
|
* @param {Object} subclass
|
|
* @param {Object} superclass
|
|
* @param {Object} mixin If present, <var>mixin</var>'s own properties are copied to receiver
|
|
* using APE.mixin(subclass.prototoype, superclass.prototype).
|
|
*/
|
|
extend : function(subclass, superclass, mixin) {
|
|
if(arguments.length === 0) return;
|
|
var f = arguments.callee, subp;
|
|
f.prototype = superclass.prototype;
|
|
subclass.prototype = subp = new f;
|
|
if(typeof mixin == "object")
|
|
APE.mixin(subp, mixin);
|
|
subp.constructor = subclass;
|
|
return subclass;
|
|
},
|
|
|
|
/**
|
|
* Shallow copy of properties; does not look up prototype chain.
|
|
* Copies all properties in s to r, using hasOwnProperty.
|
|
* @param {Object} r the receiver of properties.
|
|
* @param {Object} s the supplier of properties.
|
|
* Accounts for JScript DontEnum bug for valueOf and toString.
|
|
* @return {Object} r the receiver.
|
|
*/
|
|
mixin : function(r, s) {
|
|
var jscriptSkips = ['toString', 'valueOf'],
|
|
prop,
|
|
i = 0,
|
|
skipped;
|
|
for(prop in s) {
|
|
if(s.hasOwnProperty(prop))
|
|
r[prop] = s[prop];
|
|
}
|
|
// JScript DontEnum bug.
|
|
for( ; i < jscriptSkips.length; i++) {
|
|
skipped = jscriptSkips[i];
|
|
if(s.hasOwnProperty(skipped))
|
|
r[skipped] = s[skipped];
|
|
}
|
|
return r;
|
|
},
|
|
|
|
toString : function() { return "[APE JavaScript Library]"; },
|
|
|
|
/** Creational method meant for being cross-cut.
|
|
* Uses APE.newApply to create
|
|
* @param {HTMLElement} el An element. If el does not have
|
|
* an ID, then an ID will be automatically generated, based on the
|
|
* constructor's (this) identifier, or, If this is anonymous, "APE".
|
|
* @requires {Object} an object to be attached to as a property.
|
|
* @aspect
|
|
* @scope {Function} that accepts an HTMLElement for
|
|
* its first argument.
|
|
* APE.getByNode is intended to be bouund to a constructor function.
|
|
* @return <code>{new this(el [,args...])}</code>
|
|
*/
|
|
getByNode : function(el) {
|
|
var id = el.id,
|
|
fName;
|
|
if(!id) {
|
|
if(!APE.getByNode._i) APE.getByNode._i = 0;
|
|
fName = APE.getFunctionName(this);
|
|
if(!fName) fName = "APE";
|
|
id = el.id = fName+"_" + (APE.getByNode._i++);
|
|
}
|
|
if(!this.hasOwnProperty("instances")) this.instances = {};
|
|
return this.instances[id] || (this.instances[id] = APE.newApply(this, arguments));
|
|
},
|
|
|
|
/** Tries to get a name of a function object, returns "" if anonymous.
|
|
*/
|
|
getFunctionName : function(fun) {
|
|
if(typeof fun.name == "string") return fun.name;
|
|
var name = Function.prototype.toString.call(fun).match(/\s([a-z]+)\(/i);
|
|
return name && name[1]||"";
|
|
},
|
|
|
|
/** Creational method meant for being cross-cut.
|
|
* @param {HTMLElement} el An element that has an id.
|
|
* @requires {Object} an object to bind to.
|
|
* @aspect
|
|
* @description <code>getById</code> must be assigned to a function constructor
|
|
* that accepts an HTMLElement's <code>id</code> for
|
|
* its first argument.
|
|
* @example <pre>
|
|
* function Slider(el, config){ }
|
|
* Slider.getById = APE.getById;
|
|
* </pre>
|
|
* This allows for implementations to use a factory method with the constructor.
|
|
* <pre>
|
|
* Slider.getById( "weight", 1 );
|
|
* </pre>
|
|
* Subsequent calls to:
|
|
* <pre>
|
|
* Slider.getById( "weight" );
|
|
* </pre>
|
|
* will return the same Slider instance.
|
|
* An <code>instances</code> property is added to the constructor object
|
|
* that <code>getById</code> is assigned to.
|
|
* @return <pre>new this(id [,args...])</pre>
|
|
*/
|
|
getById : function(id) {
|
|
if(!this.hasOwnProperty("instances")) this.instances = {};
|
|
return this.instances[id] || (this.instances[id] = APE.newApply(this, arguments));
|
|
},
|
|
|
|
/** Creates a Factory method out of a function.
|
|
* @param {Function} constructor
|
|
* @param {Object} prototype
|
|
* @memberOf APE
|
|
*/
|
|
createFactory : function(constructor, prot) {
|
|
var baseObject = {},
|
|
instances = baseObject.instances = {}; // Export, for purge or cleanup.
|
|
if(prot) {
|
|
constructor.prototype = prot;
|
|
}
|
|
baseObject.getById = getById;
|
|
return baseObject;
|
|
function getById(id) {
|
|
return instances[id] || (instances[id] = APE.newApply(constructor, arguments));
|
|
}
|
|
},
|
|
|
|
newApply : (function() {
|
|
function F(){}
|
|
return newApply;
|
|
/**
|
|
* @param {Function} constructor constructor to be invoked.
|
|
* @param {Array} args arguments to pass to the constructor.
|
|
* Instantiates a constructor and uses apply().
|
|
* @memberOf APE
|
|
*/
|
|
function newApply(constructor, args) {
|
|
var i;
|
|
F.prototype = constructor.prototype;// Copy prototype.
|
|
F.prototype.constructor = constructor;
|
|
i = new F;
|
|
constructor.apply(i, args); // Apply the original constructor.
|
|
return i;
|
|
}
|
|
})(),
|
|
|
|
/** Throws the error in a setTimeout 1ms.
|
|
* Deferred errors are useful for Event Notification systems,
|
|
* Animation, and testing.
|
|
* @param {Error} error that occurred.
|
|
*/
|
|
deferError : function(error) {
|
|
//setTimeout(function(){throw error;},1);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
(function(){
|
|
|
|
APE.namespace = namespace;
|
|
|
|
/**
|
|
* @memberOf APE
|
|
* @description creates a namespace split on "."
|
|
* does <em>not</em> automatically add APE to the front of the chain, as YUI does.
|
|
* @param {String} s the namespace. "foo.bar" would create a namespace foo.bar, but only
|
|
* if that namespace did not exist.
|
|
* @return {Package} the namespace.
|
|
*/
|
|
function namespace(s) {
|
|
var packages = s.split("."),
|
|
pkg = window,
|
|
hasOwnProperty = Object.prototype.hasOwnProperty,
|
|
qName = pkg.qualifiedName,
|
|
i = 0,
|
|
len = packages.length,
|
|
name;
|
|
for (; i < len; i++) {
|
|
name = packages[i];
|
|
|
|
// Internet Explorer does not support
|
|
// hasOwnProperty on things like window, so call Object.prototype.hasOwnProperty.
|
|
// Opera does not support the global object or [[Put]] properly (see below)
|
|
if(!hasOwnProperty.call(pkg, name)) {
|
|
pkg[name] = new Package((qName||"APE")+"."+name);
|
|
}
|
|
pkg = pkg[name];
|
|
}
|
|
|
|
return pkg;
|
|
}
|
|
|
|
Package.prototype.toString = function(){
|
|
return"["+this.qualifiedName+"]";
|
|
};
|
|
|
|
/* constructor Package
|
|
*/
|
|
function Package(qualifiedName) {
|
|
this.qualifiedName = qualifiedName;
|
|
}
|
|
})();
|
|
|
|
(function(){
|
|
/**@class
|
|
* A safe patch to the Object object. This patch addresses a bug that only affects Opera.
|
|
* <strong>It does <em>not</em> affect any for-in loops in any browser</strong> (see tests).
|
|
*/
|
|
var O = Object.prototype, hasOwnProperty = O.hasOwnProperty;
|
|
if(typeof window != "undefined" && hasOwnProperty && !hasOwnProperty.call(window, "Object")) {
|
|
/**
|
|
* @overrides Object.prototype.hasOwnProperty
|
|
* @method
|
|
* This is a conditional patch that affects some versions of Opera.
|
|
* It is perfectly safe to do this and does not affect enumeration.
|
|
*/
|
|
Object.prototype.hasOwnProperty = function(p) {
|
|
if(this === window) return (p in this) && (O[p] !== this[p]);
|
|
return hasOwnProperty.call(this, p);
|
|
};
|
|
}
|
|
})();/**
|
|
* @fileoverview
|
|
* EventPublisher
|
|
*
|
|
* Released under Academic Free Licence 3.0.
|
|
* @author Garrett Smith
|
|
* @class
|
|
* <code>APE.EventPublisher</code> can be used for native browser events or custom events.
|
|
*
|
|
* <p> For native browser events, use <code>APE.EventPublisher</code>
|
|
* steals the event handler off native elements and creates a callStack.
|
|
* that fires in its place.
|
|
* </p>
|
|
* <p>
|
|
* There are two ways to create custom events.
|
|
* </p>
|
|
* <ol>
|
|
* <li>Create a function on the object that fires the "event", then call that function
|
|
* when the event fires (this happens automatically with native events).
|
|
* </li>
|
|
* <li>
|
|
* Instantiate an <code>EventPublisher</code> using the constructor, then call <code>fire</code>
|
|
* when the callbacks should be run.
|
|
* </li>
|
|
* </ol>
|
|
* <p>
|
|
* An <code>EventPublisher</code> itself publishes <code>beforeFire</code> and <code>afterFire</code>.
|
|
* This makes it possible to add AOP before advice to the callStack.
|
|
* </p><p>
|
|
* adding before-before advice is possible, but will impair performance.
|
|
* Instead, add multiple beforeAdvice with:
|
|
* <code>publisher.addBefore(fp, thisArg).add(fp2, thisArg);</code>
|
|
* </p><p>
|
|
* There are no <code>beforeEach</code> and <code>afterEach</code> methods; to create advice
|
|
* for each callback would require modification
|
|
* to the registry (see comments below). I have not yet found a real need for this.
|
|
* </p>
|
|
*/
|
|
/**
|
|
* @constructor
|
|
* @description creates an <code>EventPublisher</code> with methods <code>add()</code>,
|
|
* <code>fire</code>, et c.
|
|
*/
|
|
APE.EventPublisher = function(src, type) {
|
|
this.src = src;
|
|
// Really could use a List of bound methods here.
|
|
this._callStack = [];
|
|
this.type = type;
|
|
};
|
|
|
|
APE.EventPublisher.prototype = {
|
|
|
|
/**
|
|
* @param {Function} fp the callback function that gets called when src[sEvent] is called.
|
|
* @param {Object} thisArg the context that the function executes in.
|
|
* @return {EventPublisher} this;
|
|
*/
|
|
add : function(fp, thisArg) {
|
|
this._callStack.push([fp, thisArg||this.src]);
|
|
return this;
|
|
},
|
|
/** Adds beforeAdvice to the callStack. This fires before the callstack.
|
|
* @param {Function:boolean} fp the callback function that gets called when src[sEvent] is called.
|
|
* function's returnValue proceed false stops the callstack and returns false to the original call.
|
|
* @param {Object} thisArg the context that the function executes in.
|
|
* @return {EventPublisher} this;
|
|
*/
|
|
addBefore : function(f, thisArg) {
|
|
return APE.EventPublisher.add(this, "beforeFire", f, thisArg);
|
|
},
|
|
|
|
/** Adds afterAdvice to the callStack. This fires after the callstack.
|
|
* @param {Function:boolean} fp the callback function that gets called when src[sEvent] is called.
|
|
* function's returnValue of false returns false to the original call.
|
|
* @param {Object} thisArg the context that the function executes in.
|
|
* @return {EventPublisher} this;
|
|
*/
|
|
addAfter : function(f, thisArg) {
|
|
return APE.EventPublisher.add(this, "afterFire", f, thisArg);
|
|
},
|
|
|
|
/**
|
|
* @param {String} "beforeFire", "afterFire" conveneince.
|
|
* @return {EventPublisher} this;
|
|
*/
|
|
getEvent : function(type) {
|
|
return APE.EventPublisher.get(this, type);
|
|
},
|
|
|
|
/** Removes fp from callstack.
|
|
* @param {Function:boolean} fp the callback function to remove.
|
|
* @param {Object} [thisArg] the context that the function executes in.
|
|
* @return {Function} the function that was passed in, or null if not found;
|
|
*/
|
|
remove : function(fp, thisArg) {
|
|
var cs = this._callStack, i = 0, len, call;
|
|
if(!thisArg) thisArg = this.src;
|
|
for(len = cs.length; i < len; i++) {
|
|
call = cs[i];
|
|
if(call[0] === fp && call[1] === thisArg) {
|
|
return cs.splice(i, 1);
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/** Removes fp from callstack's beforeFire.
|
|
* @param {Function:boolean} fp the callback function to remove.
|
|
* @param {Object} [thisArg] the context that the function executes in.
|
|
* @return {Function} the function that was passed in, or null if not found (uses remove());
|
|
*/
|
|
removeBefore : function(fp, thisArg) {
|
|
return this.getEvent("beforeFire").remove(fp, thisArg);
|
|
},
|
|
|
|
|
|
/** Removes fp from callstack's afterFire.
|
|
* @param {Function:boolean} fp the callback function to remove.
|
|
* @param {Object} [thisArg] the context that the function executes in.
|
|
* @return {Function} the function that was passed in, or null if not found (uses remove());
|
|
*/
|
|
removeAfter : function(fp, thisArg) {
|
|
return this.getEvent("afterFire").remove(fp, thisArg);
|
|
},
|
|
|
|
/** Fires the event. */
|
|
fire : function(payload) {
|
|
return APE.EventPublisher.fire(this)(payload);
|
|
},
|
|
|
|
/** helpful debugging info */
|
|
toString : function() {
|
|
return "APE.EventPublisher: {src=" + this.src + ", type=" + this.type +
|
|
", length="+this._callStack.length+"}";
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @static
|
|
* @param {Object} src the object which calls the function
|
|
* @param {String} sEvent the function that gets called.
|
|
* @param {Function} fp the callback function that gets called when src[sEvent] is called.
|
|
* @param {Object} thisArg the context that the function executes in.
|
|
*/
|
|
APE.EventPublisher.add = function(src, sEvent, fp, thisArg) {
|
|
return APE.EventPublisher.get(src, sEvent).add(fp, thisArg);
|
|
};
|
|
|
|
/**
|
|
* @static
|
|
* @private
|
|
* @memberOf {APE.EventPublisher}
|
|
* @return {boolean} false if any one of callStack's methods return false.
|
|
*/
|
|
APE.EventPublisher.fire = function(publisher) {
|
|
// This closure sucks. We should have partial/bind in ES.
|
|
// If we did, this could more reasonably be a prototype method.
|
|
|
|
// return function w/identifier doesn't work in Safari 2.
|
|
return fireEvent;
|
|
function fireEvent(e) {
|
|
var preventDefault = false,
|
|
i = 0, len,
|
|
cs = publisher._callStack, csi;
|
|
|
|
// beforeFire can affect return value.
|
|
if(typeof publisher.beforeFire == "function") {
|
|
try {
|
|
if(publisher.beforeFire(e) == false)
|
|
preventDefault = true;
|
|
} catch(ex){APE.deferError(ex);}
|
|
}
|
|
|
|
for(len = cs.length; i < len; i++) {
|
|
csi = cs[i];
|
|
// If an error occurs, continue the event fire,
|
|
// but still throw the error.
|
|
try {
|
|
// TODO: beforeEach to prevent or advise each call.
|
|
if(csi[0].call(csi[1], e || window.event) == false)
|
|
preventDefault = true; // continue main callstack and return false afterwards.
|
|
// TODO: afterEach
|
|
}
|
|
catch(ex) {
|
|
APE.deferError(ex);
|
|
}
|
|
}
|
|
// afterFire can prevent default.
|
|
if(typeof publisher.afterFire == "function") {
|
|
if(publisher.afterFire(e) == false)
|
|
preventDefault = true;
|
|
}
|
|
return !preventDefault;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @static
|
|
* @param {Object} src the object which calls the function
|
|
* @param {String} sEvent the function that gets called.
|
|
* @memberOf {APE.EventPublisher}
|
|
* Looks for an APE.EventPublisher in the Registry.
|
|
* If none found, creates and adds one to the Registry.
|
|
*/
|
|
APE.EventPublisher.get = function(src, sEvent) {
|
|
|
|
var publisherList = this.Registry.hasOwnProperty(sEvent) && this.Registry[sEvent] ||
|
|
(this.Registry[sEvent] = []),
|
|
i = 0, len = publisherList.length,
|
|
publisher;
|
|
|
|
for(; i < len; i++)
|
|
if(publisherList[i].src === src)
|
|
return publisherList[i];
|
|
|
|
// not found.
|
|
publisher = new APE.EventPublisher(src, sEvent);
|
|
// Steal.
|
|
if(src[sEvent])
|
|
publisher.add(src[sEvent], src);
|
|
src[sEvent] = this.fire(publisher);
|
|
publisherList[publisherList.length] = publisher;
|
|
return publisher;
|
|
};
|
|
|
|
/**
|
|
* Map of [APE.EventPublisher], keyed by type.
|
|
* @private
|
|
* @static
|
|
* @memberOf {APE.EventPublisher}
|
|
*/
|
|
APE.EventPublisher.Registry = {};
|
|
|
|
/**
|
|
* @static
|
|
* @memberOf {APE.EventPublisher}
|
|
* called onunload, automatically onunload.
|
|
* This is only called for if window.CollectGarbage is
|
|
* supported. IE has memory leak problems; other browsers have fast forward/back,
|
|
* but that won't work if there's an onunload handler.
|
|
*/
|
|
APE.EventPublisher.cleanUp = function() {
|
|
var type, publisherList, publisher, i, len;
|
|
for(type in this.Registry) {
|
|
publisherList = this.Registry[type];
|
|
for(i = 0, len = publisherList.length; i < len; i++) {
|
|
publisher = publisherList[i];
|
|
publisher.src[publisher.type] = null;
|
|
}
|
|
}
|
|
};
|
|
if(window.CollectGarbage)
|
|
APE.EventPublisher.get( window, "onunload" ).addAfter( APE.EventPublisher.cleanUp, APE.EventPublisher );/**dom.js rollup: constants.js, viewport-f.js, position-f.js, classname-f.js, traversal-f.js, Event.js, Event-coords.js, style-f.js, gebi-f.js */
|
|
APE.namespace("APE.dom" );
|
|
(function(){
|
|
var dom = APE.dom,
|
|
docEl = document.documentElement,
|
|
textContent = "textContent",
|
|
view = document.defaultView;
|
|
|
|
dom.IS_COMPUTED_STYLE = (typeof view != "undefined" && "getComputedStyle" in view);
|
|
dom.textContent = textContent in docEl ? textContent : "innerText";
|
|
})();/**
|
|
* @author Garret Smith
|
|
*/
|
|
|
|
|
|
(function() {
|
|
|
|
// Public exports.
|
|
APE.mixin(APE.dom, {
|
|
getScrollOffsets : getScrollOffsets,
|
|
getViewportDimensions : getViewportDimensions
|
|
});
|
|
|
|
|
|
var documentElement = "documentElement",
|
|
docEl = document[documentElement],
|
|
IS_BODY_ACTING_ROOT = docEl && docEl.clientWidth === 0;
|
|
docEl = null;
|
|
|
|
/** @memberOf APE.dom
|
|
* @name getScrollOffsets
|
|
* @function
|
|
* @return an object with <code>width</code> and <code>height</code>.
|
|
* This will exhibit a bug in Mozilla, which is often 5-7 pixels off.
|
|
*/
|
|
function getScrollOffsets(win) {
|
|
win = win || window;
|
|
var f, d = win.document, node = d[documentElement];
|
|
if("pageXOffset"in win)
|
|
f = function() {
|
|
return{ left:win.pageXOffset, top: win.pageYOffset};
|
|
};
|
|
else {
|
|
if(IS_BODY_ACTING_ROOT) node = d.body;
|
|
f = function() {
|
|
return{ left : node.scrollLeft, top : node.scrollTop };
|
|
};
|
|
}
|
|
d = null;
|
|
this.getScrollOffsets = f;
|
|
return f();
|
|
}
|
|
|
|
/** @memberOf APE.dom
|
|
* @name getViewportDimensions
|
|
* @function
|
|
* @return an object with <code>width</code> and <code>height</code>.
|
|
*/
|
|
function getViewportDimensions(win) {
|
|
win = win || window;
|
|
var node = win.document, d = node, propPrefix = "client",
|
|
wName, hName;
|
|
|
|
// Safari 2 uses document.clientWidth (default).
|
|
if(typeof d.clientWidth == "number");
|
|
|
|
// Opera < 9.5, or IE in quirks mode.
|
|
else if(IS_BODY_ACTING_ROOT || isDocumentElementHeightOff(win)) {
|
|
node = d.body;
|
|
|
|
// Modern Webkit, Firefox, IE.
|
|
// Might be undefined. 0 in older mozilla.
|
|
} else if(d[documentElement].clientHeight > 0){
|
|
node = d[documentElement];
|
|
|
|
// For older versions of Mozilla.
|
|
} else if(typeof innerHeight == "number") {
|
|
node = win;
|
|
propPrefix = "inner";
|
|
}
|
|
wName = propPrefix + "Width";
|
|
hName = propPrefix + "Height";
|
|
|
|
return (this.getViewportDimensions = function() {
|
|
return{width: node[wName], height: node[hName]};
|
|
})();
|
|
|
|
// Used to feature test Opera returning wrong values
|
|
// for documentElement.clientHeight.
|
|
function isDocumentElementHeightOff(win){
|
|
var d = win.document,
|
|
div = d.createElement('div');
|
|
div.style.height = "2500px";
|
|
d.body.insertBefore(div, d.body.firstChild);
|
|
var r = d[documentElement].clientHeight > 2400;
|
|
d.body.removeChild(div);
|
|
return r;
|
|
}
|
|
}
|
|
})();/**
|
|
* @fileoverview
|
|
* @static
|
|
* @author Garrett Smith
|
|
* APE.dom package functions for calculating element position properties.
|
|
*/
|
|
/** @name APE.dom */
|
|
|
|
(function() {
|
|
APE.mixin(
|
|
APE.dom,
|
|
/** @scope APE.dom */ {
|
|
getOffsetCoords : getOffsetCoords,
|
|
isAboveElement : isAboveElement,
|
|
isBelowElement : isBelowElement,
|
|
isInsideElement: isInsideElement
|
|
});
|
|
|
|
var doc = this.document,
|
|
inited,
|
|
documentElement = doc.documentElement,
|
|
round = Math.round, max = Math.max,
|
|
|
|
// Load-time constants.
|
|
IS_BODY_ACTING_ROOT = documentElement && documentElement.clientWidth === 0,
|
|
|
|
// IE, Safari, and Opera support clientTop. FF 2 doesn't
|
|
IS_CLIENT_TOP_SUPPORTED = 'clientTop'in documentElement,
|
|
|
|
TABLE = /^h/.test(documentElement.tagName) ? "table" : "TABLE",
|
|
|
|
IS_CURRENT_STYLE_SUPPORTED = 'currentStyle'in documentElement,
|
|
|
|
// XXX Opera <= 9.2 - parent border widths are included in offsetTop.
|
|
IS_PARENT_BODY_BORDER_INCLUDED_IN_OFFSET,
|
|
|
|
// XXX Opera <= 9.2 - body offsetTop is inherited to children's offsetTop
|
|
// when body position is not static.
|
|
// opera will inherit the offsetTop/offsetLeft of body for relative offsetParents.
|
|
|
|
IS_BODY_MARGIN_INHERITED,
|
|
IS_BODY_TOP_INHERITED,
|
|
IS_BODY_OFFSET_EXCLUDING_MARGIN,
|
|
|
|
// XXX Mozilla includes a table border in the TD's offsetLeft.
|
|
// There is 1 exception:
|
|
// When the TR has position: relative and the TD has block level content.
|
|
// In that case, the TD does not include the TABLE's border in it's offsetLeft.
|
|
// We do not account for this peculiar bug.
|
|
IS_TABLE_BORDER_INCLUDED_IN_TD_OFFSET,
|
|
IS_STATIC_BODY_OFFSET_PARENT_BUT_ABSOLUTE_CHILD_SUBTRACTS_BODY_BORDER_WIDTH,
|
|
|
|
IS_BODY_OFFSET_IGNORED_WHEN_BODY_RELATIVE_AND_LAST_CHILD_POSITIONED,
|
|
|
|
IS_CONTAINER_BODY_STATIC_INCLUDING_HTML_PADDING,
|
|
IS_CONTAINER_BODY_RELATIVE_INCLUDING_HTML_PADDING_REL_CHILD,
|
|
IS_CONTAINER_BODY_RELATIVE_INCLUDING_HTML_PADDING_ABS_CHILD,
|
|
IS_CONTAINER_BODY_INCLUDING_HTML_MARGIN,
|
|
|
|
// In Safari 2.0.4, BODY can have offsetTop when offsetParent is null.
|
|
// but offsetParent will be HTML (root) when HTML has position.
|
|
// IS_BODY_OFFSET_TOP_NO_OFFSETPARENT,
|
|
|
|
IS_COMPUTED_STYLE_SUPPORTED = doc.defaultView
|
|
&& typeof doc.defaultView.getComputedStyle != "undefined",
|
|
getBoundingClientRect = "getBoundingClientRect",
|
|
relative = "relative",
|
|
borderTopWidth = "borderTopWidth",
|
|
borderLeftWidth = "borderLeftWidth",
|
|
positionedExp = /^(?:r|a)/,
|
|
absoluteExp = /^(?:a|f)/;
|
|
|
|
/**
|
|
* @memberOf APE.dom
|
|
* @param {HTMLElement} el you want coords of.
|
|
* @param {HTMLElement} positionedContainer container to look up to. The container must have
|
|
* position: (relative|absolute|fixed);
|
|
*
|
|
* @param {x:Number, y:Number} coords object to pass in.
|
|
* @return {x:Number, y:Number} coords of el from container.
|
|
*
|
|
* Passing in a container will improve performance in browsers that don't support
|
|
* getBoundingClientRect, but those that do will have a recursive call. Test accordingly.
|
|
* <p>
|
|
* Container is sometimes irrelevant. Container is irrelevant when comparing positions
|
|
* of objects who do not share a common ancestor. In this case, pass in document.
|
|
* </p>
|
|
*<p>
|
|
* Passing in re-used coords can improve performance in all browsers.
|
|
* There is a side effect to passing in coords:
|
|
* For drag drop operations, reuse coords:
|
|
*</p>
|
|
* <pre>
|
|
* // Update our coords:
|
|
* dom.getOffsetCoords(el, container, this.coords);
|
|
* </pre>
|
|
* Where <code>this.coords = {};</code>
|
|
*/
|
|
function getOffsetCoords(el, container, coords) {
|
|
|
|
var doc = el.ownerDocument,
|
|
documentElement = doc.documentElement,
|
|
body = doc.body;
|
|
|
|
if(!container)
|
|
container = doc;
|
|
|
|
if(!coords)
|
|
coords = {x:0, y:0};
|
|
|
|
if(el === container) {
|
|
coords.x = coords.y = 0;
|
|
return coords;
|
|
}
|
|
if(getBoundingClientRect in el) {
|
|
|
|
// In BackCompat mode, body's border goes to the window. BODY is ICB.
|
|
var rootBorderEl = IS_BODY_ACTING_ROOT ? body : documentElement,
|
|
box = el[getBoundingClientRect](),
|
|
x = box.left + max( documentElement.scrollLeft, body.scrollLeft ),
|
|
y = box.top + max( documentElement.scrollTop, body.scrollTop ),
|
|
bodyCurrentStyle,
|
|
borderTop = rootBorderEl.clientTop,
|
|
borderLeft = rootBorderEl.clientLeft;
|
|
|
|
if(IS_CLIENT_TOP_SUPPORTED) {
|
|
x -= borderLeft;
|
|
y -= borderTop;
|
|
}
|
|
if(container !== doc) {
|
|
box = getOffsetCoords(container, null);
|
|
x -= box.x;
|
|
y -= box.y;
|
|
if(IS_BODY_ACTING_ROOT && container === body && IS_CLIENT_TOP_SUPPORTED) {
|
|
x -= borderLeft;
|
|
y -= borderTop;
|
|
}
|
|
}
|
|
|
|
if(IS_BODY_ACTING_ROOT && IS_CURRENT_STYLE_SUPPORTED
|
|
&& container != doc && container !== body) {
|
|
bodyCurrentStyle = body.currentStyle;
|
|
x += parseFloat(bodyCurrentStyle.marginLeft)||0 +
|
|
parseFloat(bodyCurrentStyle.left)||0;
|
|
y += parseFloat(bodyCurrentStyle.marginTop)||0 +
|
|
parseFloat(bodyCurrentStyle.top)||0;
|
|
}
|
|
coords.x = x;
|
|
coords.y = y;
|
|
|
|
return coords;
|
|
}
|
|
|
|
// Crawling up the tree.
|
|
else if(IS_COMPUTED_STYLE_SUPPORTED) {
|
|
if(!inited) init();
|
|
|
|
var offsetLeft = el.offsetLeft,
|
|
offsetTop = el.offsetTop,
|
|
defaultView = doc.defaultView,
|
|
cs = defaultView.getComputedStyle(el, '');
|
|
if(cs.position == "fixed") {
|
|
coords.x = offsetLeft + documentElement.scrollLeft;
|
|
coords.y = offsetTop + documentElement.scrollTop;
|
|
return coords;
|
|
}
|
|
var bcs = defaultView.getComputedStyle(body,''),
|
|
isBodyStatic = !positionedExp.test(bcs.position),
|
|
lastOffsetParent = el,
|
|
parent = el.parentNode,
|
|
offsetParent = el.offsetParent;
|
|
|
|
// Main loop -----------------------------------------------------------------------
|
|
// Loop up, gathering scroll offsets on parentNodes.
|
|
// when we get to a parent that's an offsetParent, update
|
|
// the current offsetParent marker.
|
|
for( ; parent && parent !== container; parent = parent.parentNode) {
|
|
if(parent !== body && parent !== documentElement) {
|
|
offsetLeft -= parent.scrollLeft;
|
|
offsetTop -= parent.scrollTop;
|
|
}
|
|
if(parent === offsetParent) {
|
|
// If we get to BODY and have static position, skip it.
|
|
if(parent === body && isBodyStatic);
|
|
else {
|
|
|
|
// XXX Mozilla; Exclude static body; if static, it's offsetTop will be wrong.
|
|
// Include parent border widths. This matches behavior of clientRect approach.
|
|
// XXX Opera <= 9.2 includes parent border widths.
|
|
// See IS_PARENT_BODY_BORDER_INCLUDED_IN_OFFSET below.
|
|
if( !IS_PARENT_BODY_BORDER_INCLUDED_IN_OFFSET &&
|
|
! (parent.tagName === TABLE && IS_TABLE_BORDER_INCLUDED_IN_TD_OFFSET)) {
|
|
var pcs = defaultView.getComputedStyle(parent, "");
|
|
// Mozilla doesn't support clientTop. Add borderWidth to the sum.
|
|
offsetLeft += parseFloat(pcs[borderLeftWidth])||0;
|
|
offsetTop += parseFloat(pcs[borderTopWidth])||0;
|
|
}
|
|
if(parent !== body) {
|
|
offsetLeft += offsetParent.offsetLeft;
|
|
offsetTop += offsetParent.offsetTop;
|
|
lastOffsetParent = offsetParent;
|
|
offsetParent = parent.offsetParent; // next marker to check for offsetParent.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//--------Post - loop, body adjustments----------------------------------------------
|
|
// Complications due to CSSOM Views - the browsers try to implement a contradictory
|
|
// spec: http://www.w3.org/TR/cssom-view/#offset-attributes
|
|
|
|
// XXX Mozilla, Safari 3, Opera: body margin is never
|
|
// included in body offsetLeft/offsetTop.
|
|
// This is wrong. Body's offsetTop should work like any other element.
|
|
// In Safari 2.0.4, BODY can have offsetParent, and even
|
|
// if it doesn't, it can still have offsetTop.
|
|
// But Safari 2.0.4 doubles offsetTop for relatively positioned elements
|
|
// and this script does not account for that.
|
|
|
|
// XXX Mozilla: When body has a border, body's offsetTop === negative borderWidth;
|
|
// Don't use body.offsetTop.
|
|
var bodyOffsetLeft = 0,
|
|
bodyOffsetTop = 0,
|
|
isLastElementAbsolute,
|
|
isLastOffsetElementPositioned,
|
|
isContainerDocOrDocEl = container === doc || container === documentElement,
|
|
dcs,
|
|
lastOffsetPosition;
|
|
|
|
// If the lastOffsetParent is document,
|
|
// it is not positioned (and hence, not absolute).
|
|
if(lastOffsetParent != doc) {
|
|
lastOffsetPosition = defaultView.getComputedStyle(lastOffsetParent,'').position;
|
|
isLastElementAbsolute = absoluteExp.test(lastOffsetPosition);
|
|
isLastOffsetElementPositioned = isLastElementAbsolute ||
|
|
positionedExp.test(lastOffsetPosition);
|
|
}
|
|
|
|
// do we need to add margin?
|
|
if(
|
|
(lastOffsetParent === el && el.offsetParent === body && !IS_BODY_MARGIN_INHERITED
|
|
&& container !== body && !(isBodyStatic && IS_BODY_OFFSET_EXCLUDING_MARGIN))
|
|
|| (IS_BODY_MARGIN_INHERITED && lastOffsetParent === el && !isLastOffsetElementPositioned)
|
|
|| !isBodyStatic
|
|
&& isLastOffsetElementPositioned
|
|
&& IS_BODY_OFFSET_IGNORED_WHEN_BODY_RELATIVE_AND_LAST_CHILD_POSITIONED
|
|
&& isContainerDocOrDocEl) {
|
|
bodyOffsetTop += parseFloat(bcs.marginTop)||0;
|
|
bodyOffsetLeft += parseFloat(bcs.marginLeft)||0;
|
|
}
|
|
|
|
// Case for padding on documentElement.
|
|
if(container === body) {
|
|
dcs = defaultView.getComputedStyle(documentElement,'');
|
|
if(
|
|
(!isBodyStatic &&
|
|
((IS_CONTAINER_BODY_RELATIVE_INCLUDING_HTML_PADDING_REL_CHILD && !isLastElementAbsolute)
|
|
||
|
|
(IS_CONTAINER_BODY_RELATIVE_INCLUDING_HTML_PADDING_ABS_CHILD && isLastElementAbsolute))
|
|
)
|
|
|| isBodyStatic && IS_CONTAINER_BODY_STATIC_INCLUDING_HTML_PADDING
|
|
) {
|
|
bodyOffsetTop -= parseFloat(dcs.paddingTop)||0;
|
|
bodyOffsetLeft -= parseFloat(dcs.paddingLeft)||0;
|
|
}
|
|
|
|
if(IS_CONTAINER_BODY_INCLUDING_HTML_MARGIN){
|
|
if(!isLastOffsetElementPositioned
|
|
|| isLastOffsetElementPositioned && !isBodyStatic)
|
|
bodyOffsetTop -= parseFloat(dcs.marginTop)||0;
|
|
bodyOffsetLeft -= parseFloat(dcs.marginLeft)||0;
|
|
}
|
|
}
|
|
if(isBodyStatic) {
|
|
// XXX Safari subtracts border width of body from element's offsetTop (opera does it, too)
|
|
if(IS_STATIC_BODY_OFFSET_PARENT_BUT_ABSOLUTE_CHILD_SUBTRACTS_BODY_BORDER_WIDTH
|
|
// XXX: Safari will use HTML for containing block (CSS),
|
|
// but will subtract the body's border from the body's absolutely positioned
|
|
// child.offsetTop. Safari reports the child's offsetParent is BODY, but
|
|
// doesn't treat it that way (Safari bug).
|
|
|| (!isLastElementAbsolute && !IS_PARENT_BODY_BORDER_INCLUDED_IN_OFFSET
|
|
&& isContainerDocOrDocEl)) {
|
|
bodyOffsetTop += parseFloat(bcs[borderTopWidth]);
|
|
bodyOffsetLeft += parseFloat(bcs[borderLeftWidth]);
|
|
}
|
|
}
|
|
|
|
// body is positioned, and if it excludes margin,
|
|
// it's probably partly using the AVK-CSSOM disaster.
|
|
else if(IS_BODY_OFFSET_EXCLUDING_MARGIN) {
|
|
if(isContainerDocOrDocEl) {
|
|
if(!IS_BODY_TOP_INHERITED) {
|
|
|
|
// If the body is positioned, add its left and top value.
|
|
bodyOffsetTop += parseFloat(bcs.top)||0;
|
|
bodyOffsetLeft += parseFloat(bcs.left)||0;
|
|
|
|
// XXX: Opera normally include the parentBorder in offsetTop.
|
|
// We have a preventative measure in the loop above.
|
|
if(isLastElementAbsolute && IS_PARENT_BODY_BORDER_INCLUDED_IN_OFFSET) {
|
|
bodyOffsetTop += parseFloat(bcs[borderTopWidth]);
|
|
bodyOffsetLeft += parseFloat(bcs[borderLeftWidth]);
|
|
}
|
|
}
|
|
|
|
// Padding on documentElement is not included,
|
|
// but in this case, we're searching to documentElement, so we
|
|
// have to add it back in.
|
|
if(container === doc && !isBodyStatic
|
|
&& !IS_CONTAINER_BODY_RELATIVE_INCLUDING_HTML_PADDING_REL_CHILD) {
|
|
if(!dcs) dcs = defaultView.getComputedStyle(documentElement,'');
|
|
bodyOffsetTop += parseFloat(dcs.paddingTop)||0;
|
|
bodyOffsetLeft += parseFloat(dcs.paddingLeft)||0;
|
|
}
|
|
}
|
|
else if(IS_BODY_TOP_INHERITED) {
|
|
bodyOffsetTop -= parseFloat(bcs.top);
|
|
bodyOffsetLeft -= parseFloat(bcs.left);
|
|
}
|
|
if(IS_BODY_MARGIN_INHERITED && (!isLastOffsetElementPositioned || container === body)) {
|
|
bodyOffsetTop -= parseFloat(bcs.marginTop)||0;
|
|
bodyOffsetLeft -= parseFloat(bcs.marginLeft)||0;
|
|
}
|
|
}
|
|
coords.x = round(offsetLeft + bodyOffsetLeft);
|
|
coords.y = round(offsetTop + bodyOffsetTop);
|
|
|
|
return coords;
|
|
}
|
|
}
|
|
|
|
// For initializing load time constants.
|
|
function init() {
|
|
inited = true;
|
|
var body = doc.body;
|
|
if(!body) return;
|
|
var marginTop = "marginTop", position = "position", padding = "padding",
|
|
stat = "static",
|
|
border = "border", s = body.style,
|
|
bCssText = s.cssText,
|
|
bv = '1px solid transparent',
|
|
z = "0",
|
|
one = "1px",
|
|
offsetTop = "offsetTop",
|
|
ds = documentElement.style,
|
|
dCssText = ds.cssText,
|
|
x = doc.createElement('div'),
|
|
xs = x.style,
|
|
table = doc.createElement(TABLE);
|
|
|
|
s[padding] = s[marginTop] = s.top = z;
|
|
ds.position = stat;
|
|
|
|
s[border] = bv;
|
|
|
|
xs.margin = z;
|
|
xs[position] = stat;
|
|
|
|
// insertBefore - to avoid environment conditions with bottom script
|
|
// where appendChild would fail.
|
|
x = body.insertBefore(x, body.firstChild);
|
|
IS_PARENT_BODY_BORDER_INCLUDED_IN_OFFSET = (x[offsetTop] === 1);
|
|
|
|
s[border] = z;
|
|
|
|
// Table test.
|
|
table.innerHTML = "<tbody><tr><td>x</td></tr></tbody>";
|
|
table.style[border] = "7px solid";
|
|
table.cellSpacing = table.cellPadding = z;
|
|
|
|
body.insertBefore(table, body.firstChild);
|
|
IS_TABLE_BORDER_INCLUDED_IN_TD_OFFSET = table.getElementsByTagName("td")[0].offsetLeft === 7;
|
|
|
|
body.removeChild(table);
|
|
|
|
// Now add margin to determine if body offsetTop is inherited.
|
|
s[marginTop] = one;
|
|
s[position] = relative;
|
|
IS_BODY_MARGIN_INHERITED = (x[offsetTop] === 1);
|
|
|
|
//IS_BODY_OFFSET_TOP_NO_OFFSETPARENT = body.offsetTop && !body.offsetParent;
|
|
|
|
IS_BODY_OFFSET_EXCLUDING_MARGIN = body[offsetTop] === 0;
|
|
s[marginTop] = z;
|
|
s.top = one;
|
|
IS_BODY_TOP_INHERITED = x[offsetTop] === 1;
|
|
|
|
s.top = z;
|
|
s[marginTop] = one;
|
|
s[position] = xs[position] = relative;
|
|
IS_BODY_OFFSET_IGNORED_WHEN_BODY_RELATIVE_AND_LAST_CHILD_POSITIONED = x[offsetTop] === 0;
|
|
|
|
xs[position] = "absolute";
|
|
s[position] = stat;
|
|
if(x.offsetParent === body) {
|
|
s[border] = bv;
|
|
xs.top = "2px";
|
|
// XXX Safari gets offsetParent wrong (says 'body' when body is static,
|
|
// but then positions element from ICB and then subtracts body's clientWidth.
|
|
// Safari is half wrong.
|
|
//
|
|
// XXX Mozilla says body is offsetParent but does NOT subtract EL's offsetLeft/Top.
|
|
// Mozilla is completely wrong.
|
|
IS_STATIC_BODY_OFFSET_PARENT_BUT_ABSOLUTE_CHILD_SUBTRACTS_BODY_BORDER_WIDTH = x[offsetTop] === 1;
|
|
s[border] = z;
|
|
|
|
xs[position] = relative;
|
|
ds[padding] = one;
|
|
s[marginTop] = z;
|
|
|
|
IS_CONTAINER_BODY_STATIC_INCLUDING_HTML_PADDING = x[offsetTop] === 3;
|
|
|
|
// Opera does not respect position: relative on BODY.
|
|
s[position] = relative;
|
|
IS_CONTAINER_BODY_RELATIVE_INCLUDING_HTML_PADDING_REL_CHILD = x[offsetTop] === 3;
|
|
|
|
xs[position] = "absolute";
|
|
IS_CONTAINER_BODY_RELATIVE_INCLUDING_HTML_PADDING_ABS_CHILD = x[offsetTop] === 3;
|
|
|
|
ds[padding] = z;
|
|
ds[marginTop] = one;
|
|
|
|
// Opera inherits HTML margin when body is relative and child is relative or absolute.
|
|
IS_CONTAINER_BODY_INCLUDING_HTML_MARGIN = x[offsetTop] === 3;
|
|
}
|
|
|
|
// xs.position = "fixed";
|
|
// FIXED_HAS_OFFSETPARENT = x.offsetParent != null;
|
|
|
|
body.removeChild(x);
|
|
s.cssText = bCssText||"";
|
|
ds.cssText = dCssText||"";
|
|
}
|
|
|
|
// TODO: add an optional commonAncestor parameter to the below.
|
|
/**
|
|
* @memberOf APE.dom
|
|
* @return {boolean} true if a is vertically within b's content area (and does not overlap,
|
|
* top nor bottom).
|
|
*/
|
|
function isInsideElement(a, b) {
|
|
var aTop = getOffsetCoords(a).y,
|
|
bTop = getOffsetCoords(b).y;
|
|
return aTop + a.offsetHeight <= bTop + b.offsetHeight && aTop >= bTop;
|
|
}
|
|
|
|
/**
|
|
* @memberOf APE.dom
|
|
* @return {boolean} true if a overlaps the top of b's content area.
|
|
*/
|
|
function isAboveElement(a, b) {
|
|
return (getOffsetCoords(a).y <= getOffsetCoords(b).y);
|
|
}
|
|
|
|
/**
|
|
* @memberOf APE.dom
|
|
* @return {boolean} true if a overlaps the bottom of b's content area.
|
|
*/
|
|
function isBelowElement(a, b) {
|
|
return (getOffsetCoords(a).y + a.offsetHeight >= getOffsetCoords(b).y + b.offsetHeight);
|
|
}
|
|
|
|
// release from closure.
|
|
isInsideElement = isAboveElement = isBelowElement = null;
|
|
})();
|
|
/**
|
|
* @fileoverview dom ClassName Functions.
|
|
* @namespace APE.dom
|
|
* @author Garrett Smith
|
|
* <p>
|
|
* ClassName functions are added to APE.dom.
|
|
* </p>
|
|
*/
|
|
|
|
|
|
(function() {
|
|
APE.mixin(APE.dom,
|
|
{
|
|
hasToken : hasToken,
|
|
removeClass : removeClass,
|
|
addClass : addClass,
|
|
hasClass: hasClass,
|
|
getElementsByClassName : getElementsByClassName,
|
|
findAncestorWithClass : findAncestorWithClass
|
|
});
|
|
|
|
var className = "className";
|
|
|
|
/** @param {String} s string to search
|
|
* @param {String} token white-space delimited token the delimiter of the token.
|
|
* This is generally used with element className:
|
|
* @example if(dom.hasToken(el.className, "menu")) // element has class "menu".
|
|
*/
|
|
function hasToken (s, token) {
|
|
return getTokenizedExp(token,"").test(s);
|
|
}
|
|
|
|
/** @param {HTMLElement} el
|
|
* @param {String} klass className token(s) to be removed.
|
|
* @description removes all occurances of <code>klass</code> from element's className.
|
|
*/
|
|
function removeClass(el, klass) {
|
|
var cn = el[className];
|
|
if(!cn) return;
|
|
if(cn === klass) {
|
|
el[className] = "";
|
|
return;
|
|
}
|
|
|
|
el[className] = normalizeString(cn.replace(getTokenizedExp(klass, "g")," "));
|
|
}
|
|
/** @param {HTMLElement} el
|
|
* @param {String} klass className token(s) to be added.
|
|
* @description adds <code>klass</code> to the element's class attribute, if it does not
|
|
* exist.
|
|
*/
|
|
function addClass(el, klass) {
|
|
if(!el[className]) el[className] = klass;
|
|
if(!getTokenizedExp(klass).test(el[className])) el[className] += " " + klass;
|
|
}
|
|
|
|
/** @param {HTMLElement} el
|
|
* @param {String} klass value to be tested against
|
|
* @description Checks whether an element has <code>klass</code> as part of its <code>className</code>
|
|
*/
|
|
function hasClass(el, klass) {
|
|
if (!el[className]) return false;
|
|
return hasToken(el[className], klass);
|
|
}
|
|
|
|
var Exps = { };
|
|
function getTokenizedExp(token, flag){
|
|
var p = token + "$" + flag;
|
|
return (Exps[p] || (Exps[p] = RegExp("(?:^|\\s)"+token+"(?:$|\\s)", flag)));
|
|
}
|
|
|
|
/** @param {HTMLElement} el
|
|
* @param {String} tagName tagName to be searched. Use "*" for any tag.
|
|
* @param {String} klass className token(s) to be added.
|
|
* @return {Array|NodeList} Elements with the specified tagName and className.
|
|
* Searches will generally be faster with a smaller HTMLCollection
|
|
* and shorter tree.
|
|
*/
|
|
function getElementsByClassName(el, tagName, klass){
|
|
if(!klass) return [];
|
|
tagName = tagName||"*";
|
|
if(el.getElementsByClassName && (tagName === "*")) {
|
|
// Native performance boost.
|
|
return el.getElementsByClassName(klass);
|
|
}
|
|
var exp = getTokenizedExp(klass,""),
|
|
collection = el.getElementsByTagName(tagName),
|
|
len = collection.length,
|
|
counter = 0,
|
|
i,
|
|
ret = Array(len);
|
|
for(i = 0; i < len; i++){
|
|
if(exp.test(collection[i][className]))
|
|
ret[counter++] = collection[i];
|
|
}
|
|
ret.length = counter; // trim array.
|
|
return ret;
|
|
}
|
|
|
|
/** Finds an ancestor with specified className
|
|
* @param {Element|Document} [container] where to stop traversing up (optional).
|
|
*/
|
|
function findAncestorWithClass(el, klass, container) {
|
|
if(el == null || el === container)
|
|
return null;
|
|
var exp = getTokenizedExp(klass,""), parent;
|
|
for(parent = el.parentNode;parent != container;){
|
|
if( exp.test(parent[className]) )
|
|
return parent;
|
|
parent = parent.parentNode;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
var STRING_TRIM_EXP = /^\s+|\s+$/g,
|
|
WS_MULT_EXP = /\s\s+/g;
|
|
function normalizeString(s) { return s.replace(STRING_TRIM_EXP,'').replace(WS_MULT_EXP, " "); }
|
|
})();
|
|
(function(){
|
|
|
|
var docEl = document.documentElement,
|
|
nodeType = "nodeType",
|
|
tagName = "tagName",
|
|
parentNode = "parentNode",
|
|
compareDocumentPosition = "compareDocumentPosition",
|
|
caseTransform = /^H/.test(docEl[tagName]) ? 'toUpperCase' : 'toLowerCase',
|
|
tagExp = /^[A-Z]/;
|
|
|
|
APE.mixin(
|
|
APE.dom, {
|
|
contains : getContains(),
|
|
findAncestorWithAttribute : findAncestorWithAttribute,
|
|
findAncestorWithTagName : findAncestorWithTagName,
|
|
findNextSiblingElement : findNextSiblingElement,
|
|
findPreviousSiblingElement : findPreviousSiblingElement,
|
|
getChildElements : getChildElements,
|
|
isTagName: isTagName
|
|
});
|
|
|
|
/**
|
|
* @memberOf APE.dom
|
|
* @return {boolean} true if a contains b.
|
|
* Internet Explorer's native contains() is different. It will return true for:
|
|
* code body.contains(body);
|
|
* Whereas APE.dom.contains will return false.
|
|
*/
|
|
|
|
function getContains(){
|
|
if(compareDocumentPosition in docEl)
|
|
return function(el, b) {
|
|
return (el[compareDocumentPosition](b) & 16) !== 0;
|
|
};
|
|
else if('contains'in docEl)
|
|
return function(el, b) {
|
|
return el !== b && el.contains(b);
|
|
};
|
|
return function(el, b) {
|
|
if(el === b) return false;
|
|
while(el != b && (b = b[parentNode]) !== null);
|
|
return el === b;
|
|
};
|
|
}
|
|
|
|
//docEl = null;
|
|
|
|
/**
|
|
* @memberOf APE.dom
|
|
* @param {HTMLElement} el the element to start from.
|
|
* @param {String} attName the name of the attribute.
|
|
* @param {String} [value] the value of the attribute. If omitted, then only the
|
|
* presence of attribute is checked and the value is anything.
|
|
* @return {HTMLElement} closest ancestor with <code>attName</code> matching value.
|
|
* Returns null if not found.
|
|
*/
|
|
function findAncestorWithAttribute(el, attName, value) {
|
|
for(var map, parent = el[parentNode];parent !== null;){
|
|
map = parent.attributes;
|
|
if(!map) return null;
|
|
var att = map[attName];
|
|
if(att && att.specified)
|
|
if(att.value === value || (value === undefined))
|
|
return parent;
|
|
parent = parent[parentNode];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function findAncestorWithTagName(el, tag) {
|
|
tag = tag[caseTransform]();
|
|
for(var parent = el[parentNode];parent !== null; ){
|
|
if( parent[tagName] === tag )
|
|
return parent;
|
|
parent = parent[parentNode];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Filter out text nodes and, in IE, comment nodes. */
|
|
function findNextSiblingElement(el) {
|
|
for(var ns = el.nextSibling; ns !== null; ns = ns.nextSibling)
|
|
if(ns[nodeType] === 1)
|
|
return ns;
|
|
return null;
|
|
}
|
|
|
|
function findPreviousSiblingElement(el) {
|
|
for(var ps = el.previousSibling; ps !== null; ps = ps.previousSibling) {
|
|
if(ps[nodeType] === 1)
|
|
return ps;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getChildElements(el) {
|
|
var i = 0, ret = [], len, tag,
|
|
cn = el.children || el.childNodes, c;
|
|
|
|
// IE throws error when calling
|
|
// Array.prototype.slice.call(el.children).
|
|
// IE also includes comment nodes.
|
|
for(len = cn.length; i < len; i++) {
|
|
c = cn[i];
|
|
if(c[nodeType] !== 1) continue;
|
|
ret[ret.length] = c;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* @memberOf APE.dom
|
|
* @param {HTMLElement} el element whose <code>tagName</code> is to be tested
|
|
* @param {String} tagName value to test against
|
|
* @return {boolean} true if element's <code>tagName</code> matches given one
|
|
*/
|
|
function isTagName(el, tagName) {
|
|
return el.tagName == tagName[caseTransform]();
|
|
}
|
|
})();/**
|
|
* @requires APE.dom.Viewport
|
|
*/
|
|
/** @namespace APE.dom */
|
|
|
|
|
|
(function() {
|
|
|
|
var hasEventTarget = "addEventListener"in this,
|
|
eventTarget = hasEventTarget ? "target" : "srcElement";
|
|
|
|
APE.mixin(
|
|
APE.dom.Event = {}, {
|
|
eventTarget : eventTarget,
|
|
getTarget : getTarget,
|
|
addCallback : addCallback,
|
|
removeCallback : removeCallback,
|
|
preventDefault : preventDefault,
|
|
stopPropagation: stopPropagation
|
|
});
|
|
|
|
/**
|
|
* @param {Event}
|
|
*/
|
|
function stopPropagation(e) {
|
|
e = e || window.event;
|
|
var f;
|
|
if (typeof e.stopPropagation == 'function') {
|
|
f = function(e) {
|
|
e.stopPropagation();
|
|
}
|
|
}
|
|
else if ('cancelBubble' in e) {
|
|
f = function(e) {
|
|
e = e || window.event;
|
|
e.cancelBubble = true;
|
|
}
|
|
}
|
|
(APE.dom.Event.stopPropagation = f)(e);
|
|
}
|
|
|
|
function getTarget(e) {
|
|
return (e || event)[eventTarget];
|
|
}
|
|
|
|
/**
|
|
* If EventTarget is supported, cb (input param) is returned.
|
|
* Otherwise, a closure is used to wrap a call to the callback
|
|
* in context of o.
|
|
* @param {Object} o the desired would-be EventTarget
|
|
* @param {Function} cb the callback.
|
|
*/
|
|
function getBoundCallback(o, cb) {
|
|
return hasEventTarget ? cb : function(ev) {
|
|
cb.call(o, ev);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* addEventListener/attachEvent for DOM objects.
|
|
* @param {Object} o host object, Element, Document, Window.
|
|
* @param (string} type
|
|
* @param {Function} cb
|
|
* @return {Function} cb If EventTarget is not supported,
|
|
* a bound callback is created and returned. Otherwise,
|
|
* cb (input param) is returned.
|
|
*/
|
|
function addCallback(o, type, cb) {
|
|
if (hasEventTarget) {
|
|
o.addEventListener(type, cb, false);
|
|
} else {
|
|
var bound = getBoundCallback(o, cb);
|
|
o.attachEvent("on" + type, bound);
|
|
}
|
|
return bound||cb;
|
|
}
|
|
|
|
/**
|
|
* removeEventListener/detachEvent for DOM objects.
|
|
* @param {EventTarget} o host object, Element, Document, Window.
|
|
* @param (string} type
|
|
* @param {Function} cb
|
|
* @return {Function} bound If EventTarget is not supported,
|
|
* a bound callback is created and returned. Otherwise,
|
|
* cb (input param) is returned.
|
|
*/
|
|
function removeCallback(o, type, bound) {
|
|
if (hasEventTarget) {
|
|
o.removeEventListener(type, bound, false);
|
|
} else {
|
|
o.detachEvent("on" + type, bound);
|
|
}
|
|
return bound;
|
|
}
|
|
|
|
/**
|
|
* @param {Event}
|
|
*/
|
|
function preventDefault(ev) {
|
|
ev = ev || event;
|
|
if(typeof ev.preventDefault == "function") {
|
|
ev.preventDefault();
|
|
} else if('returnValue' in ev) {
|
|
ev.returnValue = false;
|
|
}
|
|
}
|
|
})();/**
|
|
* @requires viewport-f.js (for scrollOffsets in IE).
|
|
*/
|
|
APE.namespace("APE.dom.Event");
|
|
(function() {
|
|
var dom = APE.dom,
|
|
Event = dom.Event;
|
|
Event.getCoords = getCoords;
|
|
function getCoords(e) {
|
|
var f;
|
|
if ("pageX" in e) {
|
|
f = function(e) {
|
|
return {
|
|
x : e.pageX,
|
|
y : e.pageY
|
|
};
|
|
};
|
|
} else {
|
|
f = function(e) {
|
|
var scrollOffsets = dom.getScrollOffsets();
|
|
e = e || window.event;
|
|
return {
|
|
x : e.clientX + scrollOffsets.left,
|
|
y : e.clientY + scrollOffsets.top
|
|
}
|
|
};
|
|
}
|
|
return (Event.getCoords = f)(e);
|
|
}
|
|
})();/** @fileoverview
|
|
* Getting computed styles, opacity functions.
|
|
*
|
|
* @author Garrett Smith
|
|
*/
|
|
|
|
/**@name APE.dom
|
|
* @namespace*/
|
|
|
|
(function(){
|
|
|
|
var multiLengthPropExp = /^(?:margin|(border)(Width)|padding)$/,
|
|
borderRadiusExp = /^[a-zA-Z]*[bB]orderRadius$/,
|
|
dom = APE.dom;
|
|
|
|
APE.mixin(dom, /** @scope APE.dom */{
|
|
/** @function */ getStyle : getStyle,
|
|
setOpacity : setOpacity,
|
|
getFilterOpacity : getFilterOpacity,
|
|
|
|
// Capture (border)(Width) because we need to put "Top" in the middle.
|
|
multiLengthPropExp : /^(?:margin|(border)(Width)|padding)$/,
|
|
borderRadiusExp : /^[a-zA-Z]*[bB]orderRadius$/,
|
|
tryGetShorthandValues : tryGetShorthandValues,
|
|
getCurrentStyleValueFromAuto : getCurrentStyleValueFromAuto,
|
|
getCurrentStyleClipValues : getCurrentStyleClipValues,
|
|
convertNonPixelToPixel : convertNonPixelToPixel
|
|
});
|
|
|
|
var view = document.defaultView,
|
|
getCS = "getComputedStyle",
|
|
IS_COMPUTED_STYLE = dom.IS_COMPUTED_STYLE,
|
|
currentStyle = "currentStyle",
|
|
style = "style";
|
|
view = null;
|
|
|
|
/**
|
|
* Special method for a browser that supports el.filters and not style.opacity.
|
|
* @memberOf APE.dom
|
|
* @param {HTMLElement} el the element to find opacity on.
|
|
* @return {ufloat} [0-1] amount of opacity.
|
|
* calling this method on a browser that does not support filters
|
|
* results in 1 being returned. Use dom.getStyle or dom.getCascadedStyle instead
|
|
*/
|
|
function getFilterOpacity(el) {
|
|
var filters = el.filters;
|
|
if(!filters) return"";
|
|
try { // Will throw error if no DXImageTransform.
|
|
return filters['DXImageTransform.Microsoft.Alpha'].opacity/100;
|
|
|
|
} catch(e) {
|
|
try {
|
|
return filters('alpha').opacity/100;
|
|
} catch(e) {
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cross-browser adapter method for style.filters vs style.opacity.
|
|
* @memberOf APE.dom
|
|
* @param {HTMLElement} el the element to set opacity on.
|
|
* @param {ufloat} i [0-1] the amount of opacity.
|
|
* @return {ufloat} [0-1] amount of opacity.
|
|
*/
|
|
function setOpacity(el, i) {
|
|
var s = el[style], cs;
|
|
if("opacity"in s) {
|
|
s.opacity = i;
|
|
}
|
|
else if("filter"in s) {
|
|
cs = el[currentStyle];
|
|
s.filter = 'alpha(opacity=' + (i * 100) + ')';
|
|
if(cs && ("hasLayout"in cs) && !cs.hasLayout) {
|
|
style.zoom = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @memberOf APE.dom
|
|
* @name getStyle
|
|
*
|
|
* @function
|
|
* @description returns the computed style of property <code>p</code> of <code>el</code>.
|
|
* Returns different results in IE, so user beware! If your
|
|
* styleSheet has units like "em" or "in", this method does
|
|
* not attempt to convert those to px.
|
|
*
|
|
* Use "cssFloat" for getting an element's float and special
|
|
* "filters" treatment for "opacity".
|
|
*
|
|
* @param {HTMLElement} el the element to set opacity on.
|
|
* @param {String} p the property to retrieve.
|
|
* @return {String} the computed style value or the empty string if no value was found.
|
|
*/
|
|
function getStyle(el, p) {
|
|
var value = "", cs, matches, splitVal, i, len, doc = el.ownerDocument,
|
|
defaultView = doc.defaultView;
|
|
if(IS_COMPUTED_STYLE) {
|
|
cs = defaultView[getCS](el, "");
|
|
if(p == "borderRadius" && !("borderRadius"in cs)) {
|
|
p = "MozBorderRadius"in cs ? "MozBorderRadius" :
|
|
"WebkitBorderRadius"in cs ? "WebkitBorderRadius" : "";
|
|
}
|
|
|
|
if(!(p in cs)) return "";
|
|
value = cs[p];
|
|
if(value === "") {
|
|
// would try to get a rect, but Webkit doesn't support that.
|
|
value = (tryGetShorthandValues(cs, p)).join(" ");
|
|
}
|
|
}
|
|
else {
|
|
cs = el[currentStyle];
|
|
if(p == "opacity" && !("opacity"in el[currentStyle]))
|
|
value = getFilterOpacity(el);
|
|
else {
|
|
if(p == "cssFloat")
|
|
p = "styleFloat";
|
|
value = cs[p];
|
|
|
|
if(p == "clip" && !value && ("clipTop"in cs)) {
|
|
value = getCurrentStyleClipValues(el, cs);
|
|
}
|
|
else if(value == "auto")
|
|
value = getCurrentStyleValueFromAuto(el, p);
|
|
else if(!(p in cs)) return "";
|
|
}
|
|
matches = nonPixelExp.exec(value);
|
|
if(matches) {
|
|
splitVal = value.split(" ");
|
|
splitVal[0] = convertNonPixelToPixel( el, matches);
|
|
for(i = 1, len = splitVal.length; i < len; i++) {
|
|
matches = nonPixelExp.exec(splitVal[i]);
|
|
splitVal[i] = convertNonPixelToPixel( el, matches);
|
|
}
|
|
value = splitVal.join(" ");
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function getCurrentStyleClipValues(el, cs) {
|
|
var values = [], i = 0, prop;
|
|
for( ;i < 4; i++){
|
|
prop = props[i];
|
|
clipValue = cs['clip'+prop];
|
|
if(clipValue == "auto") {
|
|
clipValue = (prop == "Left" || prop == "Top" ? "0px" : prop == "Right" ?
|
|
el.offsetWidth + px : el.offsetHeight + px);
|
|
}
|
|
values.push(clipValue);
|
|
}
|
|
return {
|
|
top:values[0], right:values[1], bottom:values[2], left:values[3],
|
|
toString : function() {return 'rect(' + values.join(' ')+')';}
|
|
};
|
|
}
|
|
|
|
var sty = document.documentElement[style],
|
|
floatProp = 'cssFloat'in sty ? 'cssFloat': 'styleFloat',
|
|
props = ["Top", "Right", "Bottom", "Left"],
|
|
cornerProps = ["Topright", "Bottomright", "Bottomleft", "Topleft"];
|
|
sty = null;
|
|
|
|
function getCurrentStyleValueFromAuto(el, p) {
|
|
|
|
var s = el[style], v, borderWidth, doc = el.ownerDocument;
|
|
if("pixelWidth"in s && pixelDimensionExp.test(p)) {
|
|
var pp = "pixel" + (p.charAt(0).toUpperCase()) + p.substring(1);
|
|
v = s[pp];
|
|
if(v === 0) {
|
|
if(p == "width") {
|
|
borderWidth = parseFloat(getStyle(el, "borderRightWidth"))||0;
|
|
paddingWidth = parseFloat(getStyle(el, "paddingLeft"))||0
|
|
+ parseFloat(getStyle(el, "paddingRight"))||0;
|
|
|
|
return el.offsetWidth - el.clientLeft - borderWidth - paddingWidth + px;
|
|
}
|
|
else if(p == "height") {
|
|
borderWidth = parseFloat(getStyle(el, "borderBottomWidth"))||0;
|
|
paddingWidth = parseFloat(getStyle(el, "paddingTop"))||0
|
|
+ parseFloat(getStyle(el, "paddingBottom"))||0;
|
|
return el.offsetHeight - el.clientTop - borderWidth + px;
|
|
}
|
|
}
|
|
return s[pp] + px;
|
|
}
|
|
if(p == "margin" && el[currentStyle].position != "absolute" &&
|
|
doc.compatMode != "BackCompat") {
|
|
v = parseFloat(getStyle(el.parentNode, 'width')) - el.offsetWidth;
|
|
if(v == 0) return "0px";
|
|
v = "0px " + v;
|
|
return v + " " + v;
|
|
}
|
|
|
|
// Can't get borderWidth because we only have clientTop and clientLeft.
|
|
}
|
|
|
|
// TODO: Consider removing this; "don't do that."
|
|
/**
|
|
* Tries to get a shorthand value for margin|padding|borderWidth.
|
|
* @return {[string]} Either 4 values or, if all four values are equal,
|
|
* then one collapsed value (in an array).
|
|
*/
|
|
function tryGetShorthandValues(cs, p) {
|
|
var multiMatch = multiLengthPropExp.exec(p),
|
|
prefix, suffix,
|
|
prevValue, nextValue,
|
|
values,
|
|
allEqual = true,
|
|
propertyList,
|
|
i = 1;
|
|
|
|
if(multiMatch && multiMatch[0]) {
|
|
propertyList = props;
|
|
prefix = multiMatch[1]||multiMatch[0];
|
|
suffix = multiMatch[2] || ""; // ["borderWidth", "border", "Width"]
|
|
}
|
|
else if(borderRadiusExp.test(p)) {
|
|
propertyList = cornerProps;
|
|
prefix = borderRadiusExp.exec(p)[0];
|
|
suffix = "";
|
|
}
|
|
else return [""];
|
|
|
|
prevValue = cs[prefix + propertyList[0] + suffix ];
|
|
values = [prevValue];
|
|
|
|
while(i < 4) {
|
|
nextValue = cs[prefix + propertyList[i] + suffix];
|
|
allEqual = allEqual && nextValue == prevValue;
|
|
prevValue = nextValue;
|
|
values[i++] = nextValue;
|
|
}
|
|
if(allEqual)
|
|
return [prevValue];
|
|
return values;
|
|
}
|
|
|
|
var nonPixelExp = /(-?\d+|(?:-?\d*\.\d+))(?:em|ex|pt|pc|in|cm|mm\s*)/,
|
|
pixelDimensionExp = /width|height|top|left/,
|
|
px = "px";
|
|
|
|
/**
|
|
* @requires nonPixelExp
|
|
* @param {HTMLElement} el
|
|
* @param {Array} String[] of matches from nonPixelExp.exec( val ).
|
|
*/
|
|
function convertNonPixelToPixel(el, matches) {
|
|
|
|
if(el.runtimeStyle) {
|
|
|
|
// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
|
|
// If we're not dealing with a regular pixel number
|
|
// but a number that has a weird ending, we need to convert it to pixels.
|
|
|
|
var val = matches[0]; // grab the -1.2em or whatever.
|
|
if(parseFloat(val) == 0) {
|
|
return "0px";
|
|
}
|
|
|
|
var s = el[style],
|
|
sLeft = s.left,
|
|
rs = el.runtimeStyle,
|
|
rsLeft = rs.left;
|
|
|
|
rs.left = el[currentStyle].left;
|
|
s.left = (val || 0);
|
|
|
|
// The element does not need to have position: to get values.
|
|
// IE's math is a little off with converting em to px; IE rounds to
|
|
// the nearest pixel.
|
|
val = s.pixelLeft + px;
|
|
// put it back.
|
|
s.left = sLeft;
|
|
rs.left = rsLeft;
|
|
return val;
|
|
}
|
|
}
|
|
})();/**
|
|
* XXX: IE Fix for getElementById returning elements by name.
|
|
*/
|
|
(function(){
|
|
var d = document, x = d.body, c,
|
|
g = 'getElementById',
|
|
orig = document[g];
|
|
|
|
if(!x) return setTimeout(arguments.callee,50);
|
|
|
|
try {
|
|
c = d.createElement("<A NAME=0>");
|
|
x.insertBefore(c, x.firstChild);
|
|
if(d[g]('0')){
|
|
x.removeChild(c);
|
|
d[g] = getElementById;
|
|
}
|
|
} catch(x){}
|
|
function getElementById(id) {
|
|
var el = Function.prototype.call.call(orig, this, id), els, i;
|
|
|
|
if(el && el.id == id) return el;
|
|
els = this.getElementsByName(id);
|
|
|
|
for(i = 0; i < els.length; i++)
|
|
if(els[i].id === id) return els[i];
|
|
return null;
|
|
};
|
|
})();
|
|
|
|
(function(){
|
|
|
|
APE.mixin(APE.dom, {
|
|
selectOptionByValue: selectOptionByValue
|
|
});
|
|
|
|
/**
|
|
* @method selectOptionByValue
|
|
* @param {HTMLElement} element
|
|
* @param {String} value
|
|
*/
|
|
function selectOptionByValue(element, value) {
|
|
for (var i=0, l=element.options.length; i<l; i++) {
|
|
if (element.options[i].value === value) {
|
|
element.selectedIndex = i;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
})(); |