зеркало из https://github.com/mozilla/pjs.git
Bug 684958 - DOM Templater should include async functionality via promises; r=rcampbell
This commit is contained in:
Родитель
e075ac99a3
Коммит
e5126acf78
|
@ -38,7 +38,6 @@
|
|||
*
|
||||
* ***** END LICENSE BLOCK ***** */
|
||||
|
||||
// WARNING: do not 'use_strict' without reading the notes in envEval;
|
||||
|
||||
var EXPORTED_SYMBOLS = ["Templater"];
|
||||
|
||||
|
@ -47,13 +46,15 @@ const Cu = Components.utils;
|
|||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
var Node = Ci.nsIDOMNode;
|
||||
const Node = Ci.nsIDOMNode;
|
||||
|
||||
// WARNING: do not 'use_strict' without reading the notes in _envEval();
|
||||
|
||||
/**
|
||||
* A templater that allows one to quickly template DOM nodes.
|
||||
*/
|
||||
function Templater() {
|
||||
this.scope = [];
|
||||
this.stack = [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -66,10 +67,10 @@ Templater.prototype.processNode = function(node, data) {
|
|||
if (typeof node === 'string') {
|
||||
node = document.getElementById(node);
|
||||
}
|
||||
if (data === null || data === undefined) {
|
||||
if (data == null) {
|
||||
data = {};
|
||||
}
|
||||
this.scope.push(node.nodeName + (node.id ? '#' + node.id : ''));
|
||||
this.stack.push(node.nodeName + (node.id ? '#' + node.id : ''));
|
||||
try {
|
||||
// Process attributes
|
||||
if (node.attributes && node.attributes.length) {
|
||||
|
@ -77,11 +78,11 @@ Templater.prototype.processNode = function(node, data) {
|
|||
// some types of processing from happening, and foreach must come first
|
||||
// because it defines new data on which 'if' might depend.
|
||||
if (node.hasAttribute('foreach')) {
|
||||
this.processForEach(node, data);
|
||||
this._processForEach(node, data);
|
||||
return;
|
||||
}
|
||||
if (node.hasAttribute('if')) {
|
||||
if (!this.processIf(node, data)) {
|
||||
if (!this._processIf(node, data)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -93,19 +94,19 @@ Templater.prototype.processNode = function(node, data) {
|
|||
for (var i = 0; i < attrs.length; i++) {
|
||||
var value = attrs[i].value;
|
||||
var name = attrs[i].name;
|
||||
this.scope.push(name);
|
||||
this.stack.push(name);
|
||||
try {
|
||||
if (name === 'save') {
|
||||
// Save attributes are a setter using the node
|
||||
value = this.stripBraces(value);
|
||||
this.property(value, data, node);
|
||||
value = this._stripBraces(value);
|
||||
this._property(value, data, node);
|
||||
node.removeAttribute('save');
|
||||
} else if (name.substring(0, 2) === 'on') {
|
||||
// Event registration relies on property doing a bind
|
||||
value = this.stripBraces(value);
|
||||
var func = this.property(value, data);
|
||||
value = this._stripBraces(value);
|
||||
var func = this._property(value, data);
|
||||
if (typeof func !== 'function') {
|
||||
this.handleError('Expected ' + value +
|
||||
this._handleError('Expected ' + value +
|
||||
' to resolve to a function, but got ' + typeof func);
|
||||
}
|
||||
node.removeAttribute(name);
|
||||
|
@ -117,7 +118,7 @@ Templater.prototype.processNode = function(node, data) {
|
|||
} else {
|
||||
// Replace references in all other attributes
|
||||
var newValue = value.replace(/\$\{[^}]*\}/g, function(path) {
|
||||
return this.envEval(path.slice(2, -1), data, value);
|
||||
return this._envEval(path.slice(2, -1), data, value);
|
||||
}.bind(this));
|
||||
// Remove '_' prefix of attribute names so the DOM won't try
|
||||
// to use them before we've processed the template
|
||||
|
@ -129,7 +130,7 @@ Templater.prototype.processNode = function(node, data) {
|
|||
}
|
||||
}
|
||||
} finally {
|
||||
this.scope.pop();
|
||||
this.stack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -142,30 +143,31 @@ Templater.prototype.processNode = function(node, data) {
|
|||
}
|
||||
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
this.processTextNode(node, data);
|
||||
this._processTextNode(node, data);
|
||||
}
|
||||
} finally {
|
||||
this.scope.pop();
|
||||
delete data.__element;
|
||||
this.stack.pop();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle <x if="${...}">
|
||||
* @param node An element with an 'if' attribute
|
||||
* @param data The data to use with envEval
|
||||
* @param data The data to use with _envEval()
|
||||
* @returns true if processing should continue, false otherwise
|
||||
*/
|
||||
Templater.prototype.processIf = function(node, data) {
|
||||
this.scope.push('if');
|
||||
Templater.prototype._processIf = function(node, data) {
|
||||
this.stack.push('if');
|
||||
try {
|
||||
var originalValue = node.getAttribute('if');
|
||||
var value = this.stripBraces(originalValue);
|
||||
var value = this._stripBraces(originalValue);
|
||||
var recurse = true;
|
||||
try {
|
||||
var reply = this.envEval(value, data, originalValue);
|
||||
var reply = this._envEval(value, data, originalValue);
|
||||
recurse = !!reply;
|
||||
} catch (ex) {
|
||||
this.handleError('Error with \'' + value + '\'', ex);
|
||||
this._handleError('Error with \'' + value + '\'', ex);
|
||||
recurse = false;
|
||||
}
|
||||
if (!recurse) {
|
||||
|
@ -174,18 +176,22 @@ Templater.prototype.processIf = function(node, data) {
|
|||
node.removeAttribute('if');
|
||||
return recurse;
|
||||
} finally {
|
||||
this.scope.pop();
|
||||
this.stack.pop();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle <x foreach="param in ${array}"> and the special case of
|
||||
* <loop foreach="param in ${array}">
|
||||
* <loop foreach="param in ${array}">.
|
||||
* This function is responsible for extracting what it has to do from the
|
||||
* attributes, and getting the data to work on (including resolving promises
|
||||
* in getting the array). It delegates to _processForEachLoop to actually
|
||||
* unroll the data.
|
||||
* @param node An element with a 'foreach' attribute
|
||||
* @param data The data to use with envEval
|
||||
* @param data The data to use with _envEval()
|
||||
*/
|
||||
Templater.prototype.processForEach = function(node, data) {
|
||||
this.scope.push('foreach');
|
||||
Templater.prototype._processForEach = function(node, data) {
|
||||
this.stack.push('foreach');
|
||||
try {
|
||||
var originalValue = node.getAttribute('foreach');
|
||||
var value = originalValue;
|
||||
|
@ -193,62 +199,86 @@ Templater.prototype.processForEach = function(node, data) {
|
|||
var paramName = 'param';
|
||||
if (value.charAt(0) === '$') {
|
||||
// No custom loop variable name. Use the default: 'param'
|
||||
value = this.stripBraces(value);
|
||||
value = this._stripBraces(value);
|
||||
} else {
|
||||
// Extract the loop variable name from 'NAME in ${ARRAY}'
|
||||
var nameArr = value.split(' in ');
|
||||
paramName = nameArr[0].trim();
|
||||
value = this.stripBraces(nameArr[1].trim());
|
||||
value = this._stripBraces(nameArr[1].trim());
|
||||
}
|
||||
node.removeAttribute('foreach');
|
||||
try {
|
||||
var self = this;
|
||||
// Process a single iteration of a loop
|
||||
var processSingle = function(member, clone, ref) {
|
||||
ref.parentNode.insertBefore(clone, ref);
|
||||
data[paramName] = member;
|
||||
self.processNode(clone, data);
|
||||
delete data[paramName];
|
||||
};
|
||||
|
||||
// processSingle is no good for <loop> nodes where we want to work on
|
||||
// the childNodes rather than the node itself
|
||||
var processAll = function(scope, member) {
|
||||
self.scope.push(scope);
|
||||
try {
|
||||
if (node.nodeName.toLowerCase() === 'loop') {
|
||||
for (var i = 0; i < node.childNodes.length; i++) {
|
||||
var clone = node.childNodes[i].cloneNode(true);
|
||||
processSingle(member, clone, node);
|
||||
}
|
||||
} else {
|
||||
var clone = node.cloneNode(true);
|
||||
clone.removeAttribute('foreach');
|
||||
processSingle(member, clone, node);
|
||||
}
|
||||
} finally {
|
||||
self.scope.pop();
|
||||
}
|
||||
};
|
||||
|
||||
var reply = this.envEval(value, data, originalValue);
|
||||
if (Array.isArray(reply)) {
|
||||
reply.forEach(function(data, i) {
|
||||
processAll('' + i, data);
|
||||
}, this);
|
||||
} else {
|
||||
for (var param in reply) {
|
||||
if (reply.hasOwnProperty(param)) {
|
||||
processAll(param, param);
|
||||
}
|
||||
}
|
||||
}
|
||||
var evaled = this._envEval(value, data, originalValue);
|
||||
this._handleAsync(evaled, node, function(reply, siblingNode) {
|
||||
this._processForEachLoop(reply, node, siblingNode, data, paramName);
|
||||
}.bind(this));
|
||||
node.parentNode.removeChild(node);
|
||||
} catch (ex) {
|
||||
this.handleError('Error with \'' + value + '\'', ex);
|
||||
this._handleError('Error with \'' + value + '\'', ex);
|
||||
}
|
||||
} finally {
|
||||
this.scope.pop();
|
||||
this.stack.pop();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Called by _processForEach to handle looping over the data in a foreach loop.
|
||||
* This works with both arrays and objects.
|
||||
* Calls _processForEachMember() for each member of 'set'
|
||||
* @param set The object containing the data to loop over
|
||||
* @param template The node to copy for each set member
|
||||
* @param sibling The sibling node to which we add things
|
||||
* @param data the data to use for node processing
|
||||
* @param paramName foreach loops have a name for the parameter currently being
|
||||
* processed. The default is 'param'. e.g. <loop foreach="param in ${x}">...
|
||||
*/
|
||||
Templater.prototype._processForEachLoop = function(set, template, sibling, data, paramName) {
|
||||
if (Array.isArray(set)) {
|
||||
set.forEach(function(member, i) {
|
||||
this._processForEachMember(member, template, sibling, data, paramName, '' + i);
|
||||
}, this);
|
||||
} else {
|
||||
for (var member in set) {
|
||||
if (set.hasOwnProperty(member)) {
|
||||
this._processForEachMember(member, template, sibling, data, paramName, member);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Called by _processForEachLoop() to resolve any promises in the array (the
|
||||
* array itself can also be a promise, but that is resolved by
|
||||
* _processForEach()). Handle <LOOP> elements (which are taken out of the DOM),
|
||||
* clone the template, and pass the processing on to processNode().
|
||||
* @param member The data item to use in templating
|
||||
* @param template The node to copy for each set member
|
||||
* @param siblingNode The parent node to which we add things
|
||||
* @param data the data to use for node processing
|
||||
* @param paramName The name given to 'member' by the foreach attribute
|
||||
* @param frame A name to push on the stack for debugging
|
||||
*/
|
||||
Templater.prototype._processForEachMember = function(member, template, siblingNode, data, paramName, frame) {
|
||||
this.stack.push(frame);
|
||||
try {
|
||||
this._handleAsync(member, siblingNode, function(reply, node) {
|
||||
data[paramName] = reply;
|
||||
if (node.nodeName.toLowerCase() === 'loop') {
|
||||
for (var i = 0; i < node.childNodes.length; i++) {
|
||||
var clone = node.childNodes[i].cloneNode(true);
|
||||
node.parentNode.insertBefore(clone, node);
|
||||
this.processNode(clone, data);
|
||||
}
|
||||
} else {
|
||||
var clone = template.cloneNode(true);
|
||||
clone.removeAttribute('foreach');
|
||||
node.parentNode.insertBefore(clone, node);
|
||||
this.processNode(clone, data);
|
||||
}
|
||||
delete data[paramName];
|
||||
}.bind(this));
|
||||
} finally {
|
||||
this.stack.pop();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -257,9 +287,9 @@ Templater.prototype.processForEach = function(node, data) {
|
|||
* sections parsed out. We replace the node by altering node.parentNode but
|
||||
* we could probably use a DOM Text API to achieve the same thing.
|
||||
* @param node The Text node to work on
|
||||
* @param data The data to use in calls to envEval
|
||||
* @param data The data to use in calls to _envEval()
|
||||
*/
|
||||
Templater.prototype.processTextNode = function(node, data) {
|
||||
Templater.prototype._processTextNode = function(node, data) {
|
||||
// Replace references in other attributes
|
||||
var value = node.data;
|
||||
// We can't use the string.replace() with function trick (see generic
|
||||
|
@ -279,33 +309,68 @@ Templater.prototype.processTextNode = function(node, data) {
|
|||
return;
|
||||
}
|
||||
if (part.charAt(0) === '$') {
|
||||
part = this.envEval(part.slice(1), data, node.data);
|
||||
part = this._envEval(part.slice(1), data, node.data);
|
||||
}
|
||||
// It looks like this was done a few lines above but see envEval
|
||||
if (part === null) {
|
||||
part = "null";
|
||||
}
|
||||
if (part === undefined) {
|
||||
part = "undefined";
|
||||
}
|
||||
// if (isDOMElement(part)) { ... }
|
||||
if (typeof part.cloneNode !== 'function') {
|
||||
part = node.ownerDocument.createTextNode(part.toString());
|
||||
}
|
||||
node.parentNode.insertBefore(part, node);
|
||||
this._handleAsync(part, node, function(reply, siblingNode) {
|
||||
reply = this._toNode(reply, siblingNode.ownerDocument);
|
||||
siblingNode.parentNode.insertBefore(reply, siblingNode);
|
||||
}.bind(this));
|
||||
}, this);
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to convert a 'thing' to a DOM Node.
|
||||
* This is (obviously) a no-op for DOM Elements (which are detected using
|
||||
* 'typeof thing.cloneNode !== "function"' (is there a better way that will
|
||||
* work in all environments, including a .jsm?)
|
||||
* Non DOM elements are converted to a string and wrapped in a TextNode.
|
||||
*/
|
||||
Templater.prototype._toNode = function(thing, document) {
|
||||
if (thing == null) {
|
||||
thing = '' + thing;
|
||||
}
|
||||
// if (isDOMElement(reply)) { ... }
|
||||
if (typeof thing.cloneNode !== 'function') {
|
||||
thing = document.createTextNode(thing.toString());
|
||||
}
|
||||
return thing;
|
||||
};
|
||||
|
||||
/**
|
||||
* A function to handle the fact that some nodes can be promises, so we check
|
||||
* and resolve if needed using a marker node to keep our place before calling
|
||||
* an inserter function.
|
||||
* @param thing The object which could be real data or a promise of real data
|
||||
* we use it directly if it's not a promise, or resolve it if it is.
|
||||
* @param siblingNode The element before which we insert new elements.
|
||||
* @param inserter The function to to the insertion. If thing is not a promise
|
||||
* then _handleAsync() is just 'inserter(thing, siblingNode)'
|
||||
*/
|
||||
Templater.prototype._handleAsync = function(thing, siblingNode, inserter) {
|
||||
if (typeof thing.then === 'function') {
|
||||
// Placeholder element to be replaced once we have the real data
|
||||
var tempNode = siblingNode.ownerDocument.createElement('span');
|
||||
siblingNode.parentNode.insertBefore(tempNode, siblingNode);
|
||||
thing.then(function(delayed) {
|
||||
inserter(delayed, tempNode);
|
||||
tempNode.parentNode.removeChild(tempNode);
|
||||
}.bind(this));
|
||||
}
|
||||
else {
|
||||
inserter(thing, siblingNode);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Warn of string does not begin '${' and end '}'
|
||||
* @param str the string to check.
|
||||
* @return The string stripped of ${ and }, or untouched if it does not match
|
||||
*/
|
||||
Templater.prototype.stripBraces = function(str) {
|
||||
Templater.prototype._stripBraces = function(str) {
|
||||
if (!str.match(/\$\{.*\}/g)) {
|
||||
this.handleError('Expected ' + str + ' to match ${...}');
|
||||
this._handleError('Expected ' + str + ' to match ${...}');
|
||||
return str;
|
||||
}
|
||||
return str.slice(2, -1);
|
||||
|
@ -315,9 +380,9 @@ Templater.prototype.stripBraces = function(str) {
|
|||
* Combined getter and setter that works with a path through some data set.
|
||||
* For example:
|
||||
* <ul>
|
||||
* <li>property('a.b', { a: { b: 99 }}); // returns 99
|
||||
* <li>property('a', { a: { b: 99 }}); // returns { b: 99 }
|
||||
* <li>property('a', { a: { b: 99 }}, 42); // returns 99 and alters the
|
||||
* <li>_property('a.b', { a: { b: 99 }}); // returns 99
|
||||
* <li>_property('a', { a: { b: 99 }}); // returns { b: 99 }
|
||||
* <li>_property('a', { a: { b: 99 }}, 42); // returns 99 and alters the
|
||||
* input data to be { a: { b: 42 }}
|
||||
* </ul>
|
||||
* @param path An array of strings indicating the path through the data, or
|
||||
|
@ -328,8 +393,8 @@ Templater.prototype.stripBraces = function(str) {
|
|||
* @return The value pointed to by <tt>path</tt> before any
|
||||
* <tt>newValue</tt> is applied.
|
||||
*/
|
||||
Templater.prototype.property = function(path, data, newValue) {
|
||||
this.scope.push(path);
|
||||
Templater.prototype._property = function(path, data, newValue) {
|
||||
this.stack.push(path);
|
||||
try {
|
||||
if (typeof path === 'string') {
|
||||
path = path.split('.');
|
||||
|
@ -345,12 +410,12 @@ Templater.prototype.property = function(path, data, newValue) {
|
|||
return value;
|
||||
}
|
||||
if (!value) {
|
||||
this.handleError('Can\'t find path=' + path);
|
||||
this._handleError('Can\'t find path=' + path);
|
||||
return null;
|
||||
}
|
||||
return this.property(path.slice(1), value, newValue);
|
||||
return this._property(path.slice(1), value, newValue);
|
||||
} finally {
|
||||
this.scope.pop();
|
||||
this.stack.pop();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -362,22 +427,22 @@ Templater.prototype.property = function(path, data, newValue) {
|
|||
* according to the X keys in the env object, and then call that function using
|
||||
* the values in the env object. This is likely to be slow, but workable.
|
||||
* @param script The string to be evaluated.
|
||||
* @param env The environment in which to eval the script.
|
||||
* @param context Optional debugging string in case of failure
|
||||
* @param data The environment in which to eval the script.
|
||||
* @param frame Optional debugging string in case of failure.
|
||||
* @return The return value of the script, or the error message if the script
|
||||
* execution failed.
|
||||
*/
|
||||
Templater.prototype.envEval = function(script, env, context) {
|
||||
with (env) {
|
||||
Templater.prototype._envEval = function(script, data, frame) {
|
||||
with (data) {
|
||||
try {
|
||||
this.scope.push(context);
|
||||
this.stack.push(frame);
|
||||
return eval(script);
|
||||
} catch (ex) {
|
||||
this.handleError('Template error evaluating \'' + script + '\'' +
|
||||
' environment=' + Object.keys(env).join(', '), ex);
|
||||
this._handleError('Template error evaluating \'' + script + '\'' +
|
||||
' environment=' + Object.keys(data).join(', '), ex);
|
||||
return script;
|
||||
} finally {
|
||||
this.scope.pop();
|
||||
this.stack.pop();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -388,11 +453,11 @@ Templater.prototype.envEval = function(script, env, context) {
|
|||
* @param message the error message to report.
|
||||
* @param ex optional associated exception.
|
||||
*/
|
||||
Templater.prototype.handleError = function(message, ex) {
|
||||
this.logError(message);
|
||||
this.logError('In: ' + this.scope.join(' > '));
|
||||
Templater.prototype._handleError = function(message, ex) {
|
||||
this._logError(message);
|
||||
this._logError('In: ' + this.stack.join(' > '));
|
||||
if (ex) {
|
||||
this.logError(ex);
|
||||
this._logError(ex);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -402,7 +467,7 @@ Templater.prototype.handleError = function(message, ex) {
|
|||
* environments.
|
||||
* @param message the error message to report.
|
||||
*/
|
||||
Templater.prototype.logError = function(message) {
|
||||
Templater.prototype._logError = function(message) {
|
||||
Services.console.logStringMessage(message);
|
||||
};
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче