diff -r 000000000000 -r 6474c204b198 browser/components/tabview/iq.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/components/tabview/iq.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,763 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// ********** +// Title: iq.js +// Various helper functions, in the vein of jQuery. + +// ---------- +// Function: iQ +// Returns an iQClass object which represents an individual element or a group +// of elements. It works pretty much like jQuery(), with a few exceptions, +// most notably that you can't use strings with complex html, +// just simple tags like '
'. +function iQ(selector, context) { + // The iQ object is actually just the init constructor 'enhanced' + return new iQClass(selector, context); +}; + +// A simple way to check for HTML strings or ID strings +// (both of which we optimize for) +let quickExpr = /^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/; + +// Match a standalone tag +let rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/; + +// ########## +// Class: iQClass +// The actual class of iQ result objects, representing an individual element +// or a group of elements. +// +// ---------- +// Function: iQClass +// You don't call this directly; this is what's called by iQ(). +function iQClass(selector, context) { + + // Handle $(""), $(null), or $(undefined) + if (!selector) { + return this; + } + + // Handle $(DOMElement) + if (selector.nodeType) { + this.context = selector; + this[0] = selector; + this.length = 1; + return this; + } + + // The body element only exists once, optimize finding it + if (selector === "body" && !context) { + this.context = document; + this[0] = document.body; + this.selector = "body"; + this.length = 1; + return this; + } + + // Handle HTML strings + if (typeof selector === "string") { + // Are we dealing with HTML string or an ID? + + let match = quickExpr.exec(selector); + + // Verify a match, and that no context was specified for #id + if (match && (match[1] || !context)) { + + // HANDLE $(html) -> $(array) + if (match[1]) { + let doc = (context ? context.ownerDocument || context : document); + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + let ret = rsingleTag.exec(selector); + + if (ret) { + if (Utils.isPlainObject(context)) { + Utils.assert(false, 'does not support HTML creation with context'); + } else { + selector = [doc.createElement(ret[1])]; + } + + } else { + Utils.assert(false, 'does not support complex HTML creation'); + } + + return Utils.merge(this, selector); + + // HANDLE $("#id") + } else { + let elem = document.getElementById(match[2]); + + if (elem) { + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE $("TAG") + } else if (!context && /^\w+$/.test(selector)) { + this.selector = selector; + this.context = document; + selector = document.getElementsByTagName(selector); + return Utils.merge(this, selector); + + // HANDLE $(expr, $(...)) + } else if (!context || context.iq) { + return (context || iQ(document)).find(selector); + + // HANDLE $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return iQ(context).find(selector); + } + + // HANDLE $(function) + // Shortcut for document ready + } else if (typeof selector == "function") { + Utils.log('iQ does not support ready functions'); + return null; + } + + if ("selector" in selector) { + this.selector = selector.selector; + this.context = selector.context; + } + + let ret = this || []; + if (selector != null) { + // The window, strings (and functions) also have 'length' + if (selector.length == null || typeof selector == "string" || selector.setInterval) { + Array.push(ret, selector); + } else { + Utils.merge(ret, selector); + } + } + return ret; +}; + +iQClass.prototype = { + + // ---------- + // Function: toString + // Prints [iQ...] for debug use + toString: function iQClass_toString() { + if (this.length > 1) { + if (this.selector) + return "[iQ (" + this.selector + ")]"; + else + return "[iQ multi-object]"; + } + + if (this.length == 1) + return "[iQ (" + this[0].toString() + ")]"; + + return "[iQ non-object]"; + }, + + // Start with an empty selector + selector: "", + + // The default length of a iQ object is 0 + length: 0, + + // ---------- + // Function: each + // Execute a callback for every element in the matched set. + each: function iQClass_each(callback) { + if (typeof callback != "function") { + Utils.assert(false, "each's argument must be a function"); + return null; + } + for (let i = 0; this[i] != null && callback(this[i]) !== false; i++) {} + return this; + }, + + // ---------- + // Function: addClass + // Adds the given class(es) to the receiver. + addClass: function iQClass_addClass(value) { + Utils.assertThrow(typeof value == "string" && value, + 'requires a valid string argument'); + + let length = this.length; + for (let i = 0; i < length; i++) { + let elem = this[i]; + if (elem.nodeType === 1) { + value.split(/\s+/).forEach(function(className) { + elem.classList.add(className); + }); + } + } + + return this; + }, + + // ---------- + // Function: removeClass + // Removes the given class(es) from the receiver. + removeClass: function iQClass_removeClass(value) { + if (typeof value != "string" || !value) { + Utils.assert(false, 'does not support function argument'); + return null; + } + + let length = this.length; + for (let i = 0; i < length; i++) { + let elem = this[i]; + if (elem.nodeType === 1 && elem.className) { + value.split(/\s+/).forEach(function(className) { + elem.classList.remove(className); + }); + } + } + + return this; + }, + + // ---------- + // Function: hasClass + // Returns true is the receiver has the given css class. + hasClass: function iQClass_hasClass(singleClassName) { + let length = this.length; + for (let i = 0; i < length; i++) { + if (this[i].classList.contains(singleClassName)) { + return true; + } + } + return false; + }, + + // ---------- + // Function: find + // Searches the receiver and its children, returning a new iQ object with + // elements that match the given selector. + find: function iQClass_find(selector) { + let ret = []; + let length = 0; + + let l = this.length; + for (let i = 0; i < l; i++) { + length = ret.length; + try { + Utils.merge(ret, this[i].querySelectorAll(selector)); + } catch(e) { + Utils.log('iQ.find error (bad selector)', e); + } + + if (i > 0) { + // Make sure that the results are unique + for (let n = length; n < ret.length; n++) { + for (let r = 0; r < length; r++) { + if (ret[r] === ret[n]) { + ret.splice(n--, 1); + break; + } + } + } + } + } + + return iQ(ret); + }, + + // ---------- + // Function: contains + // Check to see if a given DOM node descends from the receiver. + contains: function iQClass_contains(selector) { + Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)'); + + // fast path when querySelector() can be used + if ('string' == typeof selector) + return null != this[0].querySelector(selector); + + let object = iQ(selector); + Utils.assert(object.length <= 1, 'does not yet support multi-objects'); + + let elem = object[0]; + if (!elem || !elem.parentNode) + return false; + + do { + elem = elem.parentNode; + } while (elem && this[0] != elem); + + return this[0] == elem; + }, + + // ---------- + // Function: remove + // Removes the receiver from the DOM. + remove: function iQClass_remove(options) { + if (!options || !options.preserveEventHandlers) + this.unbindAll(); + for (let i = 0; this[i] != null; i++) { + let elem = this[i]; + if (elem.parentNode) { + elem.parentNode.removeChild(elem); + } + } + return this; + }, + + // ---------- + // Function: empty + // Removes all of the reciever's children and HTML content from the DOM. + empty: function iQClass_empty() { + for (let i = 0; this[i] != null; i++) { + let elem = this[i]; + while (elem.firstChild) { + iQ(elem.firstChild).unbindAll(); + elem.removeChild(elem.firstChild); + } + } + return this; + }, + + // ---------- + // Function: width + // Returns the width of the receiver, including padding and border. + width: function iQClass_width() { + return Math.floor(this[0].offsetWidth); + }, + + // ---------- + // Function: height + // Returns the height of the receiver, including padding and border. + height: function iQClass_height() { + return Math.floor(this[0].offsetHeight); + }, + + // ---------- + // Function: position + // Returns an object with the receiver's position in left and top + // properties. + position: function iQClass_position() { + let bounds = this.bounds(); + return new Point(bounds.left, bounds.top); + }, + + // ---------- + // Function: bounds + // Returns a with the receiver's bounds. + bounds: function iQClass_bounds() { + Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)'); + let rect = this[0].getBoundingClientRect(); + return new Rect(Math.floor(rect.left), Math.floor(rect.top), + Math.floor(rect.width), Math.floor(rect.height)); + }, + + // ---------- + // Function: data + // Pass in both key and value to attach some data to the receiver; + // pass in just key to retrieve it. + data: function iQClass_data(key, value) { + let data = null; + if (value === undefined) { + Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)'); + data = this[0].iQData; + if (data) + return data[key]; + else + return null; + } + + for (let i = 0; this[i] != null; i++) { + let elem = this[i]; + data = elem.iQData; + + if (!data) + data = elem.iQData = {}; + + data[key] = value; + } + + return this; + }, + + // ---------- + // Function: html + // Given a value, sets the receiver's innerHTML to it; otherwise returns + // what's already there. + html: function iQClass_html(value) { + Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)'); + if (value === undefined) + return this[0].innerHTML; + + this[0].innerHTML = value; + return this; + }, + + // ---------- + // Function: text + // Given a value, sets the receiver's textContent to it; otherwise returns + // what's already there. + text: function iQClass_text(value) { + Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)'); + if (value === undefined) { + return this[0].textContent; + } + + return this.empty().append((this[0] && this[0].ownerDocument || document).createTextNode(value)); + }, + + // ---------- + // Function: val + // Given a value, sets the receiver's value to it; otherwise returns what's already there. + val: function iQClass_val(value) { + Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)'); + if (value === undefined) { + return this[0].value; + } + + this[0].value = value; + return this; + }, + + // ---------- + // Function: appendTo + // Appends the receiver to the result of iQ(selector). + appendTo: function iQClass_appendTo(selector) { + Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)'); + iQ(selector).append(this); + return this; + }, + + // ---------- + // Function: append + // Appends the result of iQ(selector) to the receiver. + append: function iQClass_append(selector) { + let object = iQ(selector); + Utils.assert(object.length == 1 && this.length == 1, + 'does not yet support multi-objects (or null objects)'); + this[0].appendChild(object[0]); + return this; + }, + + // ---------- + // Function: attr + // Sets or gets an attribute on the element(s). + attr: function iQClass_attr(key, value) { + Utils.assert(typeof key === 'string', 'string key'); + if (value === undefined) { + Utils.assert(this.length == 1, 'retrieval does not support multi-objects (or null objects)'); + return this[0].getAttribute(key); + } + + for (let i = 0; this[i] != null; i++) + this[i].setAttribute(key, value); + + return this; + }, + + // ---------- + // Function: css + // Sets or gets CSS properties on the receiver. When setting certain numerical properties, + // will automatically add "px". A property can be removed by setting it to null. + // + // Possible call patterns: + // a: object, b: undefined - sets with properties from a + // a: string, b: undefined - gets property specified by a + // a: string, b: string/number - sets property specified by a to b + css: function iQClass_css(a, b) { + let properties = null; + + if (typeof a === 'string') { + let key = a; + if (b === undefined) { + Utils.assert(this.length == 1, 'retrieval does not support multi-objects (or null objects)'); + + return window.getComputedStyle(this[0], null).getPropertyValue(key); + } + properties = {}; + properties[key] = b; + } else if (a instanceof Rect) { + properties = { + left: a.left, + top: a.top, + width: a.width, + height: a.height + }; + } else { + properties = a; + } + + let pixels = { + 'left': true, + 'top': true, + 'right': true, + 'bottom': true, + 'width': true, + 'height': true + }; + + for (let i = 0; this[i] != null; i++) { + let elem = this[i]; + for (let key in properties) { + let value = properties[key]; + + if (pixels[key] && typeof value != 'string') + value += 'px'; + + if (value == null) { + elem.style.removeProperty(key); + } else if (key.indexOf('-') != -1) + elem.style.setProperty(key, value, ''); + else + elem.style[key] = value; + } + } + + return this; + }, + + // ---------- + // Function: animate + // Uses CSS transitions to animate the element. + // + // Parameters: + // css - an object map of the CSS properties to change + // options - an object with various properites (see below) + // + // Possible "options" properties: + // duration - how long to animate, in milliseconds + // easing - easing function to use. Possibilities include + // "tabviewBounce", "easeInQuad". Default is "ease". + // complete - function to call once the animation is done, takes nothing + // in, but "this" is set to the element that was animated. + animate: function iQClass_animate(css, options) { + Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)'); + + if (!options) + options = {}; + + let easings = { + tabviewBounce: "cubic-bezier(0.0, 0.63, .6, 1.29)", + easeInQuad: 'ease-in', // TODO: make it a real easeInQuad, or decide we don't care + fast: 'cubic-bezier(0.7,0,1,1)' + }; + + let duration = (options.duration || 400); + let easing = (easings[options.easing] || 'ease'); + + if (css instanceof Rect) { + css = { + left: css.left, + top: css.top, + width: css.width, + height: css.height + }; + } + + + // The latest versions of Firefox do not animate from a non-explicitly + // set css properties. So for each element to be animated, go through + // and explicitly define 'em. + let rupper = /([A-Z])/g; + this.each(function(elem) { + let cStyle = window.getComputedStyle(elem, null); + for (let prop in css) { + prop = prop.replace(rupper, "-$1").toLowerCase(); + iQ(elem).css(prop, cStyle.getPropertyValue(prop)); + } + }); + + this.css({ + 'transition-property': Object.keys(css).join(", "), + 'transition-duration': (duration / 1000) + 's', + 'transition-timing-function': easing + }); + + this.css(css); + + let self = this; + setTimeout(function() { + self.css({ + 'transition-property': 'none', + 'transition-duration': '', + 'transition-timing-function': '' + }); + + if (typeof options.complete == "function") + options.complete.apply(self); + }, duration); + + return this; + }, + + // ---------- + // Function: fadeOut + // Animates the receiver to full transparency. Calls callback on completion. + fadeOut: function iQClass_fadeOut(callback) { + Utils.assert(typeof callback == "function" || callback === undefined, + 'does not yet support duration'); + + this.animate({ + opacity: 0 + }, { + duration: 400, + complete: function() { + iQ(this).css({display: 'none'}); + if (typeof callback == "function") + callback.apply(this); + } + }); + + return this; + }, + + // ---------- + // Function: fadeIn + // Animates the receiver to full opacity. + fadeIn: function iQClass_fadeIn() { + this.css({display: ''}); + this.animate({ + opacity: 1 + }, { + duration: 400 + }); + + return this; + }, + + // ---------- + // Function: hide + // Hides the receiver. + hide: function iQClass_hide() { + this.css({display: 'none', opacity: 0}); + return this; + }, + + // ---------- + // Function: show + // Shows the receiver. + show: function iQClass_show() { + this.css({display: '', opacity: 1}); + return this; + }, + + // ---------- + // Function: bind + // Binds the given function to the given event type. Also wraps the function + // in a try/catch block that does a Utils.log on any errors. + bind: function iQClass_bind(type, func) { + let handler = function(event) func.apply(this, [event]); + + for (let i = 0; this[i] != null; i++) { + let elem = this[i]; + if (!elem.iQEventData) + elem.iQEventData = {}; + + if (!elem.iQEventData[type]) + elem.iQEventData[type] = []; + + elem.iQEventData[type].push({ + original: func, + modified: handler + }); + + elem.addEventListener(type, handler, false); + } + + return this; + }, + + // ---------- + // Function: one + // Binds the given function to the given event type, but only for one call; + // automatically unbinds after the event fires once. + one: function iQClass_one(type, func) { + Utils.assert(typeof func == "function", 'does not support eventData argument'); + + let handler = function(e) { + iQ(this).unbind(type, handler); + return func.apply(this, [e]); + }; + + return this.bind(type, handler); + }, + + // ---------- + // Function: unbind + // Unbinds the given function from the given event type. + unbind: function iQClass_unbind(type, func) { + Utils.assert(typeof func == "function", 'Must provide a function'); + + for (let i = 0; this[i] != null; i++) { + let elem = this[i]; + let handler = func; + if (elem.iQEventData && elem.iQEventData[type]) { + let count = elem.iQEventData[type].length; + for (let a = 0; a < count; a++) { + let pair = elem.iQEventData[type][a]; + if (pair.original == func) { + handler = pair.modified; + elem.iQEventData[type].splice(a, 1); + if (!elem.iQEventData[type].length) { + delete elem.iQEventData[type]; + if (!Object.keys(elem.iQEventData).length) + delete elem.iQEventData; + } + break; + } + } + } + + elem.removeEventListener(type, handler, false); + } + + return this; + }, + + // ---------- + // Function: unbindAll + // Unbinds all event handlers. + unbindAll: function iQClass_unbindAll() { + for (let i = 0; this[i] != null; i++) { + let elem = this[i]; + + for (let j = 0; j < elem.childElementCount; j++) + iQ(elem.children[j]).unbindAll(); + + if (!elem.iQEventData) + continue; + + Object.keys(elem.iQEventData).forEach(function (type) { + while (elem.iQEventData && elem.iQEventData[type]) + this.unbind(type, elem.iQEventData[type][0].original); + }, this); + } + + return this; + } +}; + +// ---------- +// Create various event aliases +let events = [ + 'keyup', + 'keydown', + 'keypress', + 'mouseup', + 'mousedown', + 'mouseover', + 'mouseout', + 'mousemove', + 'click', + 'dblclick', + 'resize', + 'change', + 'blur', + 'focus' +]; + +events.forEach(function(event) { + iQClass.prototype[event] = function(func) { + return this.bind(event, func); + }; +});