Dactyloidae/devtools/shared/gcli/source/lib/gcli/util/util.js

685 lines
18 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright 2012, Mozilla Foundation and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
/*
* A number of DOM manipulation and event handling utilities.
*/
//------------------------------------------------------------------------------
var eventDebug = false;
/**
* Patch up broken console API from node
*/
if (eventDebug) {
if (console.group == null) {
console.group = function() { console.log(arguments); };
}
if (console.groupEnd == null) {
console.groupEnd = function() { console.log(arguments); };
}
}
/**
* Useful way to create a name for a handler, used in createEvent()
*/
function nameFunction(handler) {
var scope = handler.scope ? handler.scope.constructor.name + '.' : '';
var name = handler.func.name;
if (name) {
return scope + name;
}
for (var prop in handler.scope) {
if (handler.scope[prop] === handler.func) {
return scope + prop;
}
}
return scope + handler.func;
}
/**
* Create an event.
* For use as follows:
*
* function Hat() {
* this.putOn = createEvent('Hat.putOn');
* ...
* }
* Hat.prototype.adorn = function(person) {
* this.putOn({ hat: hat, person: person });
* ...
* }
*
* var hat = new Hat();
* hat.putOn.add(function(ev) {
* console.log('The hat ', ev.hat, ' has is worn by ', ev.person);
* }, scope);
*
* @param name Optional name to help with debugging
*/
exports.createEvent = function(name) {
var handlers = [];
var fireHoldCount = 0;
var heldEvents = [];
var eventCombiner;
/**
* This is how the event is triggered.
* @param ev The event object to be passed to the event listeners
*/
var event = function(ev) {
if (fireHoldCount > 0) {
heldEvents.push(ev);
if (eventDebug) {
console.log('Held fire: ' + name, ev);
}
return;
}
if (eventDebug) {
console.group('Fire: ' + name + ' to ' + handlers.length + ' listeners', ev);
}
// Use for rather than forEach because it step debugs better, which is
// important for debugging events
for (var i = 0; i < handlers.length; i++) {
var handler = handlers[i];
if (eventDebug) {
console.log(nameFunction(handler));
}
handler.func.call(handler.scope, ev);
}
if (eventDebug) {
console.groupEnd();
}
};
/**
* Add a new handler function
* @param func The function to call when this event is triggered
* @param scope Optional 'this' object for the function call
*/
event.add = function(func, scope) {
if (typeof func !== 'function') {
throw new Error(name + ' add(func,...), 1st param is ' + typeof func);
}
if (eventDebug) {
console.log('Adding listener to ' + name);
}
handlers.push({ func: func, scope: scope });
};
/**
* Remove a handler function added through add(). Both func and scope must
* be strict equals (===) the values used in the call to add()
* @param func The function to call when this event is triggered
* @param scope Optional 'this' object for the function call
*/
event.remove = function(func, scope) {
if (eventDebug) {
console.log('Removing listener from ' + name);
}
var found = false;
handlers = handlers.filter(function(test) {
var match = (test.func === func && test.scope === scope);
if (match) {
found = true;
}
return !match;
});
if (!found) {
console.warn('Handler not found. Attached to ' + name);
}
};
/**
* Remove all handlers.
* Reset the state of this event back to it's post create state
*/
event.removeAll = function() {
handlers = [];
};
/**
* Fire an event just once using a promise.
*/
event.once = function() {
if (arguments.length !== 0) {
throw new Error('event.once uses promise return values');
}
return new Promise(function(resolve, reject) {
var handler = function(arg) {
event.remove(handler);
resolve(arg);
};
event.add(handler);
});
};
/**
* Temporarily prevent this event from firing.
* @see resumeFire(ev)
*/
event.holdFire = function() {
if (eventDebug) {
console.group('Holding fire: ' + name);
}
fireHoldCount++;
};
/**
* Resume firing events.
* If there are heldEvents, then we fire one event to cover them all. If an
* event combining function has been provided then we use that to combine the
* events. Otherwise the last held event is used.
* @see holdFire()
*/
event.resumeFire = function() {
if (eventDebug) {
console.groupEnd('Resume fire: ' + name);
}
if (fireHoldCount === 0) {
throw new Error('fireHoldCount === 0 during resumeFire on ' + name);
}
fireHoldCount--;
if (heldEvents.length === 0) {
return;
}
if (heldEvents.length === 1) {
event(heldEvents[0]);
}
else {
var first = heldEvents[0];
var last = heldEvents[heldEvents.length - 1];
if (eventCombiner) {
event(eventCombiner(first, last, heldEvents));
}
else {
event(last);
}
}
heldEvents = [];
};
/**
* When resumeFire has a number of events to combine, by default it just
* picks the last, however you can provide an eventCombiner which returns a
* combined event.
* eventCombiners will be passed 3 parameters:
* - first The first event to be held
* - last The last event to be held
* - all An array containing all the held events
* The return value from an eventCombiner is expected to be an event object
*/
Object.defineProperty(event, 'eventCombiner', {
set: function(newEventCombiner) {
if (typeof newEventCombiner !== 'function') {
throw new Error('eventCombiner is not a function');
}
eventCombiner = newEventCombiner;
},
enumerable: true
});
return event;
};
//------------------------------------------------------------------------------
/**
* promiseEach is roughly like Array.forEach except that the action is taken to
* be something that completes asynchronously, returning a promise, so we wait
* for the action to complete for each array element before moving onto the
* next.
* @param array An array of objects to enumerate
* @param action A function to call for each member of the array
* @param scope Optional object to use as 'this' for the function calls
* @return A promise which is resolved (with an array of resolution values)
* when all the array members have been passed to the action function, and
* rejected as soon as any of the action function calls fails 
*/
exports.promiseEach = function(array, action, scope) {
if (array.length === 0) {
return Promise.resolve([]);
}
var allReply = [];
var promise = Promise.resolve();
array.forEach(function(member, i) {
promise = promise.then(function() {
var reply = action.call(scope, member, i, array);
return Promise.resolve(reply).then(function(data) {
allReply[i] = data;
});
});
});
return promise.then(function() {
return allReply;
});
};
/**
* Catching errors from promises isn't as simple as:
* promise.then(handler, console.error);
* for a number of reasons:
* - chrome's console doesn't have bound functions (why?)
* - we don't get stack traces out from console.error(ex);
*/
exports.errorHandler = function(ex) {
if (ex instanceof Error) {
// V8 weirdly includes the exception message in the stack
if (ex.stack.indexOf(ex.message) !== -1) {
console.error(ex.stack);
}
else {
console.error('' + ex);
console.error(ex.stack);
}
}
else {
console.error(ex);
}
};
//------------------------------------------------------------------------------
/**
* Copy the properties from one object to another in a way that preserves
* function properties as functions rather than copying the calculated value
* as copy time
*/
exports.copyProperties = function(src, dest) {
for (var key in src) {
var descriptor;
var obj = src;
while (true) {
descriptor = Object.getOwnPropertyDescriptor(obj, key);
if (descriptor != null) {
break;
}
obj = Object.getPrototypeOf(obj);
if (obj == null) {
throw new Error('Can\'t find descriptor of ' + key);
}
}
if ('value' in descriptor) {
dest[key] = src[key];
}
else if ('get' in descriptor) {
Object.defineProperty(dest, key, {
get: descriptor.get,
set: descriptor.set,
enumerable: descriptor.enumerable
});
}
else {
throw new Error('Don\'t know how to copy ' + key + ' property.');
}
}
};
//------------------------------------------------------------------------------
/**
* XHTML namespace
*/
exports.NS_XHTML = 'http://www.w3.org/1999/xhtml';
/**
* XUL namespace
*/
exports.NS_XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
/**
* Create an HTML or XHTML element depending on whether the document is HTML
* or XML based. Where HTML/XHTML elements are distinguished by whether they
* are created using doc.createElementNS('http://www.w3.org/1999/xhtml', tag)
* or doc.createElement(tag)
* If you want to create a XUL element then you don't have a problem knowing
* what namespace you want.
* @param doc The document in which to create the element
* @param tag The name of the tag to create
* @returns The created element
*/
exports.createElement = function(doc, tag) {
if (exports.isXmlDocument(doc)) {
return doc.createElementNS(exports.NS_XHTML, tag);
}
else {
return doc.createElement(tag);
}
};
/**
* Remove all the child nodes from this node
* @param elem The element that should have it's children removed
*/
exports.clearElement = function(elem) {
while (elem.hasChildNodes()) {
elem.removeChild(elem.firstChild);
}
};
var isAllWhitespace = /^\s*$/;
/**
* Iterate over the children of a node looking for TextNodes that have only
* whitespace content and remove them.
* This utility is helpful when you have a template which contains whitespace
* so it looks nice, but where the whitespace interferes with the rendering of
* the page
* @param elem The element which should have blank whitespace trimmed
* @param deep Should this node removal include child elements
*/
exports.removeWhitespace = function(elem, deep) {
var i = 0;
while (i < elem.childNodes.length) {
var child = elem.childNodes.item(i);
if (child.nodeType === 3 /*Node.TEXT_NODE*/ &&
isAllWhitespace.test(child.textContent)) {
elem.removeChild(child);
}
else {
if (deep && child.nodeType === 1 /*Node.ELEMENT_NODE*/) {
exports.removeWhitespace(child, deep);
}
i++;
}
}
};
/**
* Create a style element in the document head, and add the given CSS text to
* it.
* @param cssText The CSS declarations to append
* @param doc The document element to work from
* @param id Optional id to assign to the created style tag. If the id already
* exists on the document, we do not add the CSS again.
*/
exports.importCss = function(cssText, doc, id) {
if (!cssText) {
return undefined;
}
doc = doc || document;
if (!id) {
id = 'hash-' + hash(cssText);
}
var found = doc.getElementById(id);
if (found) {
if (found.tagName.toLowerCase() !== 'style') {
console.error('Warning: importCss passed id=' + id +
', but that pre-exists (and isn\'t a style tag)');
}
return found;
}
var style = exports.createElement(doc, 'style');
style.id = id;
style.appendChild(doc.createTextNode(cssText));
var head = doc.getElementsByTagName('head')[0] || doc.documentElement;
head.appendChild(style);
return style;
};
/**
* Simple hash function which happens to match Java's |String.hashCode()|
* Done like this because I we don't need crypto-security, but do need speed,
* and I don't want to spend a long time working on it.
* @see http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
*/
function hash(str) {
var h = 0;
if (str.length === 0) {
return h;
}
for (var i = 0; i < str.length; i++) {
var character = str.charCodeAt(i);
h = ((h << 5) - h) + character;
h = h & h; // Convert to 32bit integer
}
return h;
}
/**
* Shortcut for clearElement/createTextNode/appendChild to make up for the lack
* of standards around textContent/innerText
*/
exports.setTextContent = function(elem, text) {
exports.clearElement(elem);
var child = elem.ownerDocument.createTextNode(text);
elem.appendChild(child);
};
/**
* There are problems with innerHTML on XML documents, so we need to do a dance
* using document.createRange().createContextualFragment() when in XML mode
*/
exports.setContents = function(elem, contents) {
if (typeof HTMLElement !== 'undefined' && contents instanceof HTMLElement) {
exports.clearElement(elem);
elem.appendChild(contents);
return;
}
if ('innerHTML' in elem) {
elem.innerHTML = contents;
}
else {
try {
var ns = elem.ownerDocument.documentElement.namespaceURI;
if (!ns) {
ns = exports.NS_XHTML;
}
exports.clearElement(elem);
contents = '<div xmlns="' + ns + '">' + contents + '</div>';
var range = elem.ownerDocument.createRange();
var child = range.createContextualFragment(contents).firstChild;
while (child.hasChildNodes()) {
elem.appendChild(child.firstChild);
}
}
catch (ex) {
console.error('Bad XHTML', ex);
console.trace();
throw ex;
}
}
};
/**
* How to detect if we're in an XML document.
* In a Mozilla we check that document.xmlVersion = null, however in Chrome
* we use document.contentType = undefined.
* @param doc The document element to work from (defaulted to the global
* 'document' if missing
*/
exports.isXmlDocument = function(doc) {
doc = doc || document;
// Best test for Firefox
if (doc.contentType && doc.contentType != 'text/html') {
return true;
}
// Best test for Chrome
if (doc.xmlVersion != null) {
return true;
}
return false;
};
/**
* We'd really like to be able to do 'new NodeList()'
*/
exports.createEmptyNodeList = function(doc) {
if (doc.createDocumentFragment) {
return doc.createDocumentFragment().childNodes;
}
return doc.querySelectorAll('x>:root');
};
//------------------------------------------------------------------------------
/**
* Keyboard handling is a mess. http://unixpapa.com/js/key.html
* It would be good to use DOM L3 Keyboard events,
* http://www.w3.org/TR/2010/WD-DOM-Level-3-Events-20100907/#events-keyboardevents
* however only Webkit supports them, and there isn't a shim on Modernizr:
* https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-browser-Polyfills
* and when the code that uses this KeyEvent was written, nothing was clear,
* so instead, we're using this unmodern shim:
* http://stackoverflow.com/questions/5681146/chrome-10-keyevent-or-something-similar-to-firefoxs-keyevent
* See BUG 664991: GCLI's keyboard handling should be updated to use DOM-L3
* https://bugzilla.mozilla.org/show_bug.cgi?id=664991
*/
exports.KeyEvent = {
DOM_VK_CANCEL: 3,
DOM_VK_HELP: 6,
DOM_VK_BACK_SPACE: 8,
DOM_VK_TAB: 9,
DOM_VK_CLEAR: 12,
DOM_VK_RETURN: 13,
DOM_VK_SHIFT: 16,
DOM_VK_CONTROL: 17,
DOM_VK_ALT: 18,
DOM_VK_PAUSE: 19,
DOM_VK_CAPS_LOCK: 20,
DOM_VK_ESCAPE: 27,
DOM_VK_SPACE: 32,
DOM_VK_PAGE_UP: 33,
DOM_VK_PAGE_DOWN: 34,
DOM_VK_END: 35,
DOM_VK_HOME: 36,
DOM_VK_LEFT: 37,
DOM_VK_UP: 38,
DOM_VK_RIGHT: 39,
DOM_VK_DOWN: 40,
DOM_VK_PRINTSCREEN: 44,
DOM_VK_INSERT: 45,
DOM_VK_DELETE: 46,
DOM_VK_0: 48,
DOM_VK_1: 49,
DOM_VK_2: 50,
DOM_VK_3: 51,
DOM_VK_4: 52,
DOM_VK_5: 53,
DOM_VK_6: 54,
DOM_VK_7: 55,
DOM_VK_8: 56,
DOM_VK_9: 57,
DOM_VK_SEMICOLON: 59,
DOM_VK_EQUALS: 61,
DOM_VK_A: 65,
DOM_VK_B: 66,
DOM_VK_C: 67,
DOM_VK_D: 68,
DOM_VK_E: 69,
DOM_VK_F: 70,
DOM_VK_G: 71,
DOM_VK_H: 72,
DOM_VK_I: 73,
DOM_VK_J: 74,
DOM_VK_K: 75,
DOM_VK_L: 76,
DOM_VK_M: 77,
DOM_VK_N: 78,
DOM_VK_O: 79,
DOM_VK_P: 80,
DOM_VK_Q: 81,
DOM_VK_R: 82,
DOM_VK_S: 83,
DOM_VK_T: 84,
DOM_VK_U: 85,
DOM_VK_V: 86,
DOM_VK_W: 87,
DOM_VK_X: 88,
DOM_VK_Y: 89,
DOM_VK_Z: 90,
DOM_VK_CONTEXT_MENU: 93,
DOM_VK_NUMPAD0: 96,
DOM_VK_NUMPAD1: 97,
DOM_VK_NUMPAD2: 98,
DOM_VK_NUMPAD3: 99,
DOM_VK_NUMPAD4: 100,
DOM_VK_NUMPAD5: 101,
DOM_VK_NUMPAD6: 102,
DOM_VK_NUMPAD7: 103,
DOM_VK_NUMPAD8: 104,
DOM_VK_NUMPAD9: 105,
DOM_VK_MULTIPLY: 106,
DOM_VK_ADD: 107,
DOM_VK_SEPARATOR: 108,
DOM_VK_SUBTRACT: 109,
DOM_VK_DECIMAL: 110,
DOM_VK_DIVIDE: 111,
DOM_VK_F1: 112,
DOM_VK_F2: 113,
DOM_VK_F3: 114,
DOM_VK_F4: 115,
DOM_VK_F5: 116,
DOM_VK_F6: 117,
DOM_VK_F7: 118,
DOM_VK_F8: 119,
DOM_VK_F9: 120,
DOM_VK_F10: 121,
DOM_VK_F11: 122,
DOM_VK_F12: 123,
DOM_VK_F13: 124,
DOM_VK_F14: 125,
DOM_VK_F15: 126,
DOM_VK_F16: 127,
DOM_VK_F17: 128,
DOM_VK_F18: 129,
DOM_VK_F19: 130,
DOM_VK_F20: 131,
DOM_VK_F21: 132,
DOM_VK_F22: 133,
DOM_VK_F23: 134,
DOM_VK_F24: 135,
DOM_VK_NUM_LOCK: 144,
DOM_VK_SCROLL_LOCK: 145,
DOM_VK_COMMA: 188,
DOM_VK_PERIOD: 190,
DOM_VK_SLASH: 191,
DOM_VK_BACK_QUOTE: 192,
DOM_VK_OPEN_BRACKET: 219,
DOM_VK_BACK_SLASH: 220,
DOM_VK_CLOSE_BRACKET: 221,
DOM_VK_QUOTE: 222,
DOM_VK_META: 224
};