mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
DomRange now never removes elements except through the removeElement UI hook. If you write a hook that prevents removal, teardown still happens (e.g. templates stop updating).
This code also provides the basis for stopping updates to part of the DOM by triggering teardown without removal.
Before, DomBackend.removeElement would both trigger teardown and actually deparent the element. Now we have DomBackend.tearDownElement, which just triggers the jQuery teardown (which in turn triggers finalization of DomRanges that have been inserted in the DOM tree).
The flag to {node,members,range}Removed is now named “alreadyTornDown” and documented. Its purpose is to prevent redundant teardown walks through the DOM.
452 lines
13 KiB
JavaScript
452 lines
13 KiB
JavaScript
|
|
UI.Component.instantiate = function (parent) {
|
|
var kind = this;
|
|
|
|
// check arguments
|
|
if (UI.isComponent(kind)) {
|
|
if (kind.isInited)
|
|
throw new Error("A component kind is required, not an instance");
|
|
} else {
|
|
throw new Error("Expected Component kind");
|
|
}
|
|
|
|
var inst = kind.extend(); // XXX args go here
|
|
inst.isInited = true;
|
|
|
|
// XXX messy to define this here
|
|
inst.templateInstance = {
|
|
$: function(selector) {
|
|
// XXX check that `.dom` exists here?
|
|
return inst.dom.$(selector);
|
|
},
|
|
findAll: function (selector) {
|
|
return $.makeArray(this.$(selector));
|
|
},
|
|
find: function (selector) {
|
|
var result = this.$(selector);
|
|
return result[0] || null;
|
|
},
|
|
firstNode: null,
|
|
lastNode: null,
|
|
data: null,
|
|
__component__: inst
|
|
};
|
|
|
|
inst.parent = (parent || null);
|
|
|
|
if (inst.init)
|
|
inst.init();
|
|
|
|
if (inst.created) {
|
|
updateTemplateInstance(inst);
|
|
inst.created.call(inst.templateInstance);
|
|
}
|
|
|
|
return inst;
|
|
};
|
|
|
|
UI.Component.render = function () {
|
|
return null;
|
|
};
|
|
|
|
var Box = function (func, equals) {
|
|
var self = this;
|
|
|
|
self.func = func;
|
|
self.equals = equals;
|
|
|
|
self.curResult = null;
|
|
|
|
self.dep = new Deps.Dependency;
|
|
|
|
self.resultComputation = Deps.nonreactive(function () {
|
|
return Deps.autorun(function (c) {
|
|
var func = self.func;
|
|
|
|
var newResult = func();
|
|
|
|
if (! c.firstRun) {
|
|
var equals = self.equals;
|
|
var oldResult = self.curResult;
|
|
|
|
if (equals ? equals(newResult, oldResult) :
|
|
newResult === oldResult) {
|
|
// same as last time
|
|
return;
|
|
}
|
|
}
|
|
|
|
self.curResult = newResult;
|
|
self.dep.changed();
|
|
});
|
|
});
|
|
};
|
|
|
|
Box.prototype.stop = function () {
|
|
this.resultComputation.stop();
|
|
};
|
|
|
|
Box.prototype.get = function () {
|
|
if (Deps.active && ! this.resultComputation.stopped)
|
|
this.dep.depend();
|
|
|
|
return this.curResult;
|
|
};
|
|
|
|
// Takes a reactive function (call it `inner`) and returns a reactive function
|
|
// `outer` which is equivalent except in its reactive behavior. Specifically,
|
|
// `outer` has the following two special properties:
|
|
//
|
|
// 1. Isolation: An invocation of `outer()` only invalidates its context
|
|
// when the value of `inner()` changes. For example, `inner` may be a
|
|
// function that gets one or more Session variables and calculates a
|
|
// true/false value. `outer` blocks invalidation signals caused by the
|
|
// Session variables changing and sends a signal out only when the value
|
|
// changes between true and false (in this example). The value can be
|
|
// of any type, and it is compared with `===` unless an `equals` function
|
|
// is provided.
|
|
//
|
|
// 2. Value Sharing: The `outer` function returned by `emboxValue` can be
|
|
// shared between different contexts, for example by assigning it to an
|
|
// object as a method that can be accessed at any time, such as by
|
|
// different templates or different parts of a template. No matter
|
|
// how many times `outer` is called, `inner` is only called once until
|
|
// it changes. The most recent value is stored internally.
|
|
//
|
|
// Conceptually, an emboxed value is much like a Session variable which is
|
|
// kept up to date by an autorun. Session variables provide storage
|
|
// (value sharing) and they don't notify their listeners unless a value
|
|
// actually changes (isolation). The biggest difference is that such an
|
|
// autorun would never be stopped, and the Session variable would never be
|
|
// deleted even if it wasn't used any more. An emboxed value, on the other
|
|
// hand, automatically stops computing when it's not being used, and starts
|
|
// again when called from a reactive context. This means that when it stops
|
|
// being used, it can be completely garbage-collected.
|
|
//
|
|
// If a non-function value is supplied to `emboxValue` instead of a reactive
|
|
// function, then `outer` is still a function but it simply returns the value.
|
|
//
|
|
UI.emboxValue = function (funcOrValue, equals) {
|
|
if (typeof funcOrValue === 'function') {
|
|
|
|
var func = funcOrValue;
|
|
var box = new Box(func, equals);
|
|
|
|
var f = function () {
|
|
return box.get();
|
|
};
|
|
|
|
f.stop = function () {
|
|
box.stop();
|
|
};
|
|
|
|
return f;
|
|
|
|
} else {
|
|
var value = funcOrValue;
|
|
var result = function () {
|
|
return value;
|
|
};
|
|
result._isEmboxedConstant = true;
|
|
return result;
|
|
}
|
|
};
|
|
|
|
|
|
UI.namedEmboxValue = function (name, funcOrValue, equals) {
|
|
if (! Deps.active) {
|
|
var f = UI.emboxValue(funcOrValue, equals);
|
|
f.stop();
|
|
return f;
|
|
}
|
|
|
|
var c = Deps.currentComputation;
|
|
if (! c[name])
|
|
c[name] = UI.emboxValue(funcOrValue, equals);
|
|
|
|
return c[name];
|
|
};
|
|
|
|
////////////////////////////////////////
|
|
|
|
UI.insert = function (renderedTemplate, parentElement, nextNode) {
|
|
if (! renderedTemplate.dom)
|
|
throw new Error("Expected template rendered with UI.render");
|
|
|
|
UI.DomRange.insert(renderedTemplate.dom, parentElement, nextNode);
|
|
};
|
|
|
|
// Insert a DOM node or DomRange into a DOM element or DomRange.
|
|
//
|
|
// One of three things happens depending on what needs to be inserted into what:
|
|
// - `range.add` (anything into DomRange)
|
|
// - `UI.DomRange.insert` (DomRange into element)
|
|
// - `elem.insertBefore` (node into element)
|
|
//
|
|
// The optional `before` argument is an existing node or id to insert before in
|
|
// the parent element or DomRange.
|
|
var insert = function (nodeOrRange, parent, before) {
|
|
if (! parent)
|
|
throw new Error("Materialization parent required");
|
|
|
|
if (parent instanceof UI.DomRange) {
|
|
parent.add(nodeOrRange, before);
|
|
} else if (nodeOrRange instanceof UI.DomRange) {
|
|
// parent is an element; inserting a range
|
|
UI.DomRange.insert(nodeOrRange, parent, before);
|
|
} else {
|
|
// parent is an element; inserting an element
|
|
parent.insertBefore(nodeOrRange, before || null); // `null` for IE
|
|
}
|
|
};
|
|
|
|
// options include:
|
|
// - _nestInCurrentComputation: defaults to false. If true, then
|
|
// `render`'s autoruns will be nested inside the current
|
|
// computation, so if the current computation is invalidated, then
|
|
// the autoruns set up inside `render` will be stopped. If false,
|
|
// the autoruns will be set up in a fresh Deps context, so
|
|
// invalidating the current computation will have no effect on them.
|
|
UI.render = function (kind, parentComponent, options) {
|
|
options = options || {};
|
|
|
|
if (kind.isInited)
|
|
throw new Error("Can't render component instance, only component kind");
|
|
|
|
var inst, content, range;
|
|
|
|
Deps.nonreactive(function () {
|
|
|
|
inst = kind.instantiate(parentComponent);
|
|
|
|
content = (inst.render && inst.render());
|
|
|
|
range = new UI.DomRange;
|
|
inst.dom = range;
|
|
range.component = inst;
|
|
|
|
if (! options._nestInCurrentComputation) {
|
|
materialize(content, range, null, inst);
|
|
}
|
|
|
|
});
|
|
|
|
if (options._nestInCurrentComputation) {
|
|
materialize(content, range, null, inst);
|
|
}
|
|
|
|
range.removed = function () {
|
|
inst.isDestroyed = true;
|
|
if (inst.destroyed) {
|
|
Deps.nonreactive(function () {
|
|
updateTemplateInstance(inst);
|
|
inst.destroyed.call(inst.templateInstance);
|
|
});
|
|
}
|
|
};
|
|
|
|
return inst;
|
|
};
|
|
|
|
// options are the same as for UI.render.
|
|
UI.renderWithData = function (kind, data, parentComponent, options) {
|
|
if (! UI.isComponent(kind))
|
|
throw new Error("Component required here");
|
|
if (kind.isInited)
|
|
throw new Error("Can't render component instance, only component kind");
|
|
if (typeof data === 'function')
|
|
throw new Error("Data argument can't be a function");
|
|
|
|
return UI.render(kind.extend({data: function () { return data; }}),
|
|
parentComponent, options);
|
|
};
|
|
|
|
var contentEquals = function (a, b) {
|
|
if (a instanceof HTML.Raw) {
|
|
return (b instanceof HTML.Raw) && (a.value === b.value);
|
|
} else if (a == null) {
|
|
return (b == null);
|
|
} else {
|
|
return (a === b) &&
|
|
((typeof a === 'number') || (typeof a === 'boolean') ||
|
|
(typeof a === 'string'));
|
|
}
|
|
};
|
|
|
|
UI.InTemplateScope = function (tmplInstance, content) {
|
|
if (! (this instanceof UI.InTemplateScope))
|
|
// called without `new`
|
|
return new UI.InTemplateScope(tmplInstance, content);
|
|
|
|
var parentPtr = tmplInstance.parent;
|
|
if (parentPtr.__isTemplateWith)
|
|
parentPtr = parentPtr.parent;
|
|
|
|
this.parentPtr = parentPtr;
|
|
this.content = content;
|
|
};
|
|
|
|
UI.InTemplateScope.prototype.toHTML = function (parentComponent) {
|
|
return HTML.toHTML(this.content, this.parentPtr);
|
|
};
|
|
|
|
UI.InTemplateScope.prototype.toText = function (textMode, parentComponent) {
|
|
return HTML.toText(this.content, textMode, this.parentPtr);
|
|
};
|
|
|
|
// Convert the pseudoDOM `node` into reactive DOM nodes and insert them
|
|
// into the element or DomRange `parent`, before the node or id `before`.
|
|
var materialize = function (node, parent, before, parentComponent) {
|
|
// XXX should do more error-checking for the case where user is supplying the tags.
|
|
// For example, check that CharRef has `html` and `str` properties and no content.
|
|
// Check that Comment has a single string child and no attributes. Etc.
|
|
|
|
if (node == null) {
|
|
// null or undefined.
|
|
// do nothinge.
|
|
} else if ((typeof node === 'string') || (typeof node === 'boolean') || (typeof node === 'number')) {
|
|
node = String(node);
|
|
insert(document.createTextNode(node), parent, before);
|
|
} else if (node instanceof Array) {
|
|
for (var i = 0; i < node.length; i++)
|
|
materialize(node[i], parent, before, parentComponent);
|
|
} else if (typeof node === 'function') {
|
|
|
|
var range = new UI.DomRange;
|
|
var lastContent = null;
|
|
var rangeUpdater = Deps.autorun(function (c) {
|
|
var content = node();
|
|
// normalize content a little, for easier comparison
|
|
if (HTML.isNully(content))
|
|
content = null;
|
|
else if ((content instanceof Array) && content.length === 1)
|
|
content = content[0];
|
|
|
|
// update if content is different from last time
|
|
if (! contentEquals(content, lastContent)) {
|
|
lastContent = content;
|
|
|
|
if (! c.firstRun)
|
|
range.removeAll();
|
|
|
|
materialize(content, range, null, parentComponent);
|
|
}
|
|
});
|
|
range.removed = function () {
|
|
rangeUpdater.stop();
|
|
if (node.stop)
|
|
node.stop();
|
|
};
|
|
// XXXX HACK
|
|
if (Deps.active && node.stop) {
|
|
Deps.onInvalidate(function () {
|
|
node.stop();
|
|
});
|
|
}
|
|
insert(range, parent, before);
|
|
} else if (node instanceof HTML.Tag) {
|
|
var tagName = node.tagName;
|
|
var elem;
|
|
if (HTML.isKnownSVGElement(tagName) && document.createElementNS) {
|
|
elem = document.createElementNS('http://www.w3.org/2000/svg', tagName);
|
|
} else {
|
|
elem = document.createElement(node.tagName);
|
|
}
|
|
|
|
var rawAttrs = node.attrs;
|
|
var children = node.children;
|
|
if (node.tagName === 'textarea') {
|
|
rawAttrs = (rawAttrs || {});
|
|
rawAttrs.value = children;
|
|
children = [];
|
|
};
|
|
|
|
if (rawAttrs) {
|
|
var attrComp = Deps.autorun(function (c) {
|
|
var attrUpdater = c.updater;
|
|
if (! attrUpdater) {
|
|
attrUpdater = c.updater = new ElementAttributesUpdater(elem);
|
|
}
|
|
|
|
try {
|
|
var attrs = HTML.evaluateAttributes(rawAttrs, parentComponent);
|
|
var stringAttrs = {};
|
|
if (attrs) {
|
|
for (var k in attrs) {
|
|
stringAttrs[k] = HTML.toText(attrs[k], HTML.TEXTMODE.STRING,
|
|
parentComponent);
|
|
}
|
|
attrUpdater.update(stringAttrs);
|
|
}
|
|
} catch (e) {
|
|
reportUIException(e);
|
|
}
|
|
});
|
|
UI.DomBackend.onElementTeardown(elem, function () {
|
|
attrComp.stop();
|
|
});
|
|
}
|
|
materialize(children, elem, null, parentComponent);
|
|
|
|
insert(elem, parent, before);
|
|
} else if (typeof node.instantiate === 'function') {
|
|
// component
|
|
var instance = UI.render(node, parentComponent, {
|
|
_nestInCurrentComputation: true
|
|
});
|
|
|
|
// Call internal callback, which may take advantage of the current
|
|
// Deps computation.
|
|
if (instance.materialized)
|
|
instance.materialized(instance.dom);
|
|
|
|
insert(instance.dom, parent, before);
|
|
} else if (node instanceof HTML.CharRef) {
|
|
insert(document.createTextNode(node.str), parent, before);
|
|
} else if (node instanceof HTML.Comment) {
|
|
insert(document.createComment(node.sanitizedValue), parent, before);
|
|
} else if (node instanceof HTML.Raw) {
|
|
// Get an array of DOM nodes by using the browser's HTML parser
|
|
// (like innerHTML).
|
|
var htmlNodes = UI.DomBackend.parseHTML(node.value);
|
|
for (var i = 0; i < htmlNodes.length; i++)
|
|
insert(htmlNodes[i], parent, before);
|
|
} else if (Package['html-tools'] && (node instanceof Package['html-tools'].HTMLTools.Special)) {
|
|
throw new Error("Can't materialize Special tag, it's just an intermediate rep");
|
|
} else if (node instanceof UI.InTemplateScope) {
|
|
materialize(node.content, parent, before, node.parentPtr);
|
|
} else {
|
|
// can't get here
|
|
throw new Error("Unexpected node in htmljs: " + node);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
// XXX figure out the right names, and namespace, for these.
|
|
// for example, maybe some of them go in the HTML package.
|
|
UI.materialize = materialize;
|
|
|
|
UI.body = UI.Component.extend({
|
|
kind: 'body',
|
|
contentParts: [],
|
|
render: function () {
|
|
return this.contentParts;
|
|
},
|
|
// XXX revisit how body works.
|
|
INSTANTIATED: false,
|
|
__helperHost: true
|
|
});
|
|
|
|
UI.block = function (renderFunc) {
|
|
return UI.Component.extend({ render: renderFunc });
|
|
};
|
|
|
|
UI.toHTML = function (content, parentComponent) {
|
|
return HTML.toHTML(content, parentComponent);
|
|
};
|
|
|
|
UI.toRawText = function (content, parentComponent) {
|
|
return HTML.toText(content, HTML.TEXTMODE.STRING, parentComponent);
|
|
};
|