michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: // ********** michael@0: // Title: utils.js michael@0: michael@0: this.EXPORTED_SYMBOLS = ["Point", "Rect", "Range", "Subscribable", "Utils", "MRUList"]; michael@0: michael@0: // ######### michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: // ########## michael@0: // Class: Point michael@0: // A simple point. michael@0: // michael@0: // Constructor: Point michael@0: // If a is a Point, creates a copy of it. Otherwise, expects a to be x, michael@0: // and creates a Point with it along with y. If either a or y are omitted, michael@0: // 0 is used in their place. michael@0: this.Point = function Point(a, y) { michael@0: if (Utils.isPoint(a)) { michael@0: this.x = a.x; michael@0: this.y = a.y; michael@0: } else { michael@0: this.x = (Utils.isNumber(a) ? a : 0); michael@0: this.y = (Utils.isNumber(y) ? y : 0); michael@0: } michael@0: }; michael@0: michael@0: Point.prototype = { michael@0: // ---------- michael@0: // Function: toString michael@0: // Prints [Point (x,y)] for debug use michael@0: toString: function Point_toString() { michael@0: return "[Point (" + this.x + "," + this.y + ")]"; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: distance michael@0: // Returns the distance from this point to the given . michael@0: distance: function Point_distance(point) { michael@0: var ax = this.x - point.x; michael@0: var ay = this.y - point.y; michael@0: return Math.sqrt((ax * ax) + (ay * ay)); michael@0: } michael@0: }; michael@0: michael@0: // ########## michael@0: // Class: Rect michael@0: // A simple rectangle. Note that in addition to the left and width, it also has michael@0: // a right property; changing one affects the others appropriately. Same for the michael@0: // vertical properties. michael@0: // michael@0: // Constructor: Rect michael@0: // If a is a Rect, creates a copy of it. Otherwise, expects a to be left, michael@0: // and creates a Rect with it along with top, width, and height. michael@0: this.Rect = function Rect(a, top, width, height) { michael@0: // Note: perhaps 'a' should really be called 'rectOrLeft' michael@0: if (Utils.isRect(a)) { michael@0: this.left = a.left; michael@0: this.top = a.top; michael@0: this.width = a.width; michael@0: this.height = a.height; michael@0: } else { michael@0: this.left = a; michael@0: this.top = top; michael@0: this.width = width; michael@0: this.height = height; michael@0: } michael@0: }; michael@0: michael@0: Rect.prototype = { michael@0: // ---------- michael@0: // Function: toString michael@0: // Prints [Rect (left,top,width,height)] for debug use michael@0: toString: function Rect_toString() { michael@0: return "[Rect (" + this.left + "," + this.top + "," + michael@0: this.width + "," + this.height + ")]"; michael@0: }, michael@0: michael@0: get right() this.left + this.width, michael@0: set right(value) { michael@0: this.width = value - this.left; michael@0: }, michael@0: michael@0: get bottom() this.top + this.height, michael@0: set bottom(value) { michael@0: this.height = value - this.top; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Variable: xRange michael@0: // Gives you a new for the horizontal dimension. michael@0: get xRange() new Range(this.left, this.right), michael@0: michael@0: // ---------- michael@0: // Variable: yRange michael@0: // Gives you a new for the vertical dimension. michael@0: get yRange() new Range(this.top, this.bottom), michael@0: michael@0: // ---------- michael@0: // Function: intersects michael@0: // Returns true if this rectangle intersects the given . michael@0: intersects: function Rect_intersects(rect) { michael@0: return (rect.right > this.left && michael@0: rect.left < this.right && michael@0: rect.bottom > this.top && michael@0: rect.top < this.bottom); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: intersection michael@0: // Returns a new with the intersection of this rectangle and the give , michael@0: // or null if they don't intersect. michael@0: intersection: function Rect_intersection(rect) { michael@0: var box = new Rect(Math.max(rect.left, this.left), Math.max(rect.top, this.top), 0, 0); michael@0: box.right = Math.min(rect.right, this.right); michael@0: box.bottom = Math.min(rect.bottom, this.bottom); michael@0: if (box.width > 0 && box.height > 0) michael@0: return box; michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: contains michael@0: // Returns a boolean denoting if the or is contained inside michael@0: // this rectangle. michael@0: // michael@0: // Parameters michael@0: // - A or a michael@0: contains: function Rect_contains(a) { michael@0: if (Utils.isPoint(a)) michael@0: return (a.x > this.left && michael@0: a.x < this.right && michael@0: a.y > this.top && michael@0: a.y < this.bottom); michael@0: michael@0: return (a.left >= this.left && michael@0: a.right <= this.right && michael@0: a.top >= this.top && michael@0: a.bottom <= this.bottom); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: center michael@0: // Returns a new with the center location of this rectangle. michael@0: center: function Rect_center() { michael@0: return new Point(this.left + (this.width / 2), this.top + (this.height / 2)); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: size michael@0: // Returns a new with the dimensions of this rectangle. michael@0: size: function Rect_size() { michael@0: return new Point(this.width, this.height); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: position michael@0: // Returns a new with the top left of this rectangle. michael@0: position: function Rect_position() { michael@0: return new Point(this.left, this.top); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: area michael@0: // Returns the area of this rectangle. michael@0: area: function Rect_area() { michael@0: return this.width * this.height; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: inset michael@0: // Makes the rect smaller (if the arguments are positive) as if a margin is added all around michael@0: // the initial rect, with the margin widths (symmetric) being specified by the arguments. michael@0: // michael@0: // Paramaters michael@0: // - A or two arguments: x and y michael@0: inset: function Rect_inset(a, b) { michael@0: if (Utils.isPoint(a)) { michael@0: b = a.y; michael@0: a = a.x; michael@0: } michael@0: michael@0: this.left += a; michael@0: this.width -= a * 2; michael@0: this.top += b; michael@0: this.height -= b * 2; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: offset michael@0: // Moves (translates) the rect by the given vector. michael@0: // michael@0: // Paramaters michael@0: // - A or two arguments: x and y michael@0: offset: function Rect_offset(a, b) { michael@0: if (Utils.isPoint(a)) { michael@0: this.left += a.x; michael@0: this.top += a.y; michael@0: } else { michael@0: this.left += a; michael@0: this.top += b; michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: equals michael@0: // Returns true if this rectangle is identical to the given . michael@0: equals: function Rect_equals(rect) { michael@0: return (rect.left == this.left && michael@0: rect.top == this.top && michael@0: rect.width == this.width && michael@0: rect.height == this.height); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: union michael@0: // Returns a new with the union of this rectangle and the given . michael@0: union: function Rect_union(a) { michael@0: var newLeft = Math.min(a.left, this.left); michael@0: var newTop = Math.min(a.top, this.top); michael@0: var newWidth = Math.max(a.right, this.right) - newLeft; michael@0: var newHeight = Math.max(a.bottom, this.bottom) - newTop; michael@0: var newRect = new Rect(newLeft, newTop, newWidth, newHeight); michael@0: michael@0: return newRect; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: copy michael@0: // Copies the values of the given into this rectangle. michael@0: copy: function Rect_copy(a) { michael@0: this.left = a.left; michael@0: this.top = a.top; michael@0: this.width = a.width; michael@0: this.height = a.height; michael@0: } michael@0: }; michael@0: michael@0: // ########## michael@0: // Class: Range michael@0: // A physical interval, with a min and max. michael@0: // michael@0: // Constructor: Range michael@0: // Creates a Range with the given min and max michael@0: this.Range = function Range(min, max) { michael@0: if (Utils.isRange(min) && !max) { // if the one variable given is a range, copy it. michael@0: this.min = min.min; michael@0: this.max = min.max; michael@0: } else { michael@0: this.min = min || 0; michael@0: this.max = max || 0; michael@0: } michael@0: }; michael@0: michael@0: Range.prototype = { michael@0: // ---------- michael@0: // Function: toString michael@0: // Prints [Range (min,max)] for debug use michael@0: toString: function Range_toString() { michael@0: return "[Range (" + this.min + "," + this.max + ")]"; michael@0: }, michael@0: michael@0: // Variable: extent michael@0: // Equivalent to max-min michael@0: get extent() { michael@0: return (this.max - this.min); michael@0: }, michael@0: michael@0: set extent(extent) { michael@0: this.max = extent - this.min; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: contains michael@0: // Whether the contains the given or value or not. michael@0: // michael@0: // Parameters michael@0: // - a number or michael@0: contains: function Range_contains(value) { michael@0: if (Utils.isNumber(value)) michael@0: return value >= this.min && value <= this.max; michael@0: if (Utils.isRange(value)) michael@0: return value.min >= this.min && value.max <= this.max; michael@0: return false; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: overlaps michael@0: // Whether the overlaps with the given value or not. michael@0: // michael@0: // Parameters michael@0: // - a number or michael@0: overlaps: function Range_overlaps(value) { michael@0: if (Utils.isNumber(value)) michael@0: return this.contains(value); michael@0: if (Utils.isRange(value)) michael@0: return !(value.max < this.min || this.max < value.min); michael@0: return false; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: proportion michael@0: // Maps the given value to the range [0,1], so that it returns 0 if the value is <= the min, michael@0: // returns 1 if the value >= the max, and returns an interpolated "proportion" in (min, max). michael@0: // michael@0: // Parameters michael@0: // - a number michael@0: // - (bool) smooth? If true, a smooth tanh-based function will be used instead of the linear. michael@0: proportion: function Range_proportion(value, smooth) { michael@0: if (value <= this.min) michael@0: return 0; michael@0: if (this.max <= value) michael@0: return 1; michael@0: michael@0: var proportion = (value - this.min) / this.extent; michael@0: michael@0: if (smooth) { michael@0: // The ease function ".5+.5*Math.tanh(4*x-2)" is a pretty michael@0: // little graph. It goes from near 0 at x=0 to near 1 at x=1 michael@0: // smoothly and beautifully. michael@0: // http://www.wolframalpha.com/input/?i=.5+%2B+.5+*+tanh%28%284+*+x%29+-+2%29 michael@0: let tanh = function tanh(x) { michael@0: var e = Math.exp(x); michael@0: return (e - 1/e) / (e + 1/e); michael@0: }; michael@0: michael@0: return .5 - .5 * tanh(2 - 4 * proportion); michael@0: } michael@0: michael@0: return proportion; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: scale michael@0: // Takes the given value in [0,1] and maps it to the associated value on the Range. michael@0: // michael@0: // Parameters michael@0: // - a number in [0,1] michael@0: scale: function Range_scale(value) { michael@0: if (value > 1) michael@0: value = 1; michael@0: if (value < 0) michael@0: value = 0; michael@0: return this.min + this.extent * value; michael@0: } michael@0: }; michael@0: michael@0: // ########## michael@0: // Class: Subscribable michael@0: // A mix-in for allowing objects to collect subscribers for custom events. michael@0: this.Subscribable = function Subscribable() { michael@0: this.subscribers = null; michael@0: }; michael@0: michael@0: Subscribable.prototype = { michael@0: // ---------- michael@0: // Function: addSubscriber michael@0: // The given callback will be called when the Subscribable fires the given event. michael@0: addSubscriber: function Subscribable_addSubscriber(eventName, callback) { michael@0: try { michael@0: Utils.assertThrow(typeof callback == "function", "callback must be a function"); michael@0: Utils.assertThrow(eventName && typeof eventName == "string", michael@0: "eventName must be a non-empty string"); michael@0: } catch(e) { michael@0: Utils.log(e); michael@0: return; michael@0: } michael@0: michael@0: if (!this.subscribers) michael@0: this.subscribers = {}; michael@0: michael@0: if (!this.subscribers[eventName]) michael@0: this.subscribers[eventName] = []; michael@0: michael@0: let subscribers = this.subscribers[eventName]; michael@0: if (subscribers.indexOf(callback) == -1) michael@0: subscribers.push(callback); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: removeSubscriber michael@0: // Removes the subscriber associated with the event for the given callback. michael@0: removeSubscriber: function Subscribable_removeSubscriber(eventName, callback) { michael@0: try { michael@0: Utils.assertThrow(typeof callback == "function", "callback must be a function"); michael@0: Utils.assertThrow(eventName && typeof eventName == "string", michael@0: "eventName must be a non-empty string"); michael@0: } catch(e) { michael@0: Utils.log(e); michael@0: return; michael@0: } michael@0: michael@0: if (!this.subscribers || !this.subscribers[eventName]) michael@0: return; michael@0: michael@0: let subscribers = this.subscribers[eventName]; michael@0: let index = subscribers.indexOf(callback); michael@0: michael@0: if (index > -1) michael@0: subscribers.splice(index, 1); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: _sendToSubscribers michael@0: // Internal routine. Used by the Subscribable to fire events. michael@0: _sendToSubscribers: function Subscribable__sendToSubscribers(eventName, eventInfo) { michael@0: try { michael@0: Utils.assertThrow(eventName && typeof eventName == "string", michael@0: "eventName must be a non-empty string"); michael@0: } catch(e) { michael@0: Utils.log(e); michael@0: return; michael@0: } michael@0: michael@0: if (!this.subscribers || !this.subscribers[eventName]) michael@0: return; michael@0: michael@0: let subsCopy = this.subscribers[eventName].concat(); michael@0: subsCopy.forEach(function (callback) { michael@0: try { michael@0: callback(this, eventInfo); michael@0: } catch(e) { michael@0: Utils.log(e); michael@0: } michael@0: }, this); michael@0: } michael@0: }; michael@0: michael@0: // ########## michael@0: // Class: Utils michael@0: // Singelton with common utility functions. michael@0: this.Utils = { michael@0: // ---------- michael@0: // Function: toString michael@0: // Prints [Utils] for debug use michael@0: toString: function Utils_toString() { michael@0: return "[Utils]"; michael@0: }, michael@0: michael@0: // ___ Logging michael@0: useConsole: true, // as opposed to dump michael@0: showTime: false, michael@0: michael@0: // ---------- michael@0: // Function: log michael@0: // Prints the given arguments to the JavaScript error console as a message. michael@0: // Pass as many arguments as you want, it'll print them all. michael@0: log: function Utils_log() { michael@0: var text = this.expandArgumentsForLog(arguments); michael@0: var prefix = this.showTime ? Date.now() + ': ' : ''; michael@0: if (this.useConsole) michael@0: Services.console.logStringMessage(prefix + text); michael@0: else michael@0: dump(prefix + text + '\n'); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: error michael@0: // Prints the given arguments to the JavaScript error console as an error. michael@0: // Pass as many arguments as you want, it'll print them all. michael@0: error: function Utils_error() { michael@0: var text = this.expandArgumentsForLog(arguments); michael@0: var prefix = this.showTime ? Date.now() + ': ' : ''; michael@0: if (this.useConsole) michael@0: Cu.reportError(prefix + "tabview error: " + text); michael@0: else michael@0: dump(prefix + "TABVIEW ERROR: " + text + '\n'); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: trace michael@0: // Prints the given arguments to the JavaScript error console as a message, michael@0: // along with a full stack trace. michael@0: // Pass as many arguments as you want, it'll print them all. michael@0: trace: function Utils_trace() { michael@0: var text = this.expandArgumentsForLog(arguments); michael@0: michael@0: // cut off the first line of the stack trace, because that's just this function. michael@0: let stack = Error().stack.split("\n").slice(1); michael@0: michael@0: // if the caller was assert, cut out the line for the assert function as well. michael@0: if (stack[0].indexOf("Utils_assert(") == 0) michael@0: stack.splice(0, 1); michael@0: michael@0: this.log('trace: ' + text + '\n' + stack.join("\n")); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: assert michael@0: // Prints a stack trace along with label (as a console message) if condition is false. michael@0: assert: function Utils_assert(condition, label) { michael@0: if (!condition) { michael@0: let text; michael@0: if (typeof label != 'string') michael@0: text = 'badly formed assert'; michael@0: else michael@0: text = "tabview assert: " + label; michael@0: michael@0: this.trace(text); michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: assertThrow michael@0: // Throws label as an exception if condition is false. michael@0: assertThrow: function Utils_assertThrow(condition, label) { michael@0: if (!condition) { michael@0: let text; michael@0: if (typeof label != 'string') michael@0: text = 'badly formed assert'; michael@0: else michael@0: text = "tabview assert: " + label; michael@0: michael@0: // cut off the first line of the stack trace, because that's just this function. michael@0: let stack = Error().stack.split("\n").slice(1); michael@0: michael@0: throw text + "\n" + stack.join("\n"); michael@0: } michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: expandObject michael@0: // Prints the given object to a string, including all of its properties. michael@0: expandObject: function Utils_expandObject(obj) { michael@0: var s = obj + ' = {'; michael@0: for (let prop in obj) { michael@0: let value; michael@0: try { michael@0: value = obj[prop]; michael@0: } catch(e) { michael@0: value = '[!!error retrieving property]'; michael@0: } michael@0: michael@0: s += prop + ': '; michael@0: if (typeof value == 'string') michael@0: s += '\'' + value + '\''; michael@0: else if (typeof value == 'function') michael@0: s += 'function'; michael@0: else michael@0: s += value; michael@0: michael@0: s += ', '; michael@0: } michael@0: return s + '}'; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: expandArgumentsForLog michael@0: // Expands all of the given args (an array) into a single string. michael@0: expandArgumentsForLog: function Utils_expandArgumentsForLog(args) { michael@0: var that = this; michael@0: return Array.map(args, function(arg) { michael@0: return typeof arg == 'object' ? that.expandObject(arg) : arg; michael@0: }).join('; '); michael@0: }, michael@0: michael@0: // ___ Misc michael@0: michael@0: // ---------- michael@0: // Function: isLeftClick michael@0: // Given a DOM mouse event, returns true if it was for the left mouse button. michael@0: isLeftClick: function Utils_isLeftClick(event) { michael@0: return event.button == 0; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: isMiddleClick michael@0: // Given a DOM mouse event, returns true if it was for the middle mouse button. michael@0: isMiddleClick: function Utils_isMiddleClick(event) { michael@0: return event.button == 1; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: isRightClick michael@0: // Given a DOM mouse event, returns true if it was for the right mouse button. michael@0: isRightClick: function Utils_isRightClick(event) { michael@0: return event.button == 2; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: isDOMElement michael@0: // Returns true if the given object is a DOM element. michael@0: isDOMElement: function Utils_isDOMElement(object) { michael@0: return object instanceof Ci.nsIDOMElement; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: isValidXULTab michael@0: // A xulTab is valid if it has not been closed, michael@0: // and it has not been removed from the DOM michael@0: // Returns true if the tab is valid. michael@0: isValidXULTab: function Utils_isValidXULTab(xulTab) { michael@0: return !xulTab.closing && xulTab.parentNode; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: isNumber michael@0: // Returns true if the argument is a valid number. michael@0: isNumber: function Utils_isNumber(n) { michael@0: return typeof n == 'number' && !isNaN(n); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: isRect michael@0: // Returns true if the given object (r) looks like a . michael@0: isRect: function Utils_isRect(r) { michael@0: return (r && michael@0: this.isNumber(r.left) && michael@0: this.isNumber(r.top) && michael@0: this.isNumber(r.width) && michael@0: this.isNumber(r.height)); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: isRange michael@0: // Returns true if the given object (r) looks like a . michael@0: isRange: function Utils_isRange(r) { michael@0: return (r && michael@0: this.isNumber(r.min) && michael@0: this.isNumber(r.max)); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: isPoint michael@0: // Returns true if the given object (p) looks like a . michael@0: isPoint: function Utils_isPoint(p) { michael@0: return (p && this.isNumber(p.x) && this.isNumber(p.y)); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: isPlainObject michael@0: // Check to see if an object is a plain object (created using "{}" or "new Object"). michael@0: isPlainObject: function Utils_isPlainObject(obj) { michael@0: // Must be an Object. michael@0: // Make sure that DOM nodes and window objects don't pass through, as well michael@0: if (!obj || Object.prototype.toString.call(obj) !== "[object Object]" || michael@0: obj.nodeType || obj.setInterval) { michael@0: return false; michael@0: } michael@0: michael@0: // Not own constructor property must be Object michael@0: const hasOwnProperty = Object.prototype.hasOwnProperty; michael@0: michael@0: if (obj.constructor && michael@0: !hasOwnProperty.call(obj, "constructor") && michael@0: !hasOwnProperty.call(obj.constructor.prototype, "isPrototypeOf")) { michael@0: return false; michael@0: } michael@0: michael@0: // Own properties are enumerated firstly, so to speed up, michael@0: // if last one is own, then all properties are own. michael@0: michael@0: var key; michael@0: for (key in obj) {} michael@0: michael@0: return key === undefined || hasOwnProperty.call(obj, key); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: isEmptyObject michael@0: // Returns true if the given object has no members. michael@0: isEmptyObject: function Utils_isEmptyObject(obj) { michael@0: for (let name in obj) michael@0: return false; michael@0: return true; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: copy michael@0: // Returns a copy of the argument. Note that this is a shallow copy; if the argument michael@0: // has properties that are themselves objects, those properties will be copied by reference. michael@0: copy: function Utils_copy(value) { michael@0: if (value && typeof value == 'object') { michael@0: if (Array.isArray(value)) michael@0: return this.extend([], value); michael@0: return this.extend({}, value); michael@0: } michael@0: return value; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: merge michael@0: // Merge two array-like objects into the first and return it. michael@0: merge: function Utils_merge(first, second) { michael@0: Array.forEach(second, function(el) Array.push(first, el)); michael@0: return first; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: extend michael@0: // Pass several objects in and it will combine them all into the first object and return it. michael@0: extend: function Utils_extend() { michael@0: michael@0: // copy reference to target object michael@0: let target = arguments[0] || {}; michael@0: // Deep copy is not supported michael@0: if (typeof target === "boolean") { michael@0: this.assert(false, "The first argument of extend cannot be a boolean." + michael@0: "Deep copy is not supported."); michael@0: return target; michael@0: } michael@0: michael@0: // Back when this was in iQ + iQ.fn, so you could extend iQ objects with it. michael@0: // This is no longer supported. michael@0: let length = arguments.length; michael@0: if (length === 1) { michael@0: this.assert(false, "Extending the iQ prototype using extend is not supported."); michael@0: return target; michael@0: } michael@0: michael@0: // Handle case when target is a string or something michael@0: if (typeof target != "object" && typeof target != "function") { michael@0: target = {}; michael@0: } michael@0: michael@0: for (let i = 1; i < length; i++) { michael@0: // Only deal with non-null/undefined values michael@0: let options = arguments[i]; michael@0: if (options != null) { michael@0: // Extend the base object michael@0: for (let name in options) { michael@0: let copy = options[name]; michael@0: michael@0: // Prevent never-ending loop michael@0: if (target === copy) michael@0: continue; michael@0: michael@0: if (copy !== undefined) michael@0: target[name] = copy; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Return the modified object michael@0: return target; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: attempt michael@0: // Tries to execute a number of functions. Returns immediately the return michael@0: // value of the first non-failed function without executing successive michael@0: // functions, or null. michael@0: attempt: function Utils_attempt() { michael@0: let args = arguments; michael@0: michael@0: for (let i = 0; i < args.length; i++) { michael@0: try { michael@0: return args[i](); michael@0: } catch (e) {} michael@0: } michael@0: michael@0: return null; michael@0: } michael@0: }; michael@0: michael@0: // ########## michael@0: // Class: MRUList michael@0: // A most recently used list. michael@0: // michael@0: // Constructor: MRUList michael@0: // If a is an array of entries, creates a copy of it. michael@0: this.MRUList = function MRUList(a) { michael@0: if (Array.isArray(a)) michael@0: this._list = a.concat(); michael@0: else michael@0: this._list = []; michael@0: }; michael@0: michael@0: MRUList.prototype = { michael@0: // ---------- michael@0: // Function: toString michael@0: // Prints [List (entry1, entry2, ...)] for debug use michael@0: toString: function MRUList_toString() { michael@0: return "[List (" + this._list.join(", ") + ")]"; michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: update michael@0: // Updates/inserts the given entry as the most recently used one in the list. michael@0: update: function MRUList_update(entry) { michael@0: this.remove(entry); michael@0: this._list.unshift(entry); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: remove michael@0: // Removes the given entry from the list. michael@0: remove: function MRUList_remove(entry) { michael@0: let index = this._list.indexOf(entry); michael@0: if (index > -1) michael@0: this._list.splice(index, 1); michael@0: }, michael@0: michael@0: // ---------- michael@0: // Function: peek michael@0: // Returns the most recently used entry. If a filter exists, gets the most michael@0: // recently used entry which matches the filter. michael@0: peek: function MRUList_peek(filter) { michael@0: let match = null; michael@0: if (filter && typeof filter == "function") michael@0: this._list.some(function MRUList_peek_getEntry(entry) { michael@0: if (filter(entry)) { michael@0: match = entry michael@0: return true; michael@0: } michael@0: return false; michael@0: }); michael@0: else michael@0: match = this._list.length > 0 ? this._list[0] : null; michael@0: michael@0: return match; michael@0: }, michael@0: }; michael@0: