mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
192 lines
6.9 KiB
JavaScript
192 lines
6.9 KiB
JavaScript
// Turns HTMLjs into DOM nodes and DOMRanges.
|
|
//
|
|
// - `htmljs`: the value to materialize, which may be any of the htmljs
|
|
// types (Tag, CharRef, Comment, Raw, array, string, boolean, number,
|
|
// null, or undefined) or a View or Template (which will be used to
|
|
// construct a View).
|
|
// - `intoArray`: the array of DOM nodes and DOMRanges to push the output
|
|
// into (required)
|
|
// - `parentView`: the View we are materializing content for (optional)
|
|
// - `_existingWorkStack`: optional argument, only used for recursive
|
|
// calls when there is some other _materializeDOM on the call stack.
|
|
// If _materializeDOM called your function and passed in a workStack,
|
|
// pass it back when you call _materializeDOM (such as from a workStack
|
|
// task).
|
|
//
|
|
// Returns `intoArray`, which is especially useful if you pass in `[]`.
|
|
Blaze._materializeDOM = function (htmljs, intoArray, parentView,
|
|
_existingWorkStack) {
|
|
// In order to use fewer stack frames, materializeDOMInner can push
|
|
// tasks onto `workStack`, and they will be popped off
|
|
// and run, last first, after materializeDOMInner returns. The
|
|
// reason we use a stack instead of a queue is so that we recurse
|
|
// depth-first, doing newer tasks first.
|
|
var workStack = (_existingWorkStack || []);
|
|
materializeDOMInner(htmljs, intoArray, parentView, workStack);
|
|
|
|
if (! _existingWorkStack) {
|
|
// We created the work stack, so we are responsible for finishing
|
|
// the work. Call each "task" function, starting with the top
|
|
// of the stack.
|
|
while (workStack.length) {
|
|
// Note that running task() may push new items onto workStack.
|
|
var task = workStack.pop();
|
|
task();
|
|
}
|
|
}
|
|
|
|
return intoArray;
|
|
};
|
|
|
|
var materializeDOMInner = function (htmljs, intoArray, parentView, workStack) {
|
|
if (htmljs == null) {
|
|
// null or undefined
|
|
return;
|
|
}
|
|
|
|
switch (typeof htmljs) {
|
|
case 'string': case 'boolean': case 'number':
|
|
intoArray.push(document.createTextNode(String(htmljs)));
|
|
return;
|
|
case 'object':
|
|
if (htmljs.htmljsType) {
|
|
switch (htmljs.htmljsType) {
|
|
case HTML.Tag.htmljsType:
|
|
intoArray.push(materializeTag(htmljs, parentView, workStack));
|
|
return;
|
|
case HTML.CharRef.htmljsType:
|
|
intoArray.push(document.createTextNode(htmljs.str));
|
|
return;
|
|
case HTML.Comment.htmljsType:
|
|
intoArray.push(document.createComment(htmljs.sanitizedValue));
|
|
return;
|
|
case HTML.Raw.htmljsType:
|
|
// Get an array of DOM nodes by using the browser's HTML parser
|
|
// (like innerHTML).
|
|
var nodes = Blaze._DOMBackend.parseHTML(htmljs.value);
|
|
for (var i = 0; i < nodes.length; i++)
|
|
intoArray.push(nodes[i]);
|
|
return;
|
|
}
|
|
} else if (HTML.isArray(htmljs)) {
|
|
for (var i = htmljs.length-1; i >= 0; i--) {
|
|
workStack.push(_.bind(Blaze._materializeDOM, null,
|
|
htmljs[i], intoArray, parentView, workStack));
|
|
}
|
|
return;
|
|
} else {
|
|
if (htmljs instanceof Blaze.Template) {
|
|
htmljs = htmljs.constructView();
|
|
// fall through to Blaze.View case below
|
|
}
|
|
if (htmljs instanceof Blaze.View) {
|
|
Blaze._materializeView(htmljs, parentView, workStack, intoArray);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Error("Unexpected object in htmljs: " + htmljs);
|
|
};
|
|
|
|
var materializeTag = function (tag, parentView, workStack) {
|
|
var tagName = tag.tagName;
|
|
var elem;
|
|
if ((HTML.isKnownSVGElement(tagName) || isSVGAnchor(tag))
|
|
&& document.createElementNS) {
|
|
// inline SVG
|
|
elem = document.createElementNS('http://www.w3.org/2000/svg', tagName);
|
|
} else {
|
|
// normal elements
|
|
elem = document.createElement(tagName);
|
|
}
|
|
|
|
var rawAttrs = tag.attrs;
|
|
var children = tag.children;
|
|
if (tagName === 'textarea' && tag.children.length &&
|
|
! (rawAttrs && ('value' in rawAttrs))) {
|
|
// Provide very limited support for TEXTAREA tags with children
|
|
// rather than a "value" attribute.
|
|
// Reactivity in the form of Views nested in the tag's children
|
|
// won't work. Compilers should compile textarea contents into
|
|
// the "value" attribute of the tag, wrapped in a function if there
|
|
// is reactivity.
|
|
if (typeof rawAttrs === 'function' ||
|
|
HTML.isArray(rawAttrs)) {
|
|
throw new Error("Can't have reactive children of TEXTAREA node; " +
|
|
"use the 'value' attribute instead.");
|
|
}
|
|
rawAttrs = _.extend({}, rawAttrs || null);
|
|
rawAttrs.value = Blaze._expand(children, parentView);
|
|
children = [];
|
|
}
|
|
|
|
if (rawAttrs) {
|
|
var attrUpdater = new ElementAttributesUpdater(elem);
|
|
var updateAttributes = function () {
|
|
var expandedAttrs = Blaze._expandAttributes(rawAttrs, parentView);
|
|
var flattenedAttrs = HTML.flattenAttributes(expandedAttrs);
|
|
var stringAttrs = {};
|
|
for (var attrName in flattenedAttrs) {
|
|
stringAttrs[attrName] = Blaze._toText(flattenedAttrs[attrName],
|
|
parentView,
|
|
HTML.TEXTMODE.STRING);
|
|
}
|
|
attrUpdater.update(stringAttrs);
|
|
};
|
|
var updaterComputation;
|
|
if (parentView) {
|
|
updaterComputation =
|
|
parentView.autorun(updateAttributes, undefined, 'updater');
|
|
} else {
|
|
updaterComputation = Tracker.nonreactive(function () {
|
|
return Tracker.autorun(function () {
|
|
Tracker._withCurrentView(parentView, updateAttributes);
|
|
});
|
|
});
|
|
}
|
|
Blaze._DOMBackend.Teardown.onElementTeardown(elem, function attrTeardown() {
|
|
updaterComputation.stop();
|
|
});
|
|
}
|
|
|
|
if (children.length) {
|
|
var childNodesAndRanges = [];
|
|
// push this function first so that it's done last
|
|
workStack.push(function () {
|
|
for (var i = 0; i < childNodesAndRanges.length; i++) {
|
|
var x = childNodesAndRanges[i];
|
|
if (x instanceof Blaze._DOMRange)
|
|
x.attach(elem);
|
|
else
|
|
elem.appendChild(x);
|
|
}
|
|
});
|
|
// now push the task that calculates childNodesAndRanges
|
|
workStack.push(_.bind(Blaze._materializeDOM, null,
|
|
children, childNodesAndRanges, parentView,
|
|
workStack));
|
|
}
|
|
|
|
return elem;
|
|
};
|
|
|
|
|
|
var isSVGAnchor = function (node) {
|
|
// We generally aren't able to detect SVG <a> elements because
|
|
// if "A" were in our list of known svg element names, then all
|
|
// <a> nodes would be created using
|
|
// `document.createElementNS`. But in the special case of <a
|
|
// xlink:href="...">, we can at least detect that attribute and
|
|
// create an SVG <a> tag in that case.
|
|
//
|
|
// However, we still have a general problem of knowing when to
|
|
// use document.createElementNS and when to use
|
|
// document.createElement; for example, font tags will always
|
|
// be created as SVG elements which can cause other
|
|
// problems. #1977
|
|
return (node.tagName === "a" &&
|
|
node.attrs &&
|
|
node.attrs["xlink:href"] !== undefined);
|
|
};
|