2 Copyright 2009 British Broadcasting Corporation
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
8 http://www.apache.org/licenses/LICENSE-2.0
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
19 @version 1.7.0 (2008-09-22)
20 @description The glow namespace and core library.
22 Includes common methods for running scripts onDomReady and user agent sniffing.
26 PrivateVar: moduleRegister
27 Holds info on which modules are registered {name:true}
29 var moduleRegister = {glow: true},
31 PrivateVar: regexEscape
32 For escaping strings to go in regex
34 regexEscape = /([$^\\\/()|?+*\[\]{}.-])/g,
37 A lowercase representation of the user's useragent string
39 ua = navigator.userAgent.toLowerCase(),
42 // number of blockers blocking (when it's zero, we're ready)
45 PrivateMethod: domReadyQueue
46 array of functions to call when dom is ready
51 PrivateMethod: readyQueue
52 array of functions to call when all ready blockers are unblocked
54 //we want to set isReady to true when this is first run
57 // stops two instances of 'runReadyQueue' fighting on the call stack
58 processingReadyQueue = false,
62 @description Version of glow
63 This is in the format 1.2.3
67 @see <a href="/glow/docs/previous_versions.shtml#versionScheme">Glow's versioning scheme</a>
73 @description A unique ID for this instance of Glow
75 This will be used in glow-specific property names
76 that need to be unique to this instance of glow.
80 UID: "glow" + Math.floor(Math.random() * (1<<30)),
84 @description Is the DOM ready?
86 If glow is loaded after the page has loaded (by means other than Gloader)
87 this value should be set manually.
91 //check gloader to see if dom is already ready
92 isDomReady: window.gloader && gloader.isReady,
96 @description Is Glow ready?
98 Set to true when Glow is ready. This includes DOM ready,
99 a supported browser and any additional requirements. For example
100 Glow widgets will add the loading of their CSS file as a requirement.
104 //check gloader to see if dom is already ready
105 isReady: window.gloader && gloader.isReady,
109 @description Information about the browser / platform
113 if (glow.env.ie < 7) {
114 //this only runs in IE 6 and below
116 if (glow.env.gecko < 1.9) {
117 //this only runs in Gecko versions less than 1.9
118 //Wikipedia can be used to link engine versions to browser versions
123 @description Gecko version number to one decimal place (eg 1.9) or NaN
128 @description IE version number or NaN
133 @description Opera version (eg 8.02) or NaN
137 @name glow.env.webkit
138 @description Webkit version number to one decimal place (eg 419.3) or NaN
143 @description KHTML version number to one decimal place or NaN
147 @name glow.env.standardsMode
148 @description True if the browser reports itself to be in 'standards mode'
152 @name glow.env.version
153 @description Browser version as a string. Includes non-numerical data, eg "1.8.1" or "7b"
157 var nanArray = [0, NaN],
158 opera = (/opera[\s\/]([\w\.]+)/.exec(ua) || nanArray)[1],
159 ie = opera ? NaN : (/msie ([\w\.]+)/.exec(ua) || nanArray)[1],
160 gecko = (/rv:([\w\.]+).*gecko\//.exec(ua) || nanArray)[1],
161 webkit = (/applewebkit\/([\w\.]+)/.exec(ua) || nanArray)[1],
162 khtml = (/khtml\/([\w\.]+)/.exec(ua) || nanArray)[1],
166 gecko : toNum(gecko),
168 opera : toNum(opera),
169 webkit : toNum(webkit),
170 khtml : toNum(khtml),
171 version : ie || gecko || webkit || opera || khtml,
172 standardsMode : document.compatMode != "BackCompat" && (!ie || ie >= 6)
180 @description Registers a new module with the library, checking version numbers & dependencies.
183 Object containing all of the following. This object is
184 compatible with gloader.module, hence some properties which seem
185 unnecessary here. This is all simplified by glow's module pattern.
187 @param {String} meta.name Name of the module.
188 Eg. "glow.dom" or "glow.widgets.Panel"
190 @param {String[]} meta.library Information about Glow.
191 This must be ["glow", "1.7.0"]. 1.7.0 should be
192 the version number of glow expected.
194 @param {String[]} meta.depends The module's dependencies.
195 This must start ["glow", "1.7.0"], followed by modules
196 such as "glow.dom". 1.7.0 should be the version number
199 @param {Function} meta.builder The module's implementation.
200 A reference to glow will be passed in as the first parameter
201 to this function. Add to that object to create publicly
202 accessabile properties. Anything else in this function
205 @returns {Object} Glow
210 library: ["glow", "1.0.0"],
211 depends: ["glow", "1.0.0", "glow.dom"],
212 builder: function(glow) {
219 module: function(meta) {
221 depends = meta.depends[0] || [],
222 dependsLen = depends.length,
224 objRef = window.glow; //holds the parent object for the new module
226 //check version number match core version
227 if (meta.library[1] != glow.VERSION) {
228 throw new Error("Cannot register " + name + ": Version mismatch");
231 //check dependencies loaded
233 for (; i < dependsLen; i++) {
235 if (!moduleRegister[depends[i]]) {
236 //check again ondomready to detect if modules are being included in wrong order
237 throw new Error("Module " + depends[i] + " required before " + name);
244 //register it as built
245 moduleRegister[name] = true;
252 @description Calls a function when the DOM had loaded and the browser is supported
254 "ready" also waits for glow's CSS file to load if it has been
257 @param {Function} callback Function to call
262 glow.ready(function() {
267 //just run function if already ready
271 readyQueue[readyQueueLen++] = f;
277 @name glow._readyBlockers
280 @description A hash (by name) of blockers.
281 True if they are blocking, false if they've since unblocked
286 @name glow._addReadyBlock
289 @description Adds a blocker to anything added via glow.ready.
290 Uncalled callbacks added via glow.ready will not fire until
291 this block is removed
293 @param {String} name Name of blocker
298 glow._addReadyBlock("widgetsCss");
300 // when CSS is ready...
301 glow._removeReadyBlock("widgetsCss");
303 _addReadyBlock: function(name) {
304 if (name in glow._readyBlockers) {
305 throw new Error("Blocker '" + name +"' already exists");
307 glow._readyBlockers[name] = true;
308 glow.isReady = false;
314 @name glow._removeReadyBlock
317 @description Removes a ready blocker added via glow._addReadyBlock
319 @param {String} name Name of blocker
324 glow._addReadyBlock("widgetsCss");
326 // when CSS is ready...
327 glow._removeReadyBlock("widgetsCss");
329 _removeReadyBlock: function(name) {
330 if (glow._readyBlockers[name]) {
331 glow._readyBlockers[name] = false;
333 // if we're out of blockers
334 if (!blockersActive) {
344 @name glow.onDomReady
346 @description Calls a function when / if the DOM is ready.
348 This function does not wait for glow's CSS to load, nor
349 does it block unsupported browsers. If you want these features,
350 use {@link glow.ready}
352 @param {Function} callback Function to call
357 glow.onDomReady(function() {
361 onDomReady: function(f) {
362 //just run function if already ready
363 if (this.isDomReady) {
366 domReadyQueue[domReadyQueueLen++] = f;
373 @description Useful language functions.
374 @see <a href="../furtherinfo/glow/glow.lang.shtml">Using glow.lang.clone</a>
380 @description Removes leading and trailing whitespace from a string
382 @param {String} str String to trim
386 String without leading and trailing whitespace
389 glow.lang.trim(" Hello World "); // "Hello World"
391 trim: function(sStr) {
392 //this optimisation from http://blog.stevenlevithan.com/archives/faster-trim-javascript
393 return sStr.replace(/^\s*((?:[\S\s]*\S)?)\s*$/, '$1');
397 @name glow.lang.toArray
399 @description Converts an array-like object to a real array
401 @param {Object} arrayLike Any array-like object
406 var a = glow.lang.toArray(glow.dom.get("a"));
408 toArray: function(aArrayLike) {
409 if (aArrayLike.constructor == Array) {
412 //use array.slice if not IE? Could be faster
413 var r = [], i=0, len = aArrayLike.length;
414 for (; i < len; i++) {
415 r[i] = aArrayLike[i];
421 @name glow.lang.apply
423 @description Copies properties from one object to another
425 @param {Object} destination Destination object
427 @param {Object} source Properties of this object will be copied onto the destination
432 var obj = glow.lang.apply({foo: "hello", bar: "world"}, {bar: "everyone"});
433 //results in {foo: "hello", bar: "everyone"}
435 apply: function(destination, source) {
436 for (var i in source) {
437 destination[i] = source[i];
445 @description Runs a function for each element of an array and returns an array of the results
447 @param {Array} array Array to loop over
448 @param {Function} callback The function to run on each element. This function is passed three params, the array item, its index and the source array.
449 @param {Object} [context] The context for the callback function (the array is used if not specified)
453 Array containing one element for each value returned from the callback
456 var weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
457 var weekdaysAbbr = glow.lang.map(weekdays, function (day) {
458 return day.slice(0, 3).toLowerCase();
460 // returns ["mon", "tue", "wed", "thu", "fri"]
462 map: function (arr, callback, context) {
463 if (Array.prototype.map) { return Array.prototype.map.call(arr, callback, context || arr); }
464 if (! callback.call) { throw new TypeError(); }
466 var len = arr.length,
468 thisp = context || arr,
471 for (; i < len; i++) {
473 res[i] = callback.call(thisp, arr[i], i, arr);
480 @name glow.lang.replace
482 @description Makes a replacement in a string.
484 Has the same interface as the builtin
485 String.prototype.replace method, but takes the input
486 string as the first parameter. In general the native string
487 method should be used unless you need to pass a function as the
488 second parameter, as this method will work accross our
491 @param {String} str Input string
493 @param {String | RegExp} pattern String or regular expression to match against
495 @param {String | Function} replacement String to make replacements with, or a function to generate the replacements
498 A new string with the replacement(s) made
501 var myDays = '1 3 6';
502 var dayNames = glow.lang.replace(myDays, /(\d)/, function (day) {
503 return " MTWTFSS".charAt(day - 1);
505 // dayNames now contains "M W S"
507 replace: (function () {
508 var replaceBroken = "g".replace(/g/, function () { return 'l'; }) != 'l',
509 def = String.prototype.replace;
510 return function (inputString, re, replaceWith) {
511 var pos, match, last, buf;
512 if (! replaceBroken || typeof(replaceWith) != 'function') {
513 return def.call(inputString, re, replaceWith);
515 if (! (re instanceof RegExp)) {
516 pos = inputString.indexOf(re);
519 def.call(inputString, re, replaceWith.call(null, re, pos, inputString));
522 last = re.lastIndex = 0;
523 while ((match = re.exec(inputString)) != null) {
525 buf[buf.length] = inputString.slice(last, pos);
526 buf[buf.length] = replaceWith.apply(null, match);
530 last = pos + match[0].length;
534 buf[buf.length] = inputString.slice(last);
540 @name glow.lang.interpolate
542 @description Replaces placeholders in a string with data from an object
544 @param {String} template The string containing {placeholders}
545 @param {Object} data Object containing the data to be merged in to the template
546 <p>The object can contain nested data objects and arrays, with nested object properties and array elements are accessed using dot notation. eg foo.bar or foo.0.</p>
547 <p>The data labels in the object cannot contain characters used in the template delimiters, so if the data must be allowed to contain the default { and } delimiters, the delimters must be changed using the option below.</p>
548 @param {Object} opts Options object
549 @param {String} [opts.delimiter="{}"] Alternative label delimiter(s) for the template
550 The first character supplied will be the opening delimiter, and the second the closing. If only one character is supplied, it will be used for both ends.
551 @param {Boolean} [opts.escapeHtml=false] Escape any special html characters found in the data object
552 Use this to safely inject data from the user into an HTML template. The glow.dom module
553 must be present for this feature to work (an error will be thrown otherwise).
560 colours: ["black", "white"],
567 var template = "My cat's name is {name}. His colours are {colours.0} & {colours.1}. His mum is {family.mum}, his dad is {family.dad} and he has {family.siblings.length} brothers or sisters.";
568 var result = glow.lang.interpolate(template, data);
569 // result == "My cat's name is Domino. His colours are black & white. His mum is Spot, his dad is Patch and he has 0 brothers or sisters."
573 name: 'Haxors!!1 <script src="hackhackhack.js"></script>'
575 var template = '<p>Hello, my name is {name}</p>';
576 var result = glow.lang.interpolate(template, data, {
579 // result == '<p>Hello, my name is Haxors!!1 <script src="hackhackhack.js"></script></p>'
581 interpolate : function (template, data, opts) {
585 // div used for html escaping
590 // make sure the dom module is around
591 if (opts.escapeHtml) {
592 if (!glow.dom) { throw new Error('glow.lang.interpolate - glow.dom is needed for escapeHtml'); }
593 div = glow.dom.create('<div></div>');
596 if (opts.delimiter == undefined) {
597 placeHolderRx = /\{[^{}]+\}/g;
599 leftDelimiter = opts.delimiter.substr(0, 1).replace(regexEscape, "\\$1");
600 rightDelimiter = opts.delimiter.substr(1, 1).replace(regexEscape, "\\$1") || leftDelimiter;
601 placeHolderRx = new RegExp(leftDelimiter + "[^" + leftDelimiter + rightDelimiter + "]+" + rightDelimiter, "g");
604 return template.replace(placeHolderRx, function (placeholder) {
606 var key = placeholder.slice(1, -1),
607 keyParts = key.split("."),
610 len = keyParts.length;
613 // need to be backwards compatible with "flattened" data.
618 for (; i < len; i++) {
619 if (keyParts[i] in val) {
620 val = val[ keyParts[i] ];
627 if (opts.escapeHtml) {
628 val = div.text(val).html();
634 @name glow.lang.hasOwnProperty
636 @description Cross-browser implementation
639 Safari 1.3 doesn't support
640 <a href="http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Object:hasOwnProperty">
641 Object.hasOwnProperty
642 </a>, use this method instead.
644 @param {Object} obj The object to check
646 @param {String} property Property name
650 Returns false if a property doesn't exist in an object, or it
651 was inherited from the object's prototype. Otherwise, returns
654 hasOwnProperty: {}.hasOwnProperty ? //not supported in Safari 1.3
655 function(obj, prop) {
656 return obj.hasOwnProperty(prop);
658 function(obj, prop) {
659 var propVal = obj[prop], //value of the property
660 objProto = obj.__proto__, //prototype of obj
661 protoVal = objProto ? objProto[prop] : {}; //prototype val
662 if (propVal !== protoVal) {
665 //try changing prototype and see if obj reacts
666 var restoreProtoVal = glow.lang.hasOwnProperty(objProto, prop),
667 tempObjProtoVal = objProto[prop] = {},
668 hasOwn = (obj[prop] !== tempObjProtoVal);
670 delete objProto[prop];
671 if (restoreProtoVal) {
672 objProto[name] = tempObjProtoVal;
678 @name glow.lang.extend
680 @description Copies the prototype of one object to another.
682 The 'subclass' can also access the 'base class' via subclass.base
684 @param {Function} sub Class which inherits properties.
686 @param {Function} base Class to inherit from.
688 @param {Object} additionalProperties An object of properties and methods to add to the subclass.
691 function MyClass(arg) {
694 MyClass.prototype = {
695 showProp: function() { alert(this.prop); }
697 function MyOtherClass(arg) {
698 //call the base class's constructor
699 arguments.callee.base.apply(this, arguments);
701 glow.lang.extend(MyOtherClass, MyClass, {
702 setProp: function(newProp) { this.prop = newProp; }
705 var test = new MyOtherClass("hello");
706 test.showProp(); // alerts "hello"
707 test.setProp("world");
708 test.showProp(); // alerts "world"
711 extend: function(sub, base, additionalProperties) {
712 var f = function () {}, p;
713 f.prototype = base.prototype;
718 if (additionalProperties) {
719 glow.lang.apply(sub.prototype, additionalProperties);
724 @name glow.lang.clone
726 @description Deep clones an object / array
728 @param {Object} Data Object to clone
733 var firstObj = { name: "Bob", secondNames: ["is","your","uncle"] };
734 var clonedObj = glow.lang.clone( firstObj );
736 clone: function( obj ) {
737 var index, _index, tmp;
739 if ( typeof obj !== 'object' ) {
742 if ( obj[0] || obj.concat ) {
746 tmp[index] = arguments.callee( obj[index] );
750 for ( index in obj ) {
751 tmp[index] = arguments.callee( obj[index] );
764 //run queued ready functions when DOM is ready
766 function runDomReadyQueue() {
767 glow.isDomReady = true;
768 // run all functions in the array
769 for (var i = 0; i < domReadyQueueLen; i++) {
774 function runReadyQueue() {
775 // if we're already processing the queue, just exit, the other instance will take care of it
776 if (processingReadyQueue) return;
777 processingReadyQueue = true;
778 for (var i = 0; i < readyQueueLen;) {
781 // check if the previous function has created a blocker
782 if (blockersActive) {
786 // take items off the ready queue that have processed
787 readyQueue = readyQueue.slice(i);
789 readyQueueLen = readyQueueLen - i;
790 processingReadyQueue = false;
794 //don't do this stuff if the dom is already ready
795 if (glow.isDomReady) { return; }
797 glow._addReadyBlock("glow_domReady");
799 if (typeof window.frameElement != 'undefined') {
800 // we can't use doScroll if we're in an iframe...
801 d.attachEvent("onreadystatechange", function(){
802 if (d.readyState == "complete") {
803 d.detachEvent("onreadystatechange", arguments.callee);
805 glow._removeReadyBlock("glow_domReady");
809 // polling for no errors
812 // throws errors until after ondocumentready
813 d.documentElement.doScroll('left');
815 setTimeout(arguments.callee, 0);
820 glow._removeReadyBlock("glow_domReady");
823 } else if (glow.env.webkit < 525.13 && typeof d.readyState != 'undefined') {
825 if ( /loaded|complete/.test(d.readyState) ) {
827 glow._removeReadyBlock("glow_domReady");
834 var callback = function () {
835 if (callback.fired) { return; }
836 callback.fired = true;
838 glow._removeReadyBlock("glow_domReady");
840 if (d.addEventListener) {
841 d.addEventListener("DOMContentLoaded", callback, false);
843 var oldOnload = window.onload;
844 window.onload = function () {
845 if (oldOnload) { oldOnload(); }
852 @name glow.isSupported
853 @description Set to true in supported user agents
854 This will read false in 'level 2' browsers in BBC's Browser Support Guidelines
857 @see <a href="http://www.bbc.co.uk/guidelines/newmedia/technical/browser_support.shtml">BBC's Browser Support Guidelines</a>
859 // TODO: for v2 we should switch this to 'notSupported' as it's a blacklist
860 glow.isSupported = !(
861 //here are the browsers we don't support
863 (env.gecko < 1.9 && !/^1\.8\.1/.test(env.version)) ||
867 // block 'ready' if browser isn't supported
868 if (!glow.isSupported) {
869 glow._addReadyBlock("glow_browserSupport");
872 if (window.gloader) {
876 builder: function () {
880 } else if (window.glow) {
881 throw new Error("Glow global object already exists");
886 // this helps IE cache background images
889 document.execCommand("BackgroundImageCache", false, true);
894 /*@if (@_jscript_version > 5.5)@*/
898 @description Internationalisation Module.
900 @see <a href="../furtherinfo/i18n/index.shtml">Using glow.i18n</a>
902 (window.gloader || glow).module({
904 library: ["glow", "1.7.0"],
905 depends: [["glow", "1.7.0"]],
906 builder: function(glow) {
909 // Regexes for judging subtag types in the 'L'anguage, 'S'cript, 'R'egion, 'V'ariant form
910 // lv is a match for a subtag that can be either 'L'anguage or 'V'ariant
911 // See full explanation of tags and subtags at the end of the file
912 var subtagRegexes = {
915 s : /^[A-Z][a-z]{3}$/,
916 r : /^[A-Z]{2}|[0-9]{3}$/,
920 // Bit masks for tag signature in the 'L'anguage, 'S'cript, 'R'egion, 'V'ariant form
921 // See full explanation of tags and subtags at the end of the file
926 LSRV = L + S + R + V,
934 // Base structures for tag parsing using 'L'anguage, 'S'cript, 'R'egion, 'V'ariant form
935 // See full explanation of tags and subtags at the end of the file
936 var masks = {l: L, s: S, r: R, v: V}, // map subtag types to masks
937 subtags = ['l', 's', 'r', 'v'], // in order
938 labels = {l: 0, s: 1, r: 2, v: 3}; // reverse look up for subtags
940 // holds the actual local pack data
941 var localePacks = {};
943 // holds the main index of the pack / module structure
944 var moduleStructure = {};
946 // the currently selected locale
947 // comes from html lang if it exists and is valid, defaults to 'en'
948 var currentLocale = parseTag(document.documentElement.lang || 'en') || parseTag('en');
950 // Determine what, if any, type of subtag this is by regex matching
951 function getSubtagPattern( subtag ) {
952 for (var pattern in subtagRegexes) { // These regexes are mutually exclusive, so order is immaterial
953 if (subtagRegexes[pattern].test(subtag)) {
961 // Parse the given tag and return a queryable data set using 'L'anguage, 'S'cript, 'R'egion, 'V'ariant form
962 // See full explanation of tags and subtags at the end of the file
963 // if the tag is valid, returns the internal representation of a parsed locale tag
964 // canonical is the valid canonical form of the tag eg "en-GB", mask is a bitmask indicating which subtag types are present and subtags is an object containing the actual subtags
965 function parseTag( tag ) {
970 var parts = tag.split("-"), // get the subtags
972 canon = [], // will hold the subtags of the canonical tag
973 matchedSubtags = {l: "", s:"", r:"", v:""},
980 for (var j = 0, jlen = subtags.length; j < jlen; j++) { // order of subtag match is important
983 label = labels[subtag];
985 while ((getSubtagPattern(parts[i]).indexOf(subtag) == -1) && (i < len)) { // indexOf allows for partial match of 'lv' regex
986 i++; // if no match, move on
989 if (i < len) { // if a match is found, store it and continue with the next subtag from this point
990 canon[label] = parts[i];
991 mask += masks[subtag];
992 matchedSubtags[subtag] = parts[i];
998 // put it all back together as a string
999 var canonical = canon.join("-").replace(/-+/g, "-");
1001 if ((canonical == "") || (canonical.substring(0, 1) == "-")) { // this means there is no language subtag, so we must fail the parse
1005 return {canonical: canonical, mask: mask, subtags: matchedSubtags};
1009 // For a given tag, and mask for a subset of it, return the subset tag using 'L'anguage, 'S'cript, 'R'egion, 'V'ariant form
1010 // eg for 'en-GB-scouse' and an LR mask, return 'en-GB'
1011 // See full explanation of tags and subtags at the end of the file
1012 // ** note the bitwise operations **
1013 function getSubsetTag( parsed, test, mask ) {
1016 // this test (bitwise because the operands are bitmasks) determines that mask has no bits set that parsed.mask does not
1017 // this is to make sure that the tag being generated is genuinely a subset of the main tag
1018 if ((mask & ~parsed.mask) == 0) {
1019 subset = parsed.subtags["l"]; // there must be a language subtag, because this tags passed the parse
1022 subset = subset + "-" + parsed.subtags["s"];
1025 subset = subset + "-" + parsed.subtags["r"];
1028 subset = subset + "-" + parsed.subtags["v"];
1039 // Performs the generic tag negotiation process
1040 // The test is a function that performs an additional check to see if the subset tag currently being
1041 // checked should be used, normally based on the presence of appropriate data in the locale packs
1042 // parsed is the parsed locale tag, as returned by parseTag
1043 // testFn is the function passed to getSubsetTag, used to determine if a given subste tag is a negotiated hit
1044 // successFn, failFn are the functions to be run when the negotiation result is known
1045 function negotiate( parsed, testFn, successFn, failFn ) {
1048 switch(parsed.mask) { // NOTE the breaks are conditional because it might be OK for all the cases to execute - need to move on to the next case if the "if" fails
1050 if ((subset = getSubsetTag(parsed, testFn, LRV ))) {
1054 if ((subset = getSubsetTag(parsed, testFn, LR ))) {
1058 if ((subset = getSubsetTag(parsed, testFn, LSRV))) {
1062 if ((subset = getSubsetTag(parsed, testFn, LSR ))) {
1066 if ((subset = getSubsetTag(parsed, testFn, LSV ))) {
1070 if ((subset = getSubsetTag(parsed, testFn, LS ))) {
1074 if ((subset = getSubsetTag(parsed, testFn, LV ))) {
1078 if ((subset = getSubsetTag(parsed, testFn, L ))) {
1090 if (subset == null) {
1099 @name glow.i18n.setLocale
1101 @description Sets the locale to a new one, stacking up the old one for later retreival.
1103 Has no effect if the newLocaleTag is invalid.
1105 @param {String} newLocaleTag The new locale tag to be set.
1110 // assume locale is "en-GB" first
1111 glow.i18n.setLocale("cy-GB");
1112 // locale is now "cy-GB" with "en-GB" stacked up
1114 function setLocale( newLocaleTag ) {
1115 var old = currentLocale,
1116 parsed = parseTag(newLocaleTag);
1119 currentLocale = parsed;
1120 currentLocale.next = old;
1127 @name glow.i18n.revertLocale
1129 @description Reverts the locale to the one used immediately prior to the current one.
1131 Has no effect if the current locale was the first set (ie if no new locales have been set,
1132 or if the locale has been reverted all the way back to the beginning).
1137 // assume locale is "en-GB" first
1138 glow.i18n.setLocale("cy-GB");
1139 // locale is now "cy-GB" with "en-GB" stacked up
1140 glow.i18n.revertLocale();
1141 // locale is now back to "en-GB"
1143 function revertLocale() {
1144 currentLocale = currentLocale.next || currentLocale;
1150 @name glow.i18n.getLocale
1152 @description Returns the tag of the current locale in canonical form.
1157 loc = glow.i18n.getLocale(); // returns the current locale eg "en-GB"
1159 glow.i18n.setLocale("cy-GB");
1160 loc = glow.i18n.getLocale(); // now returns "cy
1162 glow.i18n.setLocale("en-ignoredsubtag-US");
1163 loc = glow.i18n.getLocale(); // now returns "en-US", which is the canonical form
1165 function getLocale() {
1166 return currentLocale.canonical;
1170 @name glow.i18n.addLocaleModule
1172 @description Stores the given data against the given module on the given locale.
1173 Creates any local packs and / or moduels that dio not already exist.
1174 Adds the given data to any that do.
1176 @param {String} moduleName The name of the module to be created/added to.
1177 @param {String} localeTag The locale tag to add the module to.
1178 @param {Object} data The data of the module in key : value form.
1183 // assume locale is "en-GB" first
1184 glow.i18n.setLocale("cy-nonsense-GB");
1185 var newLocale = glow.i18n.getLocale(); // returns "cy-GB"
1187 function addLocaleModule( moduleName, localeTag, data ) {
1188 var tag = parseTag(localeTag),
1194 pack = localePacks[tag.canonical] = localePacks[tag.canonical] || {};
1195 module = pack[moduleName] = pack[moduleName] || {};
1196 structure = moduleStructure[moduleName] = moduleStructure[moduleName] || {};
1198 for (var key in data) { // not using glow.lang.apply to avoid two loops
1199 module[key] = data[key];
1208 @name glow.i18n.getLocaleModule
1210 @description Retreives an object whose keys are every label that can have a value (via tag negotiation) associated with it, and whose values are the associated label values.
1212 @param {String} moduleName The name of the module retreived.
1213 @param {Object} [opts] Options object
1214 @param {String} [opts.locale] On override locale to use instaed of the system locale.
1218 function getLocaleModule( moduleName, opts ) {
1220 options = opts || {},
1221 structure = moduleStructure[moduleName] || {},
1222 localeTag = currentLocale,
1226 // define the customisers for negotiate outside the loop
1227 // oddly, the loop control variable *is* correclty accessed, becasue of var scoping and the synchronous function calls
1228 function test( nTag ) {
1229 if (localePacks[nTag] && localePacks[nTag][moduleName] && localePacks[nTag][moduleName][label]) {
1237 function success( sTag ) {
1238 module[label] = localePacks[sTag][moduleName][label];
1242 module[label] = "[Error! No " + moduleName + "." + label + " on " + localeTag.canonical + "]";
1245 if (options.locale != undefined) {
1246 tag = parseTag(options.locale);
1253 for (label in structure) {
1254 negotiate(localeTag, test, success, fail);
1261 @name glow.i18n.addLocalePack
1263 @description Shortcut for creating many locale modules on one locale (ie a brand new entire locale pack)
1265 @param {String} localeTag The name of the module retreived.
1266 @param {Object} data The data of the module in MODULE : key : value form.
1270 function addLocalePack( localeTag, data ) {
1271 for (var moduleName in data) {
1272 addLocaleModule(moduleName, localeTag, data[moduleName]);
1279 @name glow.i18n.checkLocale
1281 @description Developer focused checker for getting the locale tag(s) returned by tag negotiation
1282 if no options passed it returns a structured data object of the entire data set for the given locale
1283 if just a module name is passed, the result set is limited to that module
1284 if a module name and a label are passed it returns a string for just that label
1286 @param {String} localeTag The name of the module retreived.
1287 @param {Object} [opts] Options object
1288 @param {String} [opts.module] If set, restricts the results to that module.
1289 @param {String} [opts.label] If set alongside opts.module, restricts the results to that label on that module.
1293 function checkLocale( localeTag, opts ) {
1294 var options = opts || {},
1295 parsed = parseTag(localeTag);
1297 if (options.module) {
1298 if (options.label) {
1299 return checkLocaleByModuleAndLabel(parsed, options.module, options.label);
1302 return checkLocaleByModule(parsed, options.module);
1306 return checkLocaleByEverything(parsed);
1312 // helper for checkLocale
1313 function checkLocaleByModuleAndLabel( parsed, module, label ) {
1316 // Define the customisers for negotiate outside the function call to be consistent with other checkLocaleBy* helpers
1317 function test( nTag ) {
1318 if (localePacks[nTag] && localePacks[nTag][module] && localePacks[nTag][module][label]) {
1326 function success( sTag ) {
1331 result = "**error** - no negotiated value exists";
1334 negotiate(parsed, test, success, fail);
1339 // helper for checkLocale
1340 function checkLocaleByModule( parsed, module ) {
1341 var structure = moduleStructure[module] || {},
1345 // Define the customisers for negotiate outside the loop
1346 // Oddly, the loop control variable *is* correclty accessed, becasue of var scoping and the synchronous function calls
1347 function test( nTag ) {
1348 if (localePacks[nTag] && localePacks[nTag][module] && localePacks[nTag][module][label]) {
1356 function success( sTag ) {
1357 results[label] = sTag;
1361 results[label] = "**error** - no negotiated value exists";
1364 for (label in structure) {
1365 negotiate(parsed, test, success, fail);
1371 // helper for checkLocale
1372 function checkLocaleByEverything( parsed ) {
1377 // Define the customisers for negotiate outside the loop
1378 // Oddly, the loop control variable *is* correclty accessed, becasue of var scoping and the synchronous function calls
1379 function test( nTag ) {
1380 if (localePacks[nTag] && localePacks[nTag][module] && localePacks[nTag][module][label]) {
1388 function success( sTag ) {
1389 results[module][label] = sTag;
1393 results[module][label] = "**error** - no negotiated value exists";
1396 for (module in moduleStructure) {
1397 results[module] = {};
1399 for (label in moduleStructure[module]) {
1400 negotiate(parsed, test, success, fail);
1409 glow.i18n = self = {
1410 setLocale : setLocale,
1411 revertLocale : revertLocale,
1412 getLocale : getLocale,
1413 addLocaleModule : addLocaleModule,
1414 getLocaleModule : getLocaleModule,
1415 addLocalePack : addLocalePack,
1416 checkLocale : checkLocale
1419 // define the basic 'en' locale pack
1420 // just the properties - widgets and modules will add their own locale modules
1421 addLocalePack("en", {
1423 LANGUAGE : "English",
1430 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1431 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1433 About Locale Tags and Tag Negotiation
1434 =====================================
1436 Locale Tag negotiation is the process Glow uses to find the most specific
1437 match for a given tag given the context.
1441 Locale Tag Structure
1442 ~~~~~~~~~~~~~~~~~~~~
1444 First, let's look at the anatomy of locale tags in Glow.
1446 According to the IANA spec, locale tags have this structure
1448 language-script-region-variant-extension-privateuse
1450 There are also "grandfathered" and "redundant" tags that existed before
1451 this structure was implemented and continue for backwards compatibility.
1453 Glow only supports the following subset of that structure. Subtags are
1454 separated by "-" and must appear in the correct order.
1456 language-script-region-variant
1458 These are the IANA formats of these subtags
1460 language : /[a-z]{2,3}/ - 2 or 3 lowercase letters.
1461 script : /[A-Z][a-z]{3}/ - an uppercase letter followed by
1463 region : /[A-Z]{2}|[0-9]{3}/ - 2 upper case letters or a 3 digit number
1464 variant : /[a-z0-9]{4,}/ - lower case letters or numbers,
1465 4 or more characters
1467 eg (yes, these are all official IANA tags, even the last one...)
1469 en language generic English
1470 en-GB language-region British English
1471 en-Runr language-script English written in Runic script
1472 en-Runr-GB language-script-region British English written in Runic
1473 en-GB-scouse language-region-variant Scouse variant of British English
1475 While Glow does not support grandfathered or redundant tags or extension or
1476 private use subtags, it does not enforce compliance with the IANA registry.
1478 This means that if the subtag formats can be relaxed a little, variation
1479 from the standard could be used to fill these gaps with a tag that Glow can
1482 As a result Glow applies theses formats to subtags
1484 language : /[a-z]{1,3}/ - 1, 2 or 3 lowercase letters
1485 > allowing shorter subtag than IANA
1486 script : /[A-Z][a-z]{3}/ - an uppercase letter
1487 followed by 3 lowercase letters
1489 region : /[A-Z]{2}|[0-9]{3}/ - 2 upper case letters or a 3 digit number
1491 variant : /[a-z0-9]{2,}/ - lower case letters or numbers,
1492 2 or more characters
1493 > allowing shorter subtag than IANA
1495 This does mean that there is now potential conflict between the language
1496 and the variant subtags; a subtag that is 2 or 3 lowercase letters could be
1497 either. Glow's insistence on strict subtag order solves this. If a subtag
1498 that falls in this overlap zone is encountered, it is a language subtag
1499 first, a variant if a language has already be found, and ignored as a last
1502 Now, even though according to IANA, en-GB-oed is grandfathered (the variant
1503 is too short), it can be directly supported by Glow.
1505 (If you're interested, en-GB-oed is British English, but with Oxford
1506 English Dictionary spellings. These basically -ize rather than -ise but all
1507 other spellings as en-GB, so the OED lists "colourize")
1511 Tag Negotiation Process
1512 ~~~~~~~~~~~~~~~~~~~~~~~
1514 When parsing a tag, Glow will look for each subtag in the expected order.
1515 If a subtag is encountered out of order, or when that item has already been
1516 parsed, it will be ignored. If a subtag's type cannot be identified, it
1519 GB-cy-1-en-US will be parsed as cy-US.
1520 'GB' is out of order,
1521 'en' is a duplicated language subtag and
1522 '1' is not a recognised subtag type.
1524 Given all this, Locale negotiation occurs in the following order.
1530 language-script-region-variant language-region-variant
1531 language-script-region language-region
1532 language-script-variant /
1539 It starts at the specificity of the given tag, and skips steps which have a
1540 subtag that isn't present. eg
1542 xx-YY, which will be parsed as language-region, will negotiate as
1547 xx-Yyyy-ZZ, will be parsed as language-script-region, and negotiate as
1548 language-script-region
1553 When Locale packs are created, the tag will be checked to see if it can
1554 be negotiated. A valid tag must have at least a language, so "en-GB" and
1555 "cy" are valid, but "GB" is not.
1558 How this Module handles all this
1559 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1561 The code has a number of variables, objects and arrays whose names or
1562 values make reference to some variant or combination of l, s, r or v. These
1563 match to the 'L'anguage, 'S'cript, 'R'egion or 'V'ariant subtag.
1565 So, for instance, the variable LSRV is a bitmask for a tag structure with
1573 http://www.w3.org/International/articles/language-tags/
1574 http://www.w3.org/International/articles/bcp47/
1575 http://www.iana.org/assignments/language-subtag-registry
1579 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
1580 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
1584 @description Accessing and manipulating the DOM
1585 @see <a href="../furtherinfo/creatingnodelists/">Creating NodeLists</a>
1586 @see <a href="../furtherinfo/workingwithnodelists/">Working with NodeLists</a>
1587 @see <a href="../furtherinfo/xmlnodelists/">XML NodeLists</a>
1589 (window.gloader || glow).module({
1591 library: ["glow", "1.7.0"],
1593 builder: function(glow) {
1598 PrivateVar: cssRegex
1599 For matching CSS selectors
1602 tagName: /^(\w+|\*)/,
1603 combinator: /^\s*([>]?)\s*/,
1604 //safari 1.3 is a bit dim when it comes to unicode stuff, only dot matches them (not even \S), so some negative lookalheads are needed
1605 classNameOrId: (env.webkit < 417) ? new RegExp("^([\\.#])((?:(?![\\.#\\[:\\s\\\\]).|\\\\.)+)") : /^([\.#])((?:[^\.#\[:\\\s]+|\\.)+)/
1607 //for escaping strings in regex
1608 regexEscape = /([$^\\\/()|?+*\[\]{}.-])/g,
1611 PrivateVar: cssCache
1612 Cache of arrays representing an execution path for css selectors
1617 PrivateVar: dom0PropertyMapping
1618 Mapping of HTML attribute names to DOM0 property names.
1620 dom0PropertyMapping = {
1621 checked : "checked",
1622 "class" : "className",
1623 "disabled" : "disabled",
1625 maxlength : "maxLength"
1629 PrivateVar: dom0BooleanAttribute
1630 The HTML attributes names that should be converted from true/false
1631 to ATTRIBUTENAME/undefined (i.e. boolean attributes like checked="checked").
1633 dom0BooleanAttribute = {
1639 PrivateVar: dom0AttributeMappings
1640 Functions that map dom0 values to sane ones.
1642 dom0AttributeMappings = {
1643 maxlength : function (val) { return val.toString() == "2147483647" ? undefined : val; }
1647 Used by unique(), increased by 1 on each use
1651 PrivateVar: ucheckPropName
1652 This is the property name used by unique checks
1654 ucheckPropName = "_unique" + glow.UID,
1657 @name glow.dom-dataPropName
1660 @description The property name added to the DomElement by the NodeList#data method.
1662 dataPropName = "_uniqueData" + glow.UID,
1665 @name glow.dom-dataIndex
1668 @description The value of the dataPropName added by the NodeList#data method.
1670 dataIndex = 1, // must be a truthy value
1673 @name glow.dom-dataCache
1676 @description Holds the data used by the NodeList#data method.
1678 The structure is like:
1688 PrivateVar: htmlColorNames
1689 Mapping of colour names to hex values
1711 PrivateVar: usesYAxis
1712 regex for detecting which css properties need to be calculated relative to the y axis
1714 usesYAxis = /height|top/,
1715 colorRegex = /^rgb\(([\d\.]+)(%?),\s*([\d\.]+)(%?),\s*([\d\.]+)(%?)/i,
1716 cssPropRegex = /^(?:(width|height)|(border-(top|bottom|left|right)-width))$/,
1717 hasUnits = /width|height|top$|bottom$|left$|right$|spacing$|indent$|font-size/,
1718 //append gets set to a function below
1720 //unique gets set to a function below
1722 //we set this up at the end of the module
1724 //getByTagName gets get to a function below
1730 // true if properties of a dom node are cloned when the node is cloned (eg, true in IE)
1731 nodePropertiesCloned,
1732 // used to convert divs to strings
1733 tmpDiv = doc.createElement("div"),
1735 PrivateVars: tableArray, elmFilter
1736 Used in private function stringToNodes to capture any
1737 elements that cannot be a childNode of <div>.
1738 Each entry in JSON responds to an element.
1739 First array value is how deep the element will be created in a node tree.
1740 Second array value is the beginning of the node tree.
1741 Third array value is the end of the node tree.
1743 tableArray = [1, '<table>', '</table>'],
1744 emptyArray = [0, '', ''],
1745 // webkit won't accept <link> elms to be the only child of an element,
1746 // it steals them and hides them in the head for some reason. Using
1747 // broken html fixes it for some reason
1748 paddingElmArray = env.webkit < 526 ? [0, '', '</div>', true] : [1, 'b<div>', '</div>'],
1749 trArray = [3, '<table><tbody><tr>', '</tr></tbody></table>'],
1751 caption: tableArray,
1754 colgroup: tableArray,
1756 tr: [2, '<table><tbody>', '</tbody></table>'],
1759 option: [1, '<select>', '</select>'],
1760 legend: [1, '<fieldset>', '</fieldset>'],
1761 link: paddingElmArray,
1762 script: paddingElmArray,
1763 style: paddingElmArray
1766 // clean up IE's mess
1768 window.attachEvent("onunload", function() {
1773 glow.ready(function() {
1775 docElm = doc.documentElement;
1779 // test for nodePropertiesCloned
1781 var div = doc.createElement("div");
1783 nodePropertiesCloned = !!div.cloneNode(true).a;
1787 @name glow.dom-getFirstChildElm
1789 @description Returns the first leaf of a NodeList
1792 function getFirstChildElm(parent) {
1793 for (var child = parent.firstChild; child; child = child.nextSibling) {
1794 if (child.nodeType == 1) {
1802 PrivateMethod: removeClassRegex
1803 Get a regex that can be used to remove a class from a space separated list of classes.
1806 name - (string) the name of the class.
1811 function removeClassRegex (name) {
1812 return new RegExp(["(^|\\s)", name.replace(regexEscape, "\\$1"), "($|\\s)"].join(""), "g");
1817 PrivateMethod: stringToNodes
1818 Creates an array of nodes from a string
1820 function stringToNodes(str) {
1822 tagName = (/^\s*<([^\s>]+)/.exec(str) || [,'div'])[1],
1823 // This matches str content with potential elements that cannot
1824 // be a child of <div>. elmFilter declared at top of page.
1825 elmWrap = elmWraps[tagName] || emptyArray,
1830 // Create the new element using the node tree contents available in filteredElm.
1831 tmpDiv.innerHTML = (elmWrap[1] + str + elmWrap[2]);
1835 // Strip newElement down to just the required element and its parent
1836 nodeDepth = elmWrap[0];
1837 while(nodeDepth--) {
1838 childElm = childElm.lastChild;
1841 // pull nodes out of child
1842 while (childElm.firstChild) {
1843 r[rLen++] = childElm.removeChild(childElm.firstChild);
1852 PrivateMethod: nodelistToArray
1853 Converts a w3 NodeList to an array
1855 function nodelistToArray(nodelist) {
1857 for (; nodelist[i]; i++) {
1864 PrivateMethod: setAttribute
1865 Sets the attribute in the nodelist using the supplied function.
1868 value - (String|Function) the value/value generator.
1869 attributeSetter - (Function) a function that can be called to actually set the attribute.
1872 The <NodeList> object.
1874 // is marginal having this separated out as it is only used twice and call is almost as big
1875 // leaving it separate for now for once and only once, as well as in case attr does some more mutating stuff
1876 // could be merged back with attr later
1877 function setAttribute (value, attributeSetter) {
1878 for (var that = this, i = 0, length = that.length; i < length; i++) {
1879 attributeSetter.call(
1882 value.call(that[i], i) :
1891 PrivateMethod: append
1892 append the nodes in "b" to the array / list "a"
1894 //return different function for IE & Opera to deal with their stupid bloody expandos. Pah.
1896 append = function(a, b) {
1900 if (typeof b.length == "number") {
1901 for (; i < length; i++) {
1911 append = function(a, b) {
1912 var i = 0, ai = a.length;
1920 PrivateMethod: isXml
1921 Is this node an XML Document node or within an XML Document node
1929 function isXml(node) {
1930 //test for nodes within xml element
1931 return (node.ownerDocument && !node.ownerDocument.body) ||
1932 //test for xml document elements
1933 (node.documentElement && !node.documentElement.body);
1937 PrivateMethod: unique
1938 Get an array of nodes without duplicate nodes from an array of nodes.
1941 aNodes - (Array|<NodeList>)
1944 An array of nodes without duplicates.
1946 //worth checking if it's an XML document?
1948 unique = function(aNodes) {
1949 if (aNodes.length == 1) { return aNodes; }
1956 for (; aNodes[i]; i++) {
1957 if (aNodes[i].getAttribute(ucheckPropName) != ucheck && aNodes[i].nodeType == 1) {
1958 r[ri++] = aNodes[i];
1960 aNodes[i].setAttribute(ucheckPropName, ucheck);
1962 for (i=0; aNodes[i]; i++) {
1963 aNodes[i].removeAttribute(ucheckPropName);
1969 unique = function(aNodes) {
1970 if (aNodes.length == 1) { return aNodes; }
1977 for (; aNodes[i]; i++) {
1978 if (aNodes[i][ucheckPropName] != ucheck && aNodes[i].nodeType == 1) {
1979 r[ri++] = aNodes[i];
1981 aNodes[i][ucheckPropName] = ucheck;
1989 PrivateMethod: getElementsByTag
1990 Get elements by a specified tag name from a set of context objects. If multiple
1991 context objects are passed, then the resulting array may contain duplicates. See
1992 <unique> to remove duplicate nodes.
1995 tag - (string) Tag name. "*" for all.
1996 contexts - (array) DOM Documents and/or DOM Elements to search in.
1999 An array(like) collection of elements with the specified tag name.
2001 if (document.all) { //go the long way around for IE (and Opera)
2002 getByTagName = function(tag, context) {
2004 for (; context[i]; i++) {
2005 //need to check .all incase data is XML
2007 if (tag == "*" && context[i].all && !isXml(context[i])) { // IE 5.5 doesn't support getElementsByTagName("*")
2008 append(r, context[i].all);
2010 append(r, context[i].getElementsByTagName(tag));
2016 getByTagName = function(tag, context) {
2017 var r = [], i = 0, len = context.length;
2018 for (; i < len; i++) {
2019 append(r, context[i].getElementsByTagName(tag));
2026 Get the child elements for an html node
2028 function getChildElms(node) {
2030 childNodes = node.childNodes,
2034 for (; childNodes[i]; i++) {
2035 if (childNodes[i].nodeType == 1 && childNodes[i].nodeName != "!") {
2036 r[ri++] = childNodes[i];
2043 var horizontalBorderPadding = [
2044 'border-left-width',
2045 'border-right-width',
2049 verticalBorderPadding = [
2051 'border-bottom-width',
2057 PrivateMethod: getElmDimension
2058 Gets the size of an element as an integer, not including padding or border
2060 function getElmDimension(elm, cssProp /* (width|height) */) {
2061 var r, // val to return
2062 docElmOrBody = env.standardsMode ? docElm : docBody,
2063 isWidth = (cssProp == "width"),
2064 cssPropCaps = isWidth ? "Width" : "Height",
2067 if (elm.window) { // is window
2068 r = env.webkit < 522.11 ? (isWidth ? elm.innerWidth : elm.innerHeight) :
2069 env.webkit ? (isWidth ? docBody.clientWidth : elm.innerHeight) :
2070 env.opera < 9.5 ? (isWidth ? docBody.clientWidth : docBody.clientHeight) :
2071 /* else */ (isWidth ? docElmOrBody.clientWidth : docElmOrBody.clientHeight);
2074 else if (elm.getElementById) { // is document
2075 // we previously checked offsetWidth & clientWidth here
2076 // but they returned values too large in IE6 scrollWidth seems enough
2078 docBody["scroll" + cssPropCaps],
2079 docElm["scroll" + cssPropCaps]
2083 // get an array of css borders & padding
2084 cssBorderPadding = isWidth ? horizontalBorderPadding : verticalBorderPadding;
2085 r = elm['offset' + cssPropCaps] - parseInt( getCssValue(elm, cssBorderPadding) );
2091 PrivateMethod: getBodyElm
2092 Gets the body elm for a given element. Gets around IE5.5 nonsense. do getBodyElm(elm).parentNode to get documentElement
2094 function getBodyElm(elm) {
2096 return elm.document.body;
2098 return elm.ownerDocument.body;
2103 PrivateMethod: setElmsSize
2107 elms - (<NodeList>) Elements
2108 val - (Mixed) Set element height / width. In px unless stated
2109 type - (String) "height" or "width"
2114 function setElmsSize(elms, val, type) {
2115 if (typeof val == "number" || /\d$/.test(val)) {
2118 for (var i = 0, len = elms.length; i < len; i++) {
2119 elms[i].style[type] = val;
2124 PrivateMethod: toStyleProp
2125 Converts a css property name into its javascript name, such as "background-color" to "backgroundColor".
2128 prop - (String) CSS Property name
2131 String, javascript style property name
2133 function toStyleProp(prop) {
2134 if (prop == "float") {
2135 return env.ie ? "styleFloat" : "cssFloat";
2137 return lang.replace(prop, /-(\w)/g, function(match, p1) {
2138 return p1.toUpperCase();
2143 PrivateMethod: tempBlock
2144 Gives an element display:block (but keeps it hidden) and runs a function, then sets the element back how it was
2148 func - function to run
2151 Return value of the function
2153 function tempBlock(elm, func) {
2154 //TODO: rather than recording individual style properties, just cache cssText? This was faster for getting the element size
2156 elmStyle = elm.style,
2157 oldDisp = elmStyle.display,
2158 oldVis = elmStyle.visibility,
2159 oldPos = elmStyle.position;
2161 elmStyle.visibility = "hidden";
2162 elmStyle.position = "absolute";
2163 elmStyle.display = "block";
2164 if (!isVisible(elm)) {
2165 elmStyle.position = oldPos;
2166 r = tempBlock(elm.parentNode, func);
2167 elmStyle.display = oldDisp;
2168 elmStyle.visibility = oldVis;
2171 elmStyle.display = oldDisp;
2172 elmStyle.position = oldPos;
2173 elmStyle.visibility = oldVis;
2179 PrivateMethod: isVisible
2180 Is the element visible?
2182 function isVisible(elm) {
2183 //this is a bit of a guess, if there's a better way to do this I'm interested!
2184 return elm.offsetWidth ||
2189 PrivateMethod: getCssValue
2190 Get a computed css property
2194 prop - css property or array of properties to add together
2199 function getCssValue(elm, prop) {
2200 var r, //return value
2203 propLen = prop.length,
2204 compStyle = doc.defaultView && (doc.defaultView.getComputedStyle(elm, null) || doc.defaultView.getComputedStyle),
2205 elmCurrentStyle = elm.currentStyle,
2208 propTest = prop.push || cssPropRegex.exec(prop) || [];
2211 if (prop.push) { //multiple properties, add them up
2212 for (; i < propLen; i++) {
2213 total += parseInt( getCssValue(elm, prop[i]), 10 ) || 0;
2215 return total + "px";
2218 if (propTest[1]) { // is width / height
2219 if (!isVisible(elm)) { //element may be display: none
2220 return tempBlock(elm, function() {
2221 return getElmDimension(elm, propTest[1]) + "px";
2224 return getElmDimension(elm, propTest[1]) + "px";
2226 else if (propTest[2] //is border-*-width
2228 && getCssValue(elm, "border-" + propTest[3] + "-style") == "none"
2232 else if (compStyle) { //W3 Method
2233 //this returns computed values
2234 if (typeof compStyle == "function") {
2235 //safari returns null for compStyle when element is display:none
2237 oldDisplay = elm.style.display;
2238 r = tempBlock(elm, function() {
2239 if (prop == "display") { //get true value for display, since we've just fudged it
2240 elm.style.display = oldDisplay;
2241 if (!doc.defaultView.getComputedStyle(elm, null)) {
2244 elm.style.display = "block";
2246 return getCssValue(elm, prop);
2249 // assume equal horizontal margins in safari 3
2250 // http://bugs.webkit.org/show_bug.cgi?id=13343
2251 // The above bug doesn't appear to be closed, but it works fine in Safari 4
2252 if (env.webkit > 500 && env.webkit < 526 && prop == 'margin-right' && compStyle.getPropertyValue('position') != 'absolute') {
2253 prop = 'margin-left';
2255 r = compStyle.getPropertyValue(prop);
2257 } else if (elmCurrentStyle) { //IE method
2258 if (prop == "opacity") {
2259 match = /alpha\(opacity=([^\)]+)\)/.exec(elmCurrentStyle.filter);
2260 return match ? String(parseInt(match[1], 10) / 100) : "1";
2262 //this returns cascaded values so needs fixing
2263 r = String(elmCurrentStyle[toStyleProp(prop)]);
2264 if (/^-?[\d\.]+(?!px)[%a-z]+$/i.test(r) && prop != "font-size") {
2265 r = getPixelValue(elm, r, usesYAxis.test(prop)) + "px";
2268 //some results need post processing
2269 if (prop.indexOf("color") != -1) { //deal with colour values
2270 r = normaliseCssColor(r).toString();
2271 } else if (r.indexOf("url") == 0) { //some browsers put quotes around the url, get rid
2272 r = r.replace(/\"/g,"");
2278 PrivateMethod: getPixelValue
2279 Converts a relative value into an absolute pixel value. Only works in IE with Dimension value (not stuff like relative font-size).
2280 Based on some Dean Edwards' code
2283 element - element used to calculate relative values
2284 value - (string) relative value
2285 useYAxis - (string) calulate relative values to the y axis rather than x
2290 function getPixelValue(element, value, useYAxis) {
2291 // Remember the original values
2292 var axisPos = useYAxis ? "top" : "left",
2293 axisPosUpper = useYAxis ? "Top" : "Left",
2294 elmStyle = element.style,
2295 positionVal = elmStyle[axisPos],
2296 runtimePositionVal = element.runtimeStyle[axisPos],
2299 // copy to the runtime type to prevent changes to the display
2300 element.runtimeStyle[axisPos] = element.currentStyle[axisPos];
2301 // set value to left / top
2302 elmStyle[axisPos] = value;
2303 // get the pixel value
2304 r = elmStyle["pixel" + axisPosUpper];
2307 elmStyle[axisPos] = positionVal;
2308 element.runtimeStyle[axisPos] = runtimePositionVal;
2314 PrivateMethod: normaliseCssColor
2315 Converts a CSS colour into "rgb(255, 255, 255)" or "transparent" format
2318 function normaliseCssColor(val) {
2319 if (/^(transparent|rgba\(0, ?0, ?0, ?0\))$/.test(val)) { return 'transparent'; }
2320 var match, //tmp regex match holder
2321 r, g, b, //final colour vals
2322 hex, //tmp hex holder
2323 mathRound = Math.round,
2324 parseIntFunc = parseInt,
2325 parseFloatFunc = parseFloat;
2327 if (match = colorRegex.exec(val)) { //rgb() format, cater for percentages
2328 r = match[2] ? mathRound(((parseFloatFunc(match[1]) / 100) * 255)) : parseIntFunc(match[1]);
2329 g = match[4] ? mathRound(((parseFloatFunc(match[3]) / 100) * 255)) : parseIntFunc(match[3]);
2330 b = match[6] ? mathRound(((parseFloatFunc(match[5]) / 100) * 255)) : parseIntFunc(match[5]);
2332 if (typeof val == "number") {
2334 } else if (val.charAt(0) == "#") {
2335 if (val.length == "4") { //deal with #fff shortcut
2336 val = "#" + val.charAt(1) + val.charAt(1) + val.charAt(2) + val.charAt(2) + val.charAt(3) + val.charAt(3);
2338 hex = parseIntFunc(val.slice(1), 16);
2340 hex = htmlColorNames[val];
2344 g = (hex & 0x00ff00) >> 8;
2345 b = (hex & 0x0000ff);
2348 val = new String("rgb(" + r + ", " + g + ", " + b + ")");
2356 PrivateMethod: getTextNodeConcat
2357 Take an element and returns a string of its text nodes combined. This is useful when a browser (Safari 2) goes mad and doesn't return anything for innerText or textContent
2359 function getTextNodeConcat(elm) {
2361 nodes = elm.childNodes,
2364 for (; i < len; i++) {
2365 if (nodes[i].nodeType == 3) { //text
2366 //this function is used in safari only, string concatination is faster
2367 r += nodes[i].nodeValue;
2368 } else if (nodes[i].nodeType == 1) { //element
2369 r += getTextNodeConcat(nodes[i]);
2376 PrivateMethod: getNextOrPrev
2377 This gets the next / previous sibling element of each node in a nodeset
2378 and returns the new nodeset.
2380 function getNextOrPrev(nodelist, dir /* "next" or "previous" */) {
2385 length = nodelist.length;
2387 for (; i < length; i++) {
2388 nextTmp = nodelist[i];
2389 while (nextTmp = nextTmp[dir + "Sibling"]) {
2390 if (nextTmp.nodeType == 1 && nextTmp.nodeName != "!") {
2391 ret[ri++] = nextTmp;
2400 Get the 'real' positioned parent for an element, otherwise return null.
2402 function getPositionedParent(elm) {
2403 var offsetParent = elm.offsetParent;
2405 // get the real positioned parent
2406 // IE places elements with hasLayout in the offsetParent chain even if they're position:static
2407 // Also, <body> and <html> can appear in the offsetParent chain, but we don't want to return them if they're position:static
2408 while (offsetParent && r.get(offsetParent).css("position") == "static") {
2409 offsetParent = offsetParent.offsetParent;
2412 // sometimes the <html> element doesn't appear in the offsetParent chain, even if it has position:relative
2413 if (!offsetParent && r.get(docElm).css("position") != "static") {
2414 offsetParent = docElm;
2417 return offsetParent || null;
2421 @name glow.dom-getScrollOffset
2423 @description Get the scrollTop / scrollLeft of a particular element
2424 @param {Element} elm Element (or window object) to get the scroll position of
2425 @param {Boolean} isLeft True if we're dealing with left scrolling, otherwise top
2427 function getScrollOffset(elm, isLeft) {
2429 scrollProp = 'scroll' + (isLeft ? "Left" : "Top");
2431 // are we dealing with the window object or the document object?
2433 // get the scroll of the documentElement or the pageX/Yoffset
2434 // - some browsers use one but not the other
2435 r = elm.document.documentElement[scrollProp]
2436 || (isLeft ? elm.pageXOffset : elm.pageYOffset)
2440 r = elm[scrollProp];
2446 @name glow.dom-setScrollOffset
2448 @description Set the scrollTop / scrollLeft of a particular element
2449 @param {Element} elm Element (or window object) to get the scroll position of
2450 @param {Boolean} isLeft True if we're dealing with left scrolling, otherwise top
2451 @param {Number} newVal New scroll value
2453 function setScrollOffset(elm, isLeft, newVal) {
2455 // are we dealing with the window object or the document object?
2457 // we need to get whichever value we're not setting
2459 isLeft ? newVal : getScrollOffset(elm, true),
2460 !isLeft ? newVal : getScrollOffset(elm, false)
2464 elm['scroll' + (isLeft ? "Left" : "Top")] = newVal;
2469 @name glow.dom-scrollOffset
2471 @description Set/get the scrollTop / scrollLeft of a NodeList
2472 @param {glow.dom.NodeList} nodeList Elements to get / set the position of
2473 @param {Boolean} isLeft True if we're dealing with left scrolling, otherwise top
2474 @param {Number} [val] Val to set (if not provided, we'll get the value)
2476 @returns NodeList for sets, Number for gets
2478 function scrollOffset(nodeList, isLeft, val) {
2479 var i = nodeList.length;
2481 if (val !== undefined) {
2483 setScrollOffset(nodeList[i], isLeft, val);
2487 return getScrollOffset(nodeList[0], isLeft);
2492 var r = {}; //object to be returned
2497 @description Returns a {@link glow.dom.NodeList NodeList} from CSS selectors and/or Elements.
2499 @param {String | String[] | Element | Element[] | glow.dom.NodeList} nodespec+ One or more CSS selector strings, Elements or {@link glow.dom.NodeList NodeLists}.
2501 Will also accept arrays of these types, or any combinations thereof.
2503 Supported CSS selectors:
2506 <li>Universal selector "*".</li>
2507 <li>Type selector "div"</li>
2508 <li>Class selector ".myClass"</li>
2509 <li>ID selector "#myDiv"</li>
2510 <li>Child selector "ul > li"</li>
2511 <li>Grouping "div, p"</li>
2514 @returns {glow.dom.NodeList}
2517 // Nodelist with all links in element with id "nav"
2518 var myNodeList = glow.dom.get("#nav a");
2521 // NodeList containing the nodes passed in
2522 var myNodeList = glow.dom.get(someNode, anotherNode);
2525 // NodeList containing elements in the first form
2526 var myNodeList = glow.dom.get(document.forms[0].elements);
2528 r.get = function() {
2529 var r = new glow.dom.NodeList(),
2532 argsLen = args.length;
2534 for (; i < argsLen; i++) {
2535 if (typeof args[i] == "string") {
2536 r.push(new glow.dom.NodeList().push(doc).get(args[i]));
2545 @name glow.dom.create
2547 @description Returns a {@link glow.dom.NodeList NodeList} from an HTML fragment.
2549 @param {String} html An HTML string.
2551 All top-level nodes must be elements (i.e. text content in the
2552 HTML must be wrapped in HTML tags).
2554 @param {Object} [opts] An optional options object
2555 @param {Object} [opts.interpolate] Data for a call to {@link glow.lang.interpolate}
2556 If this option is set, the String html parameter will be passed through glow.lang.interpolate with this as the data and no options
2557 If glow.lang.interpolates options are required, an explicit call must be made
2558 @param {Boolean} [opts.escapeHtml] Escape HTML in the interpolate data object.
2559 See {@link glow.lang.interpolate}
2561 @returns {glow.dom.NodeList}
2564 // NodeList of two elements
2565 var myNodeList = glow.dom.create("<div>Hello</div><div>World</div>");
2568 // Nodelist of one list item
2569 var listItem = glow.dom.create('<li>{content}</li>', {
2570 interpolate: {content: textFromUser},
2573 // if testFromUser contains HTML, it will be correctly escaped
2574 // before being inserted into the li
2576 r.create = function(sHtml, opts) {
2582 // set default options
2583 opts = glow.lang.apply({
2588 if (opts.interpolate) {
2589 sHtml = lang.interpolate(sHtml, opts.interpolate, {
2590 escapeHtml: opts.escapeHtml
2594 toCheck = stringToNodes(sHtml);
2596 for (; toCheck[i]; i++) {
2597 if (toCheck[i].nodeType == 1 && toCheck[i].nodeName != "!") {
2598 ret[rLen++] = toCheck[i];
2599 } else if (toCheck[i].nodeType == 3 && lang.trim(toCheck[i].nodeValue) !== "") {
2600 throw new Error("glow.dom.create - Text must be wrapped in an element");
2603 return new r.NodeList().push(ret);
2607 @name glow.dom.parseCssColor
2609 @description Returns an object representing a CSS colour string.
2611 @param {String} color A CSS colour.
2613 Examples of valid values are "red", "#f00", "#ff0000",
2614 "rgb(255,0,0)", "rgb(100%, 0%, 0%)"
2618 An object with properties named "r", "g" and "b", each will have
2619 an integer value between 0 and 255.
2622 glow.dom.parseCssColor("#ff0000");
2623 // returns {r:255, g:0, b:0}
2625 r.parseCssColor = function(cssColor) {
2626 var normal = normaliseCssColor(cssColor);
2627 return {r: normal.r, g: normal.g, b: normal.b};
2631 @name glow.dom.NodeList
2633 @description An array-like collection of DOM Elements.
2635 It is recommended you create a NodeList using {@link glow.dom.get glow.dom.get},
2636 or {@link glow.dom.create glow.dom.create}, but you can also use
2637 the constructor to create an empty NodeList.
2639 Unless otherwise specified, all methods of NodeList return the
2640 NodeList itself, allowing you to chain methods calls together.
2644 var nodes = new glow.dom.NodeList();
2647 // using get to return a NodeList then chaining methods
2648 glow.dom.get("p").addClass("eg").append("<b>Hello!</b>");
2650 @see <a href="../furtherinfo/creatingnodelists/">Creating NodeLists</a>
2651 @see <a href="../furtherinfo/workingwithnodelists/">Working with NodeLists</a>
2652 @see <a href="../furtherinfo/xmlnodelists/">XML NodeLists</a>
2654 r.NodeList = function() {
2657 @name glow.dom.NodeList#length
2659 @description Number of nodes in the NodeList
2661 // get the number of paragraphs on the page
2662 glow.dom.get("p").length;
2664 this.length = 0; //number of elements in NodeList
2670 r.NodeList.prototype = {
2673 @name glow.dom.NodeList#item
2675 @description Returns a node from the NodeList.
2677 @param {Number} index The numeric index of the node to return.
2682 // get the fourth node
2683 var node = myNodeList.item(3);
2686 // another way to get the fourth node
2687 var node = myNodeList[3];
2689 item: function (nIndex) {
2690 return this[nIndex];
2694 @name glow.dom.NodeList#push
2696 @description Adds Elements to the NodeList.
2698 @param {Element | Element[] | glow.dom.NodeList} nodespec+ One or more Elements, Arrays of Elements or NodeLists.
2700 @returns {glow.dom.NodeList}
2703 var myNodeList = glow.dom.create("<div>Hello world</div>");
2704 myNodeList.push("<div>Foo</div>", glow.dom.get("#myList li"));
2707 var args = arguments,
2708 argsLen = args.length,
2713 arrayPush = Array.prototype.push;
2715 for (; i < argsLen; i++) {
2718 } else if (args[i].nodeType == 1 || args[i].nodeType == 9 || args[i].document) { //is Node
2719 arrayPush.call(that, args[i]);
2720 } else if (args[i][0]) { //is array or array like
2721 for (n = 0, nNodeListLength = args[i].length; n < nNodeListLength; n++) {
2722 arrayPush.call(that, args[i][n]);
2730 @name glow.dom.NodeList#each
2732 @description Calls a function for each node.
2734 The supplied function will be called for each node in the NodeList.
2736 The index of the node will be provided as the first parameter, and
2737 'this' will refer to the node.
2739 @param {Function} callback The function to run for each node.
2741 @returns {glow.dom.NodeList}
2744 var myNodeList = glow.dom.get("a");
2745 myNodeList.each(function(i){
2746 // in this function: this == myNodeList[i]
2749 each: function (callback) {
2750 for (var i = 0, that = this, length = that.length; i < length; i++) {
2751 callback.call(that[i], i, that);
2757 @name glow.dom.NodeList#eq
2759 @description Compares the NodeList to an element, array of elements or another NodeList
2761 Returns true if the items are the same and are in the same
2764 @param {Element | Element[] | glow.dom.NodeList} nodespec The element, array or NodeList the NodeList is being compared to.
2769 // the following returns true
2770 glow.dom.get('#blah').eq( document.getElementById('blah') );
2772 eq: function (nodelist) {
2773 var that = this, i = 0, length = that.length;
2775 if (! nodelist.push) { nodelist = [nodelist]; }
2776 if (nodelist.length != that.length) { return false; }
2777 for (; i < length; i++) {
2778 if (that[i] != nodelist[i]) { return false; }
2784 @name glow.dom.NodeList#isWithin
2786 @description Tests if all the nodes are decendents of an element.
2788 @param {Element | glow.dom.NodeList} nodespec The element or NodeList that the NodeList is being tested against.
2790 If nodespec is a NodeList, then the its first node will be used.
2792 @returns {glow.dom.NodeList}
2795 var myNodeList = glow.dom.get("input");
2796 if (myNodeList.isWithin(glow.dom.get("form")) {
2800 isWithin: function (node) {
2801 if (node.push) { node = node[0]; }
2803 // missing some nodes? Return false
2804 if ( !node || !this.length ) { return false; }
2808 length = that.length,
2809 toTest; //node to test in manual method
2811 if (node.contains && env.webkit >= 521) { //proprietary method, safari 2 has a wonky implementation of this, avoid, avoid, avoid
2813 for (; i < length; i++) {
2814 // myNode.contains(myNode) returns true in most browsers
2815 if (!(node.contains(that[i]) && that[i] != node)) { return false; }
2817 } else if (that[0].compareDocumentPosition) { //w3 method
2819 for (; i < length; i++) {
2820 //compare against bitmask
2821 if (!(that[i].compareDocumentPosition(node) & 8)) { return false; }
2823 } else { //manual method
2824 for (; i < length; i++) {
2826 while (toTest = toTest.parentNode) {
2827 if (toTest == node) { break; }
2829 if (!toTest) { return false; }
2836 @name glow.dom.NodeList#attr
2838 @description Gets or sets attributes
2840 When getting an attribute, it is retrieved from the first
2841 node in the NodeList. Setting attributes applies the change
2842 to each element in the NodeList.
2844 To set an attribute, pass in the name as the first
2845 parameter and the value as a second parameter.
2847 To set multiple attributes in one call, pass in an object of
2848 name/value pairs as a single parameter.
2850 For browsers that don't support manipulating attributes
2851 using the DOM, this method will try to do the right thing
2852 (i.e. don't expect the semantics of this method to be
2853 consistent across browsers as this is not possible with
2854 currently supported browsers).
2856 @param {String | Object} name The name of the attribute, or an object of name/value pairs
2857 @param {String} [value] The value to set the attribute to.
2859 @returns {String | glow.dom.NodeList}
2861 When setting attributes it returns the NodeList, otherwise
2862 returns the attribute value.
2865 var myNodeList = glow.dom.get(".myImgClass");
2868 myNodeList.attr("class");
2871 myNodeList.attr("class", "anotherImgClass");
2873 // set multiple attributes
2876 alt: "Cat jumping through a field"
2879 attr: function (name /* , val */) {
2882 argsLen = args.length,
2886 if (that.length === 0) {
2887 return argsLen > 1 ? that : undefined;
2889 if (typeof name == 'object') {
2891 if (lang.hasOwnProperty(name, i)) {
2892 that.attr(i, name[i]);
2897 if (env.ie && dom0PropertyMapping[name]) {
2902 // in the callback this is the dom node
2903 function (val) { this[dom0PropertyMapping[name]] = val; }
2907 value = that[0][dom0PropertyMapping[name]];
2908 if (dom0BooleanAttribute[name]) {
2909 return value ? name : undefined;
2911 else if (dom0AttributeMappings[name]) {
2912 return dom0AttributeMappings[name](value);
2920 // in the callback this is the dom node
2921 function (val) { this.setAttribute(name, val); }
2925 //2nd parameter makes IE behave, but errors for XML nodes (and isn't needed for xml nodes)
2926 return isXml(that[0]) ? that[0].getAttribute(name) : that[0].getAttribute(name, 2);
2930 @name glow.dom.NodeList#removeAttr
2932 @description Removes an attribute from each node.
2934 @param {String} name The name of the attribute to remove.
2936 @returns {glow.dom.NodeList}
2939 glow.dom.get("a").removeAttr("target");
2941 removeAttr: function (name) {
2942 var mapping = env.ie && dom0PropertyMapping[name],
2945 length = that.length;
2947 for (; i < length; i++) {
2949 that[i][mapping] = "";
2951 that[i].removeAttribute(name);
2958 @name glow.dom.NodeList#hasAttr
2960 @description Does the node have a particular attribute?
2962 The first node in the NodeList is tested
2964 @param {String} name The name of the attribute to test for.
2969 if ( glow.dom.get("#myImg").hasAttr("alt") ){
2973 hasAttr: function (name) {
2974 var firstNode = this[0],
2975 attributes = firstNode.attributes;
2977 if (isXml(firstNode) && env.ie) { //getAttributeNode not supported for XML
2978 var attributes = firstNode.attributes,
2980 len = attributes.length;
2982 //named node map doesn't seem to work properly, so need to go by index
2983 for (; i < len; i++) {
2984 if (attributes[i].nodeName == name) {
2985 return attributes[i].specified;
2989 } else if (this[0].getAttributeNode) {
2990 var attr = this[0].getAttributeNode(name);
2991 return attr ? attr.specified : false;
2994 return typeof attributes[attr] != "undefined";
2998 @name glow.dom.NodeList#prop
3000 @description Gets or sets node peropties
3002 This function gets / sets node properties, to get attributes,
3003 see {@link glow.dom.NodeList#attr NodeList#attr}.
3005 When getting a property, it is retrieved from the first
3006 node in the NodeList. Setting properties to each element in
3009 To set multiple properties in one call, pass in an object of
3012 @param {String | Object} name The name of the property, or an object of name/value pairs
3013 @param {String} [value] The value to set the property to.
3015 @returns {String | glow.dom.NodeList}
3017 When setting properties it returns the NodeList, otherwise
3018 returns the property value.
3021 var myNodeList = glow.dom.get("#formElement");
3023 // get the node name
3024 myNodeList.prop("nodeName");
3027 myNodeList.prop("_secretValue", 10);
3029 // set multiple properties
3035 prop: function(name, val) {
3038 if (name.constructor === Object) {
3042 // loop through hash
3044 this.prop(key, hash[key]);
3049 // setting single (to all in the NodeList)
3050 if (val !== undefined) {
3051 var i = this.length;
3053 this[i][name] = val;
3059 if (!this[0]) { return undefined; }
3060 return this[0][name];
3064 @name glow.dom.NodeList#hasClass
3066 @description Tests if a node has a given class.
3068 Will return true if any node in the NodeList has the supplied class
3070 <p><em>This method is not applicable to XML NodeLists.</em></p>
3072 @param {String} name The name of the class to test for.
3077 if (glow.dom.get("a").hasClass("termsLink")){
3081 hasClass: function (name) {
3082 for (var i = 0, length = this.length; i < length; i++) {
3083 if ((" " + this[i].className + " ").indexOf(" " + name + " ") != -1) {
3091 @name glow.dom.NodeList#addClass
3093 @description Adds a class to each node.
3095 <p><em>This method is not applicable to XML NodeLists.</em></p>
3097 @param {String} name The name of the class to add.
3099 @returns {glow.dom.NodeList}
3102 glow.dom.get("#login a").addClass("highlight");
3104 addClass: function (name) {
3105 for (var i = 0, length = this.length; i < length; i++) {
3106 if ((" " + this[i].className + " ").indexOf(" " + name + " ") == -1) {
3107 this[i].className += ((this[i].className)? " " : "") + name;
3114 @name glow.dom.NodeList#removeClass
3116 @description Removes a class from each node.
3118 <p><em>This method is not applicable to XML NodeLists.</em></p>
3120 @param {String} name The name of the class to remove.
3122 @returns {glow.dom.NodeList}
3125 glow.dom.get("#footer #login a").removeClass("highlight");
3127 removeClass: function (name) {
3128 var re = removeClassRegex(name),
3131 length = that.length;
3133 for (; i < length; i++) {
3134 that[i].className = that[i].className.replace(re, " ");
3140 @name glow.dom.NodeList#toggleClass
3142 @description Toggles a class on each node.
3144 <p><em>This method is not applicable to XML NodeLists.</em></p>
3146 @param {String} name The name of the class to toggle.
3148 @returns {glow.dom.NodeList}
3151 glow.dom.get(".onOffSwitch").toggleClass("on");
3153 toggleClass: function (name) {
3154 var i = this.length,
3156 paddedName = " " + name + " ";
3159 paddedClassName = " " + this[i].className + " ";
3161 if (paddedClassName.indexOf(paddedName) != -1) {
3162 this[i].className = paddedClassName.replace(paddedName, " ");
3164 this[i].className += " " + name;
3171 @name glow.dom.NodeList#val
3173 @description Gets or sets form values for the first node.
3175 <p><em>This method is not applicable to XML NodeLists.</em></p>
3177 <p><em>Getting values from form elements</em></p>
3179 The returned value depends on the type of element, see below:
3182 <dt>Radio button or checkbox</dt>
3183 <dd>If checked, then the contents of the value attribute, otherwise an empty string.</dd>
3185 <dd>The contents of value attribute of the selected option</dd>
3186 <dt>Select (multiple)</dt>
3187 <dd>An array of selected option values.</dd>
3188 <dt>Other form element</dt>
3189 <dd>The value of the input.</dd>
3192 <p><em>Getting values from a form</em></p>
3194 If the first element in the NodeList is a form, then an
3195 object is returned containing the form data. Each item
3196 property of the object is a value as above, apart from when
3197 multiple elements of the same name exist, in which case the
3198 it will contain an array of values.
3200 <p><em>Setting values for form elements</em></p>
3202 If a value is passed and the first element of the NodeList
3203 is a form element, then the form element is given that value.
3204 For select elements, this means that the first option that
3205 matches the value will be selected. For selects that allow
3206 multiple selection, the options which have a value that
3207 exists in the array of values/match the value will be
3208 selected and others will be deselected.
3210 Currently checkboxes and radio buttons are not checked or
3211 unchecked, just their value is changed. This does mean that
3212 this does not do exactly the reverse of getting the value
3213 from the element (see above) and as such may be subject to
3216 <p><em>Setting values for forms</em></p>
3218 If the first element in the NodeList is a form and the
3219 value is an object, then each element of the form has its
3220 value set to the corresponding property of the object, using
3221 the method described above.
3223 @param {String | Object} [value] The value to set the form element/elements to.
3225 @returns {glow.dom.NodeList | String | Object}
3227 When used to set a value it returns the NodeList, otherwise
3228 returns the value as described above.
3232 var username = glow.dom.get("input#username").val();
3235 // get values from a form
3236 var userDetails = glow.dom.get("form").val();
3240 glow.dom.get("input#username").val("example username");
3243 // set values in a form
3244 glow.dom.get("form").val({
3245 username : "another",
3252 PrivateFunction: elementValue
3253 Get a value for a form element.
3255 function elementValue (el) {
3256 var elType = el.type,
3257 elChecked = el.checked,
3262 if (elType == "radio") {
3263 return elChecked ? elValue : "";
3264 } else if (elType == "checkbox") {
3265 return elChecked ? elValue : "";
3267 } else if (elType == "select-one") {
3268 return el.selectedIndex > -1 ?
3269 el.options[el.selectedIndex].value : "";
3271 } else if (elType == "select-multiple") {
3272 for (var length = el.options.length; i < length; i++) {
3273 if (el.options[i].selected) {
3274 vals[vals.length] = el.options[i].value;
3284 PrivateMethod: formValues
3285 Get an object containing form data.
3287 function formValues (form) {
3290 formElements = form.elements,
3292 length = formElements.length,
3299 for (; i < length; i++) {
3300 formElement = formElements[i];
3301 nodeName = formElement.nodeName.toLowerCase();
3302 name = formElement.name;
3304 // fieldsets & objects come back as form elements, but we don't care about these
3305 // we don't bother with fields that don't have a name
3306 // switch to whitelist?
3308 nodeName == "fieldset" ||
3309 nodeName == "object" ||
3313 if (formElement.type == "checkbox" && ! formElement.checked) {
3314 if (! name in vals) {
3315 vals[name] = undefined;
3317 } else if (formElement.type == "radio") {
3319 radios[name][radios[name].length] = formElement;
3321 radios[name] = [formElement];
3324 var value = elementValue(formElement);
3326 if (vals[name].push) {
3327 vals[name][vals[name].length] = value;
3329 vals[name] = [vals[name], value];
3338 for (length = radios[i].length; j < length; j++) {
3339 radio = radios[i][j];
3341 if (radio.checked) {
3342 vals[radio.name] = radio.value;
3346 if (! name in vals) { vals[name] = undefined; }
3352 PrivateFunction: setFormValues
3353 Set values of a form to those in passed in object.
3355 function setFormValues (form, vals) {
3356 var prop, currentField,
3358 storeType, i = 0, n, len, foundOne, currentFieldType;
3360 for (prop in vals) {
3361 currentField = form[prop];
3362 if (currentField && currentField[0] && !currentField.options) { // is array of fields
3363 //normalise values to array of vals
3364 vals[prop] = vals[prop] && vals[prop].push ? vals[prop] : [vals[prop]];
3365 //order the fields by types that matter
3367 fields.checkboxesSelects = [];
3368 fields.multiSelects = [];
3371 for (i = 0; currentField[i]; i++) {
3372 currentFieldType = currentField[i].type;
3373 if (currentFieldType == "radio") {
3374 storeType = "radios";
3375 } else if (currentFieldType == "select-one" || currentFieldType == "checkbox") {
3376 storeType = "checkboxesSelects";
3377 } else if (currentFieldType == "select-multiple") {
3378 storeType = "multiSelects";
3380 storeType = "other";
3382 //add it to the correct array
3383 fields[storeType][fields[storeType].length] = currentField[i];
3386 for (i = 0; fields.multiSelects[i]; i++) {
3387 vals[prop] = setValue(fields.multiSelects[i], vals[prop]);
3389 for (i = 0; fields.checkboxesSelects[i]; i++) {
3390 setValue(fields.checkboxesSelects[i], "");
3391 for (n = 0, len = vals[prop].length; n < len; n++) {
3392 if (setValue(fields.checkboxesSelects[i], vals[prop][n])) {
3393 vals[prop].slice(n, 1);
3398 for (i = 0; fields.radios[i]; i++) {
3399 fields.radios[i].checked = false;
3401 for (n = 0, len = vals[prop].length; n < len; n++) {
3402 if (setValue(fields.radios[i], vals[prop][n])) {
3403 vals[prop].slice(n, 1);
3407 if (foundOne) { break; }
3410 for (i = 0; fields.other[i] && vals[prop][i] !== undefined; i++) {
3411 setValue(fields.other[i], vals[prop][i]);
3413 } else if (currentField && currentField.nodeName) { // is single field, easy
3414 setValue(currentField, vals[prop]);
3420 PrivateFunction: setValue
3421 Set the value of a form element.
3424 values that weren't able to set if array of vals passed (for multi select). Otherwise true if val set, false if not
3426 function setValue (el, val) {
3434 if (el.type == "select-one") {
3435 for (length = el.options.length; i < length; i++) {
3436 if (el.options[i].value == val) {
3437 el.selectedIndex = i;
3442 } else if (el.type == "select-multiple") {
3443 var isArray = !!val.push;
3444 for (i = 0, length = el.options.length; i < length; i++) {
3445 elOption = el.options[i];
3446 optionVal = elOption.value;
3448 elOption.selected = false;
3449 for (nlen = val.length; n < nlen; n++) {
3450 if (optionVal == val[n]) {
3451 elOption.selected = true;
3457 return elOption.selected = val == optionVal;
3461 } else if (el.type == "radio" || el.type == "checkbox") {
3462 el.checked = val == el.value;
3463 return val == el.value;
3470 // toplevel implementation
3471 return function (/* [value] */) {
3472 var args = arguments,
3476 length = that.length;
3478 if (args.length === 0) {
3479 return that[0].nodeName == 'FORM' ?
3480 formValues(that[0]) :
3481 elementValue(that[0]);
3483 if (that[0].nodeName == 'FORM') {
3484 if (! typeof val == 'object') {
3485 throw 'value for FORM must be object';
3487 setFormValues(that[0], val);
3489 for (; i < length; i++) {
3490 setValue(that[i], val);
3498 @name glow.dom.NodeList#slice
3500 @description Extracts nodes from a NodeList and returns them as a new NodeList.
3502 @param {Number} start The NodeList index at which to begin extraction.
3504 If negative, this param specifies a position measured from
3505 the end of the NodeList
3507 @param {Number} [end] The NodeList index immediately after the end of the extraction.
3509 If not specified the extraction includes all nodes from the
3510 start to the end of the NodeList. A Negative end specifies
3511 an position measured from the end of the NodeList.
3513 @returns {glow.dom.NodeList}
3515 Returns a new NodeList containing the extracted nodes
3518 var myNodeList = glow.dom.create("<div></div><div></div>");
3519 myNodeList = myNodeList.slice(1, 2); // just second div
3521 slice: function (/* start, end */) {
3522 return new r.NodeList().push(Array.prototype.slice.apply(this, arguments));
3526 @name glow.dom.NodeList#sort
3528 @description Returns a new NodeList containing the same nodes in order.
3530 Sort order defaults to document order if no sort function is passed in.
3532 @param {Function} [func] Function to determine sort order
3534 This function will be passed 2 nodes (a, b). The function
3535 should return a number less than 0 to sort a lower than b
3536 and greater than 0 to sort a higher than b.
3538 @returns {glow.dom.NodeList}
3540 Returns a new NodeList containing the sorted nodes
3543 // heading elements in document order
3544 var headings = glow.dom.get("h1, h2, h3, h4, h5, h6").sort();
3546 //get links in alphabetical (well, lexicographical) order
3547 var links = glow.dom.get("a").sort(function(a, b) {
3548 return ((a.textContent || a.innerText) < (b.textContent || b.innerText)) ? -1 : 1;
3551 sort: function(func) {
3552 var that = this, i=0, aNodes;
3554 if (!that.length) { return that; }
3556 if (typeof that[0].sourceIndex == "number") {
3557 // sourceIndex is IE proprietary (but Opera supports)
3558 func = function(a, b) {
3559 return a.sourceIndex - b.sourceIndex;
3561 } else if (that[0].compareDocumentPosition) {
3563 func = function(a, b) {
3564 return 3 - (a.compareDocumentPosition(b) & 6);
3567 // js emulation of sourceIndex
3568 aNodes = getByTagName("*", [doc]);
3569 for (; aNodes[i]; i++) {
3570 aNodes[i]._sourceIndex = i;
3572 func = function(a, b) {
3573 return a._sourceIndex - b._sourceIndex;
3578 return r.get([].sort.call(that, func));
3582 @name glow.dom.NodeList#filter
3584 @description Filter the NodeList using a function
3586 The supplied function will be called for each node in the NodeList.
3588 The index of the node will be provided as the first parameter, and
3589 'this' will refer to the node.
3591 Return true to keep the node, or false to remove it.
3593 @param {Function} func Function to test each node
3595 @returns {glow.dom.NodeList}
3597 Returns a new NodeList containing the filtered nodes
3600 // return images with a width greater than 320
3601 glow.dom.get("img").filter(function (i) {
3602 return this.width > 320;
3605 filter: function(callback) {
3606 var ret = [], //result
3609 length = this.length;
3610 for (; i < length; i++) {
3611 if (callback.apply(this[i], [i])) {
3612 ret[ri++] = this[i];
3619 @name glow.dom.NodeList#children
3621 @description Gets the child elements of each node as a new NodeList.
3623 @returns {glow.dom.NodeList}
3625 Returns a new NodeList containing all the child nodes
3628 // get all list items
3629 var items = glow.dom.get("ul, ol").children();
3631 children: function() {
3636 length = this.length,
3639 for (; i < length; i++) {
3640 ret = ret.concat( getChildElms(this[i]) );
3646 @name glow.dom.NodeList#parent
3648 @description Gets the unique parent nodes of each node as a new NodeList.
3649 The given nodelist will always be placed in the first element with no child elements.
3651 @returns {glow.dom.NodeList}
3653 Returns a new NodeList containing the parent nodes, with
3657 // elements which contain links
3658 var parents = glow.dom.get("a").parent();
3660 parent: function() {
3664 length = this.length;
3666 for (; i < length; i++) {
3667 ret[ri++] = this[i].parentNode;
3670 return r.get(unique(ret));
3674 @name glow.dom.NodeList#ancestors
3676 @description Gets the unique ancestor nodes of each node as a new NodeList.
3678 @returns {glow.dom.NodeList}
3683 // get ancestor elements for anchor elements
3684 var ancestors = glow.dom.get("a").ancestors();
3686 ancestors: function() {
3690 length = this.length,
3693 while (i < length) {
3694 elm = this[i].parentNode;
3696 while (elm && elm.nodeType == 1) {
3698 elm = elm.parentNode;
3703 return r.get(unique(ret));
3708 @name glow.dom.NodeList#wrap
3710 @description Wraps the given NodeList with the specified element(s).
3712 The given NodeList items will always be placed in the first child node that contains no further element nodes.
3714 Each item in a given NodeList will be wrapped individually.
3716 @returns {glow.dom.NodeList}
3718 Returns the NodeList with new wrapper parents
3721 // wrap the given element
3722 glow.dom.get("p").wrap("<div></div>");
3723 // <div><p></p></div>
3725 wrap: function(wrapper) {
3726 var length = this.length,
3731 if (typeof wrapper == 'string') {
3732 wrappingNodes = r.create(wrapper);
3735 wrappingNodes = r.get(wrapper);
3738 for (i=0; i < length; i++) {
3739 parent = wrappingNodes[0];
3742 childElm = getFirstChildElm(parent);
3752 if (this[i].parentNode) {
3753 wrappingNodes.insertBefore(this[i]);
3755 // If wrapping multiple nodes, we need to take a clean copy of the wrapping nodes
3756 if (i != length-1) {
3757 wrappingNodes = wrappingNodes.clone();
3760 parent.appendChild(this[i]);
3768 @name glow.dom.NodeList#unwrap
3770 @description Removes the first unique parent for each item of a supplied NodeList
3772 @returns {glow.dom.NodeList}
3774 Returns the unwrapped NodeList
3777 // Before: <div><div><p></p></div></div>
3778 // unwrap the given element
3779 glow.dom.get("p").unwrap();
3780 // After: <div><p></p></div>
3782 unwrap: function() {
3784 nodesToRemove = this.parent(),
3785 length = nodesToRemove.length;
3787 for (i=0; i < length; i++) {
3788 toRemove = nodesToRemove.slice(i, i+1);
3789 // if the item we're removing has no new parent (i.e. is not in document), then we just remove the child and destroy the old parent
3790 if (!toRemove[0].parentNode){
3791 toRemove.children().remove();
3795 toRemove.children().insertBefore(toRemove);
3804 @name glow.dom.NodeList#next
3806 @description Gets the next sibling element for each node as a new NodeList.
3808 @returns {glow.dom.NodeList}
3810 A new NodeList containing the next sibling elements.
3813 // gets the element following #myLink (if there is one)
3814 var next = glow.dom.get("#myLink").next();
3817 return getNextOrPrev(this, "next");
3821 @name glow.dom.NodeList#prev
3823 @description Gets the previous sibling element for each node as a new NodeList.
3825 @returns {glow.dom.NodeList}
3827 A new NodeList containing the previous sibling elements.
3830 // gets the elements before #myLink (if there is one)
3831 var previous = glow.dom.get("#myLink").previous();
3834 return getNextOrPrev(this, "previous");
3838 @name glow.dom.NodeList#is
3840 @description Tests if all the nodes match a CSS selector.
3842 Jake: I'm deprecating this until we have time to make it faster (probably when we change our CSS selector engine)
3845 @param {String} selector A CSS selector string
3850 var bigHeadings = glow.dom.get("h1, h2, h3");
3853 if (bigHeadings.is("h1, h2, h3, h4, h5, h6")) ...
3856 if (bigHeadings.is("a")) ...
3858 is: function (selector) {
3859 // TODO - this implementation needs to be optimized
3860 var nodes = glow.dom.get(selector),
3867 for (; i < iLen; i++) {
3868 for (j = 0, jLen = nodes.length; j < jLen; j++) {
3869 if (this[i] == nodes[j]) {
3879 @name glow.dom.NodeList#text
3881 @description Gets the inner text of the first node, or set the inner text of all matched nodes.
3883 @param {String} [text] String to set as inner text of nodes
3885 @returns {glow.dom.NodeList | String}
3887 If the text argument is passed then the NodeList is
3888 returned, otherwise the text is returned.
3892 var div = glow.dom.create("<div></div>").text("Hello World!");
3895 var greeting = div.text();
3897 text: function (/* text */) {
3898 var args = arguments,
3901 length = that.length;
3903 if (args.length > 0) {
3904 for (; i < length; i++) {
3905 that[i].innerHTML = "";
3906 that[i].appendChild(doc.createTextNode(args[0]));
3910 //innerText (empty) and textContent (undefined) don't work in safari 2 for hidden elements
3911 return that[0].innerText || that[0].textContent == undefined ? getTextNodeConcat(that[0]) : that[0].textContent;
3915 @name glow.dom.NodeList#empty
3917 @description Removes the contents of all the nodes.
3919 @returns {glow.dom.NodeList}
3922 // remove the contents of all textareas
3923 glow.dom.get("textarea").empty();
3925 empty: function () {
3927 NOTE: I changed this to destroy all nodes within the parent, but
3928 seemed backwards incompatible with our own timetable demo
3929 so we best hadn't do it until Glow 2
3934 for (; i < len; i++) {
3935 while(this[i].firstChild) {
3936 this[i].removeChild(this[i].firstChild);
3943 @name glow.dom.NodeList#remove
3945 @description Removes each node from its parent node.
3946 If you no longer need the nodes, consider using
3947 {@link glow.dom.NodeList#destroy destroy}
3949 @returns {glow.dom.NodeList}
3952 // take all the links out of a document
3953 glow.dom.get("a").remove();
3955 remove: function () {
3956 for (var that = this, i = 0, length = that.length, parentNode; i < length; i++) {
3957 if (parentNode = that[i].parentNode) {
3958 parentNode.removeChild(that[i]);
3965 @name glow.dom.NodeList#destroy
3967 @description Removes each node from the DOM
3968 The node will actually be destroyed to free up memory
3970 @returns {glow.dom.NodeList} An empty NodeList
3973 // destroy all links in the document
3974 glow.dom.get("a").destroy();
3976 destroy: function () {
3977 // remove any data attached to this NodeList
3978 this.get("*").push(this).removeData();
3980 this.appendTo(tmpDiv);
3982 tmpDiv.innerHTML = "";
3983 // empty the nodelist
3985 Array.prototype.splice.call(this, 0, this.length);
3990 @name glow.dom.NodeList#clone
3992 @description Gets a new NodeList containing a clone of each node.
3994 @param {Boolean} [cloneListeners=false] Also clone any event listeners assigned using Glow
3996 @returns {glow.dom.NodeList}
3998 Returns a new NodeList containing clones of all the nodes in
4002 // get a copy of all heading elements
4003 var myClones = glow.dom.get("h1, h2, h3, h4, h5, h6").clone();
4006 // get a copy of all anchor elements with
4007 var myAnchors = glow.dom.get("a").clone(true);
4009 clone: function (cloneListeners) {
4014 eventIdProp = '__eventId' + glow.UID;
4017 ret[i] = this[i].cloneNode(true);
4020 // some browsers (ie) also clone node properties as attributes
4021 // we need to get rid of the eventId.
4022 allCloneElms = r.get( ret ).get("*").push( ret );
4023 if (nodePropertiesCloned && !isXml(ret[0])) {
4024 i = allCloneElms.length;
4026 allCloneElms[i][eventIdProp] = null;
4030 // copy data from base elements to clone elements
4031 allBaseElms = this.get("*").push( this );
4032 i = allCloneElms.length;
4034 allCloneElms[i].removeAttribute(dataPropName);
4035 glow.dom.get(allCloneElms[i]).data(
4036 glow.dom.get(allBaseElms[i]).data()
4040 // shall we clone events too?
4041 if (cloneListeners) {
4042 // check the stuff we need is hanging around, we don't want
4043 // glow.dom to be dependant on glow.events as it's a circular
4045 if ( !glow.events ) {
4046 throw "glow.events required to clone event listeners";
4049 glow.events._copyListeners(
4050 this.get("*").push( this ),
4051 allCloneElms || r.get( ret ).get("*").push( ret )
4059 @name glow.dom.NodeList#html
4061 @description Gets the HTML content of the first node, or set the HTML content of all nodes.
4063 <p><em>This method is not applicable to XML NodeLists.</em></p>
4065 @param {String} [html] String to set as inner HTML of nodes
4067 @returns {glow.dom.NodeList | String}
4069 If the html argument is passed, then the NodeList is
4070 returned, otherwise the inner HTML of the first node is returned.
4073 // get the html in #footer
4074 var footerContents = glow.dom.get("#footer").html();
4078 glow.dom.get("#footer").html("<strong>Hello World!</strong>");
4080 html: function (newHtml) {
4082 length = this.length;
4084 if (newHtml !== undefined) {
4085 // not setting innerHTML, doesn't work in IE for elements like <table>
4086 return this.empty().append(newHtml);
4088 return this[0] ? this[0].innerHTML : "";
4092 @name glow.dom.NodeList#width
4094 @description Gets the width of the first node in pixels or sets the width of all nodes
4096 <p><em>This method is not applicable to XML NodeLists.</em></p>
4098 Return value does not include the padding or border of the
4099 element in browsers supporting the correct box model.
4101 You can use this to easily get the width of the document or
4102 window, see example below.
4104 @param {Number} [width] New width in pixels.
4106 @returns {glow.dom.NodeList | Number}
4108 Width of first element in pixels, or NodeList when setting widths
4111 // get the width of #myDiv
4112 glow.dom.get("#myDiv").width();
4115 // set the width of list items in #myDiv to 200 pixels
4116 glow.dom.get("#myDiv li").width(200);
4119 // get the height of the document
4120 glow.dom.get(document).width()
4123 // get the height of the window
4124 glow.dom.get(window).width()
4126 width: function(width) {
4127 if (width == undefined) {
4128 return getElmDimension(this[0], "width");
4130 setElmsSize(this, width, "width");
4135 @name glow.dom.NodeList#height
4137 @description Gets the height of the first element in pixels or sets the height of all nodes
4139 <p><em>This method is not applicable to XML NodeLists.</em></p>
4141 Return value does not include the padding or border of the element in
4142 browsers supporting the correct box model.
4144 You can use this to easily get the height of the document or
4145 window, see example below.
4147 @param {Number} [height] New height in pixels.
4149 @returns {glow.dom.NodeList | Number}
4151 Height of first element in pixels, or NodeList when setting heights.
4154 // get the height of #myDiv
4155 glow.dom.get("#myDiv").height();
4158 // set the height of list items in #myDiv to 200 pixels
4159 glow.dom.get("#myDiv li").height(200);
4162 // get the height of the document
4163 glow.dom.get(document).height()
4166 // get the height of the window
4167 glow.dom.get(window).height()
4169 height: function(height) {
4170 if (height == undefined) {
4171 return getElmDimension(this[0], "height");
4173 setElmsSize(this, height, "height");
4178 @name glow.dom.NodeList#scrollLeft
4180 @description Gets/sets the number of pixels the element has scrolled horizontally
4182 Get the value by calling without arguments, set by providing a new
4185 To get/set the scroll position of the window, use this method on
4186 a nodelist containing the window object.
4188 @param {Number} [val] New left scroll position
4190 @returns {glow.dom.NodeList | Number}
4192 Current scrollLeft value, or NodeList when setting scroll position.
4195 // get the scroll left value of #myDiv
4196 var scrollPos = glow.dom.get("#myDiv").scrollLeft();
4197 // scrollPos is a number, eg: 45
4200 // set the scroll left value of #myDiv to 20
4201 glow.dom.get("#myDiv").scrollLeft(20);
4204 // get the scrollLeft of the window
4205 var scrollPos = glow.dom.get(window).scrollLeft();
4206 // scrollPos is a number, eg: 45
4208 scrollLeft: function(val) {
4209 return scrollOffset(this, true, val);
4213 @name glow.dom.NodeList#scrollTop
4215 @description Gets/sets the number of pixels the element has scrolled vertically
4217 Get the value by calling without arguments, set by providing a new
4220 To get/set the scroll position of the window, use this method on
4221 a nodelist containing the window object.
4223 @param {Number} [val] New top scroll position
4225 @returns {glow.dom.NodeList | Number}
4227 Current scrollTop value, or NodeList when setting scroll position.
4230 // get the scroll top value of #myDiv
4231 var scrollPos = glow.dom.get("#myDiv").scrollTop();
4232 // scrollPos is a number, eg: 45
4235 // set the scroll top value of #myDiv to 20
4236 glow.dom.get("#myDiv").scrollTop(20);
4239 // get the scrollTop of the window
4240 var scrollPos = glow.dom.get(window).scrollTop();
4241 // scrollPos is a number, eg: 45
4243 scrollTop: function(val) {
4244 return scrollOffset(this, false, val);
4248 @name glow.dom.NodeList#show
4250 @description Shows all hidden items in the NodeList.
4251 @returns {glow.dom.NodeList}
4253 // Show element with ID myDiv
4254 glow.dom.get("#myDiv").show();
4256 // Show all list items within #myDiv
4257 glow.dom.get("#myDiv li").show();
4264 for (; i < len; i++) {
4265 /* Create a NodeList for the current item */
4266 currItem = r.get(this[i]);
4267 itemStyle = currItem[0].style;
4268 if (currItem.css("display") == "none") {
4269 itemStyle.display = "";
4270 itemStyle.visibility = "visible";
4271 /* If display is still none, set to block */
4272 if (currItem.css("display") == "none") {
4273 itemStyle.display = "block";
4281 @name glow.dom.NodeList#hide
4283 @description Hides all items in the NodeList.
4284 @returns {glow.dom.NodeList}
4286 // Hides all list items within #myDiv
4287 glow.dom.get("#myDiv li").hide();
4290 return this.css("display", "none").css("visibility", "hidden");
4294 @name glow.dom.NodeList#css
4296 @description Gets a CSS property for the first node or sets a CSS property on all nodes.
4298 <p><em>This method is not applicable to XML NodeLists.</em></p>
4300 If a single property name is passed, the corresponding value
4301 from the first node will be returned.
4303 If a single property name is passed with a value, that value
4304 will be set on all nodes on the NodeList.
4306 If an array of properties name is passed with no value, the return
4307 value is the sum of those values from the first node in the NodeList.
4308 This can be useful for getting the total of left and right padding,
4311 Return values are strings. For instance, "height" will return
4312 "25px" for an element 25 pixels high. You may want to use
4313 parseInt to convert these values.
4315 Here are the compatible properties you can get, if you use one
4316 which isn't on this list the return value may differ between browsers.
4319 <dt>width</dt><dd>Returns pixel width, eg "25px". Can be used even if width has not been set with CSS.</dd>
4320 <dt>height</dt><dd>Returns pixel height, eg "25px". Can be used even if height has not been set with CSS.</dd>
4321 <dt>top, right, bottom, left</dt><dd>Pixel position relative to positioned parent, or original location if position:relative, eg "10px". These can be used even if position:static.</dd>
4322 <dt>padding-top</dt><dd>Pixel size of top padding, eg "5px"</dd>
4323 <dt>padding-right</dt><dd>Pixel size of right padding, eg "5px"</dd>
4324 <dt>padding-bottom</dt><dd>Pixel size of bottom padding, eg "5px"</dd>
4325 <dt>padding-left</dt><dd>Pixel size of left padding, eg "5px"</dd>
4326 <dt>margin-top</dt><dd>Pixel size of top margin, eg "5px"</dd>
4327 <dt>margin-right</dt><dd>Pixel size of right margin, eg "5px"</dd>
4328 <dt>margin-bottom</dt><dd>Pixel size of bottom margin, eg "5px"</dd>
4329 <dt>margin-left</dt><dd>Pixel size of left margin, eg "5px"</dd>
4330 <dt>border-top-width</dt><dd>Pixel size of top border, eg "5px"</dd>
4331 <dt>border-right-width</dt><dd>Pixel size of right border, eg "5px"</dd>
4332 <dt>border-bottom-width</dt><dd>Pixel size of bottom border, eg "5px"</dd>
4333 <dt>border-left-width</dt><dd>Pixel size of left border, eg "5px"</dd>
4334 <dt>border-*-style</dt><dd>eg "dotted"</dd>
4335 <dt>border-*-color</dt><dd>returns colour in format "rgb(255, 255, 255)", return value also has properties r, g & b to get individual values as integers</dd>
4336 <dt>color</dt><dd>returns colour in format "rgb(255, 255, 255)", return value also has properties r, g & b to get individual values as integers</dd>
4337 <dt>list-style-position</dt><dd>eg "outside"</dd>
4338 <dt>list-style-type</dt><dd>eg "square"</dd>
4339 <dt>list-style-image</dt><dd>Returns full image path, eg "url(http://www.bbc.co.uk/lifestyle/images/bullets/1.gif)" or "none"</dd>
4340 <dt>background-image</dt><dd>Returns full image path, eg "url(http://www.bbc.co.uk/lifestyle/images/bgs/1.gif)" or "none"</dd>
4341 <dt>background-attachment</dt><dd>eg "scroll"</dd>
4342 <dt>background-repeat</dt><dd>eg "repeat-x"</dd>
4343 <dt>direction</dt><dd>eg "ltr"</dd>
4344 <dt>font-style</dt><dd>eg "italic"</dd>
4345 <dt>font-variant</dt><dd>eg "small-caps"</dd>
4346 <dt>line-height</dt><dd>Pixel height, eg "30px". Note, Opera may return value with 2 decimal places, eg "30.00px"</dd>
4347 <dt>letter-spacing</dt><dd>Pixel spacing, eg "10px"</dd>
4348 <dt>text-align</dt><dd>eg "right"</dd>
4349 <dt>text-decoration</dt><dd>eg "underline"</dd>
4350 <dt>text-indent</dt><dd>Pixel indent, eg "10px"</dd>
4351 <dt>white-space</dt><dd>eg "nowrap"</dd>
4352 <dt>word-spacing</dt><dd>Pixel spacing, eg "5px"</dd>
4353 <dt>float</dt><dd>eg "left"</dd>
4354 <dt>clear</dt><dd>eg "right"</dd>
4355 <dt>opacity</dt><dd>Value between 0 & 1. In IE, this value comes from the filter property (/100)</dd>
4356 <dt>position</dt><dd>eg "relative"</dd>
4357 <dt>z-index</dt><dd>eg "32"</dd>
4358 <dt>display</dt><dd>eg "block"</dd>
4359 <dt>text-transform</dt><dd>eg "uppercase"</dd>
4362 The following return values that may be usable but have differences in browsers
4365 <dt>font-family</dt><dd>Some browsers return the font used ("Verdana"), others return the full list found in the declaration ("madeup, verdana, sans-serif")</dd>
4366 <dt>font-size</dt><dd>Returns size as pixels except in IE, which will return the value in the same units it was set in ("0.9em")</dd>
4367 <dt>font-weight</dt><dd>Returns named values in some browsers ("bold"), returns computed weight in others ("700")</dd>
4370 @param {String | String[] | Object} property The CSS property name, array of names to sum, or object of key-value pairs
4372 @param {String} [value] The value to apply
4374 @returns {glow.dom.NodeList | String}
4376 Returns the CSS value from the first node, or NodeList when setting values
4379 // get value from first node
4380 glow.dom.get("#myDiv").css("display");
4383 // set left padding to 10px on all nodes
4384 glow.dom.get("#myDiv").css("padding-left", "10px");
4387 // "30px", total of left & right padding
4388 glow.dom.get("#myDiv").css(["padding-left", "padding-right"]);
4391 // where appropriate, px is assumed when no unit is passed
4392 glow.dom.get("#myDiv").css("height", 300);
4395 // set multiple CSS values at once
4396 // NOTE: Property names containing a hyphen such as font-weight must be quoted
4397 glow.dom.get("#myDiv").css({
4398 "font-weight": "bold",
4403 css: function(prop, val) {
4408 originalProp = prop;
4410 if (prop.constructor === Object) { // set multiple values
4411 for (style in prop) {
4412 this.css(style, prop[style]);
4416 else if (val != undefined) { //set one CSS value
4417 prop = toStyleProp(prop);
4418 for (; i < len; i++) {
4419 thisStyle = that[i].style;
4421 if (typeof val == "number" && hasUnits.test(originalProp)) {
4422 val = val.toString() + "px";
4424 if (prop == "opacity" && env.ie) {
4425 //in IE the element needs hasLayout for opacity to work
4426 thisStyle.zoom = "1";
4428 thisStyle.filter = "";
4430 thisStyle.filter = "alpha(opacity=" + Math.round(Number(val, 10) * 100) + ")";
4433 thisStyle[prop] = val;
4437 } else { //getting stuff
4438 if (!len) { return; }
4439 return getCssValue(that[0], prop);
4444 @name glow.dom.NodeList#offset
4446 @description Gets the offset from the top left of the document.
4448 If the NodeList contains multiple items, the offset of the
4449 first item is returned.
4452 Returns an object with "top" & "left" properties in pixels
4455 glow.dom.get("#myDiv").offset().top
4457 offset: function () {
4458 // http://weblogs.asp.net/bleroy/archive/2008/01/29/getting-absolute-coordinates-from-a-dom-element.aspx - great bit of research, most bugfixes identified here (and also jquery trac)
4461 x: getScrollOffset(window, true),
4462 y: getScrollOffset(window, false)
4465 //this is simple(r) if we can use 'getBoundingClientRect'
4466 // Sorry but the sooper dooper simple(r) way is not accurate in Safari 4
4467 if (!glow.env.webkit && elm.getBoundingClientRect) {
4468 var rect = elm.getBoundingClientRect();
4472 getBoundingClientRect is realive to top left of
4473 the viewport, so we need to sort out scrolling offset
4477 IE adds the html element's border to the value. We can
4478 deduct this value using client(Top|Left). However, if
4479 the user has done html{border:0} clientTop will still
4480 report a 2px border in IE quirksmode so offset will be off by 2.
4481 Hopefully this is an edge case but we may have to revisit this
4486 left: rect.left //see above for docs on all this stuff
4490 } else { //damnit, let's go the long way around
4491 var top = elm.offsetTop,
4492 left = elm.offsetLeft,
4495 //does the parent chain contain a position:fixed element
4496 involvesFixedElement = false,
4497 offsetParentBeforeBody = elm;
4499 //add up all the offset positions
4500 while (elm = elm.offsetParent) {
4501 left += elm.offsetLeft;
4502 top += elm.offsetTop;
4504 //if css position is fixed, we need to add in the scroll offset too, catch it here
4505 if (getCssValue(elm, "position") == "fixed") {
4506 involvesFixedElement = true;
4509 //gecko & webkit (safari 3) don't add on the border for positioned items
4510 if (env.gecko || env.webkit > 500) {
4511 left += parseInt(getCssValue(elm, "border-left-width")) || 0;
4512 top += parseInt(getCssValue(elm, "border-top-width")) || 0;
4515 //we need the offset parent (before body) later
4516 if (elm.nodeName.toLowerCase() != "body") {
4517 offsetParentBeforeBody = elm;
4521 //deduct all the scroll offsets
4523 while ((elm = elm.parentNode) && (elm != docBody) && (elm != docElm)) {
4524 left -= elm.scrollLeft;
4525 top -= elm.scrollTop;
4528 //gecko doesn't add the border of contained elements to the offset (overflow!=visible)
4529 if (env.gecko && getCssValue(elm, "overflow") != "visible") {
4530 left += parseInt(getCssValue(elm, "border-left-width"));
4531 top += parseInt(getCssValue(elm, "border-top-width"));
4535 //if we found a fixed position element we need to add the scroll offsets
4536 if (involvesFixedElement) {
4537 left += docScrollPos.x;
4538 top += docScrollPos.y;
4542 // Webkit < 500 body's offset gets counted twice for absolutely-positioned elements (or if there's a fixed element)
4543 // Gecko - non-absolutely positioned elements that are direct children of body get the body offset counted twice
4545 (env.webkit < 500 && (involvesFixedElement || getCssValue(offsetParentBeforeBody, "position") == "absolute")) ||
4546 (env.gecko && getCssValue(offsetParentBeforeBody, "position") != "absolute")
4548 left -= docBody.offsetLeft;
4549 top -= docBody.offsetTop;
4552 return {left:left, top:top};
4557 @name glow.dom.NodeList#position
4559 @description Get the top & left position of an element relative to its positioned parent
4561 This is useful if you want to make a position:static element position:absolute
4562 and retain the original position of the element
4564 @returns {Object} An object with 'top' and 'left' number properties
4567 // get the top distance from the positioned parent
4568 glow.dom.get("#elm").position().top
4570 position: function() {
4571 var positionedParent = r.get( getPositionedParent(this[0]) ),
4572 hasPositionedParent = !!positionedParent[0],
4574 // element margins to deduct
4575 marginLeft = parseInt( this.css("margin-left") ) || 0,
4576 marginTop = parseInt( this.css("margin-top") ) || 0,
4578 // offset parent borders to deduct, set to zero if there's no positioned parent
4579 positionedParentBorderLeft = ( hasPositionedParent && parseInt( positionedParent.css("border-left-width") ) ) || 0,
4580 positionedParentBorderTop = ( hasPositionedParent && parseInt( positionedParent.css("border-top-width") ) ) || 0,
4583 elOffset = this.offset(),
4584 positionedParentOffset = hasPositionedParent ? positionedParent.offset() : {top: 0, left: 0};
4587 left: elOffset.left - positionedParentOffset.left - marginLeft - positionedParentBorderLeft,
4588 top: elOffset.top - positionedParentOffset.top - marginTop - positionedParentBorderTop
4593 @name glow.dom.NodeList#append
4595 @description Appends the given elements to each node.
4597 If there is more than one node in the NodeList, then the given elements
4598 are appended to the first node and clones are appended to the other
4601 <p><em>String nodespecs cannot be used with XML NodeLists</em></p>
4603 @param {String | Element | glow.dom.NodeList} nodespec HTML string, Element or NodeList to append to each node.
4605 @returns {glow.dom.NodeList}
4608 // ends every paragraph with '...'
4609 glow.dom.get('p').append(
4613 append: function (nodeSpec) {
4617 length = that.length,
4620 if (length == 0) { return that; }
4621 nodes = typeof nodeSpec == "string" ? nodelistToArray(stringToNodes(nodeSpec)) :
4622 nodeSpec.nodeType ? [nodeSpec] : nodelistToArray(nodeSpec);
4624 for (; nodes[j]; j++) {
4625 that[0].appendChild(nodes[j]);
4627 for (; i < length; i++) {
4628 for (j = 0; nodes[j]; j++) {
4629 that[i].appendChild(nodes[j].cloneNode(true));
4636 @name glow.dom.NodeList#prepend
4638 @description Prepends the given elements to each node.
4640 If there is more than one node in the NodeList, then the given elements
4641 are prepended to the first node and clones are prepended to the other
4644 <p><em>String nodespecs cannot be used with XML NodeLists</em></p>
4646 @param {String | Element | glow.dom.NodeList} nodespec HTML string, Element or NodeList to prepend to each node.
4648 @returns {glow.dom.NodeList}
4651 // prepends every paragraph with 'Paragraph: '
4652 glow.dom.get('p').prepend(
4653 '<span>Paragraph: </span>'
4656 prepend: function (nodeSpec) {
4660 length = that.length,
4664 if (length == 0) { return that; }
4666 nodes = typeof nodeSpec == "string" ? nodelistToArray(stringToNodes(nodeSpec)) :
4667 nodeSpec.nodeType ? [nodeSpec] : nodelistToArray(nodeSpec);
4669 first = that[0].firstChild;
4671 for (; nodes[j]; j++) {
4672 that[0].insertBefore(nodes[j], first);
4675 for (; i < length; i++) {
4676 first = that[i].firstChild;
4677 for (j = 0; nodes[j]; j++) {
4678 that[i].insertBefore(nodes[j].cloneNode(true), first);
4685 @name glow.dom.NodeList#appendTo
4687 @description Appends the NodeList to elements.
4689 If more than one element is given (i.e. if the nodespec argument
4690 is an array of nodes or a NodeList) the NodeList will be
4691 appended to the first element and clones to each subsequent
4694 @param {String | Element | glow.dom.NodeList} nodespec CSS selector string, Element or NodeList to append the NodeList to.
4696 @returns {glow.dom.NodeList}
4699 // appends '...' to every paragraph
4700 glow.dom.create('<span>...</span>').appendTo('p');
4702 appendTo: function (nodes) {
4703 if (! (nodes instanceof r.NodeList)) { nodes = r.get(nodes); }
4709 @name glow.dom.NodeList#prependTo
4711 @description Prepends the NodeList to elements.
4713 If more than one element is given (i.e. if the nodespec argument
4714 is an array of nodes or a NodeList) the NodeList will be
4715 prepended to the first element and clones to each subsequent
4718 @param {String | Element | glow.dom.NodeList} nodespec CSS selector string, Element or NodeList to prepend the NodeList to.
4720 @returns {glow.dom.NodeList}
4723 // prepends 'Paragraph: ' to every paragraph
4724 glow.dom.create('<span>Paragraph: </span>').prependTo('p');
4726 prependTo: function (nodes) {
4727 if (! (nodes instanceof r.NodeList)) { nodes = r.get(nodes); }
4728 nodes.prepend(this);
4733 @name glow.dom.NodeList#after
4735 @description Inserts elements after each node.
4737 If there is more than one node in the NodeList, the elements
4738 will be inserted after the first node and clones inserted
4739 after each subsequent node.
4741 @param {String | Element | glow.dom.NodeList} nodespec HTML string, Element or NodeList to insert after each node
4743 @returns {glow.dom.NodeList}
4746 // adds a paragraph after each heading
4747 glow.dom.get('h1, h2, h3').after('<p>...</p>');
4749 after: function (nodeSpec) {
4751 length = that.length,
4758 if (length == 0) { return that; }
4760 nodes = typeof nodeSpec == "string" ? r.create(nodeSpec) :
4761 nodeSpec instanceof r.NodeList ? nodeSpec :
4764 nodesLen = nodes.length;
4766 for (j = nodesLen - 1; j >= 0; j--) {
4767 that[0].parentNode.insertBefore(nodes[j], that[0].nextSibling);
4769 for (; i < length; i++) {
4770 cloned = nodes.clone();
4772 for (j = nodesLen - 1; j >= 0; j--) {
4773 that[i].parentNode.insertBefore(cloned[j], that[i].nextSibling);
4780 @name glow.dom.NodeList#before
4782 @description Inserts elements before each node.
4784 If there is more than one node in the NodeList, the elements
4785 will be inserted before the first node and clones inserted
4786 before each subsequent node.
4788 @param {String | Element | glow.dom.NodeList} nodespec HTML string, Element or NodeList to insert before each node
4790 @returns {glow.dom.NodeList}
4793 // adds a heading before each
4794 glow.dom.get('p').before('<h1>Paragraph:</h1>');
4796 before: function (nodeSpec) {
4798 length = that.length,
4805 if (length == 0) { return that; }
4807 nodes = typeof nodeSpec == "string" ? r.create(nodeSpec) :
4808 nodeSpec instanceof r.NodeList ? nodeSpec :
4811 nodesLen = nodes.length;
4813 for (; j < nodesLen; j++) {
4814 that[0].parentNode.insertBefore(nodes[j], that[0]);
4816 for (; i < length; i++) {
4817 cloned = nodes.clone();
4818 for (j = 0; j < nodesLen; j++) {
4819 that[i].parentNode.insertBefore(cloned[j], that[i]);
4826 @name glow.dom.NodeList#insertAfter
4828 @description Insert the NodeList after each of the given elements.
4830 If more than one element is passed in, the NodeList will be
4831 inserted after the first element and clones inserted after each
4834 @param {String | Element | glow.dom.NodeList} nodespec CSS selector string, Element or NodeList to insert the NodeList after
4836 @returns {glow.dom.NodeList}
4839 // adds a paragraph after each heading
4840 glow.dom.create('<p>HAI!</p>').insertAfter('h1, h2, h3');
4842 insertAfter: function (nodes) {
4843 if (! (nodes instanceof r.NodeList)) { nodes = r.get(nodes); }
4849 @name glow.dom.NodeList#insertBefore
4851 @description Insert the NodeList before each of the given elements.
4853 If more than one element is passed in, the NodeList will be
4854 inserted before the first element and clones inserted before each
4857 @param {String | Element | glow.dom.NodeList} nodespec CSS selector string, Element or NodeList to insert the NodeList before
4859 @returns {glow.dom.NodeList}
4862 // adds a heading before each paragraph
4863 glow.dom.create('<h1>Paragraph:</h1>').insertBefore('p');
4865 insertBefore: function (nodes) {
4866 if (! (nodes instanceof r.NodeList)) { nodes = r.get(nodes); }
4873 @name glow.dom.NodeList#replaceWith
4875 @description Replace nodes on the page with given elements.
4877 @param {glow.dom.NodeList | String} nodespec Elements to insert into the document.
4879 If more than one node is to be replaced then nodespec will be
4880 cloned for additional elements. If a string is provided it will
4881 be treated as HTML and converted into elements.
4883 @returns {glow.dom.NodeList}
4884 Returns a new NodeList containing the nodes which have been removed
4886 replaceWith: function (nodeSpec) {
4888 we need to put a placeholder in first in case the new stuff
4889 has the same ids as stuff being replaced. This causes issues
4890 in safari 2, the new element can't be picked up with getElementById
4892 if (env.webkit < 500) {
4893 this.after(placeholderElm).remove();
4894 r.get("u.glow-placeholder").after(nodeSpec).remove();
4896 this.after(nodeSpec).remove()
4902 @name glow.dom.NodeList#data
4904 @description Use this to safely attach arbitrary data to any DOM Element.
4906 This method is useful when you wish to avoid memory leaks that are possible when adding your own data directly to DOM Elements.
4908 When called with no arguments, will return glow's entire data store for the first item in the NodeList.
4910 Otherwise, when given a stringy key, will return the associated value from the first item in the NodeList.
4912 When given both a key and a value, will store that data on every item in the NodeList.
4914 Optionally you can pass in a single object composed of multiple key, value pairs.
4916 @param {String|Object} [key] The name of the value in glow's data store for the NodeList item.
4917 @param {Object} [val] The the value you wish to associate with the given key.
4918 @see glow.dom.NodeList#removeData
4921 glow.dom.get("p").data("tea", "milky");
4922 var colour = glow.dom.get("p").data("tea"); // milky
4923 @returns {Object} When setting a value this method can be chained, as in that case it will return itself.
4925 data: function (key, val) { /*debug*///console.log("data("+key+", "+val+")");
4926 if (typeof key === "object") { // setting many values
4927 for (var prop in key) { this.data(prop, key[prop]); }
4928 return this; // chainable with ({key: val}) signature
4933 // uses private class-scoped variables: dataCache, dataPropName, dataIndex
4935 switch (arguments.length) {
4936 case 0: // getting entire cache from first node
4937 if (this[0] === undefined) { return undefined; }
4938 index = this[0][dataPropName] || dataIndex++;
4939 return dataCache[index] || (dataCache[index] = {}); // create a new cache when reading entire cache
4940 case 1: // getting val from first node
4941 if (this[0] === undefined) { return undefined; }
4942 index = this[0][dataPropName]; // don't create a new cache when reading just a specific val
4943 return index? dataCache[index][key] : undefined;
4944 case 2: // setting key:val on every node
4945 // TODO - need to defend against reserved words being used as keys?
4946 for (var i = this.length; i--;) {
4949 if ( !(index = elm[dataPropName]) ) { // assumes index is always > 0
4950 index = dataIndex++;
4952 elm[dataPropName] = index;
4953 dataCache[index] = {};
4955 dataCache[index][key] = val;
4958 return this; // chainable with (key, val) signature
4960 throw new Error("glow.dom.NodeList#data expects 2 or less arguments, not "+arguments.length+".");
4965 @name glow.dom.NodeList#removeData
4967 @description Removes data previously added by {@link glow.dom.NodeList#data} from items in a NodeList.
4969 When called with no arguments, will delete glow's entire data store for every item in the NodeList.
4971 Otherwise, when given a key, will delete the associated value from every item in the NodeList.
4973 @param {String} [key] The name of the value in glow's data store for the NodeList item.
4975 removeData: function (key) { /*debug*///console.log("removeData("+key+")");
4979 // uses private class-scoped variables: dataCache, dataPropName
4983 index = elm[dataPropName];
4985 if (index !== undefined) {
4986 switch (arguments.length) {
4988 dataCache[index] = undefined;
4989 elm[dataPropName] = undefined;
4991 delete elm[dataPropName]; // IE 6 goes wobbly here
4993 catch(e) { // remove expando from IE 6
4994 elm.removeAttribute && elm.removeAttribute(dataPropName);
4998 dataCache[index][key] = undefined;
4999 delete dataCache[index][key];
5002 throw new Error("glow.dom.NodeList#removeData expects 1 or less arguments, not "+arguments.length+".");
5007 return this; // chainable
5011 @name glow.dom.NodeList#get
5013 @description Gets decendents of nodes that match a CSS selector.
5015 @param {String} selector CSS selector
5017 @returns {glow.dom.NodeList}
5018 Returns a new NodeList containing matched elements
5021 // create a new NodeList
5022 var myNodeList = glow.dom.create("<div><a href='s.html'>Link</a></div>");
5024 // get 'a' tags that are decendants of the NodeList nodes
5025 myNewNodeList = myNodeList.get("a");
5030 PrivateFunction: compileSelector
5031 Compile a CSS selector to an AST.
5034 sSelector - A string containing the CSS selector (or comma separated group of selectors).
5037 An array containing an AST for each selector in the group.
5039 function compileSelector(sSelector) {
5040 //return from cache if possible
5041 if (cssCache[sSelector]) {
5042 return cssCache[sSelector];
5045 var r = [], //array of result objects
5046 ri = 0, //results index
5047 comb, //current combinator
5050 aRx, //temp regex result
5051 matchedCondition, //have we matched a condition?
5052 sLastSelector, //holds last copy of selector to prevent infinite loop
5054 originalSelector = sSelector;
5056 while (sSelector && sSelector != sLastSelector) {
5059 //protect us from infinite loop
5060 sLastSelector = sSelector;
5062 //start by getting the scope (combinator)
5063 if (aRx = cssRegex.combinator.exec(sSelector)) {
5065 sSelector = sSelector.slice(aRx[0].length);
5067 //look for optimal id & tag searching
5068 if (aRx = cssRegex.tagName.exec(sSelector)) {
5070 sSelector = sSelector.slice(aRx[0].length);
5072 if (aRx = cssRegex.classNameOrId.exec(sSelector)) {
5073 if (aRx[1] == "#") {
5075 sSelector = sSelector.slice(aRx[0].length);
5078 if (!comb) { //use native stuff
5079 if (idTmp && firstLoop) {
5080 r[ri++] = [getByIdQuick, [idTmp.replace(/\\/g, ""), tagTmp || "*", null]];
5082 r[ri++] = [getByTagName, [tagTmp || "*", null]];
5084 r[ri++] = [hasId, [idTmp.replace(/\\/g, ""), null]];
5087 } else if (comb == ">") {
5088 r[ri++] = [getChildren, [null]];
5090 r[ri++] = [hasId, [idTmp.replace(/\\/g, ""), null]];
5092 if (tagTmp && tagTmp != "*") { //uses tag
5093 r[ri++] = [isTag, [tagTmp, null]];
5097 //other conditions can appear in any order, so here we go:
5098 matchedCondition = true;
5099 while (matchedCondition) {
5100 //look for class or ID
5101 if (sSelector.charAt(0) == "#" || sSelector.charAt(0) == ".") {
5102 if (aRx = cssRegex.classNameOrId.exec(sSelector)) {
5103 if (sSelector.charAt(0) == "#") { //is ID
5104 //define ID, remove escape chars
5105 r[ri++] = [hasId, [aRx[2].replace(/\\/g, ""), null]];
5107 r[ri++] = [hasClassName, [aRx[2].replace(/\\/g, ""), null]];
5109 sSelector = sSelector.slice(aRx[0].length);
5111 throw new Error("Invalid Selector " + originalSelector);
5114 matchedCondition = false;
5121 if (sSelector !== "") {
5122 throw new Error("Invalid Selector " + originalSelector);
5125 //add to cache and return
5126 return cssCache[sSelector] = r;
5130 PrivateFunction: fetchElements
5131 Get elements which match array of compiled conditions based on
5135 a - (object) CSS selector AST object.
5136 initialContext - DOM Element or DOM Document to search within.
5139 An array of matching elements.
5141 function fetchElements(a, initialContext) {
5142 var context = initialContext; //elements to look within
5144 for (var i = 0, al = a.length; i < al; i++) {
5145 a[i][1][a[i][1].length - 1] = context;
5146 context = a[i][0].apply(this, a[i][1]);
5152 PrivateFunction: getByIdQuick
5153 Get an element with a specific tag name, within a context.
5156 id - (string) the id of the element.
5157 tagName - (string) the name of the element.
5158 context - DOM Element or DOM Document in which to find the element.
5161 A DOM Element matching the specified criteria.
5163 function getByIdQuick(id, tagName, context) {
5164 var r = [], ri = 0, notQuick = [], notQuicki = 0, tmpNode;
5165 for (var i = 0, length = context.length; i < length; i++) {
5166 if (context[i].getElementById) {
5167 tmpNode = context[i].getElementById(id);
5168 if (tmpNode && (tmpNode.tagName == tagName.toUpperCase() || tagName == "*" || tmpNode.tagName == tagName)) {
5172 notQuick[notQuicki++] = context[i];
5175 //deal with the ones we couldn't do quick
5177 notQuick = getByTagName(tagName, notQuick);
5178 notQuick = hasId(id, notQuick);
5180 return r.concat(notQuick);
5183 function getChildren(context) {
5186 len = context.length;
5188 for (; i < len; i++) {
5189 append( r, getChildElms(context[i]) );
5194 function hasId(id, context) {
5195 for (var i = 0, length = context.length; i < length; i++) {
5196 if (context[i].id == id) {
5197 //is this a safe optimisation?
5198 return [context[i]];
5205 PrivateFunction: isTag
5206 Get an array of elements within an array that have a given tag name.
5209 tagName - (string) the name of the element.
5210 context - (array) elements to match.
5213 An array of matching elements.
5215 function isTag(tagName, context) {
5217 for (var i = 0, length = context.length; i < length; i++) {
5218 if (context[i].tagName == tagName.toUpperCase() || context[i].tagName == tagName) {
5219 r[ri++] = context[i];
5226 PrivateFunction: hasClassName
5227 Get elements that have a given class name from a provided array of elements.
5230 className - (string) the name of the class.
5231 context - (array) the DOM Elements to match.
5234 An array of matching DOM Elements.
5236 function hasClassName(className, context) {
5238 for (var i = 0, length = context.length; i < length; i++) {
5239 if ((" " + context[i].className + " ").indexOf(" " + className + " ") != -1) {
5240 r[ri++] = context[i];
5247 PrivateFunction: getBySelector
5248 Get elements within a context by a CSS selector.
5251 sSelector - (string) CSS selector.
5252 context - DOM Document or DOM Element to search within.
5255 An array of DOM Elements.
5257 function getBySelector(sSelector, context) {
5258 var aCompiledCSS; // holds current compiled css statement
5260 //split multiple selectors up
5261 var aSelectors = sSelector.split(",");
5263 for (var i = 0, nSelLen = aSelectors.length; i < nSelLen; i++) {
5264 aCompiledCSS = compileSelector(glow.lang.trim(aSelectors[i]));
5265 //get elements from DOM
5266 r = r.concat(fetchElements(aCompiledCSS, context));
5272 PrivateFunction: getIfWithinContext
5273 Get elements from a set of elements that are within at least one of another
5274 set of DOM Elements.
5277 nodes - DOM Elements to take the results from.
5278 context - DOM Elements that returned elements must be within.
5281 An array of DOM Elements.
5283 function getIfWithinContext(nodes, context) {
5284 nodes = nodes.length ? nodes : [nodes];
5285 var r = []; //to return
5288 //loop through nodes
5289 for (var i = 0; nodes[i]; i++) {
5290 nl = glow.dom.get(nodes[i]);
5291 //loop through context nodes
5292 for (var n = 0; context[n]; n++) {
5293 if (nl.isWithin(context[n])) {
5294 r[r.length] = nl[0];
5302 // main implementation
5303 return function(sSelector) {
5304 // no point trying if there's no current context
5305 if (!this.length) { return this; }
5307 var r = []; //nodes to return
5308 // decide what to do by arg
5309 for (var i = 0, argLen = arguments.length; i < argLen; i++) {
5310 if (typeof arguments[i] == "string") { // css selector string
5311 r = r.concat(getBySelector(arguments[i], this));
5312 // append(r, getBySelector(arguments[i], this));
5313 } else { // nodelist, array, node
5314 r = r.concat(getIfWithinContext(arguments[i], this));
5315 // append(r, getIfWithinContext(arguments[i], this));
5319 // strip out duplicates, wrap in nodelist
5320 return glow.dom.get(unique(r));
5325 //some init stuff. Using u to make it quicker to get by tag name :)
5326 placeholderElm = r.create('<u class="glow-placeholder"></u>');
5335 @description Native browser and custom events
5336 @see <a href="../furtherinfo/events/">Using event listeners</a>
5337 @see <a href="../furtherinfo/events/firing_events.shtml">Firing event listeners</a>
5338 @see <a href="../furtherinfo/events/removing_event_listeners.shtml">Removing event listeners</a>
5340 (window.gloader || glow).module({
5341 name: "glow.events",
5342 library: ["glow", "1.7.0"],
5343 depends: [["glow", "1.7.0", 'glow.dom']],
5344 builder: function(glow) {
5346 var $ = glow.dom.get;
5350 // object (keyed by obj id), containing object (keyed by event name), containing arrays of listeners
5351 var listenersByObjId = {};
5352 // object (keyed by ident) containing listeners
5353 var listenersByEventId = {};
5354 var domListeners = {};
5355 var psuedoPrivateEventKey = '__eventId' + glow.UID;
5356 var psuedoPreventDefaultKey = psuedoPrivateEventKey + 'PreventDefault';
5357 var psuedoStopPropagationKey = psuedoPrivateEventKey + 'StopPropagation';
5359 var topKeyListeners = {};
5360 var keyListenerId = 1;
5361 var keyListeners = {};
5368 var specialPrintables = {
5375 var keyNameAliases = {
5379 var keyNameToCode = {
5380 CAPSLOCK : 20, NUMLOCK : 144, SCROLLLOCK : 145, BREAK : 19,
5381 BACKTICK : 223, BACKSPACE : 8, PRINTSCREEN : 44, MENU : 93, SPACE : 32,
5382 SHIFT : 16, CTRL : 17, ALT : 18,
5383 ESC : 27, TAB : 9, META : 91, RIGHTMETA : 92, ENTER : 13,
5384 F1 : 112, F2 : 113, F3 : 114, F4 : 115,
5385 F5 : 116, F6 : 117, F7 : 118, F8 : 119,
5386 F9 : 120, F10 : 121, F11 : 122, F12 : 123,
5387 INS : 45, HOME : 36, PAGEUP : 33,
5388 DEL : 46, END : 35, PAGEDOWN : 34,
5389 LEFT : 37, UP : 38, RIGHT : 39, DOWN : 40
5391 var codeToKeyName = {};
5392 for (var i in keyNameToCode) {
5393 codeToKeyName['' + keyNameToCode[i]] = i;
5396 var operaBrokenChars = '0123456789=;\'\\\/#,.-';
5399 PrivateMethod: removeKeyListener
5400 Removes a listener for a key combination.
5403 ident - identifier returned from addKeyListener.
5406 function removeKeyListener (ident) {
5407 var keyType = keyTypes[ident];
5408 if (! keyType) { return false; }
5409 var listeners = keyListeners[keyType];
5410 if (! listeners) { return false; }
5411 for (var i = 0, len = listeners.length; i < len; i++) {
5412 if (listeners[i][0] == ident) {
5413 listeners.splice(i, 1);
5421 PrivateMethod: initTopKeyListner
5422 Adds an event listener for keyboard events, for key listeners added by addKeyListener.
5425 type - press|down|up - the type of key event.
5428 function initTopKeyListener (type) {
5429 topKeyListeners[type] = r.addListener(document, 'key' + type, function (e) {
5431 if (e.ctrlKey) { mods += CTRL; }
5432 if (e.altKey) { mods += ALT; }
5433 if (e.shiftKey) { mods += SHIFT; }
5434 var keyType = e.chr ? e.chr.toLowerCase() : e.key ? e.key.toLowerCase() : e.keyCode;
5435 var eventType = mods + ':' + keyType + ':' + type;
5436 var listeners = keyListeners[eventType] ? keyListeners[eventType].slice(0) : [];
5437 // if the user pressed shift, but event didn't specify that, include it
5438 if (e.shiftKey) { // upper-case letter, should match regardless of shift
5439 var shiftEventType = (mods & ~ SHIFT) + ':' + keyType + ':' + type;
5440 if (keyListeners[shiftEventType]) {
5441 for (var i = 0, len = keyListeners[shiftEventType].length; i < len; i++) {
5442 listeners[listeners.length] = keyListeners[shiftEventType][i];
5447 if (! listeners) { return; }
5449 for (var i = 0, len = listeners.length; i < len; i++) {
5450 //call listener and look out for preventing the default
5451 if (listeners[i][2].call(listeners[i][3] || this, e) === false) {
5455 return !e.defaultPrevented();
5460 PrivateMethod: clearEvents
5461 Removes all current listeners to avoid IE mem leakage
5463 function clearEvents() {
5465 for (ident in listenersByEventId) {
5466 r.removeListener(ident);
5470 // for opera madness
5471 var previousKeyDownKeyCode;
5473 var operaResizeListener,
5474 operaDocScrollListener;
5477 PrivateMethod: addDomListener
5478 Adds an event listener to a browser object. Manages differences with certain events.
5481 attachTo - the browser object to attach the event to.
5482 name - the generic name of the event (inc. mousewheel)
5484 function addDomListener (attachTo, name, capturingMode) {
5487 capturingMode = !!capturingMode;
5489 if (glow.env.opera) {
5490 if (name.toLowerCase() == 'resize' && !operaResizeListener && attachTo == window) {
5491 operaResizeListener = r.addListener(window.document.body, 'resize', function (e) { r.fire(window, 'resize', e); });
5492 } else if (name.toLowerCase() == 'scroll' && !operaDocScrollListener && attachTo == window) {
5493 operaDocScrollListener = r.addListener(window.document, 'scroll', function (e) { r.fire(window, 'scroll', e); });
5497 var callback = function (e) {
5498 if (! e) { e = window.event; }
5499 var event = new r.Event(),
5500 lowerCaseName = name.toLowerCase();
5501 event.nativeEvent = e;
5502 event.source = e.target || e.srcElement;
5504 event.relatedTarget = e.relatedTarget || (lowerCaseName == "mouseover" ? e.fromElement : e.toElement);
5505 event.button = glow.env.ie ? (e.button & 1 ? 0 : e.button & 2 ? 2 : 1) : e.button;
5506 if (e.pageX || e.pageY) {
5507 event.pageX = e.pageX;
5508 event.pageY = e.pageY;
5509 } else if (e.clientX || e.clientY) {
5510 event.pageX = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
5511 event.pageY = e.clientY + document.body.scrollTop + document.documentElement.scrollTop;
5513 if (lowerCaseName == 'mousewheel') {
5514 // this works in latest opera, but have read that it needs to be switched in direction
5515 // if there was an opera bug, I can't find which version it was fixed in
5517 e.wheelDelta ? e.wheelDelta / 120 :
5518 e.detail ? - e.detail / 3 :
5520 if (event.wheelDelta == 0) { return; }
5522 if (lowerCaseName.indexOf("key") != -1) {
5523 event.altKey = !! e.altKey;
5524 event.ctrlKey = !! e.ctrlKey;
5525 event.shiftKey = !! e.shiftKey;
5527 if (name == 'keydown') {
5528 previousKeyDownKeyCode = e.keyCode;
5531 event.charCode = e.keyCode && e.charCode !== 0 ? undefined : e.charCode;
5533 if (lowerCaseName == 'keypress') {
5534 if (typeof(event.charCode) == 'undefined') {
5535 event.charCode = e.keyCode;
5538 if (glow.env.opera && event.charCode && event.charCode == previousKeyDownKeyCode &&
5539 operaBrokenChars.indexOf(String.fromCharCode(event.charCode)) == -1
5541 event.charCode = undefined;
5542 event.keyCode = previousKeyDownKeyCode;
5546 // make things a little more sane in opera
5547 if (event.charCode && event.charCode <= 49) { event.charCode = undefined; }
5549 if (event.charCode) {
5550 event.chr = String.fromCharCode(event.charCode);
5552 else if (e.keyCode) {
5553 event.charCode = undefined;
5554 event.keyCode = keyNameAliases[e.keyCode.toString()] || e.keyCode;
5555 event.key = codeToKeyName[event.keyCode];
5556 if (specialPrintables[event.key]) {
5557 event.chr = specialPrintables[event.key];
5558 event.charCode = event.chr.charCodeAt(0);
5564 event.chr.toUpperCase() != event.chr ? // is lower case
5566 event.chr.toLowerCase() != event.chr ? // is upper case
5568 undefined; // can only tell for keys with case
5572 r.fire(this, name, event);
5573 if (event.defaultPrevented()) {
5579 if (attachTo.addEventListener && (!glow.env.webkit || glow.env.webkit > 418)) {
5581 // This is to fix an issue between Opera and everything else.
5582 // Opera needs to have an empty eventListener attached to the parent
5583 // in order to fire a captured event (in our case we are using capture if
5584 // the event is focus/blur) on an element when the element is the eventTarget.
5586 // It is only happening in Opera 9, Opera 10 doesn't show this behaviour.
5587 // It looks like Opera has a bug, but according to the W3C Opera is correct...
5589 // "A capturing EventListener will not be triggered by events dispatched
5590 // directly to the EventTarget upon which it is registered."
5591 // http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow-capture
5593 ( name == 'focus' || name == 'blur' )
5596 attachTo.parentNode.addEventListener(name, function(){}, true);
5599 attachTo.addEventListener(name.toLowerCase() == 'mousewheel' && glow.env.gecko ? 'DOMMouseScroll' : name, callback, capturingMode);
5603 var onName = 'on' + name;
5604 var existing = attachTo[onName];
5606 attachTo[onName] = function () {
5607 // we still need to return false if either the existing or new callback returns false
5608 var existingReturn = existing.apply(this, arguments),
5609 callbackReturn = callback.apply(this, arguments);
5611 return (existingReturn !== false) && (callbackReturn !== false);
5614 attachTo[onName] = callback;
5624 Add mouseEnter or mouseLeave 'event' to an element
5626 @param {HTMLElement} attachTo Element to create mouseenter / mouseleave for
5627 @param {Boolean} isLeave Create mouseleave or mouseenter?
5629 function addMouseEnterLeaveEvent(attachTo, isLeave) {
5630 var elm = $(attachTo),
5631 listenFor = isLeave ? "mouseout" : "mouseover",
5632 toFire = isLeave ? "mouseleave" : "mouseenter";
5634 r.addListener(attachTo, listenFor, function(e) {
5635 var relatedTarget = $(e.relatedTarget);
5636 // if the mouse has come from outside elm...
5637 if ( !relatedTarget.eq(elm) && !relatedTarget.isWithin(elm) ) {
5638 // return false if default is prevented by mouseEnter event
5639 return !r.fire(elm[0], toFire, e).defaultPrevented();
5645 @name glow.events._copyListeners
5648 @description Maps event listeners from one set of nodes to another in the order they appear in each NodeList.
5649 Note, it doesn't copy events from a node's children.
5652 @param {NodeList} from NodeList to copy events from
5653 @param {NodeList} to NodeList to copy events to
5658 var listener = glow.events.addListener(...);
5659 glow.events.removeListener(listener);
5661 r._copyListeners = function(from, to) {
5662 // grab all the elements (including children)
5663 var i = from.length,
5664 // events attached to a particular element
5666 // name of the current event we're looking at
5668 // current listener index
5670 // number of listeners to an event
5672 // listener definition from listenersByObjId
5675 // loop through all items
5677 // has a glow event been assigned to this node?
5678 if ( from[i][psuedoPrivateEventKey] ) {
5679 // get listeners for that event
5680 elementEvents = listenersByObjId[ from[i][psuedoPrivateEventKey] ];
5681 // loop through event names
5682 for (eventName in elementEvents) {
5684 listenersLen = elementEvents[eventName].length;
5685 // loop through listeners to that event
5686 for (; listenerIndex < listenersLen; listenerIndex++) {
5687 listener = elementEvents[eventName][listenerIndex];
5688 // listen to them on the clone
5689 r.addListener(to[i], eventName, listener[2], listener[3]);
5697 @name glow.events.addListener
5699 @description Adds an event listener to an object (e.g. a DOM Element or Glow widget).
5701 Some non-standard dom events are available:
5705 <dd>Fires when the mouse enters this element specifically, does not bubble</dd>
5707 <dd>Fires when the mouse leaves this element specifically, does not bubble</dd>
5710 @param {String | NodeList | Object} attachTo The object to attach the event listener to.
5712 If the parameter is a string, then it is treated as a CSS selector
5713 and the listener is attached to all matching elements.
5715 If the parameter is a {@link glow.dom.NodeList}, then the listener
5716 is attached to all elements in the NodeList.
5718 @param {String} name The event name.
5720 Listeners for DOM events should not begin with 'on' (i.e. 'click'
5721 rather than 'onclick')
5723 @param {Function} callback The function to be called when the event fires.
5725 @param {Object} [context] The execution scope of the callback.
5727 If this parameter is not passed then the attachTo object will be the
5728 scope of the callback.
5730 @returns {Number | Undefined}
5732 A unique identifier for the event suitable for passing to
5733 {@link glow.events.removeListener}. If an empty NodeList or CSS
5734 selector that returns no elements is passed, then undefined is returned.
5737 glow.events.addListener('#nav', 'click', function () {
5738 alert('nav clicked');
5741 glow.events.addListener(myLightBox, 'close', this.showSurvey, this);
5743 r.addListener = function (attachTo, name, callback, context) {
5744 var capturingMode = false;
5745 if (! attachTo) { throw 'no attachTo paramter passed to addListener'; }
5747 if (typeof attachTo == 'string') {
5748 if (! glow.dom) { throw "glow.dom must be loaded to use a selector as the first argument to glow.events.addListener"; }
5749 attachTo = $(attachTo);
5752 if (glow.dom && attachTo instanceof glow.dom.NodeList) {
5753 var listenerIds = [],
5754 i = attachTo.length;
5756 //attach the event for each element, return an array of listener ids
5758 listenerIds[i] = r.addListener(attachTo[i], name, callback, context);
5765 if (! (objIdent = attachTo[psuedoPrivateEventKey])) {
5766 objIdent = attachTo[psuedoPrivateEventKey] = objid++;
5768 var ident = eventid++;
5769 var listener = [ objIdent, name, callback, context, ident ];
5770 listenersByEventId[ident] = listener;
5772 var objListeners = listenersByObjId[objIdent];
5773 if (! objListeners) { objListeners = listenersByObjId[objIdent] = {}; }
5774 var objEventListeners = objListeners[name];
5775 if (! objEventListeners) { objEventListeners = objListeners[name] = []; }
5776 objEventListeners[objEventListeners.length] = listener;
5778 if ((attachTo.addEventListener || attachTo.attachEvent) && ! domListeners[objIdent + ':' + name]) {
5779 // handle 'special' dom events (ie, ones that aren't directly mapped to real dom events)
5780 // we don't actually add a dom listener for these event names
5783 addMouseEnterLeaveEvent(attachTo, false);
5786 addMouseEnterLeaveEvent(attachTo, true);
5789 // Focus and blur events:
5790 // Convert focus/blur events to use capturing.
5791 // This allows form elements to be used with event delegation.
5795 // If IE then change focus/blur events to focusin/focusout events.
5796 // This allows input elements to bubble, so form elements work with event delegation.
5798 addFocusInOutEvent(attachTo, true);
5803 capturingMode = true
5809 // If IE then change focus/blur events to focusin/focusout events.
5810 // This allows input elements to bubble, so form elements work with event delegation.
5812 addFocusInOutEvent(attachTo, false);
5817 capturingMode = true
5823 addDomListener(attachTo, name, capturingMode);
5824 domListeners[objIdent + ':' + name] = true;
5830 Add focusIn or focusOut 'event' to an element
5832 @param {HTMLElement} attachTo Element to create focusIn / focusOut for
5833 @param {Boolean} event Create focusIn or focusOut?
5835 function addFocusInOutEvent(attachTo, event) {
5836 var listenFor = event ? 'focusin' : 'focusout',
5837 toFire = event ? 'focus' : 'blur';
5838 r.addListener(attachTo, listenFor, function(e) {
5839 return !r.fire(attachTo, toFire, e).defaultPrevented();
5844 @name glow.events.removeListener
5846 @description Removes a listener created with addListener
5848 @param {Number} ident An identifier returned from {@link glow.events.addListener}.
5853 var listener = glow.events.addListener(...);
5854 glow.events.removeListener(listener);
5856 r.removeListener = function (ident) {
5857 if (ident && ident.toString().indexOf('k:') != -1) {
5858 return removeKeyListener(ident);
5860 if (ident instanceof Array) {
5861 //call removeListener for each array member
5862 var i = ident.length; while(i--) {
5863 r.removeListener(ident[i]);
5867 var listener = listenersByEventId[ident];
5868 if (! listener) { return false; }
5869 delete listenersByEventId[ident];
5870 var listeners = listenersByObjId[listener[0]][listener[1]];
5871 for (var i = 0, len = listeners.length; i < len; i++) {
5872 if (listeners[i] == listener) {
5873 listeners.splice(i, 1);
5877 if (! listeners.length) {
5878 delete listenersByObjId[listener[0]][listener[1]];
5880 var listenersLeft = false;
5881 for (var i in listenersByObjId[listener[0]]) { listenersLeft = true; break; }
5882 if (! listenersLeft) {
5883 delete listenersByObjId[listener[0]];
5889 @name glow.events.removeAllListeners
5891 @description Removes all listeners attached to a given object
5893 @param {String | glow.dom.NodeList | Object | Object[] } detachFrom The object(s) to remove listeners from.
5895 If the parameter is a string, then it is treated as a CSS selector,
5896 listeners are removed from all nodes.
5898 @returns glow.events
5901 glow.events.removeAllListeners("#myDiv");
5903 r.removeAllListeners = function(obj) {
5911 // cater for selector
5912 if (typeof obj == "string") {
5916 // cater for arrays & nodelists
5917 if (obj instanceof Array || obj instanceof glow.dom.NodeList) {
5918 //call removeAllListeners for each array member
5919 i = obj.length; while(i--) {
5920 r.removeAllListeners(obj[i]);
5925 // get the objects id
5926 objId = obj[psuedoPrivateEventKey];
5928 // if it doesn't have an id it doesn't have events... return
5932 events = listenersByObjId[objId];
5933 for (eventName in events) {
5934 i = events[eventName].length; while(i--) {
5935 listenerIds[listenerIdsLen++] = events[eventName][i][4];
5938 // remove listeners for that object
5939 if (listenerIds.length) {
5940 r.removeListener( listenerIds );
5946 @name glow.events.fire
5948 @description Fires an event on an object.
5950 @param {Object} attachedTo The object that the event is associated with.
5952 @param {String} name The name of the event.
5954 Event names should not start with the word 'on'.
5956 @param {Object | glow.events.Event} [event] An event object or properties to add to a default event object
5958 If not specified, a generic event object is created. If you provide a simple
5959 object, a default Event will be created with the properties from the provided object.
5966 // firing a custom event
5967 Ball.prototype.move = function () {
5969 // check its position
5970 if (this._height == 0) {
5971 var event = glow.events.fire(this, 'bounce', {
5972 bounceCount: this._bounceCount
5975 // handle what to do if a listener returned false
5976 if ( event.defaultPrevented() ) {
5983 // listening to a custom event
5984 var myBall = new Ball();
5986 glow.events.addListener(myBall, "bounce", function(event) {
5987 if (event.bounceCount == 3) {
5988 // stop bouncing after 3 bounces
5993 r.fire = function (attachedTo, name, e) {
5994 if (! attachedTo) throw 'glow.events.fire: required parameter attachedTo not passed (name: ' + name + ')';
5995 if (! name) throw 'glow.events.fire: required parameter name not passed';
5996 if (! e) { e = new r.Event(); }
5997 if ( e.constructor === Object ) { e = new r.Event( e ) }
5999 if (typeof attachedTo == 'string') {
6000 if (! glow.dom) { throw "glow.dom must be loaded to use a selector as the first argument to glow.events.addListener"; }
6001 attachedTo = $(attachedTo);
6005 e.attachedTo = attachedTo;
6006 if (! e.source) { e.source = attachedTo; }
6008 if (attachedTo instanceof glow.dom.NodeList) {
6010 attachedTo.each(function(i){
6012 callListeners(attachedTo[i], e);
6018 callListeners(attachedTo, e);
6025 function callListeners(attachedTo, e) {
6029 objEventListeners = objListeners && objListeners[e.type];
6031 // 3 assignments, but stop assigning if any of them are false
6032 (objIdent = attachedTo[psuedoPrivateEventKey]) &&
6033 (objListeners = listenersByObjId[objIdent]) &&
6034 (objEventListeners = objListeners[e.type]);
6036 if (! objEventListeners) { return e; }
6039 var listeners = objEventListeners.slice(0);
6041 // we make a copy of the listeners before calling them, as the event handlers may
6042 // remove themselves (took me a while to track this one down)
6043 for (var i = 0, len = listeners.length; i < len; i++) {
6044 listener = listeners[i];
6045 if ( listener[2].call(listener[3] || attachedTo, e) === false ) {
6054 @name glow.events.addKeyListener
6057 @description Adds an event listener for a keyboard event HELLO.
6059 <p><em>Notes for Opera</em></p>
6061 It is currently impossible to differentiate certain key events in
6062 Opera (for example the RIGHT (the right arrow key) and the
6063 apostrope (') result in the same code). For this reason pressing
6064 either of these keys will result in key listeners specified as
6065 "RIGHT" and/or "'" to be fired.
6067 <p><em>Key Identifiers</em></p>
6069 The key param uses the following strings to refer to special keys,
6070 i.e. non alpha-numeric keys.
6079 <li>PRINTSCREEN</li>
6111 @param {String} key The key or key combination to listen to.
6113 This parameter starts with modifier keys 'CTRL', 'ALT' and
6114 'SHIFT'. Modifiers can appear in any combination and order and are
6117 Following any modifiers is the key character. To specify a
6118 character code, use the appropriate escape sequence (e.g.
6119 "CTRL+\u0065" = CTRL+e").
6121 To specify a special key, the key character should be replaced with
6122 a key identifier, see description below (e.g. "RIGHT" specifies the
6125 @param {String} type The type of key press to listen to.
6127 Possible values for this parameter are:
6130 <dt>press</dt><dd>the key is pressed (comparable to a mouse click)</dd>
6131 <dt>down</dt><dd>the key is pushed down</dd>
6132 <dt>up</dt><dd>the key is released</dd>
6135 @param {Function} callback The function to be called when the event fires.
6137 @param {Object} [context] The execution scope of the callback.
6139 If this parameter is not passed then the attachTo object will be the
6140 context of the callback.
6144 A unique identifier for the event suitable for passing to
6145 {@link glow.events.removeListener}.
6148 glow.events.addKeyListener("CTRL+ALT+a", "press",
6149 function () { alert("CTRL+ALT+a pressed"); }
6151 glow.events.addKeyListener("SHIFT+\u00A9", "down",
6152 function () { alert("SHIFT+© pushed") }
6155 var keyRegex = /^((?:(?:ctrl|alt|shift)\+)*)(?:(\w+|.)|[\n\r])$/i;
6156 r.addKeyListener = function (key, type, callback, context) {
6157 type.replace(/^key/i, "");
6158 type = type.toLowerCase();
6159 if (! (type == 'press' || type == 'down' || type == 'up')) {
6160 throw 'event type must be press, down or up';
6162 if (! topKeyListeners[type]) { initTopKeyListener(type); }
6163 var res = key.match(keyRegex),
6166 if (! res) { throw 'key format not recognised'; }
6167 if (res[1].toLowerCase().indexOf('ctrl') != -1) { mods += CTRL; }
6168 if (res[1].toLowerCase().indexOf('alt') != -1) { mods += ALT; }
6169 if (res[1].toLowerCase().indexOf('shift') != -1) { mods += SHIFT; }
6170 var eventKey = mods + ':' + (res[2] ? res[2].toLowerCase() : '\n') + ':' + type;
6171 var ident = 'k:' + keyListenerId++;
6172 keyTypes[ident] = eventKey;
6173 var listeners = keyListeners[eventKey];
6174 if (! listeners) { listeners = keyListeners[eventKey] = []; }
6175 listeners[listeners.length] = [ident, type, callback, context];
6180 @name glow.events.Event
6182 @param {Object} [properties] Properties to add to the Event instance.
6183 Each key-value pair in the object will be added to the Event as
6185 @description Object passed into all events
6187 Some of the properties described below are only available for
6188 certain event types. These are listed in the property descriptions
6191 <p><em>Notes for Opera</em></p>
6193 The information returned from Opera about key events does not allow
6194 certain keys to be differentiated. This mainly applies to special
6195 keys, such as the arrow keys, function keys, etc. which conflict
6196 with some printable characters.
6199 r.Event = function ( obj ) {
6201 glow.lang.apply( this, obj );
6206 @name glow.events.Event#attachedTo
6207 @type Object | Element
6208 @description The object/element that the listener is attached to.
6210 See the description for 'source' for more details.
6214 @name glow.events.Event#source
6216 @description The actual object/element that the event originated from.
6218 For example, you could attach a listener to an 'ol' element to
6219 listen for clicks. If the user clicked on an 'li' the source property
6220 would be the 'li' element, and 'attachedTo' would be the 'ol'.
6224 @name glow.events.Event#pageX
6226 @description The horizontal position of the mouse pointer in the page in pixels.
6228 <p><em>Only available for mouse events.</em></p>
6232 @name glow.events.Event#pageY
6234 @description The vertical position of the mouse pointer in the page in pixels.
6236 <p><em>Only available for mouse events.</em></p>
6240 @name glow.events.Event#button
6242 @description A number representing which button was pressed.
6244 <p><em>Only available for mouse events.</em></p>
6246 0 for the left button, 1 for the middle button or 2 for the right button.
6250 @name glow.events.Event#relatedTarget
6252 @description The element that the mouse has come from or is going to.
6254 <p><em>Only available for mouse over/out events.</em></p>
6258 @name glow.events.Event#wheelDelta
6260 @description The number of clicks up (positive) or down (negative) that the user moved the wheel.
6262 <p><em>Only available for mouse wheel events.</em></p>
6266 @name glow.events.Event#ctrlKey
6268 @description Whether the ctrl key was pressed during the key event.
6270 <p><em>Only available for keyboard events.</em></p>
6274 @name glow.events.Event#shiftKey
6276 @description Whether the shift key was pressed during the key event.
6278 <p><em>Only available for keyboard events.</em></p>
6282 @name glow.events.Event#altKey
6284 @description Whether the alt key was pressed during the key event.
6286 <p><em>Only available for keyboard events.</em></p>
6290 @name glow.events.Event#capsLock
6291 @type Boolean | Undefined
6292 @description Whether caps-lock was on during the key event
6294 <p><em>Only available for keyboard events.</em></p>
6296 If the key is not alphabetic, this property will be undefined
6297 as it is not possible to tell if caps-lock is on in this scenario.
6301 @name glow.events.Event#keyCode
6303 @description An integer number represention of the keyboard key that was pressed.
6305 <p><em>Only available for keyboard events.</em></p>
6309 @name glow.events.Event#key
6310 @type String | Undefined
6311 @description A short identifier for the key for special keys.
6313 <p><em>Only available for keyboard events.</em></p>
6315 If the key was not a special key this property will be undefined.
6317 See the list of key identifiers in {@link glow.events.addKeyListener}
6321 @name glow.events.Event#charCode
6322 @type Number | Undefined
6323 @description The unicode character code for a printable character.
6325 <p><em>Only available for keyboard events.</em></p>
6327 This will be undefined if the key was not a printable character.
6331 @name glow.events.Event#chr
6333 @description A printable character string.
6335 <p><em>Only available for keyboard events.</em></p>
6337 The string of the key that was pressed, for example 'j' or 's'.
6339 This will be undefined if the key was not a printable character.
6344 @name glow.events.Event#preventDefault
6346 @description Prevent the default action for events.
6348 This can also be achieved by returning false from an event callback
6351 r.Event.prototype.preventDefault = function () {
6352 if (this[psuedoPreventDefaultKey]) { return; }
6353 this[psuedoPreventDefaultKey] = true;
6354 if (this.nativeEvent && this.nativeEvent.preventDefault) {
6355 this.nativeEvent.preventDefault();
6356 this.nativeEvent.returnValue = false;
6361 @name glow.events.Event#defaultPrevented
6363 @description Test if the default action has been prevented.
6367 True if the default action has been prevented.
6369 r.Event.prototype.defaultPrevented = function () {
6370 return !! this[psuedoPreventDefaultKey];
6374 @name glow.events.Event#stopPropagation
6376 @description Stops the event propagating.
6378 For DOM events, this stops the event bubbling up through event
6379 listeners added to parent elements. The event object is marked as
6380 having had propagation stopped (see
6381 {@link glow.events.Event#propagationStopped propagationStopped}).
6384 // catch all click events that are not links
6385 glow.events.addListener(
6388 function () { alert('document clicked'); }
6391 glow.events.addListener(
6394 function (e) { e.stopPropagation(); }
6397 r.Event.prototype.stopPropagation = function () {
6398 if (this[psuedoStopPropagationKey]) { return; }
6399 this[psuedoStopPropagationKey] = true;
6400 var e = this.nativeEvent;
6402 e.cancelBubble = true;
6403 if (e.stopPropagation) { e.stopPropagation(); }
6408 @name glow.events.Event#propagationStopped
6410 @description Tests if propagation has been stopped for this event.
6414 True if event propagation has been prevented.
6417 r.Event.prototype.propagationStopped = function () {
6418 return !! this[psuedoStopPropagationKey];
6421 //cleanup to avoid mem leaks in IE
6422 if (glow.env.ie < 8 || glow.env.webkit < 500) {
6423 r.addListener(window, "unload", clearEvents);
6427 glow.events.listenersByObjId = listenersByObjId;
6432 @description Serialising and de-serialising data
6433 @see <a href="../furtherinfo/data/data.shtml">Using glow.data</a>
6435 (window.gloader || glow).module({
6437 library: ["glow", "1.7.0"],
6438 depends: [["glow", "1.7.0", "glow.dom"]],
6439 builder: function(glow) {
6443 PrivateProperty: TYPES
6444 hash of strings representing data types
6447 UNDEFINED : "undefined",
6450 BOOLEAN : "boolean",
6453 FUNCTION : "function",
6458 PrivateProperty: TEXT
6459 hash of strings used in encoding/decoding
6472 PrivateProperty: JSON
6473 nested hash of strings and regular expressions used in encoding/decoding Json
6488 DATA_SEPARATOR : ",",
6489 KEY_SEPARATOR : ":",
6490 KEY_DELIMITER : "\"",
6491 STRING_DELIMITER : "\"",
6493 SAFE_PT1 : /^[\],:{}\s]*$/,
6495 SAFE_PT3 : /\"[^\"\\\n\r]*\"|true|false|null|-?\d+(?:\.\d*)?(:?[eE][+\-]?\d+)?/g,
6496 SAFE_PT4 : /(?:^|:|,)(?:\s*\[)+/g
6500 PrivateProperty: SLASHES
6501 hash of strings and regular expressions used in encoding strings
6504 TEST : /[\b\n\r\t\\\f\"]/g,
6505 B : {PLAIN : "\b", ESC : "\\b"},
6506 N : {PLAIN : "\n", ESC : "\\n"},
6507 R : {PLAIN : "\r", ESC : "\\r"},
6508 T : {PLAIN : "\t", ESC : "\\t"},
6509 F : {PLAIN : "\f", ESC : "\\f"},
6510 SL : {PLAIN : "\\", ESC : "\\\\"},
6511 QU : {PLAIN : "\"", ESC : "\\\""}
6515 PrivateMethod: _replaceSlashes
6516 Callback function for glow.lang.replace to escape appropriate characters
6519 s - the regex match to be tested
6522 The escaped version of the input.
6524 function _replaceSlashes(s) {
6526 case SLASHES.B.PLAIN: return SLASHES.B.ESC;
6527 case SLASHES.N.PLAIN: return SLASHES.N.ESC;
6528 case SLASHES.R.PLAIN: return SLASHES.R.ESC;
6529 case SLASHES.T.PLAIN: return SLASHES.T.ESC;
6530 case SLASHES.F.PLAIN: return SLASHES.F.ESC;
6531 case SLASHES.SL.PLAIN: return SLASHES.SL.ESC;
6532 case SLASHES.QU.PLAIN: return SLASHES.QU.ESC;
6538 PrivateMethod: _getType
6539 Returns the data type of the object
6542 object - the object to be tested
6545 A one of the TYPES constant properties that represents the data type of the object.
6547 function _getType(object) {
6548 if((typeof object) == TYPES.OBJECT) {
6549 if (object == null) {
6552 return (object instanceof Array)?TYPES.ARRAY:TYPES.OBJECT;
6555 return (typeof object);
6562 @name glow.data.encodeUrl
6564 @description Encodes an object for use as a query string.
6566 Returns a string representing the object suitable for use
6567 as a query string, with all values suitably escaped.
6568 It does not include the initial question mark. Where the
6569 input field was an array, the key is repeated in the output.
6571 @param {Object} object The object to be encoded.
6573 This must be a hash whose values can only be primitives or
6574 arrays of primitives.
6579 var getRef = glow.data.encodeUrl({foo: "Foo", bar: ["Bar 1", "Bar2"]});
6580 // will return "foo=Foo&bar=Bar%201&bar=Bar2"
6582 encodeUrl : function (object) {
6583 var objectType = _getType(object);
6584 var paramsList = [];
6587 if (objectType != TYPES.OBJECT) {
6588 throw new Error("glow.data.encodeUrl: cannot encode item");
6590 for (var key in object) {
6591 switch(_getType(object[key])) {
6592 case TYPES.FUNCTION:
6594 throw new Error("glow.data.encodeUrl: cannot encode item");
6597 for(var i = 0, l = object[key].length; i < l; i++) {
6598 switch(_getType(object[key])[i]) {
6599 case TYPES.FUNCTION:
6602 throw new Error("glow.data.encodeUrl: cannot encode item");
6605 paramsList[listLength++] = key + TEXT.EQ + encodeURIComponent(object[key][i]);
6610 paramsList[listLength++] = key + TEXT.EQ + encodeURIComponent(object[key]);
6614 return paramsList.join(TEXT.AND);
6618 @name glow.data.decodeUrl
6620 @description Decodes a query string into an object.
6622 Returns an object representing the data given by the query
6623 string, with all values suitably unescaped. All keys in the
6624 query string are keys of the object. Repeated keys result
6627 @param {String} string The query string to be decoded.
6629 It should not include the initial question mark.
6634 var getRef = glow.data.decodeUrl("foo=Foo&bar=Bar%201&bar=Bar2");
6635 // will return the object {foo: "Foo", bar: ["Bar 1", "Bar2"]}
6637 decodeUrl : function (text) {
6638 if(_getType(text) != TYPES.STRING) {
6639 throw new Error("glow.data.decodeUrl: cannot decode item");
6640 } else if (text === "") {
6645 var keyValues = text.split(/[&;]/);
6647 var thisPair, key, value;
6649 for(var i = 0, l = keyValues.length; i < l; i++) {
6650 thisPair = keyValues[i].split(TEXT.EQ);
6651 if(thisPair.length != 2) {
6652 throw new Error("glow.data.decodeUrl: cannot decode item");
6654 key = glow.lang.trim( decodeURIComponent(thisPair[0]) );
6655 value = glow.lang.trim( decodeURIComponent(thisPair[1]) );
6657 switch (_getType(result[key])) {
6659 result[key][result[key].length] = value;
6661 case TYPES.UNDEFINED:
6662 result[key] = value;
6665 result[key] = [result[key], value];
6673 @name glow.data.encodeJson
6675 @description Encodes an object into a string JSON representation.
6677 Returns a string representing the object as JSON.
6679 @param {Object} object The object to be encoded.
6681 This can be arbitrarily nested, but must not contain
6682 functions or cyclical structures.
6687 var myObj = {foo: "Foo", bar: ["Bar 1", "Bar2"]};
6688 var getRef = glow.data.encodeJson(myObj);
6689 // will return '{"foo": "Foo", "bar": ["Bar 1", "Bar2"]}'
6691 encodeJson : function (object, options) {
6692 function _encode(object, options)
6694 if(_getType(object) == TYPES.ARRAY) {
6695 var type = JSON.ARRAY;
6697 var type = JSON.HASH;
6700 var serial = [type.START];
6703 var notFirst = false;
6705 for(var key in object) {
6706 dataType = _getType(object[key]);
6708 if(dataType != TYPES.UNDEFINED) { /* ignore undefined data */
6710 serial[len++] = JSON.DATA_SEPARATOR;
6714 if(type.SHOW_KEYS) {
6715 serial[len++] = JSON.KEY_DELIMITER;
6716 serial[len++] = key;
6717 serial[len++] = JSON.KEY_DELIMITER;
6718 serial[len++] = JSON.KEY_SEPARATOR;
6722 case TYPES.FUNCTION:
6723 throw new Error("glow.data.encodeJson: cannot encode item");
6727 serial[len++] = JSON.STRING_DELIMITER;
6728 serial[len++] = glow.lang.replace(object[key], SLASHES.TEST, _replaceSlashes);
6729 serial[len++] = JSON.STRING_DELIMITER;
6733 serial[len++] = object[key];
6737 serial[len++] = _encode(object[key], options);
6740 serial[len++] = TYPES.NULL;
6745 serial[len++] = type.END;
6747 return serial.join(TEXT.EMPTY);
6750 options = options || {};
6751 var type = _getType(object);
6753 if((type == TYPES.OBJECT) || (type == TYPES.ARRAY)) {
6754 return _encode(object, options);
6756 throw new Error("glow.data.encodeJson: cannot encode item");
6760 @name glow.data.decodeJson
6762 @description Decodes a string JSON representation into an object.
6764 Returns a JavaScript object that mirrors the data given.
6766 @param {String} string The string to be decoded.
6769 @param {Object} opts
6771 Zero or more of the following as properties of an object:
6772 @param {Boolean} [opts.safeMode=false] Whether the string should only be decoded if it is deemed "safe".
6773 The json.org regular expression checks are used.
6778 var getRef = glow.data.decodeJson('{foo: "Foo", bar: ["Bar 1", "Bar2"]}');
6779 // will return {foo: "Foo", bar: ["Bar 1", "Bar2"]}
6781 var getRef = glow.data.decodeJson('foobar', {safeMode: true});
6782 // will throw an error
6784 decodeJson : function (text, options) {
6785 if(_getType(text) != TYPES.STRING) {
6786 throw new Error("glow.data.decodeJson: cannot decode item");
6789 options = options || {};
6790 options.safeMode = options.safeMode || false;
6794 if(options.safeMode) {
6795 canEval = (JSON.SAFE_PT1.test(text.replace(JSON.SAFE_PT2, TEXT.AT).replace(JSON.SAFE_PT3, JSON.ARRAY.END).replace(JSON.SAFE_PT4, TEXT.EMPTY)));
6800 return eval(TEXT.OPEN + text + TEXT.CLOSE);
6802 catch(e) {/* continue to error */}
6805 throw new Error("glow.data.decodeJson: cannot decode item");
6808 @name glow.data.escapeHTML
6810 @description Escape HTML entities.
6812 Returns a string with HTML entities escaped.
6814 @param {String} string The string to be escaped.
6819 // useful for protecting against XSS attacks:
6820 var fieldName = '" onclick="alert(\'hacked\')" name="';
6822 // but should be used in all cases like this:
6823 glow.dom.create('<input name="' + glow.data.escapeHTML(untrustedString) + '"/>');
6825 escapeHTML : function (html) {
6826 return glow.dom.create('<div></div>').text(html).html();
6834 @description Sending data to & from the server
6835 @see <a href="../furtherinfo/net/net.shtml">Using glow.net</a>
6837 (window.gloader || glow).module({
6839 library: ["glow", "1.7.0"],
6840 depends: [["glow", "1.7.0", "glow.data", "glow.events"]],
6841 builder: function(glow) {
6845 XML_ERR:"Cannot get response as XML, check the mime type of the data",
6846 POST_DEFAULT_CONTENT_TYPE:'application/x-www-form-urlencoded;'
6848 endsPlusXml = /\+xml$/,
6850 * @name glow.net.scriptElements
6852 * @description Script elements that have been added via {@link glow.net.loadScript loadScript}
6855 scriptElements = [],
6857 * @name glow.net.callbackPrefix
6859 * @description Callbacks in _jsonCbs will be named this + a number
6862 callbackPrefix = "c",
6864 * @name glow.net.globalObjectName
6866 * @description Name of the global object used to store loadScript callbacks
6869 globalObjectName = "_" + glow.UID + "loadScriptCbs",
6871 events = glow.events,
6872 emptyFunc = function(){};
6875 * @name glow.net.xmlHTTPRequest
6878 * @description Creates an xmlHTTPRequest transport
6881 function xmlHTTPRequest() {
6882 //try IE first. IE7's xmlhttprequest and XMLHTTP6 are broken. Avoid avoid avoid!
6883 if (window.ActiveXObject) {
6884 return (xmlHTTPRequest = function() { return new ActiveXObject("Microsoft.XMLHTTP"); })();
6886 return (xmlHTTPRequest = function() { return new XMLHttpRequest(); })();
6891 * @name glow.net.populateOptions
6894 * @description Adds defaults to get / post option object
6895 * @param {Object} opts Object to add defaults to
6898 function populateOptions(opts) {
6899 return glow.lang.apply(
6916 PrivateMethod: noCacheUrl
6917 Adds random numbers to the querystring of a url so the browser doesn't use a cached version
6920 function noCacheUrl(url) {
6921 return [url, (/\?/.test(url) ? "&" : "?"), "a", new Date().getTime(), parseInt(Math.random()*100000)].join("");
6925 PrivateMethod: makeXhrRequest
6926 Makes an http request
6929 * @name glow.net.makeXhrRequest
6932 * @description Makes an xhr http request
6933 * @param {String} method HTTP Method
6934 * @param {String} url URL of the request
6935 * @param {Object} Options, see options for {@link glow.net.get}
6938 function makeXhrRequest(method, url, opts) {
6939 var req = xmlHTTPRequest(), //request object
6940 data = opts.data && (typeof opts.data == "string" ? opts.data : glow.data.encodeUrl(opts.data)),
6942 request = new Request(req, opts);
6944 if (!opts.useCache) {
6945 url = noCacheUrl(url);
6948 //open needs to go first to maintain cross-browser support for readystates
6949 req.open(method, url, opts.async);
6951 //add custom headers
6952 for (i in opts.headers) {
6953 req.setRequestHeader(i, opts.headers[i]);
6957 request.send = emptyFunc;
6959 //sort out the timeout if there is one
6961 request._timeout = setTimeout(function() {
6962 abortRequest(request);
6963 var response = new Response(req, true, request);
6964 events.fire(request, "error", response);
6965 }, opts.timeout * 1000);
6968 req.onreadystatechange = function() {
6969 if (req.readyState == 4) {
6971 request._timeout && clearTimeout(request._timeout);
6973 request.completed = true;
6974 var response = new Response(req, false, request);
6975 if (response.wasSuccessful) {
6976 events.fire(request, "load", response);
6978 events.fire(request, "error", response);
6980 // prevent parent scopes leaking (cross-page) in IE
6981 req.onreadystatechange = new Function();
6988 request.completed = true;
6989 var response = new Response(req, false, request);
6990 if (response.wasSuccessful) {
6991 events.fire(request, "load", response);
6993 events.fire(request, "error", response);
6999 request.send = send;
7000 return opts.defer ? request : send();
7004 var r = {}; //the module
7009 @description Makes an HTTP GET request to a given url
7012 Url to make the request to. This can be a relative path. You cannot make requests
7013 for files on other domains, to do that you must put your data in a javascript
7014 file and use {@link glow.net.loadScript} to fetch it.
7015 @param {Object} opts
7016 Options Object of options.
7017 @param {Function} [opts.onLoad] Callback to execute when the request has sucessfully loaded
7018 The callback is passed a Response object as its first parameter.
7019 @param {Function} [opts.onError] Callback to execute if the request was unsucessful
7020 The callback is passed a Response object as its first parameter.
7021 This callback will also be run if a request times out.
7022 @param {Function} [opts.onAbort] Callback to execute if the request is aborted
7023 @param {Object} [opts.headers] A hash of headers to send along with the request
7024 Eg {"Accept-Language": "en-gb"}
7025 @param {Boolean} [opts.async=true] Should the request be performed asynchronously?
7026 @param {Boolean} [opts.useCache=false] Allow a cached response
7027 If false, a random number is added to the query string to ensure a fresh version of the file is being fetched
7028 @param {Number} [opts.timeout] Time to allow for the request in seconds
7029 No timeout is set by default. Only applies for async requests. Once
7030 the time is reached, the error event will fire with a "408" status code.
7031 @param {Boolean} [opts.defer=false] Do not send the request straight away
7032 Deferred requests need to be triggered later using myRequest.send()
7033 @param {Boolean} [opts.forceXml=false] Treat the response as XML.
7034 This will allow you to use {@link glow.net.Response#xml response.xml()}
7035 even if the response has a non-XML mime type.
7037 @returns {glow.net.Request|glow.net.Response}
7038 A response object for non-defered sync requests, otherwise a
7039 request object is returned
7042 var request = glow.net.get("myFile.html", {
7043 onLoad: function(response) {
7044 alert("Got file:\n\n" + response.text());
7046 onError: function(response) {
7047 alert("Error getting file: " + response.statusText());
7051 r.get = function(url, o) {
7052 o = populateOptions(o);
7053 return makeXhrRequest('GET', url, o);
7059 @description Makes an HTTP POST request to a given url
7062 Url to make the request to. This can be a relative path. You cannot make requests
7063 for files on other domains, to do that you must put your data in a javascript
7064 file and use {@link glow.net.loadScript} to fetch it.
7065 @param {Object|String} data
7066 Data to post, either as a JSON-style object or a urlEncoded string
7067 @param {Object} opts
7068 Same options as {@link glow.net.get}
7070 @returns {Number|glow.net.Response}
7071 An integer identifying the async request, or the response object for sync requests
7074 var postRef = glow.net.post("myFile.html",
7075 {key:"value", otherkey:["value1", "value2"]},
7077 onLoad: function(response) {
7078 alert("Got file:\n\n" + response.text());
7080 onError: function(response) {
7081 alert("Error getting file: " + response.statusText());
7086 r.post = function(url, data, o) {
7087 o = populateOptions(o);
7089 if (!o.headers["Content-Type"]) {
7090 o.headers["Content-Type"] = STR.POST_DEFAULT_CONTENT_TYPE;
7092 return makeXhrRequest('POST', url, o);
7096 @name glow.net.loadScript
7098 @description Loads data by adding a script element to the end of the page
7099 This can be used cross domain, but should only be used with trusted
7100 sources as any javascript included in the script will be executed.
7103 Url of the script. Use "{callback}" in the querystring as the callback
7104 name if the data source supports it, then you can use the options below
7105 @param {Object} [opts]
7106 An object of options to use if "{callback}" is specified in the url.
7107 @param {Function} [opts.onLoad] Called when loadScript succeeds.
7108 The parameters are passed in by the external data source
7109 @param {Function} [opts.onError] Called on timeout
7110 No parameters are passed
7111 @param {Function} [opts.onAbort] Called if the request is aborted
7112 @param {Boolean} [opts.useCache=false] Allow a cached response
7113 @param {Number} [opts.timeout] Time to allow for the request in seconds
7114 @param {String} [opts.charset] Charset attribute value for the script
7116 @returns {glow.net.Request}
7119 glow.net.loadScript("http://www.server.com/json/tvshows.php?jsoncallback={callback}", {
7120 onLoad: function(data) {
7121 alert("Data loaded");
7125 r.loadScript = function(url, opts) {
7127 var newIndex = scriptElements.length,
7128 //script element that gets inserted on the page
7130 //generated name of the callback, may not be used
7131 callbackName = callbackPrefix + newIndex,
7132 opts = populateOptions(opts),
7133 request = new Request(newIndex, opts),
7134 url = opts.useCache ? url : noCacheUrl(url),
7135 //the global property used to hide callbacks
7136 globalObject = window[globalObjectName] || (window[globalObjectName] = {});
7139 if (opts.onLoad != emptyFunc) {
7140 globalObject[callbackName] = function() {
7142 request._timeout && clearTimeout(request._timeout);
7144 request.completed = true;
7145 // call the user's callback
7146 opts.onLoad.apply(this, arguments);
7147 // cleanup references to prevent leaks
7149 script = globalObject[callbackName] = undefined;
7150 delete globalObject[callbackName];
7152 url = glow.lang.interpolate(url, {callback: globalObjectName + "." + callbackName});
7155 script = scriptElements[newIndex] = document.createElement("script");
7158 script.charset = opts.charset;
7162 events.addListener(request, "abort", opts.onAbort);
7164 glow.ready(function() {
7165 //sort out the timeout
7167 request._timeout = setTimeout(function() {
7168 abortRequest(request);
7170 }, opts.timeout * 1000);
7172 //using setTimeout to stop Opera 9.0 - 9.26 from running the loaded script before other code
7173 //in the current script block
7174 if (glow.env.opera) {
7175 setTimeout(function() {
7176 if (script) { //script may have been removed already
7183 //add script to page
7184 document.body.appendChild(script);
7191 * @name glow.net.abortRequest
7194 * @description Aborts the request
7195 * Doesn't trigger any events
7197 * @param {glow.net.Request} req Request Object
7200 function abortRequest(req) {
7201 var nativeReq = req.nativeRequest,
7202 callbackIndex = req._callbackIndex;
7205 req._timeout && clearTimeout(req._timeout);
7206 //different if request came from loadScript
7209 // prevent parent scopes leaking (cross-page) in IE
7210 nativeReq.onreadystatechange = new Function();
7212 } else if (callbackIndex) {
7214 window[globalObjectName][callbackPrefix + callbackIndex] = emptyFunc;
7215 //remove script element
7216 glow.dom.get(scriptElements[callbackIndex]).destroy();
7221 * @name glow.net.Request
7223 * @description Returned by {@link glow.net.post post}, {@link glow.net.get get} async requests and {@link glow.net.loadScript loadScript}
7224 * @glowPrivateConstructor There is no direct constructor, since {@link glow.net.post post} and {@link glow.net.get get} create the instances.
7228 * @name glow.net.Request#event:load
7230 * @param {glow.events.Event} event Event Object
7231 * @description Fired when the request is sucessful
7232 * For a get / post request, this will be fired when request returns
7233 * with an HTTP code of 2xx. loadScript requests will fire 'load' only
7234 * if {callback} is used in the URL.
7238 * @name glow.net.Request#event:abort
7240 * @param {glow.events.Event} event Event Object
7241 * @description Fired when the request is aborted
7242 * If you cancel the default (eg, by returning false) the request
7244 * @description Returned by {@link glow.net.post glow.net.post}, {@link glow.net.get glow.net.get} async requests and {@link glow.net.loadScript glow.net.loadScript}
7245 * @see <a href="../furtherinfo/net/net.shtml">Using glow.net</a>
7246 * @glowPrivateConstructor There is no direct constructor, since {@link glow.net.post glow.net.post} and {@link glow.net.get glow.net.get} create the instances.
7250 * @name glow.net.Request#event:error
7252 * @param {glow.events.Event} event Event Object
7253 * @description Fired when the request is unsucessful
7254 * For a get/post request, this will be fired when request returns
7255 * with an HTTP code which isn't 2xx or the request times out. loadScript
7256 * calls will fire 'error' only if the request times out.
7261 We don't want users to create instances of this class, so the constructor is documented
7262 out of view of jsdoc
7264 @param {Object} requestObj
7265 Object which represents the request type.
7266 For XHR requests it should be an XmlHttpRequest object, for loadScript
7267 requests it should be a number, the Index of the callback in glow.net._jsonCbs
7268 @param {Object} opts
7269 Zero or more of the following as properties of an object:
7270 @param {Function} [opts.onLoad] Called when the request is sucessful
7271 @param {Function} [opts.onError] Called when a request is unsucessful
7272 @param {Function} [opts.onAbort] Called when a request is aborted
7275 function Request(requestObj, opts) {
7277 * @name glow.net.Request#_timeout
7279 * @description timeout ID. This is set by makeXhrRequest or loadScript
7282 this._timeout = null;
7285 @name glow.net.Request#_forceXml
7288 @description Force the response to be treated as xml
7290 this._forceXml = opts.forceXml;
7292 // force the reponse to be treated as xml
7293 // IE doesn't support overrideMineType, we need to deal with that in {@link glow.net.Response#xml}
7294 if (opts.forceXml && requestObj.overrideMimeType) {
7295 requestObj.overrideMimeType('application/xml');
7299 * @name glow.net.Request#complete
7300 * @description Boolean indicating whether the request has completed
7302 // request.complete with an asynchronous call
7303 var request = glow.net.get(
7307 onload: function(response) {
7308 alert(request.complete); // returns true
7312 alert(request.complete); // returns boolean depending on timing of asynchronous call
7314 // request.complete with a synchronous call
7315 var request = glow.net.get("myFile.html", {async: false;});
7316 alert(request.complete); // returns true
7319 this.complete = false;
7321 if (typeof requestObj == "number") {
7323 * @name glow.net.Request#_callbackIndex
7325 * @description Index of the callback in glow.net._jsonCbs
7326 * This is only relavent for requests made via loadscript using the
7327 * {callback} placeholder
7330 this._callbackIndex = requestObj;
7333 * @name glow.net.Request#nativeRequest
7334 * @description The request object from the browser.
7335 * This may not have the same properties and methods across user agents.
7336 * Also, this will be undefined if the request originated from loadScript.
7338 var request = glow.net.get(
7342 onload: function(response) {
7343 alert(request.NativeObject); // returns Object()
7349 this.nativeRequest = requestObj;
7353 var eventNames = ["Load", "Error", "Abort"], i=0;
7355 for (; i < 3; i++) {
7356 events.addListener(this, eventNames[i].toLowerCase(), opts["on" + eventNames[i]]);
7360 Request.prototype = {
7362 @name glow.net.Request#send
7364 @description Sends the request.
7365 This is done automatically unless the defer option is set
7367 var request = glow.net.get(
7370 onload : function(response) {alert("Loaded");},
7374 request.send(); // returns "Loaded"
7376 This for async requests or a response object for sync requests
7378 //this function is assigned by makeXhrRequest
7379 send: function() {},
7381 * @name glow.net.Request#abort
7383 * @description Aborts an async request
7384 * The load & error events will not fire. If the request has been
7385 * made using {@link glow.net.loadScript loadScript}, the script
7386 * may still be loaded but the callback will not be fired.
7388 var request = glow.net.get(
7393 onabort: function() {
7394 alert("Something bad happened. The request was aborted.");
7398 request.abort(); // returns "Something bad happened. The request was aborted"
7402 if (!this.completed && !events.fire(this, "abort").defaultPrevented()) {
7408 @name glow.net.Request#destroy
7410 @description Release memory from a {@link glow.net.loadScript} call.
7412 This is called automatically by {@link glow.net.loadScript loadScript}
7413 calls that have {callback} in the URL. However, if you are not using
7414 {callback}, you can use this method manually to release memory when
7415 the request has finished.
7418 var request = glow.net.loadScript('http://www.bbc.co.uk/whatever.js');
7422 destroy: function() {
7423 if (this._callbackIndex !== undefined) {
7424 // set timeout is used here to prevent a crash in IE7 (possibly other version) when the script is from the filesystem
7425 setTimeout(function() {
7426 $( scriptElements[this._callbackIndex] ).destroy();
7427 scriptElements[this._callbackIndex] = undefined;
7428 delete scriptElements[this._callbackIndex];
7436 @name glow.net.Response
7438 @description Provided in callbacks to {@link glow.net.post glow.net.post} and {@link glow.net.get glow.net.get}
7439 @see <a href="../furtherinfo/net/net.shtml">Using glow.net</a>
7441 @glowPrivateConstructor There is no direct constructor, since {@link glow.net.post glow.net.post} and {@link glow.net.get glow.net.get} create the instances.
7444 These params are hidden as we don't want users to try and create instances of this...
7446 @param {XMLHttpRequest} nativeResponse
7447 @param {Boolean} [timedOut=false] Set to true if the response timed out
7448 @param {glow.net.Request} [request] Original request object
7450 function Response(nativeResponse, timedOut, request) {
7451 //run Event constructor
7452 events.Event.call(this);
7455 @name glow.net.Response#_request
7457 @description Original request object
7458 @type glow.net.Request
7460 this._request = request;
7463 @name glow.net.Response#nativeResponse
7464 @description The response object from the browser.
7465 This may not have the same properties and methods across user agents.
7468 this.nativeResponse = nativeResponse;
7470 @name glow.net.Response#status
7471 @description HTTP status code of the response
7474 //IE reports status as 1223 rather than 204, for laffs
7475 this.status = timedOut ? 408 :
7476 nativeResponse.status == 1223 ? 204 : nativeResponse.status;
7479 * @name glow.net.Response#timedOut
7480 * @description Boolean indicating if the requests time out was reached.
7483 this.timedOut = !!timedOut;
7486 * @name glow.net.Response#wasSuccessful
7487 * @description Boolean indicating if the request returned successfully.
7490 this.wasSuccessful = (this.status >= 200 && this.status < 300) ||
7492 this.status == 304 ||
7493 //watch our for requests from file://
7494 (this.status == 0 && nativeResponse.responseText);
7499 @name glow.net-shouldParseAsXml
7501 @description Should the response be treated as xml? This function is used by IE only
7502 'this' is the response object
7505 function shouldParseAsXml() {
7506 var contentType = this.header("Content-Type");
7507 // IE 6 & 7 fail to recognise Content-Types ending +xml (eg application/rss+xml)
7508 // Files from the filesystem don't have a content type, but could be xml files, parse them to be safe
7509 return endsPlusXml.test(contentType) || contentType === '';
7512 //don't want to document this inheritance, it'll just confuse the user
7513 glow.lang.extend(Response, events.Event, {
7515 @name glow.net.Response#text
7517 @description Gets the body of the response as plain text
7522 return this.nativeResponse.responseText;
7525 @name glow.net.Response#xml
7527 @description Gets the body of the response as xml
7532 var nativeResponse = this.nativeResponse;
7535 // IE fails to recognise the doc as XML in some cases
7536 ( glow.env.ie && shouldParseAsXml.call(this) )
7537 // If the _forceXml option is set, we need to turn the response text into xml
7538 || ( this._request._forceXml && !this._request.nativeRequest.overrideMimeType && window.ActiveXObject )
7540 var doc = new ActiveXObject("Microsoft.XMLDOM");
7541 doc.loadXML( nativeResponse.responseText );
7545 // check property exists
7546 if (!nativeResponse.responseXML) {
7547 throw new Error(STR.XML_ERR);
7549 return nativeResponse.responseXML;
7554 @name glow.net.Response#json
7556 @description Gets the body of the response as a json object
7558 @param {Boolean} [safeMode=false]
7559 If true, the response will be parsed using a string parser which
7560 will filter out non-JSON javascript, this will be slower but
7561 recommended if you do not trust the data source.
7565 json: function(safe) {
7566 return glow.data.decodeJson(this.text(), {safeMode:safe});
7570 @name glow.net.Response#header
7572 @description Gets a header from the response
7574 @param {String} name
7580 @example var contentType = myResponse.header("Content-Type");
7582 header: function(name) {
7583 return this.nativeResponse.getResponseHeader(name);
7587 @name glow.net.Response#statusText
7589 @description Gets the meaning of {@link glow.net.Response#status myResponse.status}
7593 statusText: function() {
7594 return this.timedOut ? "Request Timeout" : this.nativeResponse.statusText;
7605 @description Functions for modifying animations
7606 @see <a href="../furtherinfo/tweens">What are tweens?</a>
7609 (window.gloader || glow).module({
7610 name: "glow.tweens",
7611 library: ["glow", "1.7.0"],
7613 builder: function(glow) {
7616 PrivateMethod: _reverse
7617 Takes a tween function and returns a function which does the reverse
7619 function _reverse(tween) {
7620 return function(t) {
7621 return 1 - tween(1 - t);
7627 @name glow.tweens.linear
7629 @description Returns linear tween.
7631 Will transition values from start to finish with no
7632 acceleration or deceleration.
7636 linear: function() {
7637 return function(t) { return t; };
7640 @name glow.tweens.easeIn
7642 @description Creates a tween which starts off slowly and accelerates.
7644 @param {Number} [strength=2] How strong the easing is.
7646 A higher number means the animation starts off slower and
7651 easeIn: function(strength) {
7652 strength = strength || 2;
7653 return function(t) {
7654 return Math.pow(1, strength - 1) * Math.pow(t, strength);
7658 @name glow.tweens.easeOut
7660 @description Creates a tween which starts off fast and decelerates.
7662 @param {Number} [strength=2] How strong the easing is.
7664 A higher number means the animation starts off faster and
7669 easeOut: function(strength) {
7670 return _reverse(this.easeIn(strength));
7673 @name glow.tweens.easeBoth
7675 @description Creates a tween which starts off slowly, accelerates then decelerates after the half way point.
7677 This produces a smooth and natural looking transition.
7679 @param {Number} [strength=2] How strong the easing is.
7681 A higher number produces a greater difference between
7682 start / end speed and the mid speed.
7686 easeBoth: function(strength) {
7687 return this.combine(this.easeIn(strength), this.easeOut(strength));
7690 @name glow.tweens.overshootIn
7692 @description Returns the reverse of {@link glow.tweens.overshootOut overshootOut}
7694 @param {Number} [amount=1.70158] How much to overshoot.
7696 The default is 1.70158 which results in a 10% overshoot.
7700 overshootIn: function(amount) {
7701 return _reverse(this.overshootOut(amount));
7704 @name glow.tweens.overshootOut
7706 @description Creates a tween which overshoots its end point then returns to its end point.
7708 @param {Number} [amount=1.70158] How much to overshoot.
7710 The default is 1.70158 which results in a 10% overshoot.
7714 overshootOut: function(amount) {
7715 amount = amount || 1.70158;
7716 return function(t) {
7717 if (t == 0 || t == 1) { return t; }
7718 return ((t -= 1)* t * ((amount + 1) * t + amount) + 1);
7722 @name glow.tweens.overshootBoth
7724 @description Returns a combination of {@link glow.tweens.overshootIn overshootIn} and {@link glow.tweens.overshootOut overshootOut}
7726 @param {Number} [amount=1.70158] How much to overshoot.
7728 The default is 1.70158 which results in a 10% overshoot.
7732 overshootBoth: function(amount) {
7733 return this.combine(this.overshootIn(amount), this.overshootOut(amount));
7736 @name glow.tweens.bounceIn
7738 @description Returns the reverse of {@link glow.tweens.bounceOut bounceOut}
7742 bounceIn: function() {
7743 return _reverse(this.bounceOut());
7746 @name glow.tweens.bounceOut
7748 @description Returns a tween which bounces against the final value 3 times before stopping
7752 bounceOut: function() {
7753 return function(t) {
7754 if (t < (1 / 2.75)) {
7755 return 7.5625 * t * t;
7756 } else if (t < (2 / 2.75)) {
7757 return (7.5625 * (t -= (1.5 / 2.75)) * t + .75);
7758 } else if (t < (2.5 / 2.75)) {
7759 return (7.5625 * (t -= (2.25 / 2.75)) * t + .9375);
7761 return (7.5625 * (t -= (2.625 / 2.75)) * t + .984375);
7766 @name glow.tweens.bounceBoth
7768 @description Returns a combination of {@link glow.tweens.bounceIn bounceIn} and {@link glow.tweens.bounceOut bounceOut}
7772 bounceBoth: function() {
7773 return this.combine(this.bounceIn(), this.bounceOut());
7776 @name glow.tweens.elasticIn
7778 @description Returns the reverse of {@link glow.tweens.elasticOut elasticOut}
7780 @param {Number} [amplitude=1] How strong the elasticity is.
7782 @param {Number} [period=0.3] The frequency period.
7786 elasticIn: function(a, p) {
7787 return _reverse(this.elasticOut(a, p));
7790 @name glow.tweens.elasticOut
7792 @description Creates a tween which has an elastic movement.
7794 You can tweak the tween using the parameters but you'll
7795 probably find the defaults sufficient.
7797 @param {Number} [amplitude=1] How strong the elasticity is.
7799 @param {Number} [period=0.3] The frequency period.
7803 elasticOut: function(a, p) {
7804 return function (t) {
7805 if (t == 0 || t == 1) {
7815 var s = p / (2 * Math.PI) * Math.asin(1 / a);
7817 return a * Math.pow(2, -10 * t) * Math.sin( (t-s) * (2 * Math.PI) / p) + 1;
7821 @name glow.tweens.elasticBoth
7823 @description Returns a combination of {@link glow.tweens.elasticIn elasticIn} and {@link glow.tweens.elasticOut elasticOut}
7825 @param {Number} [amplitude=1] How strong the elasticity is.
7827 @param {Number} [period=0.3] The frequency period.
7831 elasticBoth: function(a, p) {
7833 return this.combine(this.elasticIn(a, p), this.elasticOut(a, p));
7836 @name glow.tweens.combine
7838 @description Create a tween from two tweens.
7840 This can be useful to make custom tweens which, for example,
7841 start with an easeIn and end with an overshootOut. To keep
7842 the motion natural, you should configure your tweens so the
7843 first ends and the same velocity that the second starts.
7845 @param {Function} tweenIn Tween to use for the first half
7847 @param {Function} tweenOut Tween to use for the second half
7850 // 4.5 has been chosen for the easeIn strength so it
7851 // ends at the same velocity as overshootOut starts.
7852 var myTween = glow.tweens.combine(
7853 glow.tweens.easeIn(4.5),
7854 glow.tweens.overshootOut()
7859 combine: function(tweenIn, tweenOut) {
7860 return function (t) {
7862 return tweenIn(t * 2) / 2;
7864 return tweenOut((t - 0.5) * 2) / 2 + 0.5;
7874 @description Simple and powerful animations.
7875 @requires glow, glow.tweens, glow.events, glow.dom
7876 @see <a href="../furtherinfo/tweens/">What are tweens?</a>
7877 @see <a href="../furtherinfo/anim/">Guide to creating animations, with interactive examples</a>
7879 (window.gloader || glow).module({
7881 library: ["glow", "1.7.0"],
7882 depends: [["glow", "1.7.0", "glow.tweens", "glow.events", "glow.dom"]],
7883 builder: function(glow) {
7885 var $ = glow.dom.get,
7887 events = glow.events,
7890 hasUnits = /width|height|top$|bottom$|left$|right$|spacing$|indent$|font-size/,
7891 noNegatives = /width|height|padding|opacity/,
7892 usesYAxis = /height|top/,
7894 testElement = dom.create('<div style="position:absolute;visibility:hidden"></div>');
7897 Converts event shortcuts in an options object to real events
7898 instance - the object to add events to
7899 opts - the options object containing the listener functions
7900 eventProps - an array of property names of potential listeners, eg ['onFrame', 'onComplete']
7902 function addEventsFromOpts(instance, opts, eventProps) {
7903 for (var i = 0, len = eventProps.length; i < len; i++) {
7904 // does opts.onWhatever exist?
7905 if (opts[ eventProps[i] ]) {
7908 // convert "onWhatever" to "whatever"
7909 eventProps[i].slice(2).toLowerCase(),
7910 opts[ eventProps[i] ]
7917 var queue = [], //running animations
7919 intervalTime = 1, //ms between intervals
7920 interval; //holds the number for the interval
7923 @name glow.anim-manager.addToQueue
7926 @description Adds an animation to the queue.
7928 addToQueue: function(anim) {
7929 //add the item to the queue
7930 queue[queueLen++] = anim;
7931 anim._playing = true;
7932 anim._timeAnchor = anim._timeAnchor || new Date().valueOf();
7934 this.startInterval();
7938 @name glow.anim-manager.removeFromQueue
7941 @description Removes an animation from the queue.
7943 removeFromQueue: function(anim) {
7944 for (var i = 0; i < queueLen; i++) {
7945 if (queue[i] == anim) {
7947 anim._timeAnchor = null;
7948 anim._playing = false;
7949 //stop the queue if there's nothing in it anymore
7950 if (--queueLen == 0) {
7951 this.stopInterval();
7958 @name glow.anim-manager.startInterval
7961 @description Start processing the queue every interval.
7963 startInterval: function() {
7964 interval = window.setInterval(this.processQueue, intervalTime);
7967 @name glow.anim-manager.stopInterval
7970 @description Stop processing the queue.
7972 stopInterval: function() {
7973 window.clearInterval(interval);
7977 @name glow.anim-manager.processQueue
7980 @description Animate each animation in the queue.
7982 processQueue: function() {
7983 var anim, i, now = new Date().valueOf();
7984 for (i = 0; i < queueLen; i++) {
7986 if (anim.position == anim.duration) {
7987 manager.removeFromQueue(anim);
7988 //need to decrement the index because we've just removed an item from the queue
7990 events.fire(anim, "complete");
7991 if (anim._opts.destroyOnComplete) {
7996 if (anim.useSeconds) {
7997 anim.position = (now - anim._timeAnchor) / 1000;
7998 if (anim.position > anim.duration) {
7999 anim.position = anim.duration;
8004 anim.value = anim.tween(anim.position / anim.duration);
8005 events.fire(anim, "frame");
8012 @name glow.anim.convertCssUnit
8015 @param {nodelist} element
8016 @param {string|number} fromValue Assumed pixels.
8017 @param {string} toUnit (em|%|pt...)
8018 @param {string} axis (x|y)
8019 @description Converts a css unit.
8021 We need to know the axis for calculating relative values, since they're
8022 relative to the width / height of the parent element depending
8026 function convertCssUnit(element, fromValue, toUnit, axis) {
8027 var elmStyle = testElement[0].style,
8028 axisProp = (axis == "x") ? "width" : "height",
8031 //reset stuff that may affect the width / height
8032 elmStyle.margin = elmStyle.padding = elmStyle.border = "0";
8033 startPixelValue = testElement.css(axisProp, fromValue).insertAfter(element)[axisProp]();
8034 //using 10 of the unit then dividing by 10 to increase accuracy
8035 toUnitPixelValue = testElement.css(axisProp, 10 + toUnit)[axisProp]() / 10;
8036 testElement.remove();
8037 return startPixelValue / toUnitPixelValue;
8041 @name glow.anim.keepWithinRange
8048 Takes a number then an (optional) lower range and an (optional) upper range. If the number
8049 is outside the range the nearest range boundary is returned, else the number is returned.
8051 function keepWithinRange(num, start, end) {
8052 if (start !== undefined && num < start) {
8055 if (end !== undefined && num > end) {
8062 @name glow.anim.buildAnimFunction
8067 @description Builds a function for an animation.
8069 function buildAnimFunction(element, spec) {
8071 r = ["a=(function(){"],
8074 unitDefault = [0,"px"],
8080 for (cssProp in spec) {
8081 r[rLen++] = 'element.css("' + cssProp + '", ';
8082 //fill in the blanks
8083 if (typeof spec[cssProp] != "object") {
8086 to = spec[cssProp].to;
8088 if ((from = spec[cssProp].from) === undefined) {
8089 if (cssProp == "font-size" || cssProp == "background-position") {
8090 throw new Error("From value must be set for " + cssProp);
8092 from = element.css(cssProp);
8094 //TODO help some multi value things?
8095 if (hasUnits.test(cssProp)) {
8096 //normalise the units for unit-ed values
8097 unit = (getUnit.exec(to) || unitDefault)[1];
8098 fromUnit = (getUnit.exec(from) || unitDefault)[1];
8099 //make them numbers, we have the units seperate
8100 from = parseFloat(from) || 0;
8101 to = parseFloat(to) || 0;
8102 //if the units don't match, we need to have a play
8103 if (from && unit != fromUnit) {
8104 if (cssProp == "font-size") {
8105 throw new Error("Units must be the same for font-size");
8107 from = convertCssUnit(element, from + fromUnit, unit, usesYAxis.test(cssProp) ? "y" : "x");
8109 if (noNegatives.test(cssProp)) {
8110 r[rLen++] = 'keepWithinRange((' + (to - from) + ' * this.value) + ' + from + ', 0) + "' + unit + '"';
8112 r[rLen++] = '(' + (to - from) + ' * this.value) + ' + from + ' + "' + unit + '"';
8114 } else if (! (isNaN(from) || isNaN(to))) { //both pure numbers
8115 from = Number(from);
8117 r[rLen++] = '(' + (to - from) + ' * this.value) + ' + from;
8118 } else if (cssProp.indexOf("color") != -1) {
8119 to = dom.parseCssColor(to);
8120 if (! glow.lang.hasOwnProperty(from, "r")) {
8121 from = dom.parseCssColor(from);
8123 r[rLen++] = '"rgb(" + keepWithinRange(Math.round(' + (to.r - from.r) + ' * this.value + ' + from.r +
8124 '), 0, 255) + "," + keepWithinRange(Math.round(' + (to.g - from.g) + ' * this.value + ' + from.g +
8125 '), 0, 255) + "," + keepWithinRange(Math.round(' + (to.b - from.b) + ' * this.value + ' + from.b +
8127 } else if (cssProp == "background-position") {
8129 fromTo = ["from", "to"],
8130 unit = (getUnit.exec(from) || unitDefault)[1];
8131 vals.fromOrig = from.toString().split(/\s/);
8132 vals.toOrig = to.toString().split(/\s/);
8134 if (vals.fromOrig[1] === undefined) {
8135 vals.fromOrig[1] = "50%";
8137 if (vals.toOrig[1] === undefined) {
8138 vals.toOrig[1] = "50%";
8141 for (var i = 0; i < 2; i++) {
8142 vals[fromTo[i] + "X"] = parseFloat(vals[fromTo[i] + "Orig"][0]);
8143 vals[fromTo[i] + "Y"] = parseFloat(vals[fromTo[i] + "Orig"][1]);
8144 vals[fromTo[i] + "XUnit"] = (getUnit.exec(vals[fromTo[i] + "Orig"][0]) || unitDefault)[1];
8145 vals[fromTo[i] + "YUnit"] = (getUnit.exec(vals[fromTo[i] + "Orig"][1]) || unitDefault)[1];
8148 if ((vals.fromXUnit !== vals.toXUnit) || (vals.fromYUnit !== vals.toYUnit)) {
8149 throw new Error("Mismatched axis units cannot be used for " + cssProp);
8152 r[rLen++] = '(' + (vals.toX - vals.fromX) + ' * this.value + ' + vals.fromX + ') + "' + vals.fromXUnit + ' " + (' +
8153 (vals.toY - vals.fromY) + ' * this.value + ' + vals.fromY + ') + "' + vals.fromYUnit + '"';
8158 return eval(r.join(""));
8162 var r = {}; //return object
8167 @description Animates CSS properties of an element.
8168 @param {String | glow.dom.NodeList | Element} element Element to animate.
8170 This can be a CSS selector (first match will be used),
8171 {@link glow.dom.NodeList} (first node will be used), or a DOM element.
8173 @param {Number} duration Animation duration, in seconds by default.
8175 @param {Object} spec An object describing the properties to animate.
8177 This object should consist of property names corresponding to the
8178 CSS properties you wish to animate, and values which are objects
8179 with 'from' and 'to' properties with the values to animate between
8180 or a number/string representing the value to animate to.
8182 If the 'from' property is absent, the elements current CSS value
8183 will be used instead.
8185 See the spec example below for more information.
8187 @param {Object} opts Optional options object.
8189 @param {Boolean} [opts.useSeconds=true] Specifies whether duration should be in seconds rather than frames.
8191 @param {Function} [opts.tween=linear tween] The way the value moves through time. See {@link glow.tweens}.
8193 @param {Boolean} [opts.destroyOnComplete=false] Destroy the animation once it completes?
8194 This will free any DOM references the animation may have created. Once
8195 the animation completes, you won't be able to start it again.
8197 @param {Function} [opts.onStart] Shortcut for adding a "start" event listener
8198 @param {Function} [opts.onFrame] Shortcut for adding a "frame" event listener
8199 @param {Function} [opts.onStop] Shortcut for adding a "stop" event listener
8200 @param {Function} [opts.onComplete] Shortcut for adding a "complete" event listener
8201 @param {Function} [opts.onResume] Shortcut for adding a "resume" event listener
8204 // an example of an spec object
8206 "height": {from: "10px", to: "100px"},
8208 "font-size": {from: "0.5em", to: "1.3em"}
8212 // animate an elements height and opacity to 0 from current values over 1 second
8213 glow.anim.css("#myElement", 1, {
8218 @returns {glow.anim.Animation}
8220 r.css = function(element, duration, spec, opts) {
8222 element = get(element);
8224 var anim = new r.Animation(duration, opts);
8226 // Fix for trac 156 - glow.anim.css should fail better if the element doesn't exist
8228 events.addListener(anim, "frame", buildAnimFunction(element, spec));
8234 @name glow.anim-slideElement
8237 @param {String | glow.dom.NodeList} element Element to animate. CSS Selector can be used.
8238 @param {Number} duration Animation duration in seconds.
8239 @param {Function} [opts.tween=easeBoth tween] The way the value moves through time. See {@link glow.tweens}.
8240 @param {Function} [opts.onStart] The function to be called when the first element in the NodeList starts the animation.
8241 @param {Function} [opts.onComplete] The function to be called when the first element in the NodeList completes the animation.
8242 @description Builds a function for an animation.
8244 slideElement = function slideElement(element, duration, action, opts) {
8245 duration = duration || 0.5;
8246 // normalise 'element' to NodeList
8247 element = $(element);
8249 opts = glow.lang.apply({
8250 tween: glow.tweens.easeBoth(),
8251 onStart: function(){},
8252 onComplete: function(){}
8256 thatlength = element.length,
8262 for(; i < thatlength; i++) {
8263 if (action == "up" || (action == "toggle" && element.slice(i, i+1).height() > 0)) {
8264 element[i].style.overflow = 'hidden';
8265 // give the element layout in IE
8266 if (glow.env.ie < 8) {
8267 element[i].style.zoom = 1;
8270 fromHeight = element.slice(i, i+1).height();
8271 } else if (action == "down" || (action == "toggle" && element.slice(i, i+1).height() == 0)) {
8272 fromHeight = element.slice(i, i+1).height();
8273 element[i].style.height = "auto";
8274 completeHeight = element.slice(i, i+1).height();
8275 element[i].style.height = fromHeight + "px";
8279 glow.anim.css(element[i], duration, {
8280 'height': {from: fromHeight, to: completeHeight}
8281 }, { tween: opts.tween })
8286 timeline = new glow.anim.Timeline(channels, {
8287 destroyOnComplete: true
8290 events.addListener(timeline, "complete", function() {
8291 // return heights to "auto" for slide down
8292 element.each(function() {
8293 if (this.style.height != "0px") {
8294 this.style.height = "auto";
8299 events.addListener(timeline, "start", opts.onStart);
8300 events.addListener(timeline, "complete", opts.onComplete);
8302 // return & start our new timeline
8303 return timeline.start();
8307 @name glow.anim.slideDown
8309 @description Slide a NodeList down from a height of 0
8311 @param {String | glow.dom.NodeList} element Element to animate. CSS Selector can be used.
8313 @param {Number} duration Animation duration in seconds.
8315 @param {Function} opts Object
8317 @param {Function} [opts.tween=easeBoth tween] The way the value moves through time. See {@link glow.tweens}.
8319 @param {Function} [opts.onStart] The function to be called when the first element in the NodeList starts the animation.
8321 @param {Function} [opts.onComplete] The function to be called when the first element in the NodeList completes the animation.
8323 @returns {glow.anim.Timeline}
8329 glow.anim.slideDown("#menu", 1);
8332 r.slideDown = function(element, duration, opts) {
8333 return slideElement(element, duration, 'down', opts);
8337 @name glow.anim.slideUp
8339 @description Slide a NodeList up to a height of 0
8341 @param {String | glow.dom.NodeList} element Element to animate. CSS Selector can be used.
8343 @param {Number} duration Animation duration in seconds.
8345 @param {Function} opts Object
8347 @param {Function} [opts.tween=easeBoth tween] The way the value moves through time. See {@link glow.tweens}.
8349 @param {Function} [opts.onStart] The function to be called when the first element in the NodeList starts the animation.
8351 @param {Function} [opts.onComplete] The function to be called when the first element in the NodeList completes the animation.
8353 @returns {glow.anim.Timeline}
8359 glow.anim.slideUp("#menu", 1);
8362 r.slideUp = function(element, duration, opts) {
8363 return slideElement(element, duration, 'up', opts);
8367 @name glow.anim.slideToggle
8369 @description Toggle a NodeList Up or Down depending on it's present state.
8371 @param {String | glow.dom.NodeList} element Element to animate. CSS Selector can be used.
8373 @param {Number} duration Animation duration in seconds.
8375 @param {Function} opts Object
8377 @param {Function} [opts.tween=easeBoth tween] The way the value moves through time. See {@link glow.tweens}.
8379 @param {Function} [opts.onStart] The function to be called when the first element in the NodeList starts the animation.
8381 @param {Function} [opts.onComplete] The function to be called when the first element in the NodeList completes the animation.
8383 @returns {glow.anim.Timeline}
8389 glow.anim.slideToggle("#menu", 1);
8392 r.slideToggle = function(element, duration, opts) {
8393 return slideElement(element, duration, 'toggle', opts);
8398 @name glow.anim.fadeOut
8400 @description Fade out a set of elements
8402 @param {String | glow.dom.NodeList} element Element to animate. CSS Selector can be used.
8404 @param {Number} duration Animation duration in seconds.
8406 @param {Function} opts Object
8408 @param {Function} [opts.tween=easeBoth tween] The way the value moves through time. See {@link glow.tweens}.
8410 @param {Function} [opts.onStart] The function to be called when the first element in the NodeList starts the animation.
8412 @param {Function} [opts.onComplete] The function to be called when the first element in the NodeList completes the animation.
8414 @returns {glow.anim.Timeline}
8420 glow.anim.fadeOut("#menu", 1);
8423 r.fadeOut = function(element, duration, opts) {
8424 return r.fadeTo(element, 0, duration, opts)
8428 @name glow.anim.fadeIn
8430 @description Fade in a set of elements
8432 @param {String | glow.dom.NodeList} element Element to animate. CSS Selector can be used.
8434 @param {Number} duration Animation duration in seconds.
8436 @param {Function} opts Object
8438 @param {Function} [opts.tween=easeBoth tween] The way the value moves through time. See {@link glow.tweens}.
8440 @param {Function} [opts.onStart] The function to be called when the first element in the NodeList starts the animation.
8442 @param {Function} [opts.onComplete] The function to be called when the first element in the NodeList completes the animation.
8444 @returns {glow.anim.Timeline}
8450 glow.anim.fadeIn("#menu", 1);
8453 r.fadeIn = function(element, duration, opts){
8454 r.fadeTo(element, 1, duration, opts);
8458 @name glow.anim.fadeTo
8460 @description Fade a set of elements to a given opacity
8462 @param {String | glow.dom.NodeList} element Element to animate. CSS Selector can be used.
8464 @param {Number} opacity fade to opacity level between 0 & 1.
8466 @param {Number} duration Animation duration in seconds.
8468 @param {Function} opts Object
8470 @param {Function} [opts.tween=easeBoth tween] The way the value moves through time. See {@link glow.tweens}.
8472 @param {Function} [opts.onStart] The function to be called when the first element in the NodeList starts the animation.
8474 @param {Function} [opts.onComplete] The function to be called when the first element in the NodeList completes the animation.
8476 @returns {glow.anim.Timeline}
8482 glow.anim.fadeTo("#menu", 0.5, 1);
8485 r.fadeTo = function(element, opacity, duration, opts){
8486 duration = duration || 0.5;
8487 // normalise 'element' to NodeList
8488 element = $(element);
8490 opts = glow.lang.apply({
8491 tween: glow.tweens.easeBoth(),
8492 onStart: function(){},
8493 onComplete: function(){}
8497 thatlength = element.length,
8501 for(; i < thatlength; i++) {
8503 glow.anim.css(element[i], duration, {
8505 }, { tween: opts.tween })
8509 timeline = new glow.anim.Timeline(channels, {
8510 destroyOnComplete: true
8513 events.addListener(timeline, "start", opts.onStart);
8514 events.addListener(timeline, "complete", opts.onComplete);
8516 // return & start our new timeline
8517 return timeline.start();
8522 @name glow.anim.highlight
8524 @description Highlight an element by fading the background colour
8526 @param {String | glow.dom.NodeList} element Element to animate. CSS Selector can be used.
8528 @param {String} highlightColour highlight colour in hex, "rgb(r, g, b)" or css colour name.
8530 @param {Number} duration Animation duration in seconds.
8532 @param {Function} opts Object
8534 @param {Function} [opts.completeColour] The background colour of the element once the highlight is complete.
8536 If none supplied Glow assumes the element's existing background color (e.g. #336699),
8537 if the element has no background color specified (e.g. Transparent)
8538 the highlight will transition to white.
8540 @param {Function} [opts.tween=easeBoth tween] The way the value moves through time. See {@link glow.tweens}.
8542 @param {Function} [opts.onStart] The function to be called when the first element in the NodeList starts the animation.
8544 @param {Function} [opts.onComplete] The function to be called when the first element in the NodeList completes the animation.
8546 @returns {glow.anim.Timeline}
8552 glow.anim.highlight("#textInput", "#ff0", 1);
8554 r.highlight = function(element, highlightColour, duration, opts){
8555 // normalise element
8556 element = $(element);
8558 duration = duration || 1;
8559 highlightColour = highlightColour || '#ffff99';
8561 opts = glow.lang.apply({
8562 tween: glow.tweens.easeBoth(),
8563 onStart: function(){},
8564 onComplete: function(){}
8569 elmsLength = element.length,
8574 for(; i < elmsLength; i++) {
8576 completeColour = opts.completeColour || element.slice(i, i+1).css("background-color");
8578 if (completeColour == "transparent" || completeColour == "") {
8579 completeColour = "#fff";
8582 r.css(element[i], duration, {
8583 "background-color" : {from:highlightColour, to:completeColour}
8584 }, {tween: opts.tween})
8588 timeline = new glow.anim.Timeline(channels, {
8589 destroyOnComplete: true
8592 events.addListener(timeline, "start", opts.onStart);
8593 events.addListener(timeline, "complete", opts.onComplete);
8594 return timeline.start();
8598 @name glow.anim.Animation
8600 @description Controls modifying values over time.
8602 You can create an animtion instance using the constructor, or use
8603 one of the helper methods in {@link glow.anim}.
8605 Once you have created your animation instance, you can use
8606 events such as "frame" to change values over time.
8608 @param {Number} duration Length of the animation in seconds / frames.
8610 Animations which are given a duration in seconds may drop frames to
8611 finish in the given time.
8613 @param {Object} opts Object of options.
8615 @param {Boolean} [opts.useSeconds=true] Specifies whether duration should be in seconds rather than frames.
8617 @param {Function} [opts.tween=linear tween] The way the value moves through time.
8619 See {@link glow.tweens}.
8621 @param {Boolean} [opts.destroyOnComplete=false] Destroy the animation once it completes?
8622 This will free any DOM references the animation may have created. Once
8623 the animation completes, you won't be able to start it again.
8625 @param {Function} [opts.onStart] Shortcut for adding a "start" event listener
8626 @param {Function} [opts.onFrame] Shortcut for adding a "frame" event listener
8627 @param {Function} [opts.onStop] Shortcut for adding a "stop" event listener
8628 @param {Function} [opts.onComplete] Shortcut for adding a "complete" event listener
8629 @param {Function} [opts.onResume] Shortcut for adding a "resume" event listener
8632 var myAnim = new glow.anim.Animation(5, {
8633 tween:glow.tweens.easeBoth()
8639 @name glow.anim.Animation#event:start
8641 @description Fired when the animation is started from the beginning.
8642 @param {glow.events.Event} event Event Object
8644 var myAnim = new glow.anim.Animation(5, {
8645 tween:glow.tweens.easeBoth()
8647 glow.events.addListener(myAnim, "start", function() {
8648 alert("Started animation which lasts " + this.duration + " seconds");
8654 @name glow.anim.Animation#event:frame
8656 @description Fired in each frame of the animation.
8658 This is where you'll specify what your animation does.
8660 @param {glow.events.Event} event Event Object
8662 var myAnim = new glow.anim.Animation(5, {
8663 tween:glow.tweens.easeBoth()
8666 var myDiv = glow.dom.get("#myDiv"),
8667 divStartHeight = myDiv.height(),
8669 divHeightChange = divEndHeight - divStartHeight;
8671 glow.events.addListener(myAnim, "frame", function() {
8672 myDiv.height(divStartHeight + (divHeightChange * this.value));
8678 @name glow.anim.Animation#event:stop
8680 @description Fired when the animation is stopped before its end.
8682 If your listener prevents the default action (for instance,
8683 by returning false) the animtion will not be stopped.
8685 @param {glow.events.Event} event Event Object
8689 @name glow.anim.Animation#event:complete
8691 @description Fired when the animation ends.
8692 @param {glow.events.Event} event Event Object
8696 @name glow.anim.Animation#event:resume
8698 @description Fired when the animation resumes after being stopped.
8700 If your listener prevents the default action (for instance, by
8701 returning false) the animation will not be resumed.
8703 @param {glow.events.Event} event Event Object
8705 // constructor items that relate to events
8706 var animationEventConstructorNames = ["onStart", "onStop", "onComplete", "onResume", "onFrame"];
8708 r.Animation = function(duration, opts) {
8709 this._opts = opts = glow.lang.apply({
8711 tween: glow.tweens.linear(),
8712 destroyOnComplete: false,
8721 @name glow.anim.Animation#_playing
8725 @description Indicates whether the animation is playing.
8727 this._playing = false;
8730 @name glow.anim.Animation#_timeAnchor
8734 @description A timestamp used to keep the animation in the right position.
8736 this._timeAnchor = null;
8739 @name glow.anim.Animation#duration
8741 @description Length of the animation in seconds / frames.
8743 this.duration = duration;
8746 @name glow.anim.Animation#useSeconds
8748 @description Indicates whether duration is in seconds rather than frames.
8750 this.useSeconds = opts.useSeconds;
8753 @name glow.anim.Animation#tween
8755 @description The tween used by the animation.
8757 this.tween = opts.tween;
8760 @name glow.anim.Animation#position
8763 @description Seconds since starting, or current frame.
8768 @name glow.anim.Animation#value
8771 @description Current tweened value of the animtion, usually between 0 & 1.
8772 The value may become greater than 1 or less than 0 depending
8775 {@link glow.tweens.elasticOut} for instance will result
8776 in values higher than 1, but will still end at 1.
8780 // add events from constructor opts
8781 addEventsFromOpts(this, opts, animationEventConstructorNames);
8783 r.Animation.prototype = {
8786 @name glow.anim.Animation#start
8788 @description Starts playing the animation from the beginning.
8790 var myAnim = new glow.anim.Animation(5, {
8791 tween:glow.tweens.easeBoth()
8793 //attach events here
8795 @returns {glow.anim.Animation}
8798 if (this._playing) {
8801 var e = events.fire(this, "start");
8802 if (e.defaultPrevented()) { return this; }
8803 this._timeAnchor = null;
8805 manager.addToQueue(this);
8811 @name glow.anim.Animation#stop
8813 @description Stops the animation playing.
8814 @returns {glow.anim.Animation}
8817 if (this._playing) {
8818 var e = events.fire(this, "stop");
8819 if (e.defaultPrevented()) { return this; }
8820 manager.removeFromQueue(this);
8826 @name glow.anim.Animation#destroy
8828 @description Destroys the animation & detatches references to DOM nodes
8829 Call this on animations you no longer need to free memory.
8830 @returns {glow.anim.Animation}
8832 destroy: function() {
8833 // stop the animation in case it's still playing
8835 events.removeAllListeners(this);
8840 @name glow.anim.Animation#resume
8842 @description Resumes the animation from where it was stopped.
8843 @returns {glow.anim.Animation}
8845 resume: function() {
8846 if (! this._playing) {
8847 var e = events.fire(this, "resume");
8848 if (e.defaultPrevented()) { return this; }
8849 //set the start time to cater for the pause
8850 this._timeAnchor = new Date().valueOf() - (this.position * 1000);
8851 manager.addToQueue(this);
8857 @name glow.anim.Animation#isPlaying
8859 @description Returns true if the animation is playing.
8862 isPlaying: function() {
8863 return this._playing;
8866 @name glow.anim.Animation#goTo
8868 @description Goes to a specific point in the animation.
8869 @param {Number} pos Position in the animation to go to.
8871 This should be in the same units as the duration of your
8872 animation (seconds or frames).
8875 var myAnim = new glow.anim.Animation(5, {
8876 tween:glow.tweens.easeBoth()
8878 //attach events here
8879 //start the animation from half way through
8880 myAnim.goTo(2.5).resume();
8883 goTo: function(pos) {
8884 this._timeAnchor = new Date().valueOf() - ((this.position = pos) * 1000);
8885 this.value = this.tween(this.duration && this.position / this.duration);
8886 events.fire(this, "frame");
8892 @name glow.anim.Timeline
8894 @description Synchronises and chains animations.
8895 @param {Array | Array[]} channels An array of channels or a single channel.
8897 A channel is defined as an array containing numbers, animations and
8900 Numbers indicate a number of seconds to wait before proceeding to
8901 the next item. Animations will be played, when the animation is
8902 complete the next item is processed. Functions will be called, then
8903 the next item is processed.
8905 @param {Object} opts An object of options.
8906 @param {Boolean} [opts.loop=false] Specifies whether the timeline loops.
8908 The "complete" event does not fire for looping animations.
8910 @param {Boolean} [opts.destroyOnComplete=false] Destroy the animation once it completes?
8911 This will free any DOM references the animation may have created. Once
8912 the animation completes, you won't be able to start it again.
8914 @param {Function} [opts.onStart] Shortcut for adding a "start" event listener
8915 @param {Function} [opts.onStop] Shortcut for adding a "stop" event listener
8916 @param {Function} [opts.onComplete] Shortcut for adding a "complete" event listener
8917 @param {Function} [opts.onResume] Shortcut for adding a "resume" event listener
8920 // in the simplest form, a timeline can be used to
8921 // string multiple animations together:
8924 // make our animations
8925 var moveUp = glow.anim.css(myDiv, 2, {
8928 var moveDown = glow.anim.css(myDiv, 1, {
8931 // string them together
8932 new glow.anim.Timeline([moveUp, moveDown]).start();
8935 // if you wanted a one second gap between the animations, the last line would be:
8936 new glow.anim.Timeline([moveUp, 1, moveDown]).start();
8939 // you can run animations simutainiously with multiple channels.
8940 new glow.anim.Timeline([
8941 [moveDivUp, 1, moveDivDown],
8942 [moveListDown, 1, moveListUp]
8944 @see <a href="../furtherinfo/animtimeline/">Creating a mexican wave with an animation timeline</a>
8947 @name glow.anim.Timeline#event:start
8949 @description Fired when the timeline is started from the beginning.
8951 This event will also trigger during each loop of a looping animation.
8952 If your listener prevents the default action (for instance, by
8953 returning false) the timeline will not start.
8955 @param {glow.events.Event} event Event Object
8957 var myTimeline = new glow.anim.Timeline([anim1, anim2]);
8958 glow.events.addListener(myTimeline, "start", function() {
8959 alert("Started timeline");
8964 @name glow.anim.Timeline#event:stop
8966 @description Fired when the timeline is stopped before its end.
8968 If your listener prevents the default action (for instance, by
8969 returning false) the timeline will not stop.
8971 @param {glow.events.Event} event Event Object
8974 @name glow.anim.Timeline#event:complete
8976 @description Fired when the timeline ends.
8978 This event does not fire on looping timelines.
8980 @param {glow.events.Event} event Event Object
8983 @name glow.anim.Timeline#event:resume
8985 @description Fired when the timeline resumes after being stopped.
8987 If your listener prevents the default action (for instance, by
8988 returning false) the timeline will not resume.
8990 @param {glow.events.Event} event Event Object
8992 var timelineEventConstructorNames = ["onStart", "onStop", "onComplete", "onResume"];
8993 r.Timeline = function(channels, opts) {
8994 this._opts = opts = glow.lang.apply({
8996 destroyOnComplete: false,
9003 PrivateProperty: _channels
9006 //normalise channels so it's always an array of array(s)
9007 this._channels = (channels[0] && channels[0].push) ? channels : [channels];
9009 PrivateProperty: _channelPos
9010 index of each currently playing animation
9012 this._channelPos = [];
9014 PrivateProperty: _playing
9015 Is the timeline playing?
9017 this._playing = false;
9020 @name glow.anim.Timeline#loop
9022 @description Inidcates whether the timeline loops.
9024 The "complete" event does not fire for looping animations.
9025 This can be set while a timeline is playing.
9027 this.loop = opts.loop;
9029 var i, j, iLen, jLen,
9031 allChannels = this._channels,
9036 for (i = 0, iLen = allChannels.length; i < iLen; i++) {
9037 channel = allChannels[i];
9038 channelDuration = 0;
9039 for (j = 0, jLen = channel.length; j < jLen; j++) {
9040 //create a blank animation for time waiting
9041 if (typeof channel[j] == "number") {
9042 channel[j] = new r.Animation(channel[j]);
9044 if (channel[j] instanceof r.Animation) {
9045 if (! channel[j].useSeconds) {
9046 throw new Error("Timelined animations must be timed in seconds");
9048 channel[j]._timelineOffset = channelDuration * 1000;
9049 channelDuration += channel[j].duration;
9050 channel[j]._channelIndex = i;
9054 @name glow.anim.Timeline#duration
9056 @description Length of the animation in seconds
9058 this.duration = totalDuration = Math.max(channelDuration, totalDuration);
9061 PrivateProperty: _controlAnim
9062 This is used to keep the animation in time
9064 this._controlAnim = new r.Animation(totalDuration);
9065 events.addListener(this._controlAnim, "frame", this._processFrame, this);
9066 events.addListener(this._controlAnim, "complete", this._complete, this);
9068 // add events from constructor
9069 addEventsFromOpts(this, opts, timelineEventConstructorNames);
9071 r.Timeline.prototype = {
9073 PrivateMethod: _advanceChannel
9074 Move to the next position in a particular channel
9076 _advanceChannel: function(i) {
9077 //is there a next animation in the channel?
9078 var currentAnim = this._channels[i][this._channelPos[i]],
9079 nextAnim = this._channels[i][++this._channelPos[i]];
9081 if (currentAnim && currentAnim._playing) {
9082 currentAnim._playing = false;
9083 events.fire(currentAnim, "complete");
9084 if (currentAnim._opts.destroyOnComplete) {
9085 currentAnim.destroy();
9088 if ((nextAnim) !== undefined) {
9089 if (typeof nextAnim == "function") {
9091 this._advanceChannel(i);
9093 nextAnim.position = 0;
9094 nextAnim._channelIndex = i;
9095 events.fire(nextAnim, "start");
9096 nextAnim._playing = true;
9100 _complete: function() {
9105 this._playing = false;
9106 events.fire(this, "complete");
9107 if (this._opts.destroyOnComplete) {
9111 _processFrame: function() {
9112 var i, len, anim, controlAnim = this._controlAnim,
9113 msFromStart = (new Date().valueOf()) - controlAnim._timeAnchor;
9115 for (i = 0, len = this._channels.length; i < len; i++) {
9116 if (! (anim = this._channels[i][this._channelPos[i]])) { continue; }
9117 anim.position = (msFromStart - anim._timelineOffset) / 1000;
9118 if (anim.position > anim.duration) {
9119 anim.position = anim.duration;
9121 anim.value = anim.tween(anim.position / anim.duration);
9122 events.fire(anim, "frame");
9123 if (anim.position == anim.duration) {
9124 this._advanceChannel(i);
9130 @name glow.anim.Timeline#start
9132 @description Starts playing the timeline from the beginning.
9136 var e = events.fire(this, "start");
9137 if (e.defaultPrevented()) { return this; }
9138 var i, iLen, j, jLen, anim;
9139 this._playing = true;
9140 for (i = 0, iLen = this._channels.length; i < iLen; i++) {
9141 this._channelPos[i] = -1;
9142 this._advanceChannel(i);
9143 for (j = this._channels[i].length; j; j--) {
9144 anim = this._channels[i][j];
9145 if (anim instanceof r.Animation) {
9150 this._controlAnim.start();
9155 @name glow.anim.Timeline#stop
9157 @description Stops the timeline.
9161 if (this._playing) {
9162 var e = events.fire(this, "stop");
9163 if (e.defaultPrevented()) { return this; }
9164 this._playing = false;
9166 for (var i = 0, len = this._channels.length; i<len; i++) {
9167 anim = this._channels[i][this._channelPos[i]];
9168 if (anim instanceof r.Animation && anim._playing) {
9169 events.fire(anim, "stop");
9170 anim._playing = false;
9173 this._controlAnim.stop();
9179 @name glow.anim.Timeline#destroy
9181 @description Destroys the timeline & animations within it
9182 Call this on timeline you no longer need to free memory.
9185 destroy: function() {
9187 // stop the animation in case it's still playing
9189 events.removeAllListeners(this);
9190 this._controlAnim.destroy();
9192 // loop through all the channels
9193 i = this._channels.length; while (i--) {
9194 // loop through all the animations
9195 j = this._channels[i].length; while (j--) {
9196 // check it has a destroy method (making sure it's an animation & not a function)
9197 if (this._channels[i][j].destroy) {
9199 this._channels[i][j].destroy();
9207 @name glow.anim.Timeline#resume
9209 @description Resumes the timeline from wherever it was stopped.
9212 resume: function() {
9213 if (! this._playing) {
9214 var e = events.fire(this, "resume");
9215 if (e.defaultPrevented()) { return this; }
9216 this._playing = true;
9218 for (var i = 0, len = this._channels.length; i<len; i++) {
9219 anim = this._channels[i][ this._channelPos[i] ];
9220 if (anim instanceof r.Animation && !anim._playing) {
9221 events.fire(anim, "resume");
9222 anim._playing = true;
9225 this._controlAnim.resume();
9231 @name glow.anim.Timeline#isPlaying
9234 @description Returns true if the timeline is playing.
9236 isPlaying: function() {
9237 return this._playing;
9241 @name glow.anim.Timeline#goTo
9244 @description Go to a specific point in the timeline
9245 @param {Number|glow.anim.Animation} pos Position in the timeline to go to.
9247 You can go to a specific point in time (in seconds) or provide
9248 a reference to a particular animation to begin at.
9251 var myTimeline = new glow.anim.Timeline([anim1, anim2]);
9253 //start the Timeline 2.5 seconds in
9254 myTimeline.goTo(2.5).resume();
9257 var myTimeline = new glow.anim.Timeline([anim1, anim2]);
9259 //start the Timeline from anim2
9260 myTimeline.goTo(anim2).resume();
9262 goTo: function(pos) {
9266 channelsLen = this._channels.length,
9268 // holding var for an anim
9272 if (typeof pos == "number") {
9274 if (pos > this.duration) {
9275 // if the position is greater than the total, 'loop' the value
9277 pos = pos % this.duration;
9279 pos = this.duration;
9283 // advance the control anim
9284 this._controlAnim.goTo(pos);
9286 // loop through the animations in all the channels, find out which
9287 // one to start playing from
9288 for (i = 0; i < channelsLen; i++) {
9290 runningDuration = 0;
9291 // go through animations in that channel
9292 for (j = 0, channelLen = this._channels[i].length; j < channelLen; j++) {
9293 anim = this._channels[i][j];
9295 if (anim instanceof r.Animation) {
9296 // we found an animation we should be playing
9297 if ( (runningDuration + anim.duration) > pos) {
9298 // record its position and leave the loop
9299 this._channelPos[i] = j;
9300 anim.goTo(pos - runningDuration);
9303 // we're moving to a position where this animation has
9304 // finished playing, need to fire its final frame
9305 anim.goTo(anim.duration);
9306 // add that anim to the running total
9307 runningDuration += anim.duration;
9311 // right, now we need to move all animations after this
9312 // one to the start...
9313 for (k = channelLen; k > j; k--) {
9318 // ok, we've been provided with an object rather than a number of seconds
9319 // Let's convert it to seconds and rerun this function
9320 for (i = 0; i < channelsLen; i++) {
9321 // let's count this to find out what "time" the item the user wants to play is at
9322 runningDuration = 0;
9324 // let's loop through animations in that channel
9325 for (j = 0, channelLen = this._channels[i].length; j < channelLen; j++) {
9326 anim = this._channels[i][j];
9329 // oh! We've found the thing they want to play
9330 return this.goTo(runningDuration);
9332 if (anim instanceof r.Animation) {
9333 // add that anim to the running total
9334 runningDuration += anim.duration;
9338 throw "Animation not found in animation channels";
9349 @see <a href="../furtherinfo/forms/">Validating Forms</a>
9350 @see <a href="../furtherinfo/forms/defaultfeedback/">Using default form feedback</a>
9351 @see <a href="../furtherinfo/forms/example.shtml">Working example</a>
9352 @description Validating HTML Forms.
9353 To get started, you'll need to create a {@link glow.forms.Form Form instance}.
9355 (window.gloader || glow).module({
9357 library: ["glow", "1.7.0"],
9358 depends: [["glow", "1.7.0", 'glow.dom', 'glow.events', 'glow.anim', 'glow.net', 'glow.i18n']],
9359 builder: function(glow) {
9361 var $i18n = glow.i18n,
9362 $interpolate = glow.lang.interpolate;
9364 $i18n.addLocaleModule("GLOW_FORMS", "en", {
9365 TEST_MESSAGE_REQUIRED : "Value is required",
9366 TEST_MESSAGE_IS_NUMBER : "Must be a number.",
9367 TEST_MESSAGE_MIN : "The value must be at least {arg}.",
9368 TEST_MESSAGE_MAX : "The value must be less than {arg}.",
9369 TEST_MESSAGE_RANGE : "The value must be {min} or greater, and less than {max}.",
9370 TEST_MESSAGE_MIN_COUNT : "Must be have at least {arg} values.",
9371 TEST_MESSAGE_MAX_COUNT : "Must be have at most {arg} values.",
9372 TEST_MESSAGE_COUNT : "Must have {arg} values.",
9373 TEST_MESSAGE_REGEX : "Must be in the correct format.",
9374 TEST_MESSAGE_MIN_LEN : "Must be at least {arg} characters.",
9375 TEST_MESSAGE_MAX_LEN : "Must be at most {arg} characters.",
9376 TEST_MESSAGE_IS_EMAIL : "Must be a valid email address.",
9377 TEST_MESSAGE_SAME_AS : "Must be the same as: {arg}",
9378 TEST_MESSAGE_AJAX : "server responded",
9379 TEST_MESSAGE_IS : "Must be {arg}",
9380 TEST_MESSAGE_IS_NOT : "Must not be {arg}"
9386 @name glow.forms.Form
9388 @description Create an object to add tests to.
9389 @see <a href="../furtherinfo/forms/">Validating Forms</a>
9390 @see <a href="../furtherinfo/forms/defaultfeedback/">Using default form feedback</a>
9391 @param {glow.dom.NodeList | Selector} formNode
9392 @param {Object} [opts]
9393 @param {Function} [opts.onValidate] Handles the 'validate' event when all tests are complete. Default is glow.forms.feedback.defaultFeedback.
9395 myForm = new glow.forms.Form(
9396 glow.dom.get("#htmlFormId"),
9398 onValidate: function(results) {
9404 glow.forms.Form = function(formNode, opts) { /*debug*///console.log("glow.forms.Form#new("+formNode+", "+opts+")");
9406 @name glow.forms.Form#formNode
9407 @type glow.dom.NodeList
9408 @description NodeList containing the form element
9410 this.formNode = glow.dom.get(formNode);
9411 if (!this.formNode[0]) throw "Could not find form. Possibly run before DOM ready.";
9413 this._result = null;
9414 this.opts = opts || {};
9415 glow.events.addListener(this, "validate", this.opts.onValidate || feedback.defaultFeedback);
9417 this._idleTimer = null;
9419 this._localeModule = $i18n.getLocaleModule("GLOW_FORMS");
9421 // add event listener to form
9422 var thisForm = this;
9423 glow.events.addListener(
9427 thisForm.validate('submit');
9428 return false; // submit() will be called from the nextField method instead
9434 @name glow.forms.Form#event:validate
9436 @description Fired whenever the glow tries to validate a form.
9438 If you prefer you can set a handler for this event via the <code>onValidate</code> option of the glow.forms.Form constructor.
9440 @param {glow.forms.ValidateResult} event A specialised Event object.
9443 glow.events.addListener(myForm, "validate", function(e) {
9444 if (e.eventName == 'submit') {
9445 if (e.errorCount == 0) { alert("Well done!"); }
9446 else { e.preventDefault(); } // stop the submit from happening
9452 @name glow.forms.Form#validate
9454 @description Run validation tests.
9455 This is called automatically depending on the tests added to the form.
9456 However, you can trigger validation manually
9457 @param {String} [eventName='submit'] Run only tests tied to this eventname.
9458 @param {String} [fieldName] Run only tests attached to the form element with this name.
9460 glow.forms.Form.prototype.validate = function(eventName, fieldName) { /*debug*///console.log("glow.forms.Form#validate("+eventName+", "+fieldName+")");
9461 this.eventName = eventName || 'submit'; // default
9462 this._result = new glow.forms.ValidateResult(this.eventName);
9463 this._result.form = this;
9468 this._fieldName = fieldName;
9469 nextTest.call(this);
9473 Advance the test cursor to get the next test in the queue and then run it.
9476 @this {glow.forms.Form}
9477 @calledBy glow.forms.Form#validate
9478 @calledBy onTestResult
9481 var nextTest = function() { /*debug*///console.log("glow.forms.Form#nextTest()");
9483 if (this._testCur >= this._fields[this._fieldCur]._tests.length) { // run out of tests for the current field?
9484 if (!nextField.call(this)) return;
9487 var currentTest = this._fields[this._fieldCur]._tests[this._testCur]; // shortcut
9489 // get value from form element, normalize into an array
9491 if (currentTest.opts.field) { // a conditional test
9492 fieldValue = this.formNode.val()[currentTest.opts.field] || "";
9493 currentTest.isConditional = true;
9496 fieldValue = this.formNode.val()[this._fields[this._fieldCur].name] || "";
9499 // values should always be an array
9500 if (!fieldValue.join) fieldValue = [fieldValue];
9502 var callback = function(that) { // closure
9503 return function() { onTestResult.apply(that, arguments) };
9506 // only run tests that are tied to the eventName being validated
9507 currentTest.opts.on = currentTest.opts.on || "submit";
9509 this._result.eventName
9510 && (" "+currentTest.opts.on+" ").indexOf(" "+this._result.eventName+" ") != -1 // assume space delimited event names
9512 // skip tests that are not tied to the fieldName being validated
9513 if (this._fieldName && this._fieldName != currentTest.name) {
9514 nextTest.call(this);
9518 // run the test, if it exists
9519 if (typeof glow.forms.tests[currentTest.type] != "function") {
9520 throw "Unimplemented test: no test exists of type '"+currentTest.type+"'.";
9523 currentTest.opts._localeModule = this._localeModule;
9524 glow.forms.tests[currentTest.type](fieldValue, currentTest.opts, callback, this.formNode.val());
9527 nextTest.call(this);
9532 Advance the field cursor to get the next field in the queue.
9535 @this {glow.forms.Form}
9536 @calledBy glow.forms.Form#nextTest
9539 var nextField = function() { /*debug*///console.log("glow.forms.Form#nextField()");
9540 // start at the beginning of the next field
9544 if (this._fieldCur >= this._fields.length) { // run out of fields?
9546 // ready to fire the validate event now
9547 glow.events.fire(this, "validate", this._result);
9549 if ( this.eventName == "submit" && this._result && !this._result.defaultPrevented() ) { // ready to submit now
9551 // we cannot initiate a form submit by simply returning true or false from
9552 // this event handler because there may be asynchronous tests still pending at this point,
9553 // so we must call submit ourselves, after the last field has finally been tested
9554 this.formNode[0].submit();
9557 throw new Error("Glow can't submit the form because the submit function can't be called. Perhaps that form's submit was replaced by an input element named 'submit'?");
9561 return false; // don't keep going
9564 return true; // do keep going
9570 @this {glow.forms.Form}
9571 @calledBy glow.forms.tests.*
9572 @param {Number} result One of: glow.forms.PASS, glow.forms.FAIL
9573 @param {String} message
9576 var onTestResult = function(result, message) { /*debug*///console.log("glow.forms.Form#onTestResult("+result+", "+message+")");
9577 // convert result from a boolean to glow.forms.FAIL / glow.forms.PASS
9578 if (typeof result == "boolean") result = (result)? glow.forms.PASS : glow.forms.FAIL;
9580 // a conditional test has failed?
9581 if (this._fields[this._fieldCur]._tests[this._testCur].isConditional && result === glow.forms.FAIL) {
9582 result = glow.forms.SKIP; // failure of a conditional test becomes a skip
9585 this._result.fields.push(
9587 name: this._fields[this._fieldCur].name,
9593 if (result !== glow.forms.PASS) { // might be a fail or a skip
9594 if (result === glow.forms.FAIL) this._result.errorCount++;
9596 // skip over all further tests for this field
9597 this._testCur = this._fields[this._fieldCur]._tests.length;
9600 nextTest.call(this);
9604 @name glow.forms.Form#addTests
9606 @description Add one or more tests to a field.
9607 @param {String} fieldName The name of the field to add tests to.
9608 @param {Array} [spec]
9610 Test specifications identify the type of test to be run on a field to
9611 determine whether it contains desired data. See docs on the
9612 {@link glow.forms.tests types of tests}.
9615 //pattern for a test specification
9617 "testName", //name of the test to run
9619 arg : 5, //an argument for the test, not all tests need this
9620 on : "submit change", //when should this test be run?
9621 message : "Incorrect value" //a custom error message to display
9626 //setting a form up for validation
9627 var myForm = new glow.forms.Form(glow.dom.get("#myFormId"))
9633 message: "Name must be les than 12 characters long."
9641 glow.forms.Form.prototype.addTests = function(fieldName /*...*/) { /*debug*///console.log("glow.forms.Form#addTests("+fieldName+", ...)");
9642 var field = {name: fieldName, _tests:[]};
9644 var changeCallback = function(that) {
9646 that.validate.apply(that, ["change", fieldName])
9650 var clickCallback = function(that) {
9652 that.validate.apply(that, ["click", fieldName])
9656 var idleCallback = function(that) {
9658 that.validate.apply(that, ["idle", fieldName]);
9662 // loop over test specifications
9663 for (var i = 1; i < arguments.length; i++) {
9664 var testType = arguments[i][0];
9665 var testOpts = (arguments[i].length > 1)? arguments[i][1] : {}; // default opts
9667 field._tests.push({name: fieldName, type: testType, opts: testOpts});
9669 // add event listeners to form fields for change events
9670 if (!changeCallback.added && (" "+testOpts.on+" ").indexOf(" change ") != -1) {
9671 var inputs = this.formNode.get("*").each(function (i) {
9672 if (this.name == fieldName) {
9673 glow.events.addListener(this, "change", changeCallback);
9674 changeCallback.added = true;
9679 // add event listeners to form fields for click events
9680 if (!clickCallback.added && (" "+testOpts.on+" ").indexOf(" click ") != -1) {
9681 var inputs = this.formNode.get("*").each(function (i) {
9682 if (this.name == fieldName) {
9683 glow.events.addListener(this, "click", clickCallback);
9684 clickCallback.added = true;
9689 if (!idleCallback.added && (" "+testOpts.on+" ").indexOf(" idle ") != -1) {
9690 var idleDelay = (typeof testOpts.delay != "undefined")? parseInt(testOpts.delay) : 1000; // default delay before idle handler is run
9692 var inputs = this.formNode.get("*").each(function (i) {
9693 if (this.name == fieldName) {
9694 // FIXME: adding idleTimeoutID to HTML element, is this the best way?
9695 glow.events.addListener(this, "keyup", function(t){ return function() {window.clearTimeout(this.idleTimeoutID); if (this.value) this.idleTimeoutID = window.setTimeout(idleCallback, t)} }(idleDelay));
9696 glow.events.addListener(this, "blur", function() {window.clearTimeout(this.idleTimeoutID)});
9698 idleCallback.added = true;
9704 this._fields.push(field);
9706 return this; // chained
9710 @name glow.forms.ValidateResult
9712 @extends glow.events.Event
9713 @description The overall result returned by an attempt to validate the current state of the form.
9715 The ValidateResult object is used by glow.forms.Form to accumulate and record the test results as they are run. It is created automatically by the running validation so it is not necessary for the user to instantiate it directly, but it is useful to know the properties and their meanings as these will likely be referred to when a custom <code>onValidate</code> handler is run.
9716 @param {String} eventName
9717 @property {String} eventName The name of the event that was associated with this validation event.
9719 Validation can happen based on one of several different user interactions, this property allows you to identify the type of interaction that initiated this validation. Examples are:
9723 <dd>The user has done something to submit the form, for example by pressing the Submit button.</dd>
9725 <dd>The user has modified the value of a form field.</dd>
9727 <dd>The user is typing in a form field but has paused for a moment (by default 1 second).</dd>
9729 <dd>The user has clicked the mouse on or in a form field.</dd>
9732 Which user interaction is associated with which tests is determined by the options you used when you added the test. See the documentation for {@link glow.forms.Form#addTests} for more information.
9734 @property {Object[]} fields Each object in this array has a name of a field, a test result such as glow.forms.PASS, glow.forms.FAIL or glow.forms.SKIP, and a message describing the test result.
9736 The effect of validation is that the value or state of each field in the form is compared to the tests the developer has added to the fields. In each tested field a determination is made that the current value either passes or fails (or, if the test wasn't run at all, is skipped). The <code>fields</code> property provides information on the overall result of the validation, on each field that was tested and the results.
9738 @property {Number} errorCount The number of fields that had a failing test.
9740 From the <code>fields</code> property you can determine how many fields have failing or passing values; this is property is simply a more convenient way to access the total failing count of tests that fail. If no tests fail then this value will be 0 and you can consider the form to have validated.
9743 glow.forms.ValidateResult = function(eventName) {
9744 glow.events.Event.apply(this);
9746 this.eventName = eventName;
9747 this.errorCount = 0;
9748 this.value = undefined;
9752 glow.lang.extend(glow.forms.ValidateResult, glow.events.Event);
9755 @name glow.forms.PASS
9757 @description Constant for a passed test.
9758 This indicates that the value in a field passes all the tests associated with it. You can use this when creating {@link glow.forms.tests.custom custom tests}
9760 glow.forms.PASS = 1;
9762 @name glow.forms.FAIL
9764 @description Constant for a failing test.
9765 This indicates that the value in a field fails at least one of the tests associated with it. You can use this when creating {@link glow.forms.tests.custom custom tests}
9767 glow.forms.FAIL = 0;
9769 @name glow.forms.SKIP
9771 @description Constant for a skipped test.
9772 This indicates that there was some unmet condition associated with the applied tests, so they were not run. This state is not considered a fail, and will not affect glow.forms.ValidateResult#errorCount. You can use this when creating {@link glow.forms.tests.custom custom tests}.
9774 glow.forms.SKIP = -1;
9777 @name glow.forms.tests
9779 @see <a href="../furtherinfo/forms/">Validating Forms</a>
9780 @see <a href="../furtherinfo/forms/example.shtml">Working example</a>
9781 @description Collection of built-in tests that can be added to any form field as a way of validating that field's value.
9783 <p>You do not need to call these functions directly, the devloper use a test by passing its name to the {@link glow.forms.Form#addTests addTests} method.</p>
9785 <p>For more information about tests, how to create your own custom tests, or to see what arguments these tests take, you may refer to the <a href="../furtherinfo/forms/#custom_tests">Creating
9786 Custom Tests</a> section of the Validating Forms user guide.</p>
9788 glow.forms.tests = {
9790 @name glow.forms.tests.required
9792 @description The value must contain at least one non-whitespace character.
9794 A text input field that is empty, or contains only spaces for example will fail this test.
9802 required: function(values, opts, callback) { /*debug*///console.log("glow.forms.tests.required()");
9803 var message = opts.message || opts._localeModule.TEST_MESSAGE_REQUIRED;
9805 for (var i = 0, len = values.length; i < len; i++) {
9806 if (/^\s*$/.test(values[i])) {
9807 callback(glow.forms.FAIL, message);
9811 callback(glow.forms.PASS, message);
9815 @name glow.forms.tests.isNumber
9817 @description The value must be a valid number.
9819 A field that is empty, or contains a value that is not a number like 1 or 3.14 will fail this test.
9827 isNumber: function(values, opts, callback) { /*debug*///console.log("glow.forms.tests.isNumber()");
9828 var message = opts.message || opts._localeModule.TEST_MESSAGE_IS_NUMBER;
9830 for (var i = 0, len = values.length; i < len; i++) {
9831 if (values[i] == "" || isNaN(values[i])) {
9832 callback(glow.forms.FAIL, message);
9836 callback(glow.forms.PASS, message);
9840 @name glow.forms.tests.min
9842 @description The numeric value must be at least the given value.
9844 A field whose value, when converted to a number, is not less than the given arg will fail this test.
9854 min: function(values, opts, callback) { /*debug*///console.log("glow.forms.tests.min()");
9855 var message = opts.message || $interpolate(opts._localeModule.TEST_MESSAGE_MIN, {arg: opts.arg});
9856 for (var i = 0, len = values.length; i < len; i++) {
9857 if (Number(values[i]) < Number(opts.arg)) {
9858 callback(glow.forms.FAIL, message);
9862 callback(glow.forms.PASS, message);
9866 @name glow.forms.tests.max
9868 @description The numeric value must be no more than the given value.
9870 A field whose value, when converted to a number, is not more than the given arg will fail this test.
9880 max: function(values, opts, callback) { /*debug*///console.log("glow.forms.tests.max()");
9881 var message = opts.message || $interpolate(opts._localeModule.TEST_MESSAGE_MAX, {arg: opts.arg});
9882 for (var i = 0, len = values.length; i < len; i++) {
9883 if (Number(values[i]) > Number(opts.arg)) {
9884 callback(glow.forms.FAIL, message);
9888 callback(glow.forms.PASS, message);
9892 @name glow.forms.tests.range
9894 @description The numeric value must be between x..y.
9896 A field whose value, when converted to a number, is not more than x and less than y will fail this test.
9906 range: function(values, opts, callback) { /*debug*///console.log("glow.forms.tests.range()");
9907 var minmax = opts.arg.split(".."); // like "0..10"
9908 if (typeof minmax[0] == "undefined" || typeof minmax[1] == "undefined") {
9909 throw "Range test requires a parameter like 0..10."
9911 var message = opts.message || $interpolate(opts._localeModule.TEST_MESSAGE_RANGE, {min: minmax[0], max : minmax[1]});
9913 // cast to numbers to avoid stringy comparisons
9917 // reverse if the order is hi..lo
9918 if (minmax[0] > minmax[1]) {
9919 var temp = minmax[0];
9920 minmax[0] = minmax[1];
9924 for (var i = 0, len = values.length; i < len; i++) {
9925 if (values[i] < minmax[0] || values[i] > minmax[1]) {
9926 callback(glow.forms.FAIL, message);
9930 callback(glow.forms.PASS, message);
9934 @name glow.forms.tests.minCount
9936 @description There must be at least the given number of values submitted with this name.
9937 This is useful for multiple selects and checkboxes that have the same name.
9946 minCount: function(values, opts, callback) { /*debug*///console.log("glow.forms.tests.minCount()");
9947 var message = opts.message || $interpolate(opts._localeModule.TEST_MESSAGE_MIN_COUNT, {arg: opts.arg});
9951 for (var i = 0; i < values.length; i++ ) {
9952 if (values[i] != "") count++;
9955 if (count < opts.arg) {
9956 callback(glow.forms.FAIL, message);
9959 callback(glow.forms.PASS, message);
9963 @name glow.forms.tests.maxCount
9965 @description There must be no more than the given number of values submitted with this name.
9966 This is useful for multiple selects and checkboxes that have the same name.
9975 maxCount: function(values, opts, callback) { /*debug*///console.log("glow.forms.tests.maxCount()");
9976 var message = opts.message || $interpolate(opts._localeModule.TEST_MESSAGE_MAX_COUNT, {arg: opts.arg});
9980 for (var i = 0; i < values.length; i++ ) {
9981 if (values[i] != "") count++;
9984 if (count > opts.arg) {
9985 callback(glow.forms.FAIL, message);
9988 callback(glow.forms.PASS, message);
9992 @name glow.forms.tests.count
9994 @description There must be exactly the given number of values submitted with this name.
9995 This is useful for multiple selects and checkboxes that have the same name.
10004 count: function(values, opts, callback) { /*debug*///console.log("glow.forms.tests.count()");
10005 var message = opts.message || $interpolate(opts._localeModule.TEST_MESSAGE_COUNT, {arg: opts.arg});
10009 for (var i = 0; i < values.length; i++ ) {
10010 if (values[i] != "") count++;
10013 if (count != opts.arg) {
10014 callback(glow.forms.FAIL, message);
10017 callback(glow.forms.PASS, message);
10021 @name glow.forms.tests.regex
10023 @description The value must match the given regular expression.
10032 regex: function(values, opts, callback) { /*debug*///console.log("glow.forms.tests.regex()");
10033 var message = opts.message || opts._localeModule.TEST_MESSAGE_REGEX;
10035 var regex = (typeof opts.arg == "string")? new RegExp(opts.arg) : opts.arg; // if its not a string assume its a regex literal
10036 for (var i = 0, len = values.length; i < len; i++) {
10037 if (!regex.test(values[i])) {
10038 callback(glow.forms.FAIL, message);
10042 callback(glow.forms.PASS, message);
10046 @name glow.forms.tests.minLen
10048 @description The value must be at least the given number of characters long.
10057 minLen: function(values, opts, callback) { /*debug*///console.log("glow.forms.tests.minLen()");
10058 var message = opts.message || $interpolate(opts._localeModule.TEST_MESSAGE_MIN_LEN, {arg: opts.arg});
10060 for (var i = 0, len = values.length; i < len; i++) {
10061 if (values[i].length < opts.arg) {
10062 callback(glow.forms.FAIL, message);
10066 callback(glow.forms.PASS, message);
10070 @name glow.forms.tests.maxLen
10072 @description The value must be at most the given number of characters long.
10081 maxLen: function(values, opts, callback) { /*debug*///console.log("glow.forms.tests.maxLen()");
10082 var message = opts.message || $interpolate(opts._localeModule.TEST_MESSAGE_MAX_LEN, {arg: opts.arg});
10084 for (var i = 0, len = values.length; i < len; i++) {
10085 if (values[i].length > opts.arg) {
10086 callback(glow.forms.FAIL, message);
10090 callback(glow.forms.PASS, message);
10094 @name glow.forms.tests.isEmail
10096 @description The value must be a valid email address.
10097 This checks the formatting of the address, not whether the address
10105 isEmail: function(values, opts, callback) { /*debug*///console.log("glow.forms.tests.isEmail()");
10106 var message = opts.message || opts._localeModule.TEST_MESSAGE_IS_EMAIL;
10108 for (var i = 0, len = values.length; i < len; i++) {
10109 if (!/^\s*[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}\s*$/i.test(values[i])) {
10110 callback(glow.forms.FAIL, message);
10114 callback(glow.forms.PASS, message);
10118 @name glow.forms.tests.sameAs
10120 @description The value must be the same as the value in the given field.
10129 sameAs: function(values, opts, callback, formValues) { /*debug*///console.log("glow.forms.tests.sameAs()");
10130 var message = opts.message || $interpolate(opts._localeModule.TEST_MESSAGE_SAME_AS, {arg: opts.arg});
10131 var compareTo = formValues[opts.arg];
10133 for (var i = 0, len = values.length; i < len; i++) {
10134 if (values[i] != compareTo) {
10135 callback(glow.forms.FAIL, message);
10139 callback(glow.forms.PASS, message);
10143 @name glow.forms.tests.ajax
10145 @description Send the data to the server for testing.
10146 A request to the given URL will be made and the response will be passed to the given callback.
10148 'arg' is the function to handle the response from the server.
10150 This function should return a truthy value to indicate whether the server
10151 response should be considered a PASS or a FAIL. Optionally you can include a bespoke
10152 error message by returning an array of two elements, the first being
10153 the PASS or FAIL verdict, and the second being the error message to display.
10155 'url' is the url to call. You can use placeholders in here for form values (see example).
10161 url: "/cgi/checkname.cgi?name={username}",
10162 arg: function (response) {
10163 if (response.text() == "OK") {
10164 return glow.forms.PASS;
10166 return [glow.forms.FAIL, "That name is already taken."];
10169 message: "The server responded: that name is not permitted."
10173 ajax: function(values, opts, callback, formValues) { /*debug*///console.log("glow.forms.tests.ajax() - "+opts.url);
10174 var queryValues = {},
10175 message = (opts.message || opts._localeModule.TEST_MESSAGE_AJAX);
10177 for (var p in formValues) {
10178 if (typeof formValues[p] == "string") {
10179 queryValues[p] = escape(formValues[p]);
10181 else if (typeof formValues[p].push != "undefined") {
10182 queryValues[p] = glow.lang.map(formValues[p], function(i) { return escape(i); }).join(",");
10185 var url = glow.lang.interpolate(opts.url, queryValues);
10187 var request = glow.net.get(url, {
10188 onLoad: function(response) { /*debug*///console.log("glow.forms.tests.ajax - onLoad()");
10189 var verdict = opts.arg(response);
10190 if (typeof verdict.push == "undefined") verdict = [verdict, message];
10191 callback(verdict[0], verdict[1]);
10193 onError: function(response) {
10194 alert("Error getting file: "+url);
10200 @name glow.forms.tests.custom
10202 @description Create a custom test.
10204 'arg' is a function which tests the form value.
10206 The function is given the following parameters:
10211 An array of values submitted for that form field. If you
10212 are only expecting one value, it can be accessed via values[0]
10216 An object of any additional data included with the test
10220 This is a function used to tell Glow whether the test has
10221 passed or not. A callback is used rather than 'return' to
10222 allow async tests. The first parameter is either glow.forms.PASS
10223 or glow.forms.FAIL, the second is the success or failure message.
10227 This is an object of all values captured in the form.
10235 arg: function(values, opts, callback, formData) {
10236 for (var i = 0, len = values.length; i < len; i++) {
10237 if (values[i] == "Jake") {
10238 callback(glow.forms.FAIL, "The name Jake is not allowed.");
10242 callback(glow.forms.PASS, "Good name.");
10247 custom: function(values, opts, callback) { /*debug*///console.log("glow.forms.tests.custom()");
10248 opts.arg.apply(this, arguments);
10252 @name glow.forms.tests.is
10254 @description The value must be equal to a particular value
10256 // this test ensures "other" is required *if* the "reason" field is equal to "otherReason"
10266 "is": function(values, opts, callback) {
10267 var message = opts.message || $interpolate(opts._localeModule.TEST_MESSAGE_IS, {arg: opts.arg});
10269 for (var i = 0, len = values.length; i < len; i++) {
10270 if (values[i] != opts.arg) {
10271 callback(glow.forms.FAIL, message);
10275 callback(glow.forms.PASS, message);
10279 @name glow.forms.tests.isNot
10281 @description The value must not be equal to a particular value
10283 // you may have a dropdown select where the first option is "none" for serverside reasons
10291 "isNot": function(values, opts, callback) {
10292 var message = opts.message || $interpolate(opts._localeModule.TEST_MESSAGE_IS_NOT, {arg: opts.arg});
10294 for (var i = 0, len = values.length; i < len; i++) {
10295 if (values[i] == opts.arg) {
10296 callback(glow.forms.FAIL, message);
10300 callback(glow.forms.PASS, message);
10306 @name glow.forms.feedback
10308 @description Collection of functions for displaying validation results to the user
10310 <p>These functions should be used as handlers for {@link glow.forms.Form}'s validate event. At the
10311 moment there is only one provided handler, more may be added in the future.</p>
10313 <p>Of course, you don't have to use any of the methods here, you can provide your own.</p>
10315 @see <a href="../furtherinfo/forms/defaultfeedback">Using the default form feedback</a>
10318 var feedback = glow.forms.feedback = {};
10321 @name glow.forms.feedback.defaultFeedback
10323 @description Default handler used by {@link glow.forms.Form}.
10325 <p>This method outputs messages to the user informing them which fields
10326 contain invalid data. The output is unstyled and flexible.</p>
10328 @param {glow.forms.ValidateResult} result Object provided by the validate event
10330 @see <a href="../furtherinfo/forms/defaultfeedback">Using the default form feedback</a>
10332 feedback.defaultFeedback = (function() {
10334 //a hidden form element used to update a screenreader's buffer
10335 var screenReaderBufferUpdater;
10337 //attempts to update the buffer of the screen reader
10338 function updateScreenReaderBuffer() {
10339 if (!screenReaderBufferUpdater) {
10340 screenReaderBufferUpdater = glow.dom.create('<input type="hidden" value="0" name="1.7.0" id="1.7.0" />').appendTo(document.body);
10342 screenReaderBufferUpdater[0].value++;
10345 //write out the messages which appear next to (or near) the fields
10346 function inlineErrors(response) {
10347 var fields = response.fields, //field test results
10348 fieldElm, //holder for the field element(s) being processed
10349 msgContainer, //holder for contextual message holder
10350 labelError, //error holder within label element
10353 for (i = 0, len = fields.length; i < len; i++) {
10354 fieldElm = glow.dom.get(response.form.formNode[0].elements[fields[i].name]);
10355 //here's where we get the error container, which is the label by default
10356 //also we need to escape invalid css chars CSS
10357 msgContainer = glow.dom.get("." + fields[i].name.replace(/(\W)/g, "\\$1") + "-msgContainer");
10358 if (!msgContainer[0] && fieldElm.length == 1) {
10359 //none found, try and get the label
10360 msgContainer = response.form.formNode.get("label").filter(function() { return this.htmlFor == fieldElm[0].id })
10363 labelError = msgContainer.get("span.glow-errorMsg");
10365 if (fields[i].result) {
10366 //clear error messages & classes
10367 labelError.remove();
10368 fieldElm.removeClass("glow-invalid");
10370 if (msgContainer.length) {
10371 //add the error span to the label if it isn't already there
10372 if (!labelError[0]) {
10373 msgContainer.append( (labelError = glow.dom.create('<span class="glow-errorMsg"></span>')) );
10375 labelError.text(fields[i].message);
10376 fieldElm.addClass("glow-invalid");
10382 //write out a list of errors to appear at the top of the form
10383 function summaryError(response) {
10384 var fields = response.fields, //field test results
10385 fieldElm, //holder for the field element(s) being processed
10386 errorSummary, //div containing error summary
10387 errorList, //list of errors inside the error summary
10388 promptContainer, //holds the 'question' of the field
10389 prompt, //text to prefix each error line with
10393 //remove existing summary
10394 response.form.formNode.get("div.glow-errorSummary").remove();
10395 //create a summary div
10396 errorSummary = glow.dom.create('<div class="glow-errorSummary" tabindex="-1"><ul></ul></div>');
10397 errorList = errorSummary.get("ul");
10398 for (i = 0, len = fields.length; i < len; i++) {
10399 fieldElm = glow.dom.get(response.form.formNode[0].elements[fields[i].name]);
10400 promptContainer = glow.dom.get("." + fields[i].name.replace(/(\W)/g, "\\$1") + "-prompt");
10401 if (!promptContainer[0] && fieldElm.length == 1) {
10402 //we don't have a default, get the label
10403 promptContainer = response.form.formNode.get("label").filter(function() { return this.htmlFor == fieldElm[0].id })
10405 //did we get a prompt container?
10406 if (promptContainer[0]) {
10407 //get rid of superflous content in the prompt container (such as errors)
10408 promptContainer.get("span.glow-errorMsg").remove();
10409 prompt = glow.lang.trim(promptContainer.text());
10410 if (prompt.slice(-1) == ":") {
10411 prompt = prompt.slice(0, -1);
10414 //else we just use the field name
10415 prompt = fields[i].name.replace(/^\w/, function(s) { return s.toUpperCase() } );
10418 if (!fields[i].result) {
10419 errorList.append( glow.dom.create("<li></li>").text(prompt + ": " + fields[i].message) );
10422 response.form.formNode.prepend(errorSummary.css("opacity", "0"));
10423 glow.anim.css(errorSummary, "0.5", {
10424 opacity: {from: 0, to: 1}
10425 }, {tween: glow.tweens.easeOut()}).start();
10427 // if the error summary has been hidden, IE7 throws an exception here
10429 errorSummary[0].focus();
10432 updateScreenReaderBuffer();
10435 return function(response) {
10436 if (response.eventName == "submit") {
10437 //do we have any errors?
10438 if (!response.errorCount) {
10439 //remove existing summary
10440 response.form.formNode.get("div.glow-errorSummary").remove();
10443 summaryError(response);
10445 // display inline errors
10446 // we put this inside setTimeout to avoid an IE bug that can result
10447 // in the cursor appearing outside the form element
10448 setTimeout(function() {
10449 inlineErrors(response);
10461 @description Detect and embed Flash objects
10462 @see <a href="../furtherinfo/embed/">Flash embedding</a>
10464 (window.gloader || glow).module({
10465 name: "glow.embed",
10466 library: ["glow", "1.7.0"],
10467 depends: [["glow", "1.7.0", "glow.dom", "glow.data", "glow.i18n"]],
10468 builder: function(glow) {
10470 var $i18n = glow.i18n;
10472 $i18n.addLocaleModule("GLOW_EMBED", "en", {
10473 FLASH_MESSAGE : "This content requires Flash Player version {min} (installed version: {installed})",
10474 NO_PLAYER_MESSAGE : "No Flash Flayer installed, or version is pre 6.0.0"
10478 @name to_attributes
10482 @returns attribute string suitable for inclusion within Flash embed/object tag
10483 @description converts a hash to a space delimited string of attribute assignments
10485 Simple values are assumed for the object properties, with the exception of
10486 'flashVars' which is treated as a special case. If 'flashVars' is itself an object,
10487 it will be serialised in querystring format.
10488 returns string representation of object as attribute assignments eg:
10489 {id:"myId",name:"my-name"} becomes 'id="myId" name="my-name"'
10492 function to_attributes(object){
10494 var attributes = "";
10496 for (var $param in object){
10497 if ($param.toLowerCase() == "flashvars" && typeof object[$param] == "object"){
10498 attributes += ' FlashVars="' + glow.data.encodeUrl(object[$param]) + '"';
10501 attributes += ' ' + $param + '="' + object[$param] + '"';
10512 @returns string of param tags or an object element
10513 @description converts a hash to a string of param tags
10515 Simple values are assumed for the object properties, with the exception of
10516 'flashVars' which is treated as a special case. If 'flashVars' is itself an object,
10517 it will be serialised in querystring format.
10519 function toParams(object) {
10524 for (key in object) {
10525 if (key.toLowerCase() == "flashvars" && typeof object[key] == "object"){
10526 value = glow.data.encodeUrl(object[key]);
10528 value = object[key];
10531 r += '<param name="' + key + '" value="' + value + '" />\n';
10537 @name _set_defaults
10543 @description applies a hash of default property values to a target object
10545 Properties on the defaults object are copied to the target object if no such property is present.
10548 function _set_defaults(target,defaults){
10549 target = target || {};
10551 for (var param in defaults) {
10552 if (typeof target[param] == "undefined") {
10553 // is it safe to assign an object reference or should it be cloned ?
10554 target[param] = defaults[param];
10556 else if (typeof defaults[param] == "object") {
10557 target[param] = _set_defaults(target[param],defaults[param]);
10567 @returns {string} 'win' or 'mac' or 'other'
10568 @description identify the operating system
10570 function _platform(){
10571 var platform = (navigator.platform || navigator.userAgent);
10572 return platform.match(/win/i) ? "win" : platform.match(/mac/i) ? "mac" : "other";
10576 @name _askFlashPlayerForVersion
10579 @param {Shockwave.Flash} flash_player a reference to an ActiveX Shockwave Flash player object
10580 @returns version object with structure: {major:n,minor:n,release:n,actual:"string"}
10581 @description returns flash_player version, as reported by the GetVariable("$version") method
10583 function _askFlashPlayerForVersion(flash_player){
10585 var $regexFLASH_VERSION = /^WIN (\d+),(\d+),(\d+),\d+$/;
10586 var $version = flash_player.GetVariable("$version");
10587 if ($match = $regexFLASH_VERSION.exec($version)){
10589 major : parseInt($match[1]),
10590 minor : parseInt($match[2]),
10591 release : parseInt($match[3]),
10596 // throw an exception, something very strange going on if flash player returns version in any other format ?
10602 @name _getFlashPlayerVersion
10605 @returns version object with structure: {major:n,minor:n,release:n,actual:"string"}
10606 @description returns flash_player version
10608 Query installed Flash player version, using either ActiveX object creation (for Internet Explorer) or
10609 navigator plugins collection.
10612 function _getFlashPlayerVersion(){
10613 var $match, flash_player, NO_FLASH = {major : 0, minor : 0, release : 0}, result = NO_FLASH;
10617 flash_player = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");
10618 result = _askFlashPlayerForVersion(flash_player);
10621 // Version 6 needs kid-glove treatment as releases 21 thru 29 crash if GetVariable("$version") is called
10623 flash_player = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");
10625 // This works from release 47 onward...(first public release after 29)
10626 flash_player.AllowScriptAccess = "always";
10627 result = _askFlashPlayerForVersion(flash_player);
10630 // we cannot safely test between releases 21...29, so assume the latest and hope for the best...
10631 result = {major:6,minor:0,release:29};
10635 // nothing more we can do, either no flash installed, flash player version is ancient or ActiveX is disabled
10640 var regexFLASH_VERSION = /^Shockwave Flash\s*(\d+)\.(\d+)\s*\w(\d+)$/;
10642 if ((flash_player = navigator.plugins["Shockwave Flash"]) && ($match = regexFLASH_VERSION.exec(flash_player.description))){
10644 major : parseInt($match[1]),
10645 minor : parseInt($match[2]),
10646 release : parseInt($match[3]),
10647 actual : flash_player.description
10652 result.toString = function(){return this.major ? [this.major,this.minor,this.release].join(".") : $i18n.getLocaleModule("GLOW_EMBED").NO_PLAYER_MESSAGE};
10659 @name installed_flash_player
10661 @description version of installed Flash player, initialised at startup.
10663 var installed_flash_player = _getFlashPlayerVersion();
10667 @name _meetsVersionRequirements
10670 @param {string|object} requiredVersion string or version object
10672 major version must be specified. minor version is optional. release is optional, but both major and
10673 minor versions must also be supplied if release is specified.
10674 eg, as string: "9.0.0", "9.1", "9"
10675 or, as object: {major:9,minor:0,release:0}, {major:9,minor:0}, {major:9}
10677 @returns {boolean}.
10678 @description returns true if installed Flash player version meets requested version requirements
10681 function _meetsVersionRequirements(requiredVersion){
10683 if (typeof requiredVersion != "object"){
10684 var match = String(requiredVersion).match(/^(\d+)(?:\.(\d+)(?:\.(\d+))?)?$/);
10686 throw new Error('glow.embed._meetsVersionRequirements: invalid format for version string, require "n.n.n" or "n.n" or simply "n" where n is a numeric value');
10689 requiredVersion = {
10690 major : parseInt(match[1],10),
10691 minor : parseInt(match[2]||0,10),
10692 release : parseInt(match[3]||0,10)
10696 var v = installed_flash_player, rv = requiredVersion;
10698 // return true if we meet the minimum version requirement...
10699 return (v.major > rv.major ||
10700 (v.major == rv.major && v.minor > rv.minor) ||
10701 (v.major == rv.major && v.minor == rv.minor && v.release >= rv.release));
10706 @name _wrap_embedding_tag
10709 @param {string} src
10710 @param {object} attributes Hash of attributes to be included
10711 @param {string} params Hash of params to be included. These will be included as attributes for all but IE
10712 @returns {string} object or embed tag
10713 @description returns a complete object or embed tag, as appropriate (ie browser dependent)
10715 var _wrap_embedding_tag = glow.env.ie ? _wrap_object_tag : _wrap_embed_tag;
10716 function _wrap_embed_tag(src, attributes, params){
10717 return '<embed type="application/x-shockwave-flash" src="' + src + '"' + to_attributes(attributes) + to_attributes(params) + '></embed>';
10719 function _wrap_object_tag(src, attributes, params){
10720 return '<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" ' + to_attributes(attributes) + '><param name="movie" value="' + src + '" />' + toParams(params) + '</object>';
10724 idIndex = 0; // Used with private function _getId()
10730 @returns {string} unique string to use for id of a flash object
10731 @description Returns a unique to the page string to use for the id of the flash object. This function ensures that all flash objects embedded into the page get an id.
10733 function _getId() {
10734 return glow.UID + "FlashEmbed" + (idIndex++);
10738 @name glow.embed.Flash
10740 @description A wrapper for a Flash movie so it can be embedded into a page
10741 @see <a href="../furtherinfo/embed/">Flash embedding</a>
10743 @param {String} src Absolute or relative URL of a Flash movie file to embed
10744 @param {selector | glow.dom.NodeList} container The element to embed the movie into
10746 <p>If a CSS selector is provided then the first matching element is used as the
10749 <p>If the parameter is a {@link glow.dom.NodeList}, then the first
10750 element of the list is used as the container.</p>
10752 @param {String|Object} minVersion The minimum required version of the Flash plugin.
10754 <p>The Flash plugin has a version numbering scheme comprising of major, minor and
10755 release numbers.</p>
10757 <p>This param can be a string with the major number only, major plus minor numbers, or
10758 full three-part version number, e.g. "9", "9.1" , "6.0.55" are all valid values.</p>
10760 <p>If minVersion is set as an object, it must use a similar structure to the object
10761 returned by the {@link glow.embed.Flash#version} method, e.g: <code>{major: 9, minor:0,
10762 release:0}</code>.</p>
10764 @param {Object} [opts]
10766 Hash of optional parameters.
10768 @param {String} [opts.width] Width of the Flash movie. Defaults to "100%"
10769 @param {String} [opts.height] Height of the Flash movie. Defaults to "100%"
10770 @param {String} [opts.id] Unique id to be assigned to Flash movie instance.
10771 @param {String} [opts.className] CSS class to be assigned to the embedding element.
10772 @param {Object} [opts.attributes] A hash of attributes to assign to the embedded element tag.
10773 @param {Object} [opts.params] A hash of optional Flash-specific parameters.
10774 For example quality, wmode, bgcolor, flashvars.
10775 @param {String|Function} [opts.message] Error handling message or function.
10777 A message to display in the event that the Flash player is either not
10778 installed or is an earlier version than the specified minVersion. This message will
10779 be written into the container element instead of the Flash movie.
10781 If a function is supplied, it will be invoked and any return value will be used
10782 as the message to write into the container.
10785 var myFlash = new glow.embed.Flash("/path/to/flash.swf", "#flashContainer", "9");
10788 var myFlash = new glow.embed.Flash("/path/to/flash.swf", "#flashContainer", "9", {
10794 var myFlash = new glow.embed.Flash("/path/to/flash.swf", "#flashContainer", "9", {
10798 wmode: "transparent",
10801 username: "Frankie"
10806 r.Flash = function(src, container, minVersion, opts){
10808 opts = _set_defaults(opts,{
10812 allowscriptaccess : "always",
10813 allowfullscreen : "true",
10817 //expressInstall : false, // TODO add this in a later release
10818 message : glow.lang.interpolate($i18n.getLocaleModule("GLOW_EMBED").FLASH_MESSAGE, {min: minVersion, installed: installed_flash_player}),
10819 // create a default ID one hasn't been created as an attribute
10820 id : (opts && opts.attributes && opts.attributes.id) || _getId() // Fix for trac 165
10825 @name glow.embed.Flash#container
10826 @type glow.dom.NodeList
10827 @description The element containing the embedded movie.
10829 container = glow.dom.get(container);
10830 if (!container.length){
10831 throw new Error("glow.embed.Flash unable to locate container");
10833 this.container = container;
10835 //this.expressInstall = opts.expressInstall; // TODO add this in a later release
10838 @name glow.embed.Flash#movie
10840 @description A reference to the actual Flash movie element, for direct script access.
10842 myFlash.movie.exposedFlashMethod();
10846 this._displayErrorMessage = typeof opts.message == "function" ? opts.message : function(){return opts.message};
10849 @name glow.embed.Flash#isSupported
10851 @description Does the user have the correct version of Flash?
10852 This will be false if the user has an earler version of Flash
10853 installed than this movie requires, or the user doesn't have
10854 any version of Flash instaled.
10856 if ( !myFlash.isSupported ) {
10857 alert('Please download the latest version of Flash');
10862 // Check that the min version requirement is satisfied and store this status, so we don't later try to embed the thing
10863 // if we don't can't meet the version requirements.
10865 if (this.isSupported = _meetsVersionRequirements(minVersion)){
10866 var attrs = opts.attributes,
10867 overwrites = ["id", "width", "height"],
10868 i = overwrites.length;
10870 // copies stuff like opts.id to attr.id
10872 if (opts[overwrites[i]]) { attrs[overwrites[i]] = opts[overwrites[i]]; }
10875 if (opts.className) { attrs["class"] = opts.className; }
10876 this._embed_tag = _wrap_embedding_tag(src, attrs, opts.params);
10879 else if (this.expressInstall && _meetsVersionRequirements("6.0.65") && _platform().match(/^win|mac$/)) {
10881 // Callback to be invokes in case of express install error
10882 window[glow.UID + "flashExpressInstallCancelled"] = function(){
10883 alert("Flash update cancelled");
10885 window[glow.UID + "flashExpressInstallFailed"] = function(){
10886 alert("Unable to complete update, please go to adobe.com...");
10888 window[glow.UID + "flashExpressInstallComplete"] = function(){
10890 alert("New version of flash installed");
10894 new glow.embed.Flash("expressInstall.swf",
10897 //TODO check minimum width/height
10899 height:opts.height,
10902 MMredirectURL : window.location.toString(),
10903 MMplayerType : glow.env.ie ? "ActiveX" : "PlugIn",
10904 MMdoctitle : document.title,
10905 GlowCallback : glow.UID + "flashExpressInstall"
10911 this.expressInstalling = true;
10916 @name glow.embed.Flash.version
10918 @description Get details of the current users Flash plugin
10919 @returns An object with details of the currently installed Flash plugin.
10922 <dt>major (number)</dt><dd>Flash player major version mumber</dd>
10923 <dt>minor (number)</dt><dd>Flash player minor version mumber.</dd>
10924 <dt>release (number)</dt><dd>Flash player release version mumber.</dd>
10925 <dt>actual (string)</dt><dd>The Flash version exactly as reported by the Flash player.</dd>
10926 <dt>toString (function)</dt><dd>toString implementation in the form "major.minor.release" Eg "9.0.2"</dd>
10930 var version = glow.embed.Flash.version();
10931 alert("curr = " + version.major) // "curr = 9"
10932 alert("curr = " + version) // "curr = 9.0.2"
10934 r.Flash.version = function(){
10935 return installed_flash_player;
10939 @name glow.embed.Flash#embed
10941 @description Embed the Flash movie into the document
10942 @returns {glow.embed.Flash}
10945 var myFlash = new glow.embed.Flash(...);
10948 r.Flash.prototype.embed = function(){
10949 var containerElm = this.container[0];
10950 if (this.isSupported){
10952 containerElm.innerHTML = this._embed_tag;
10954 this.movie = containerElm.firstChild;
10957 else if (this.expressInstalling){
10958 // wait for expressInstall to complete
10962 var message = this._displayErrorMessage();
10964 containerElm.innerHTML = message;
10977 @name glow.dragdrop
10979 @description Simplifying drag and drop behaviour
10981 (window.gloader || glow).module({
10982 name: "glow.dragdrop",
10983 library: ["glow", "1.7.0"],
10984 depends: [["glow", "1.7.0", "glow.tweens", "glow.events", "glow.dom", "glow.anim"]],
10985 builder: function(glow) {
10986 var events = glow.events,
10987 addListener = events.addListener,
10988 fire = events.fire,
10989 removeListener = events.removeListener,
10992 create = dom.create;
10997 _ieStrict = (document.compatMode == "CSS1Compat" && glow.env.ie >= 5) ? true : false,
10998 _ieTrans= (document.compatMode != "CSS1Compat" && glow.env.ie >= 5) ? true : false,
10999 _ie = glow.env.ie >= 5,
11000 sides = ['top', 'right', 'bottom', 'left'];
11003 PrivateFunction: memoize(clss, name)
11005 Replace a method with a version that caches the result after the first run.
11011 The class whose method is being memoized.
11015 The name of the method to memoize.
11018 function memoize (clss, name) {
11019 var orig = clss.prototype[name];
11020 var cachedName = 'cached_' + name;
11021 clss.prototype[name] = function () {
11022 if (cachedName in this) return this[cachedName];
11023 return this[cachedName] = orig.apply(this, arguments);
11028 Copy margins from one element to another
11030 function copyMargins(to, from) {
11031 var i = sides.length, margin;
11034 margin = 'margin-' + sides[i];
11035 to.css(margin, from.css(margin));
11040 PrivateFunction: memoizeNamed(clss, methodName)
11042 Replace a method that takes a name with a version that caches the result for each name after the first run.
11048 The class whose method is being memoized.
11052 The name of the method to memoize.
11055 function memoizeNamed (clss, methodName) {
11056 var orig = clss.prototype[methodName];
11057 var cachedName = 'cached_' + methodName;
11058 clss.prototype[methodName] = function (name) {
11059 if (! this[cachedName]) this[cachedName] = {};
11060 if (name in this[cachedName]) return this[cachedName][name];
11061 return this[cachedName][name] = orig.apply(this, arguments);
11066 PrivateFunction: reset(obj, names)
11068 Remove cached values for a set of memoized methods.
11074 The object containing cached values.
11076 *names* (array of strings)
11078 The names of methods whose values have been cached.
11081 function reset (obj, names) {
11082 for (var i = 0, l = names.length; i < l; i++) {
11083 delete obj['cached_' + names[i]];
11088 PrivateFunction: resetNamed(obj, meth, names)
11090 Remove cached values for a set of named properties for a method.
11096 The object containing cached values.
11100 The name of the method whose values have been cached.
11102 *names* (array of strings)
11104 The names of the cached properties.
11106 function resetNamed (obj, meth, names) {
11107 var cache = obj['cached_' + meth];
11108 if (! cache) return;
11109 for (var i = 0, l = names.length; i < l; i++) {
11110 delete cache[names[i]];
11118 Calculates and caches information about an element in the box model.
11128 *el* (glow.dom.NodeList)
11130 The element that calculations will be performed on.
11133 var Box = function (el) {
11142 Get an pixel value for a CSS style.
11148 The name of a CSS style (e.g. "margin-top".
11151 An integer number of pixels.
11154 val: function (style) {
11155 var val = parseInt(this.el.css(style));
11156 // TODO - fix dom so margin-left return value is always defined?
11157 // if (isNaN(val)) throw 'got NaN in val for ' + style + ': ' + this.el.css(style);
11163 PrivateMethod: width
11165 Get the width of the element.
11168 An integer number of pixels.
11171 width: function () {
11172 return this.borderWidth()
11173 - this.val('border-left-width')
11174 - this.val('border-right-width');
11178 PrivateMethod: height
11180 Get the height of the element.
11183 An integer number of pixels.
11186 height: function () {
11187 return this.borderHeight()
11188 - this.val('border-top-width')
11189 - this.val('border-bottom-width');
11193 PrivateMethod: offsetParentPageTop
11195 Get the number of pixels from the top of nearest element with absolute, relative or fixed position to the
11199 An integer number of pixels.
11201 offsetParentPageTop: function () {
11202 var el = this.el[0], pos, top;
11203 while (el = el.offsetParent) {
11204 if ( $(el).css('position') != 'static' ) {
11209 $(el).offset().top :
11214 PrivateMethod: offsetTop
11216 This gets what CSS 'top' would be if the element were position "absolute"
11219 An integer number of pixels.
11221 offsetTop: function () {
11222 return this.el.position().top;
11226 PrivateMethod: offsetLeft
11228 This gets what CSS 'left' would be if the element were position "absolute"
11231 An integer number of pixels.
11233 offsetLeft: function () {
11234 return this.el.position().left;
11238 PrivateMethod: borderWidth
11240 Get the width of the element from the left edge of the left border to the right
11241 edge of the right border.
11244 An integer number of pixels.
11247 borderWidth: function () {
11248 var width = this.el[0].offsetWidth;
11249 if (glow.env.khtml) {
11250 width -= this.val('margin-left')
11251 + this.val('margin-right')
11252 + this.val('border-left-width')
11253 + this.val('border-right-width');
11259 PrivateMethod: borderHeight
11261 Get the height of an element from the top edge of the top border to the bottom
11262 edge of the bottom border.
11265 An integer number of pixels.
11268 borderHeight: function () {
11269 if (this._logicalBottom) {
11270 return this._logicalBottom - this.offsetTop();
11272 var height = this.el[0].offsetHeight;
11273 if (glow.env.khtml) {
11274 height -= this.val('margin-top')
11275 + this.val('margin-bottom')
11276 + this.val('border-top-width')
11277 + this.val('border-bottom-width');
11284 PrivateMethod: outerWidth
11286 Get the width of the element in including margin, borders and padding.
11289 An integer number of pixels.
11292 outerWidth: function () {
11293 return this.borderWidth() + this.val('margin-left') + this.val('margin-right');
11297 PrivateMethod: outerHeight
11299 Get the height of the element in including margin, borders and padding. This
11300 does not take account of collapsable margins (i.e. it assumes the margins are
11304 An integer number of pixels.
11307 outerHeight: function () {
11308 return this.borderHeight() + this.val('margin-top') + this.val('margin-bottom');
11312 PrivateMethod: innerLeftPos
11314 Get the offset of the left edge of the content of the box (i.e. excluding
11315 margin, border and padding).
11318 An integer number of pixels.
11321 innerLeftPos: function () {
11322 return this.offsetLeft()
11323 + this.val('margin-left')
11324 + this.val('border-left-width')
11325 + this.val('padding-left');
11329 PrivateMethod: innerTopPos
11331 Get the offset of the top edge of the content of the box (i.e. excluding
11332 margin, border and padding).
11335 An integer number of pixels.
11338 innerTopPos: function () {
11339 return this.offsetTop()
11340 + this.val('margin-top')
11341 + this.val('border-top-width')
11342 + this.val('padding-top');
11346 PrivateMethod: surroundWidth
11348 Get the combined width of the horizontal margins, borders and paddings.
11351 An integer number of pixels.
11354 surroundWidth: function () {
11355 return this.val('border-left-width')
11356 + this.val('padding-left')
11357 + this.val('padding-right')
11358 + this.val('border-right-width');
11362 PrivateMethod: surroundHeight
11364 Get the combined height of the horizontal margins, borders and paddings.
11367 An integer number of pixels.
11370 surroundHeight: function () {
11371 return this.val('border-top-width')
11372 + this.val('padding-top')
11373 + this.val('padding-bottom')
11374 + this.val('border-bottom-width');
11378 PrivateMethod: verticalCenter
11380 Get the vertical offset of the center of the element from it's offset parent.
11383 An integer number of pixels.
11386 verticalCenter: function () {
11387 return this.offsetTop() + (this.outerHeight() / 2);
11391 PrivateMethod: verticalCenter
11393 Get the vertical offset of the center of the element from it's offset parent.
11396 An integer number of pixels.
11399 horizontalCenter: function () {
11400 return this.offsetTop() + (this.outerWidth() / 2);
11405 for (var i in Box.prototype) {
11406 if (i == 'val') memoizeNamed(Box, i);
11407 else memoize(Box, i);
11410 glow.lang.apply(Box.prototype, {
11413 PrivateMethod: resetPosition
11415 Reset cached position values for the element.
11418 resetPosition: function () {
11432 PrivateMethod: setLogicalBottom
11434 Set the logical value for the position of the bottom of the border (offsetTop + offsetHeight).
11440 The value to use for the bottom of the box.
11442 setLogicalBottom: function (bottom) {
11443 this._logicalBottom = bottom;
11447 PrivateMethod: boundsFor
11449 Get the the bounds for the left and top css properties of a child box to
11450 ensure that it stays within this element.
11456 A Box object representing the space taken up by the child element.
11459 An array of top, right, bottom and left pixel bounds for the top and left
11460 css properties of the child element.
11463 boundsFor: function (childBox) {
11464 var top, left, pos = this.el.css('position');
11465 if (pos != 'static') {
11469 top = this.innerTopPos();
11470 left = this.innerLeftPos();
11474 left + this.width() - childBox.outerWidth(), // right
11475 top + this.height() - childBox.outerHeight(), // bottom
11481 PrivateMethod: outerBounds
11483 Get the top, right, bottom and left offsets of the outside edge of the border
11487 An array of integer pixel offsets for the top, right, bottom, left edges of the
11491 outerBounds: function () {
11492 var offset = this.el.offset(),
11493 left = offset.left,
11497 left + this.borderWidth(),
11498 top + this.borderHeight(),
11504 PrivateMethod: intersectSize
11506 Get the intersection of this box with another box.
11512 A Box object to test for intersection with this box.
11514 *touches* (boolean)
11516 If true, then the boxes don't have to intersect but can merely touch.
11519 An integer number of square pixels that the the outside of the
11520 edge of the border of this box intersects with that of the passed
11524 intersectSize: function (that, touches) {
11525 var a = this.outerBounds(), b = that.outerBounds();
11528 a[1]++; b[1]++; a[2]++; b[2]++;
11533 a[0] < b[0] ? (a[2] < b[2] ? a[2] - b[0] : b[2] - b[0]) :
11534 b[2] < a[2] ? b[2] - a[0] : a[2] - a[0]
11538 a[3] < b[3] ? (a[1] < b[1] ? a[1] - b[3] : b[1] - b[3]) :
11539 b[1] < a[1] ? b[1] - a[3] : a[1] - a[3]
11544 PrivateMethod: sizePlaceholder
11546 Size and position a placeholder/drop indicator element to match that
11551 *placeholder* (glow.dom.NodeList)
11553 The element that will be sized.
11555 *pos* (optional string)
11557 The value for the placeholder's CSS position. Defaults to the position
11560 *startLeft* (integer)
11562 The original left position of the element.
11564 *startTop* (integer)
11566 The original top position of the element.
11569 sizePlaceholder: function (placeholder, pos, startLeft, startTop) {
11570 var placeholderBox = new Box(placeholder),
11572 position = pos || el.css('position');
11574 placeholder.css('display', 'none');
11576 el.after(placeholder);
11578 placeholder.css('width', (el[0].offsetWidth - placeholderBox.surroundWidth()) + 'px')
11579 .css('height', (el[0].offsetHeight - placeholderBox.surroundHeight()) + 'px');
11581 // copy margin values
11582 copyMargins(placeholder, el);
11584 placeholder.remove();
11586 placeholder.css('display', 'block');
11588 if (position != 'static') {
11589 placeholder.css('left', startLeft + 'px');
11590 placeholder.css('top', startTop + 'px');
11592 placeholder.css('position', position);
11596 PrivateMethod: contains
11598 Check if a box is contained within this box.
11607 Boolean, true if contained.
11610 contains: function (box) {
11611 var bounds = this.boundsFor(box),
11612 position = box.el.position(),
11613 top = position.top,
11614 left = position.left;
11616 return top >= bounds[0] // top
11617 && left <= bounds[1] // right
11618 && top <= bounds[2] // bottom
11619 && left >= bounds[3]; // left
11623 PrivateMethod: containsPoint
11629 The offset to check - an object containing x and y integer pixel values.
11632 Boolean, true if the point over the visible part of the element (i.e. including the borders).
11635 containsPoint: function (offset) {
11636 var elementOffset = this.el.offset();
11637 return offset.x >= elementOffset.left
11638 && offset.y >= elementOffset.top
11639 && offset.x <= elementOffset.left + this.borderWidth()
11640 && offset.y <= elementOffset.top + this.borderHeight();
11644 PrivateMethod: positionedAncestorBox
11646 Get a new Box for nearest ancestor of the element that has position 'absolute', 'fixed' or 'relative'.
11649 An integer pixel offset.
11652 positionedAncestorBox: function () {
11653 var el = this.el.parent(), pos;
11655 pos = el.css('position') || 'static';
11656 if (pos == 'relative' || pos == 'absolute' || pos == 'fixed')
11657 return new Box(el);
11664 function placeholderElement (el) {
11665 var tag = el[0].tagName.toLowerCase() == 'li' ? 'li' : 'div';
11666 var placeholder = create('<' + tag + '></' + tag + '>');
11667 if (tag == 'li') placeholder.css('list-style-type', 'none');
11668 return placeholder;
11672 @name glow.dragdrop.Draggable
11674 @description An element that can be dragged using the mouse.
11675 @see <a href="../furtherinfo/dragdrop/draggables.shtml">Draggable examples</a>
11677 @param {String | Element | glow.dom.NodeList} element The element or CSS selector for an element to be made draggable.
11679 If a {@link glow.dom.NodeList NodeList} or CSS selector matching
11680 multiple elements is passed only the first element is made draggable.
11682 @param {Object} [opts]
11684 An object of options.
11686 The opts object allows you to pass in functions to use as event
11687 listeners. This is purely for convenience, you can also use
11688 {@link glow.events.addListener} to add them the normal way.
11690 @param {String} [opts.placeholder=spacer] Defines what to leave in place of the draggable whilst being dragged.
11692 Possible values for this param are:
11695 <dt>spacer</dt><dd>an empty div is created where the draggable started.</dd>
11696 <dt>clone</dt><dd>an exact clone of the original element.</dd>
11697 <dt>none</dt><dd>no placeholder will be created.</dd>
11700 @param {String} [opts.placeholderClass=glow-dragdrop-placeholder] A class be applied to the placeholder element.
11702 This can be used to to add styling for indicating where the element
11703 has been dragged from, add opacity, etc.
11705 @param {Selector | Element | glow.dom.NodeList} [opts.handle] Restrict the drag 'handle' to an element within the draggable.
11707 @param {Selector | Element | glow.dom.NodeList} [opts.container] Constrain dragging to within the bounds of the specified element.
11709 @param {Array} [opts.dropTargets] An array of {@link glow.dragdrop.DropTarget DropTargets}.
11711 Specifies which {@link glow.dragdrop.DropTarget DropTargets} this draggable is associated with.
11713 @param {String} [opts.axis] Restrict dragging to an axis.
11715 Possible values for this param are:
11718 <dt>x</dt><dd>Restricts dragging to the x-axis</dd>
11719 <dt>y</dt><dd>Restricts dragging to the y-axis</dd>
11722 @param {String[]} [opts.dragPrevention=input, textarea, button, select, option, a] Disables dragging from the specified array of element names
11724 By default dragging will not work when the user clicks in form
11725 elements, otherwise these elements would be unusable.
11727 @param {Number|Object} [opts.step=1] The pixel interval the draggable snaps to.
11728 If a number, the draggable will step by that number of pixels on the x and y axis. You
11729 can provide an object in the form <code>{x:2, y:4}</code> to set different steps to each
11732 @param {Function} [opts.onDrag] An event listener that fires when the draggable starts being dragged.
11734 @param {Function} [opts.onEnter] An event listener that fires when the draggable is dragged over a drop target.
11736 @param {Function} [opts.onLeave] An event listener that fires when the draggable is dragged out of a drop target.
11738 @param {Function} [opts.onDrop] An event listener that fires when the draggable is dropped.
11740 @param {Function} [opts.onAfterDrop] An event listener that fires after the element has dropped, including any animations
11742 The default action is to animate the draggable back to it's start
11743 position. This can be cancelled by returning false from the listener
11744 or calling {@link glow.events.Event.preventDefault} on the
11745 {@link glow.events.Event} param.
11748 // create a draggable element with a corresponding DropTarget,
11749 // container and two event listeners
11750 var myDraggable = new glow.dragdrop.Draggable('#draggable', {
11751 dropTargets : [ myDropTarget ],
11752 container : '#container',
11753 onDrag : function () {
11754 this.element.css('opacity', '0.7');
11756 onDrop : function () {
11757 this.element.css('opacity', '1');
11762 @name glow.dragdrop.Draggable#event:drag
11764 @description Fired when the draggable starts being dragged.
11766 Concelling this event results in the user being unable to pick up
11769 @param {glow.events.Event} event Event Object
11772 @name glow.dragdrop.Draggable#event:enter
11774 @description Fired when the draggable is dragged over a drop target.
11775 @param {glow.events.Event} event Event Object
11778 @name glow.dragdrop.Draggable#event:leave
11780 @description Fired when the draggable is dragged out of a drop target.
11781 @param {glow.events.Event} event Event Object
11784 @name glow.dragdrop.Draggable#event:drop
11786 @description Fired when the draggable is dropped.
11787 @param {glow.events.Event} event Event Object
11790 @name glow.dragdrop.Draggable#event:afterDrop
11792 @description Fired after the element has dropped, including any animations
11793 @param {glow.events.Event} event Event Object
11795 r.Draggable = function (el, opts) {
11798 @name glow.dragdrop.Draggable#element
11799 @type glow.dom.NodeList
11800 @description glow.dom.NodeList containing the draggable element
11802 this.element = $(el);
11803 this._opts = opts = glow.lang.apply({
11804 dragPrevention : ['input', 'textarea', 'button', 'select', 'option', 'a'],
11805 placeholder : 'spacer',
11806 placeholderClass : 'glow-dragdrop-placeholder',
11810 //normalise the step param to an object
11811 if (typeof opts.step == "number") {
11812 opts.step = {x: opts.step, y: opts.step};
11814 opts.step.x = opts.step.x || 1;
11815 opts.step.y = opts.step.y || 1;
11818 this._preventDrag = [];
11819 for (var i = 0, l = opts.dragPrevention.length; i < l; i++) {
11820 this._preventDrag[i] = opts.dragPrevention[i].toLowerCase();
11823 if (opts.container) { this.container = $(opts.container); }
11824 this._handle = opts.handle && this.element.get(opts.handle) || this.element;
11826 if (opts.dropTargets) this.dropTargets = $(opts.dropTargets);
11828 //used for IE literal edge case bug fix
11829 //this._mouseUp = true;
11830 //bug fix to get document.body.scrollTop to return true value (not 0) if using transitional 4.01 doctype
11831 //get('body')[0].style.overflow = 'auto';
11832 //this._opts = o, this._targetCoords = [], this.isOverTarget = false;
11834 var listeners = this._listeners = [],
11837 if (opts.onDrag) listeners[i++] = addListener(this, 'drag', this._opts.onDrag, this);
11838 if (opts.onEnter) listeners[i++] = addListener(this, 'enter', this._opts.onEnter, this);
11839 if (opts.onLeave) listeners[i++] = addListener(this, 'leave', this._opts.onLeave, this);
11840 if (opts.onDrop) listeners[i++] = addListener(this, 'drop', this._opts.onDrop, this);
11842 this._dragListener = addListener(this._handle, 'mousedown', this._startDragMouse, this);
11852 //var applyFloatBugfix = glow.env.ie;
11854 r.Draggable.prototype = {
11857 PrivateMethod: _createPlaceholder
11859 Create an element that occupies the space where the draggable has been dragged from.
11862 _createPlaceholder: function () {
11863 var el = this.element,
11867 if (this._opts.placeholder == 'clone') {
11868 placeholder = el.clone();
11870 else { // placeholder == 'spacer'
11871 placeholder = placeholderElement(el);
11873 if (this._opts.placeholderClass) {
11874 placeholder.addClass(this._opts.placeholderClass);
11876 box.sizePlaceholder(placeholder, null, this._startLeft, this._startTop);
11877 el.after(placeholder);
11878 this._placeholder = placeholder;
11882 PrivateMethod: _removePlaceholder
11884 Removes the placeholder (see above) from the document.
11887 _removePlaceholder: function () {
11888 this._placeholder.remove();
11892 PrivateMethod: _resetPosition
11894 Sets the position CSS property to what it started as without moving the draggable. If the
11895 original position was 'static' and making it 'static' again would mean moving the draggable,
11896 then the position is set to 'relative'.
11899 _resetPosition: function () {
11900 var origPos = this._preDragPosition,
11903 startOffset = this._startOffset,
11904 pos = el.css('position'),
11908 box.resetPosition();
11910 var position = box.el.position(),
11916 if (this._placeholder || this._dropIndicator) {
11920 if (origPos == 'static' && offset.y == startOffset.y && offset.x == startOffset.x) {
11921 el.css('position', 'static');
11922 el.css('left', '');
11926 el.css('z-index', this._preDragZIndex);
11927 el.css('position', origPos == 'static' ? 'relative' : origPos);
11928 if (origPos == 'static') {
11929 newLeft = offset.x - startOffset.x;
11930 newTop = offset.y - startOffset.y;
11932 else if (origPos == 'relative' && pos != 'relative') {
11933 newLeft = this._startLeft + (offset.x - startOffset.x);
11934 newTop = this._startTop + (offset.y - startOffset.y);
11936 if (pos != origPos) {
11937 el.css('left', newLeft ? newLeft + 'px' : '');
11938 el.css('top', newTop ? newTop + 'px' : '');
11941 if (this._dropIndicator) {
11942 var parent = this._dropIndicator.parent()[0];
11943 if (parent) parent.replaceChild(el[0], this._dropIndicator[0]);
11944 delete this._dropIndicator;
11945 if (this._placeholder) {
11946 this._placeholder.remove();
11947 delete this._placeholder;
11949 // this is canceling out some of the stuff done in the if statement above, could be done better
11950 el.css('position', origPos);
11951 if (origPos == 'relative' && pos != 'relative') {
11952 el.css('left', this._startLeft);
11953 el.css('top', this._startTop);
11956 else if (this._placeholder) {
11957 var parent = this._placeholder.parent()[0];
11958 if (parent) parent.replaceChild(el[0], this._placeholder[0]);
11959 delete this._placeholder;
11964 PrivateFunction: _startDragMouse
11966 Start the draggable dragging when the mousedown event is fired.
11970 *e* (glow.events.Event)
11972 The mousedown event that caused the listener to be fired.
11975 _startDragMouse: function (e) {
11976 var preventDrag = this._preventDrag,
11978 tag = source.tagName.toLowerCase();
11980 for (var i = 0, l = preventDrag.length; i < l; i++) {
11981 if (preventDrag[i] == tag) {
11986 //fire the drag event
11987 if (fire(this, 'drag').defaultPrevented()) {
11988 //the default action was prevented, don't do any dragging
11992 if (this._dragging == 1)
11993 return this.endDrag();
11994 else if (this._dragging)
11997 // _dragging set to 1 during drag, 2 while ending drag and back to 0 when ready for new drag
11998 this._dragging = 1;
12000 var el = this.element,
12001 container = this.container,
12003 box = this._box = new Box(el),
12006 this._preDragPosition = el.css('position');
12008 var position = box.el.position(),
12009 startOffset = this._startOffset = {
12015 this._containerBox = new Box(container);
12016 this._bounds = this._containerBox.boundsFor(box);
12017 //we may need to decrease the bounds to keep the element in step (if it's using stepped dragging)
12018 //basically makes the bounding box smaller to fit in with the stepping
12020 this._bounds[3] -= (this._bounds[3] - startOffset.x) % step.x;
12021 this._bounds[1] -= (this._bounds[1] - startOffset.x) % step.x;
12024 this._bounds[0] -= (this._bounds[0] - startOffset.y) % step.y;
12025 this._bounds[2] -= (this._bounds[2] - startOffset.y) % step.y;
12029 delete this._bounds;
12032 this._mouseStart = {
12039 this._preDragStyle = el.attr('style');
12041 this._preDragZIndex = el.css('z-index');
12043 el.css('z-index', _zIndex++);
12045 this._startLeft = el[0].style.left ? parseInt(el[0].style.left) : 0;
12046 this._startTop = el[0].style.top ? parseInt(el[0].style.top) : 0;
12049 if (opts.placeholder && opts.placeholder != 'none') {
12050 this._createPlaceholder();
12053 el.css('position', 'absolute');
12054 el.css('left', startOffset.x + 'px');
12055 el.css('top', startOffset.y + 'px');
12058 this._scrollY = document.documentElement.scrollTop;
12059 this._innerHeight = document.documentElement.clientHeight;
12062 this._scrollY = document.body.scrollTop;
12063 this._innerHeight = document.body.clientHeight;
12066 this._scrollY = window.scrollY;
12067 this._innerHeight = window.innerHeight;
12070 var cancelFunc = function () { return false },
12071 doc = document.documentElement;
12073 if (this.dropTargets) {
12074 var event = new events.Event();
12075 event.draggable = this;
12076 for (var i = 0, l = this.dropTargets.length; i < l; i++) {
12077 fire(this.dropTargets[i], 'active', event);
12084 this._testForDropTargets();
12087 this._dragListeners = [
12088 addListener(doc, 'selectstart', cancelFunc),
12089 addListener(doc, 'dragstart', cancelFunc),
12090 addListener(doc, 'mousedown', cancelFunc),
12091 addListener(doc, 'mousemove', this._dragMouse, this),
12092 addListener(doc, 'mouseup', this._releaseElement, this)
12098 PrivateFunction: _dragMouse
12100 Move the draggable when a mousemove event is received.
12104 *e* (glow.events.Event)
12106 The mousedown event that caused the listener to be fired.
12109 _dragMouse: function (e) {
12110 var element = this.element,
12111 axis = this._opts.axis,
12112 //do we need to do axis here, or just not apply the newX/Y if axis is used? May be faster
12113 newX = axis == 'y' ?
12114 this._startOffset.x:
12115 (this._startOffset.x + e.pageX - this._mouseStart.x),
12116 newY = axis == 'x' ?
12117 this._startOffset.y:
12118 (this._startOffset.y + e.pageY - this._mouseStart.y),
12119 bounds = this._bounds,
12120 step = this._opts.step;
12123 //round position to the nearest step
12125 newX = Math.round((newX - this._startOffset.x) / step.x) * step.x + this._startOffset.x;
12128 newY = Math.round((newY - this._startOffset.y) / step.y) * step.y + this._startOffset.y;
12131 // only pay for the function call if we have a container or an axis
12133 // only apply bounds on the axis we're using
12135 newX = newX < bounds[3] ? bounds[3] : newX > bounds[1] ? bounds[1] : newX;
12138 newY = newY < bounds[0] ? bounds[0] : newY > bounds[2] ? bounds[2] : newY;
12142 // set the new position
12143 element[0].style.left = newX + 'px';
12144 element[0].style.top = newY + 'px';
12146 //if there are dragTargets check if the draggable is over the target
12147 if (this.dropTargets) {
12148 this._mousePos = { x: e.pageX, y: e.pageY };
12150 // check for IE mouseup outside of page boundary
12151 if(_ie && e.nativeEvent.button == 0) {
12152 this._releaseElement(e);
12159 PrivateFunction: _testForDropTarget
12161 Check if the draggable is over a drop target. Sets the activeTarget property of the draggable
12162 to the drop target that the draggable is over, if any.
12166 *mousePos* (object)
12168 The position of the mouse pointer relative to the document. The object has x and y integer
12171 _testForDropTargets: function (fromTimeout) {
12173 if (! this._lock) this._lock = 0;
12174 if (fromTimeout) this._lock--;
12175 else if (this.lock) return;
12177 if (this._dragging != 1) return;
12179 var previousTarget = this.activeTarget,
12181 targets = this.dropTargets,
12185 mousePos = this._mousePos;
12187 box.resetPosition();
12189 var maxIntersectSize = 0;
12190 for (var i = 0, l = targets.length; i < l; i++) {
12191 target = targets[i];
12192 targetBox = target._box;
12193 if (target._opts.tolerance == 'contained') {
12194 if (targetBox.contains(box)) {
12195 activeTarget = target;
12199 else if (target._opts.tolerance == 'cursor') {
12200 if (targetBox.containsPoint(mousePos)) {
12201 activeTarget = target;
12206 var intersectSize = targetBox.intersectSize(box, true);
12207 if (intersectSize > maxIntersectSize) {
12208 maxIntersectSize = intersectSize;
12209 activeTarget = target;
12213 this.activeTarget = activeTarget;
12216 if (activeTarget !== previousTarget) {
12217 if (activeTarget) {
12218 // enter on the target
12219 var draggableEnterEvent = new events.Event();
12220 draggableEnterEvent.draggable = this;
12221 fire(activeTarget, 'enter', draggableEnterEvent);
12223 // enter on this (the draggable)
12224 var enterTargetEvent = new events.Event();
12225 enterTargetEvent.dropTarget = activeTarget;
12226 fire(this, 'enter', enterTargetEvent);
12229 if (previousTarget) {
12231 var draggableLeaveEvent = new events.Event();
12232 draggableLeaveEvent.draggable = this;
12233 fire(previousTarget, 'leave', draggableLeaveEvent);
12235 // leave on this (draggable)
12236 var leaveTargetEvent = new events.Event();
12237 leaveTargetEvent.dropTarget = previousTarget;
12238 fire(this, 'leave', leaveTargetEvent);
12241 // place the drop indicator in the drop target (not in the drop target class for speed)
12242 if (activeTarget && activeTarget._opts.dropIndicator != 'none') {
12244 childBoxes = activeTarget._childBoxes,
12245 children = activeTarget._children;
12246 box.resetPosition();
12247 var totalHeight = activeTarget._box.innerTopPos();
12248 var draggablePosition = mousePos.y - box.offsetParentPageTop();
12250 for (var i = 0, l = childBoxes.length; i < l; i++) {
12251 if (children[i] == this.element[0]) continue;
12252 childBox = childBoxes[i];
12253 totalHeight += childBox.outerHeight();
12254 if (draggablePosition <= totalHeight) {
12255 if (activeTarget._dropIndicatorAt != i) {
12256 $(childBox.el).before(activeTarget._dropIndicator);
12257 activeTarget._dropIndicatorAt = i;
12265 $(childBox.el).after(activeTarget._dropIndicator);
12266 activeTarget._dropIndicatorAt = i + 1;
12269 activeTarget.element.append(activeTarget._dropIndicator);
12270 activeTarget._dropIndicatorAt = 0;
12277 setTimeout(function () { this_._testForDropTargets(1) }, 100);
12281 PrivateMethod: releaseElement
12283 Finish the drag when a mouseup event is recieved.
12287 *e* (glow.events.Event)
12289 The mouseup event that caused the listener to be fired.
12292 _releaseElement: function () {
12293 if (this._dragging != 1) return;
12294 this._dragging = 2;
12298 //call the onInactive function on all the dropTargets for this draggable
12299 var dropTargets = this.dropTargets,
12300 activeTarget = this.activeTarget;
12303 for (i = 0, l = dropTargets.length; i < l; i++) {
12304 var event = new events.Event();
12305 event.draggable = this;
12306 event.droppedOnThis = activeTarget && activeTarget == dropTargets[i];
12307 fire(dropTargets[i], 'inactive', event);
12311 if (activeTarget) {
12312 var event = new events.Event();
12313 event.draggable = this;
12314 fire(activeTarget, 'drop', event);
12317 var dragListeners = this._dragListeners;
12318 for (i = 0, l = dragListeners.length; i < l; i++) {
12319 events.removeListener(dragListeners[i]);
12322 var dropEvent = fire(this, "drop");
12323 if (! dropEvent.defaultPrevented() && this.dropTargets) {
12335 Finishes dragging the draggable. Removes the placeholder (if any) and resets the position CSS property
12338 TODO - revist this code example
12340 N.B. This is called by default but if you overwrite the onDrop function then you will have to call it youoriginal
12343 var myDraggable = new glow.dragdrop.Draggable('#draggable', {
12344 onDrop = function(e){
12345 do some stuff that takes a while.....
12353 endDrag: function(){
12354 if (this._dragging != 2) return;
12355 this._dragging = 0;
12357 //remove any helpers/placeholders
12361 delete this._reset;
12364 if (this.placeholder) {
12365 this.placeholder.remove();
12367 this._resetPosition();
12368 delete this.activeTarget;
12369 fire(this, "afterDrop");
12375 Animates the Draggable back to it's start position and calls endDrag() at the end of the
12376 transition. This is called by default when the Draggable, that has a DragTarget, is dropped.
12377 However if you override the default onDrop function you may want to call this function your
12382 The animation you wish to used for easing the Draggable. See <glow.tweens>. This is optional, the default is a linear tween. e.g. glow.tweens.easeboth
12385 returnHome: function(tween){
12386 var mytween = (tween) ? tween : glow.tweens.linear(),
12390 position = this._box.el.position(),
12391 distance = Math.pow(
12392 Math.pow(this._startOffset.x - position.left, 2)
12393 + Math.pow(this._startOffset.y - position.top, 2),
12396 duration = 0.3 + (distance / 1000);
12399 glow.anim.css(el, duration, {
12400 left: this._startOffset.x,
12401 top : this._startOffset.y
12402 }, { tween: mytween })
12405 if (this._dropIndicator) {
12406 channels.push([glow.anim.css(this._dropIndicator, duration - 0.1, { opacity: { to: 0 } })]);
12409 var timeline = new glow.anim.Timeline(channels);
12410 addListener(timeline, 'complete', function () {
12419 var dropTargetId = 0;
12422 @name glow.dragdrop.DropTarget
12424 @description An element that can react to Draggables.
12425 @see <a href="../furtherinfo/dragdrop/droptargets.shtml">DropTarget examples</a>
12427 @param {String | Element | glow.dom.NodeList} element The element or CSS selector for an element to be made droppable.
12429 If a {@link glow.dom.NodeList NodeList} or CSS selector matching
12430 multiple elements is passed only the first element is made droppable.
12432 @param {Object} [opts]
12434 An object of options.
12436 The opts object allows you to pass in functions to use as event
12437 listeners. This is purely for convenience, you can also use
12438 {@link glow.events.addListener} to add them the normal way.
12440 @param {String} [opts.tolerance=intersect] The point at which the target becomes active when a draggable moves over it.
12442 Possible values for this param are:
12445 <dt>intersect</dt><dd>The target becomes active as soon as any part of the draggable is over the target.</dd>
12446 <dt>cursor</dt><dd>The target becomes active when the cursor is over the target.</dd>
12447 <dt>contained</dt><dd>The target only becomes active once the whole draggable is within the target.</dd>
12450 @param {String} [opts.dropIndicator=none] Whether to create an element when a Draggable is over the DropTarget.
12452 Possible values for this param are:
12455 <dt>spacer</dt><dd>an empty div will be added to the drop target to indicate where the Draggable will be dropped.</dd>
12456 <dt>none</dt><dd>no drop indicator will be created.</dd>
12459 @param {String} [opts.dropIndicatorClass=glow-dragdrop-dropindicator] The class apply to the dropIndicator element.
12461 This is useful if you want to style the drop indicator.
12463 @param {Function} [opts.onEnter] An event listener to fire when an associated Draggable is dragged over the drop target.
12465 @param {Function} [opts.onLeave] An event listener to fire when an associated Draggable is dragged out of the drop target.
12467 @param {Function} [opts.onDrop] An event listener to fire when an associated Draggable is dropped on the drop target.
12469 @param {Function} [opts.onActive] An event listener to fire when an associated Draggable starts being dragged.
12471 @param {Function} [opts.onInactive] An event listener to fire when an associated Draggable stops being dragged.
12474 var myDropTarget = new glow.dragdrop.DropTarget('#dropTarget', {
12475 onActive: function(e){
12476 this.element.css('border', '2px solid blue');
12478 onInactive: function(e){
12479 this.element.css('border', '');
12480 this.element.css('opacity', '1');
12482 onEnter: function(e){
12483 this.element.css('opacity', '0.2');
12485 onLeave: function(e){
12486 this.element.css('opacity', '1');
12488 onDrop: function(e){
12489 this.element.css('backgroundColor', 'green');
12494 @name glow.dragdrop.DropTarget#event:active
12496 @description Fired when a draggable linked to this drop target starts being dragged.
12497 @param {glow.events.Event} event Event Object
12500 @name glow.dragdrop.DropTarget#event:inactive
12502 @description Fired when a draggable linked to this drop target stops dragging.
12503 @param {glow.events.Event} event Event Object
12506 @name glow.dragdrop.DropTarget#event:enter
12508 @description Fired when a draggable linked to this drop target is dragged over the target.
12509 @param {glow.events.Event} event Event Object
12512 @name glow.dragdrop.DropTarget#event:leave
12514 @description Fired when a draggable linked to this drop target is dragged out of the target.
12515 @param {glow.events.Event} event Event Object
12518 @name glow.dragdrop.DropTarget#event:drop
12520 @description Fired when a draggable linked is dropped on this drop target.
12521 @param {glow.events.Event} event Event Object
12523 r.DropTarget = function(el, opts) {
12525 @name glow.dragdrop.DropTarget#element
12526 @type glow.dom.NodeList
12527 @description glow.dom.NodeList containing the draggable element
12529 el = this.element = $(el);
12530 if (! el.length) throw 'no element passed into DropTarget constuctor';
12531 if (el.length > 1) throw 'more than one element passed into DropTarget constructor';
12533 // id is for indexing drop targets in an object for getting to them quickly
12534 this._id = ++dropTargetId;
12536 this._opts = opts = glow.lang.apply({
12537 dropIndicator : 'none',
12538 dropIndicatorClass : 'glow-dragdrop-dropindicator',
12539 tolerance : 'intersect'
12542 if (opts.onActive) addListener(this, 'active', opts.onActive);
12543 if (opts.onInactive) addListener(this, 'inactive', opts.onInactive);
12544 if (opts.onEnter) addListener(this, 'enter', opts.onEnter);
12545 if (opts.onLeave) addListener(this, 'leave', opts.onLeave);
12546 if (opts.onDrop) addListener(this, 'drop', opts.onDrop);
12548 addListener(this, 'active', this._onActive);
12549 addListener(this, 'inactive', this._onInactive);
12555 r.DropTarget.prototype = {
12558 Method: setLogicalBottom(height)
12560 Set a bottom pos to use for detecting if a draggable is over the drop target to use
12561 other than the actual bottom of the drop target (offsetTop + offsetHeight).
12567 The number of pixels to use for the bottom of the drop target.
12569 setLogicalBottom: function (bottom) {
12570 this._logicalBottom = bottom;
12574 PrivateMethod: _onActive
12576 Respond to an associated draggable when it starts to be dragged.
12580 *e* (glow.events.Event)
12582 The active event that caused the event listener to be fired.
12585 _onActive: function (e) {
12586 var draggable = e.draggable;
12588 this._box = new Box(this.element);
12589 if (this._logicalBottom) this._box.setLogicalBottom(this._logicalBottom);
12591 if (this._opts.dropIndicator == 'none') return;
12593 this._onEnterListener = addListener(this, 'enter', this._onEnter);
12594 this._onLeaveListener = addListener(this, 'leave', this._onLeave);
12596 this._dropIndicator = placeholderElement(draggable.element);
12598 if (this._opts.dropIndicatorClass) {
12599 this._dropIndicator.addClass(this._opts.dropIndicatorClass);
12601 draggable._box.sizePlaceholder(this._dropIndicator, 'relative', 0, 0);
12604 var children = this._children = $(this.element.children()).filter(function () {
12606 return (! e.draggable._placeholder || ! el.eq(e.draggable._placeholder))
12607 && (! this._dropIndicator || ! el.eq(this._dropIndicator));
12609 var childBoxes = this._childBoxes = [];
12610 children.each(function (i) {
12611 childBoxes[i] = new Box($(children[i]));
12616 PrivateMethod: _onInactive
12618 Respond to an associated draggable when it finishes being dragged.
12622 *e* (glow.events.Event)
12624 The inactive event that caused the event listener to be fired.
12627 _onInactive: function (e) {
12628 removeListener(this._onEnterListener);
12629 removeListener(this._onLeaveListener);
12633 if (this._opts.dropIndicator == 'none') return;
12635 if (! e.droppedOnThis && this._dropIndicator) {
12636 this._dropIndicator.remove();
12637 delete this._dropIndicator;
12639 delete this._childBoxes;
12640 delete this._children;
12644 PrivateMethod: _onEnter
12646 Respond to an associated draggable being dragged over the drop target.
12650 *e* (glow.events.Event)
12652 The enter event that caused the event listener to be fired.
12655 _onEnter: function () {
12656 this._dropIndicatorAt = -1;
12660 PrivateMethod: _onLeave
12662 Respond to an associated draggable being dragged out of the drop target.
12666 *e* (glow.events.Event)
12668 The leave event that caused the event listener to be fired.
12671 _onLeave: function () {
12672 this._dropIndicator.remove();
12676 Method: moveToPosition
12678 Insert the draggable's element within the drop target where the drop indicator currently is. Sets
12679 the start offset of the drag to the position of the drop indicator so that it will be animated
12680 to it's final location, rather than where the drag started.
12683 moveToPosition : function (draggable) {
12684 var dropIndicator = this._dropIndicator,
12685 box = new Box(dropIndicator);
12686 //dropIndicator.after(draggable.element);
12687 var marginLeft = parseInt(dropIndicator.css('margin-left')) || 0,
12688 marginTop = parseInt(dropIndicator.css('margin-top')) || 0,
12689 position = box.el.position();
12691 draggable._startOffset = {
12695 draggable._dropIndicator = dropIndicator;
12696 delete this._dropIndicator;