Wed, 31 Dec 2014 06:55:46 +0100
Added tag TORBROWSER_REPLICA for changeset 6474c204b198
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 // **********
6 // Title: iq.js
7 // Various helper functions, in the vein of jQuery.
9 // ----------
10 // Function: iQ
11 // Returns an iQClass object which represents an individual element or a group
12 // of elements. It works pretty much like jQuery(), with a few exceptions,
13 // most notably that you can't use strings with complex html,
14 // just simple tags like '<div>'.
15 function iQ(selector, context) {
16 // The iQ object is actually just the init constructor 'enhanced'
17 return new iQClass(selector, context);
18 };
20 // A simple way to check for HTML strings or ID strings
21 // (both of which we optimize for)
22 let quickExpr = /^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/;
24 // Match a standalone tag
25 let rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/;
27 // ##########
28 // Class: iQClass
29 // The actual class of iQ result objects, representing an individual element
30 // or a group of elements.
31 //
32 // ----------
33 // Function: iQClass
34 // You don't call this directly; this is what's called by iQ().
35 function iQClass(selector, context) {
37 // Handle $(""), $(null), or $(undefined)
38 if (!selector) {
39 return this;
40 }
42 // Handle $(DOMElement)
43 if (selector.nodeType) {
44 this.context = selector;
45 this[0] = selector;
46 this.length = 1;
47 return this;
48 }
50 // The body element only exists once, optimize finding it
51 if (selector === "body" && !context) {
52 this.context = document;
53 this[0] = document.body;
54 this.selector = "body";
55 this.length = 1;
56 return this;
57 }
59 // Handle HTML strings
60 if (typeof selector === "string") {
61 // Are we dealing with HTML string or an ID?
63 let match = quickExpr.exec(selector);
65 // Verify a match, and that no context was specified for #id
66 if (match && (match[1] || !context)) {
68 // HANDLE $(html) -> $(array)
69 if (match[1]) {
70 let doc = (context ? context.ownerDocument || context : document);
72 // If a single string is passed in and it's a single tag
73 // just do a createElement and skip the rest
74 let ret = rsingleTag.exec(selector);
76 if (ret) {
77 if (Utils.isPlainObject(context)) {
78 Utils.assert(false, 'does not support HTML creation with context');
79 } else {
80 selector = [doc.createElement(ret[1])];
81 }
83 } else {
84 Utils.assert(false, 'does not support complex HTML creation');
85 }
87 return Utils.merge(this, selector);
89 // HANDLE $("#id")
90 } else {
91 let elem = document.getElementById(match[2]);
93 if (elem) {
94 this.length = 1;
95 this[0] = elem;
96 }
98 this.context = document;
99 this.selector = selector;
100 return this;
101 }
103 // HANDLE $("TAG")
104 } else if (!context && /^\w+$/.test(selector)) {
105 this.selector = selector;
106 this.context = document;
107 selector = document.getElementsByTagName(selector);
108 return Utils.merge(this, selector);
110 // HANDLE $(expr, $(...))
111 } else if (!context || context.iq) {
112 return (context || iQ(document)).find(selector);
114 // HANDLE $(expr, context)
115 // (which is just equivalent to: $(context).find(expr)
116 } else {
117 return iQ(context).find(selector);
118 }
120 // HANDLE $(function)
121 // Shortcut for document ready
122 } else if (typeof selector == "function") {
123 Utils.log('iQ does not support ready functions');
124 return null;
125 }
127 if ("selector" in selector) {
128 this.selector = selector.selector;
129 this.context = selector.context;
130 }
132 let ret = this || [];
133 if (selector != null) {
134 // The window, strings (and functions) also have 'length'
135 if (selector.length == null || typeof selector == "string" || selector.setInterval) {
136 Array.push(ret, selector);
137 } else {
138 Utils.merge(ret, selector);
139 }
140 }
141 return ret;
142 };
144 iQClass.prototype = {
146 // ----------
147 // Function: toString
148 // Prints [iQ...] for debug use
149 toString: function iQClass_toString() {
150 if (this.length > 1) {
151 if (this.selector)
152 return "[iQ (" + this.selector + ")]";
153 else
154 return "[iQ multi-object]";
155 }
157 if (this.length == 1)
158 return "[iQ (" + this[0].toString() + ")]";
160 return "[iQ non-object]";
161 },
163 // Start with an empty selector
164 selector: "",
166 // The default length of a iQ object is 0
167 length: 0,
169 // ----------
170 // Function: each
171 // Execute a callback for every element in the matched set.
172 each: function iQClass_each(callback) {
173 if (typeof callback != "function") {
174 Utils.assert(false, "each's argument must be a function");
175 return null;
176 }
177 for (let i = 0; this[i] != null && callback(this[i]) !== false; i++) {}
178 return this;
179 },
181 // ----------
182 // Function: addClass
183 // Adds the given class(es) to the receiver.
184 addClass: function iQClass_addClass(value) {
185 Utils.assertThrow(typeof value == "string" && value,
186 'requires a valid string argument');
188 let length = this.length;
189 for (let i = 0; i < length; i++) {
190 let elem = this[i];
191 if (elem.nodeType === 1) {
192 value.split(/\s+/).forEach(function(className) {
193 elem.classList.add(className);
194 });
195 }
196 }
198 return this;
199 },
201 // ----------
202 // Function: removeClass
203 // Removes the given class(es) from the receiver.
204 removeClass: function iQClass_removeClass(value) {
205 if (typeof value != "string" || !value) {
206 Utils.assert(false, 'does not support function argument');
207 return null;
208 }
210 let length = this.length;
211 for (let i = 0; i < length; i++) {
212 let elem = this[i];
213 if (elem.nodeType === 1 && elem.className) {
214 value.split(/\s+/).forEach(function(className) {
215 elem.classList.remove(className);
216 });
217 }
218 }
220 return this;
221 },
223 // ----------
224 // Function: hasClass
225 // Returns true is the receiver has the given css class.
226 hasClass: function iQClass_hasClass(singleClassName) {
227 let length = this.length;
228 for (let i = 0; i < length; i++) {
229 if (this[i].classList.contains(singleClassName)) {
230 return true;
231 }
232 }
233 return false;
234 },
236 // ----------
237 // Function: find
238 // Searches the receiver and its children, returning a new iQ object with
239 // elements that match the given selector.
240 find: function iQClass_find(selector) {
241 let ret = [];
242 let length = 0;
244 let l = this.length;
245 for (let i = 0; i < l; i++) {
246 length = ret.length;
247 try {
248 Utils.merge(ret, this[i].querySelectorAll(selector));
249 } catch(e) {
250 Utils.log('iQ.find error (bad selector)', e);
251 }
253 if (i > 0) {
254 // Make sure that the results are unique
255 for (let n = length; n < ret.length; n++) {
256 for (let r = 0; r < length; r++) {
257 if (ret[r] === ret[n]) {
258 ret.splice(n--, 1);
259 break;
260 }
261 }
262 }
263 }
264 }
266 return iQ(ret);
267 },
269 // ----------
270 // Function: contains
271 // Check to see if a given DOM node descends from the receiver.
272 contains: function iQClass_contains(selector) {
273 Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)');
275 // fast path when querySelector() can be used
276 if ('string' == typeof selector)
277 return null != this[0].querySelector(selector);
279 let object = iQ(selector);
280 Utils.assert(object.length <= 1, 'does not yet support multi-objects');
282 let elem = object[0];
283 if (!elem || !elem.parentNode)
284 return false;
286 do {
287 elem = elem.parentNode;
288 } while (elem && this[0] != elem);
290 return this[0] == elem;
291 },
293 // ----------
294 // Function: remove
295 // Removes the receiver from the DOM.
296 remove: function iQClass_remove(options) {
297 if (!options || !options.preserveEventHandlers)
298 this.unbindAll();
299 for (let i = 0; this[i] != null; i++) {
300 let elem = this[i];
301 if (elem.parentNode) {
302 elem.parentNode.removeChild(elem);
303 }
304 }
305 return this;
306 },
308 // ----------
309 // Function: empty
310 // Removes all of the reciever's children and HTML content from the DOM.
311 empty: function iQClass_empty() {
312 for (let i = 0; this[i] != null; i++) {
313 let elem = this[i];
314 while (elem.firstChild) {
315 iQ(elem.firstChild).unbindAll();
316 elem.removeChild(elem.firstChild);
317 }
318 }
319 return this;
320 },
322 // ----------
323 // Function: width
324 // Returns the width of the receiver, including padding and border.
325 width: function iQClass_width() {
326 return Math.floor(this[0].offsetWidth);
327 },
329 // ----------
330 // Function: height
331 // Returns the height of the receiver, including padding and border.
332 height: function iQClass_height() {
333 return Math.floor(this[0].offsetHeight);
334 },
336 // ----------
337 // Function: position
338 // Returns an object with the receiver's position in left and top
339 // properties.
340 position: function iQClass_position() {
341 let bounds = this.bounds();
342 return new Point(bounds.left, bounds.top);
343 },
345 // ----------
346 // Function: bounds
347 // Returns a <Rect> with the receiver's bounds.
348 bounds: function iQClass_bounds() {
349 Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)');
350 let rect = this[0].getBoundingClientRect();
351 return new Rect(Math.floor(rect.left), Math.floor(rect.top),
352 Math.floor(rect.width), Math.floor(rect.height));
353 },
355 // ----------
356 // Function: data
357 // Pass in both key and value to attach some data to the receiver;
358 // pass in just key to retrieve it.
359 data: function iQClass_data(key, value) {
360 let data = null;
361 if (value === undefined) {
362 Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)');
363 data = this[0].iQData;
364 if (data)
365 return data[key];
366 else
367 return null;
368 }
370 for (let i = 0; this[i] != null; i++) {
371 let elem = this[i];
372 data = elem.iQData;
374 if (!data)
375 data = elem.iQData = {};
377 data[key] = value;
378 }
380 return this;
381 },
383 // ----------
384 // Function: html
385 // Given a value, sets the receiver's innerHTML to it; otherwise returns
386 // what's already there.
387 html: function iQClass_html(value) {
388 Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)');
389 if (value === undefined)
390 return this[0].innerHTML;
392 this[0].innerHTML = value;
393 return this;
394 },
396 // ----------
397 // Function: text
398 // Given a value, sets the receiver's textContent to it; otherwise returns
399 // what's already there.
400 text: function iQClass_text(value) {
401 Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)');
402 if (value === undefined) {
403 return this[0].textContent;
404 }
406 return this.empty().append((this[0] && this[0].ownerDocument || document).createTextNode(value));
407 },
409 // ----------
410 // Function: val
411 // Given a value, sets the receiver's value to it; otherwise returns what's already there.
412 val: function iQClass_val(value) {
413 Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)');
414 if (value === undefined) {
415 return this[0].value;
416 }
418 this[0].value = value;
419 return this;
420 },
422 // ----------
423 // Function: appendTo
424 // Appends the receiver to the result of iQ(selector).
425 appendTo: function iQClass_appendTo(selector) {
426 Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)');
427 iQ(selector).append(this);
428 return this;
429 },
431 // ----------
432 // Function: append
433 // Appends the result of iQ(selector) to the receiver.
434 append: function iQClass_append(selector) {
435 let object = iQ(selector);
436 Utils.assert(object.length == 1 && this.length == 1,
437 'does not yet support multi-objects (or null objects)');
438 this[0].appendChild(object[0]);
439 return this;
440 },
442 // ----------
443 // Function: attr
444 // Sets or gets an attribute on the element(s).
445 attr: function iQClass_attr(key, value) {
446 Utils.assert(typeof key === 'string', 'string key');
447 if (value === undefined) {
448 Utils.assert(this.length == 1, 'retrieval does not support multi-objects (or null objects)');
449 return this[0].getAttribute(key);
450 }
452 for (let i = 0; this[i] != null; i++)
453 this[i].setAttribute(key, value);
455 return this;
456 },
458 // ----------
459 // Function: css
460 // Sets or gets CSS properties on the receiver. When setting certain numerical properties,
461 // will automatically add "px". A property can be removed by setting it to null.
462 //
463 // Possible call patterns:
464 // a: object, b: undefined - sets with properties from a
465 // a: string, b: undefined - gets property specified by a
466 // a: string, b: string/number - sets property specified by a to b
467 css: function iQClass_css(a, b) {
468 let properties = null;
470 if (typeof a === 'string') {
471 let key = a;
472 if (b === undefined) {
473 Utils.assert(this.length == 1, 'retrieval does not support multi-objects (or null objects)');
475 return window.getComputedStyle(this[0], null).getPropertyValue(key);
476 }
477 properties = {};
478 properties[key] = b;
479 } else if (a instanceof Rect) {
480 properties = {
481 left: a.left,
482 top: a.top,
483 width: a.width,
484 height: a.height
485 };
486 } else {
487 properties = a;
488 }
490 let pixels = {
491 'left': true,
492 'top': true,
493 'right': true,
494 'bottom': true,
495 'width': true,
496 'height': true
497 };
499 for (let i = 0; this[i] != null; i++) {
500 let elem = this[i];
501 for (let key in properties) {
502 let value = properties[key];
504 if (pixels[key] && typeof value != 'string')
505 value += 'px';
507 if (value == null) {
508 elem.style.removeProperty(key);
509 } else if (key.indexOf('-') != -1)
510 elem.style.setProperty(key, value, '');
511 else
512 elem.style[key] = value;
513 }
514 }
516 return this;
517 },
519 // ----------
520 // Function: animate
521 // Uses CSS transitions to animate the element.
522 //
523 // Parameters:
524 // css - an object map of the CSS properties to change
525 // options - an object with various properites (see below)
526 //
527 // Possible "options" properties:
528 // duration - how long to animate, in milliseconds
529 // easing - easing function to use. Possibilities include
530 // "tabviewBounce", "easeInQuad". Default is "ease".
531 // complete - function to call once the animation is done, takes nothing
532 // in, but "this" is set to the element that was animated.
533 animate: function iQClass_animate(css, options) {
534 Utils.assert(this.length == 1, 'does not yet support multi-objects (or null objects)');
536 if (!options)
537 options = {};
539 let easings = {
540 tabviewBounce: "cubic-bezier(0.0, 0.63, .6, 1.29)",
541 easeInQuad: 'ease-in', // TODO: make it a real easeInQuad, or decide we don't care
542 fast: 'cubic-bezier(0.7,0,1,1)'
543 };
545 let duration = (options.duration || 400);
546 let easing = (easings[options.easing] || 'ease');
548 if (css instanceof Rect) {
549 css = {
550 left: css.left,
551 top: css.top,
552 width: css.width,
553 height: css.height
554 };
555 }
558 // The latest versions of Firefox do not animate from a non-explicitly
559 // set css properties. So for each element to be animated, go through
560 // and explicitly define 'em.
561 let rupper = /([A-Z])/g;
562 this.each(function(elem) {
563 let cStyle = window.getComputedStyle(elem, null);
564 for (let prop in css) {
565 prop = prop.replace(rupper, "-$1").toLowerCase();
566 iQ(elem).css(prop, cStyle.getPropertyValue(prop));
567 }
568 });
570 this.css({
571 'transition-property': Object.keys(css).join(", "),
572 'transition-duration': (duration / 1000) + 's',
573 'transition-timing-function': easing
574 });
576 this.css(css);
578 let self = this;
579 setTimeout(function() {
580 self.css({
581 'transition-property': 'none',
582 'transition-duration': '',
583 'transition-timing-function': ''
584 });
586 if (typeof options.complete == "function")
587 options.complete.apply(self);
588 }, duration);
590 return this;
591 },
593 // ----------
594 // Function: fadeOut
595 // Animates the receiver to full transparency. Calls callback on completion.
596 fadeOut: function iQClass_fadeOut(callback) {
597 Utils.assert(typeof callback == "function" || callback === undefined,
598 'does not yet support duration');
600 this.animate({
601 opacity: 0
602 }, {
603 duration: 400,
604 complete: function() {
605 iQ(this).css({display: 'none'});
606 if (typeof callback == "function")
607 callback.apply(this);
608 }
609 });
611 return this;
612 },
614 // ----------
615 // Function: fadeIn
616 // Animates the receiver to full opacity.
617 fadeIn: function iQClass_fadeIn() {
618 this.css({display: ''});
619 this.animate({
620 opacity: 1
621 }, {
622 duration: 400
623 });
625 return this;
626 },
628 // ----------
629 // Function: hide
630 // Hides the receiver.
631 hide: function iQClass_hide() {
632 this.css({display: 'none', opacity: 0});
633 return this;
634 },
636 // ----------
637 // Function: show
638 // Shows the receiver.
639 show: function iQClass_show() {
640 this.css({display: '', opacity: 1});
641 return this;
642 },
644 // ----------
645 // Function: bind
646 // Binds the given function to the given event type. Also wraps the function
647 // in a try/catch block that does a Utils.log on any errors.
648 bind: function iQClass_bind(type, func) {
649 let handler = function(event) func.apply(this, [event]);
651 for (let i = 0; this[i] != null; i++) {
652 let elem = this[i];
653 if (!elem.iQEventData)
654 elem.iQEventData = {};
656 if (!elem.iQEventData[type])
657 elem.iQEventData[type] = [];
659 elem.iQEventData[type].push({
660 original: func,
661 modified: handler
662 });
664 elem.addEventListener(type, handler, false);
665 }
667 return this;
668 },
670 // ----------
671 // Function: one
672 // Binds the given function to the given event type, but only for one call;
673 // automatically unbinds after the event fires once.
674 one: function iQClass_one(type, func) {
675 Utils.assert(typeof func == "function", 'does not support eventData argument');
677 let handler = function(e) {
678 iQ(this).unbind(type, handler);
679 return func.apply(this, [e]);
680 };
682 return this.bind(type, handler);
683 },
685 // ----------
686 // Function: unbind
687 // Unbinds the given function from the given event type.
688 unbind: function iQClass_unbind(type, func) {
689 Utils.assert(typeof func == "function", 'Must provide a function');
691 for (let i = 0; this[i] != null; i++) {
692 let elem = this[i];
693 let handler = func;
694 if (elem.iQEventData && elem.iQEventData[type]) {
695 let count = elem.iQEventData[type].length;
696 for (let a = 0; a < count; a++) {
697 let pair = elem.iQEventData[type][a];
698 if (pair.original == func) {
699 handler = pair.modified;
700 elem.iQEventData[type].splice(a, 1);
701 if (!elem.iQEventData[type].length) {
702 delete elem.iQEventData[type];
703 if (!Object.keys(elem.iQEventData).length)
704 delete elem.iQEventData;
705 }
706 break;
707 }
708 }
709 }
711 elem.removeEventListener(type, handler, false);
712 }
714 return this;
715 },
717 // ----------
718 // Function: unbindAll
719 // Unbinds all event handlers.
720 unbindAll: function iQClass_unbindAll() {
721 for (let i = 0; this[i] != null; i++) {
722 let elem = this[i];
724 for (let j = 0; j < elem.childElementCount; j++)
725 iQ(elem.children[j]).unbindAll();
727 if (!elem.iQEventData)
728 continue;
730 Object.keys(elem.iQEventData).forEach(function (type) {
731 while (elem.iQEventData && elem.iQEventData[type])
732 this.unbind(type, elem.iQEventData[type][0].original);
733 }, this);
734 }
736 return this;
737 }
738 };
740 // ----------
741 // Create various event aliases
742 let events = [
743 'keyup',
744 'keydown',
745 'keypress',
746 'mouseup',
747 'mousedown',
748 'mouseover',
749 'mouseout',
750 'mousemove',
751 'click',
752 'dblclick',
753 'resize',
754 'change',
755 'blur',
756 'focus'
757 ];
759 events.forEach(function(event) {
760 iQClass.prototype[event] = function(func) {
761 return this.bind(event, func);
762 };
763 });