mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
1154 lines
36 KiB
JavaScript
1154 lines
36 KiB
JavaScript
// A very basic operation like Underscore's `_.extend` that
|
|
// copies `src`'s own, enumerable properties onto `tgt` and
|
|
// returns `tgt`.
|
|
_extend = function (tgt, src) {
|
|
for (var k in src)
|
|
if (src.hasOwnProperty(k))
|
|
tgt[k] = src[k];
|
|
return tgt;
|
|
};
|
|
|
|
// @export UI
|
|
UI = {
|
|
nextGuid: 2, // Component is 1!
|
|
|
|
// Components and Component "classes" are the same thing, just
|
|
// objects; there are no constructor functions, no `new`,
|
|
// and no `instanceof`. A Component object is like a class,
|
|
// until it is inited, at which point it becomes more like
|
|
// an instance.
|
|
//
|
|
// `y = x.extend({ ...new props })` creates a new Component
|
|
// `y` with `x` as its prototype, plus additional properties
|
|
// on `y` itself. `extend` is used both to subclass and to
|
|
// create instances (and the hope is we can gloss over the
|
|
// difference in the docs).
|
|
|
|
Component: (function (constr) {
|
|
// Make sure the "class name" that Chrome infers for
|
|
// UI.Component is "Component", and that
|
|
// `new UI.Component._constr` (which is what `extend`
|
|
// does) also produces objects whose inferred class
|
|
// name is "Component". Chrome's name inference rules
|
|
// are a little mysterious, but a function name in
|
|
// the source code (as in `function Component() {}`)
|
|
// seems to be reliable and high precedence.
|
|
return _extend(new constr, {_constr: constr});
|
|
})(function Component() {}),
|
|
|
|
isComponent: function (obj) {
|
|
return obj && obj.isa === UI.Component.isa;
|
|
},
|
|
attachRoot: function (comp, parentNode, beforeNode) {
|
|
comp._requireNotDestroyed();
|
|
|
|
if (! comp.isInited)
|
|
comp.makeRoot();
|
|
|
|
if (comp.parent)
|
|
throw new Error("Component is inited with a parent (not a root)");
|
|
|
|
comp._attach(parentNode, beforeNode);
|
|
},
|
|
// global append to body; experimental
|
|
append: function (comp) {
|
|
UI.attachRoot(comp, document.body);
|
|
}
|
|
};
|
|
|
|
Component = UI.Component;
|
|
|
|
_extend(UI.Component, {
|
|
// If a Component has a `typeName` property set via `extend`,
|
|
// we make it use that name when printed in Chrome Dev Tools.
|
|
// If you then extend this Component and don't supply any
|
|
// new typeName, it should use the same typeName (or the
|
|
// most specific one in the case of an `extend` chain with
|
|
// `typeName` set at multiple points).
|
|
//
|
|
// To accomplish this, keeping performance in mind,
|
|
// any Component where `typeName` is explicitly set
|
|
// also has a function property `_constr` whose source-code
|
|
// name is `typeName`. `extend` creates this `_constr`
|
|
// function, which can then be used internally as a
|
|
// constructor to quickly create new instances that
|
|
// pretty-print correctly.
|
|
typeName: "Component",
|
|
_constr: function Component() {},
|
|
|
|
_super: null,
|
|
guid: "1",
|
|
|
|
extend: function (props) {
|
|
// this function should never cause `props` to be
|
|
// mutated in case people want to reuse `props` objects
|
|
// in a mixin-like way.
|
|
|
|
if (this.isInited)
|
|
// Disallow extending inited Components so that
|
|
// inited Components don't inherit instance-specific
|
|
// properties from other inited Components, just
|
|
// default values.
|
|
throw new Error("Can't extend an inited Component");
|
|
|
|
var constr;
|
|
var constrMade = false;
|
|
// Any Component with a typeName of "Foo" (say) is given
|
|
// a `._constr` of the form `function Foo() {}`.
|
|
if (props && props.typeName) {
|
|
constr = Function("return function " +
|
|
sanitizeTypeName(props.typeName) +
|
|
"() {};")();
|
|
constrMade = true;
|
|
} else {
|
|
constr = this._constr;
|
|
}
|
|
|
|
// We don't know where we're getting `constr` from --
|
|
// it might be from some supertype -- just that it has
|
|
// the right function name. So set the `prototype`
|
|
// property each time we use it as a constructor.
|
|
constr.prototype = this;
|
|
|
|
var c = new constr;
|
|
if (constrMade)
|
|
c._constr = constr;
|
|
|
|
if (props)
|
|
_extend(c, props);
|
|
|
|
// for efficient Component instantiations, we assign
|
|
// as few things as possible here.
|
|
c._super = this;
|
|
c.guid = String(UI.nextGuid++);
|
|
|
|
return c;
|
|
},
|
|
|
|
// `x.isa(Foo)` where `x` is a Component returns `true`
|
|
// if `x` is `Foo` or a Component that descends from
|
|
// (transitively extends) `Foo`.
|
|
isa: function (obj) {
|
|
var x = this;
|
|
while (x) {
|
|
if (x === obj)
|
|
return true;
|
|
x = x._super;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
Empty = Component.extend({
|
|
render: function (buf) {}
|
|
});
|
|
|
|
callChainedCallback = function (comp, propName, orig) {
|
|
// Call `comp.foo`, `comp._super.foo`,
|
|
// `comp._super._super.foo`, and so on, but in reverse
|
|
// order, and only if `foo` is an "own property" in each
|
|
// case. Furthermore, the passed value of `this` should
|
|
// remain `comp` for all calls (which is achieved by
|
|
// filling in `orig` when recursing).
|
|
if (comp._super)
|
|
callChainedCallback(comp._super, propName, orig || comp);
|
|
|
|
if (comp.hasOwnProperty(propName))
|
|
comp[propName].call(orig || comp);
|
|
};
|
|
|
|
// Make `typeName` a non-empty string starting with an ASCII
|
|
// letter or underscore and containing only letters, underscores,
|
|
// and numbers. This makes it safe to insert into evaled JS
|
|
// code.
|
|
var sanitizeTypeName = function (typeName) {
|
|
return String(typeName).replace(/^[^a-zA-Z_]|[^a-zA-Z_0-9]+/g,
|
|
'') || 'Component';
|
|
};
|
|
|
|
var SEALED_EMPTY_OBJECT = {};
|
|
if (Object.seal)
|
|
// IE 9+, FF, Chrome, Safari
|
|
Object.seal(SEALED_EMPTY_OBJECT);
|
|
|
|
_extend(UI.Component, {
|
|
// Has this Component ever been inited?
|
|
isInited: false,
|
|
// Has this Component ever been built into DOM nodes?
|
|
// Implies isInited.
|
|
isBuilt: false,
|
|
// Has this Component been destroyed? Only inited Components
|
|
// can be destroyed, but built and unbuilt Components
|
|
// can both be destroyed (and their value of isBuilt
|
|
// stays the same when they are).
|
|
isDestroyed: false,
|
|
|
|
destroy: function () {
|
|
if (! this.isInited)
|
|
throw new Error("Can't destroy an uninited Component");
|
|
|
|
if (this.isDestroyed)
|
|
return;
|
|
|
|
this.isDestroyed = true;
|
|
|
|
// recursively destroy children as well
|
|
for (var k in this.children)
|
|
this.children[k].destroy();
|
|
|
|
// clean up any data associated with offscreen nodes
|
|
if (this._offscreen)
|
|
$.cleanData(this._offscreen.childNodes);
|
|
|
|
// stop all computations (rebuilding and comp.autorun)
|
|
var comps = this._computations;
|
|
if (comps)
|
|
for (var i = 0; i < comps.length; i++)
|
|
comps[i].stop();
|
|
|
|
callChainedCallback(this, 'destroyed');
|
|
},
|
|
|
|
// use this to produce error messages for developers
|
|
// (though throwing a more specific error message is
|
|
// even better)
|
|
_requireNotDestroyed: function () {
|
|
if (this.isDestroyed)
|
|
throw new Error("Component has been destroyed; can't perform this operation");
|
|
},
|
|
|
|
_requireInited: function () {
|
|
if (! this.isInited)
|
|
throw new Error("Component must be inited to perform this operation");
|
|
},
|
|
|
|
_requireBuilt: function () {
|
|
if (! this.isBuilt)
|
|
throw new Error("Component must be built into DOM to perform this operation");
|
|
}
|
|
|
|
});
|
|
|
|
_extend(UI.Component, {
|
|
// Parent Component in the composition hierarchy.
|
|
// An inited
|
|
parent: null,
|
|
// Child Components in the composition hierarchy,
|
|
// in a dictionary keyed on their `guid` property.
|
|
//
|
|
// For memory efficiency, childless Components share
|
|
// the same dictionary.
|
|
children: SEALED_EMPTY_OBJECT,
|
|
|
|
// # component.add(child)
|
|
//
|
|
// Adds `child` to this component in the parent/child
|
|
// hierarchy.
|
|
//
|
|
// Components must be assembled from "top to bottom." Each
|
|
// component must either be added as a child of another,
|
|
// or made a root using `component.makeRoot()`, before
|
|
// it can receive its own children. This ensures that
|
|
// every component already knows its parent when it is
|
|
// initialized. A component's parent is permanent; the
|
|
// component cannot be removed or reparented without
|
|
// destroying it.
|
|
//
|
|
// The child is not built or put into the DOM.
|
|
// The `append`, `prepend`, and `insertBefore`
|
|
// methods all add their argument as a child in addition
|
|
// to building it if necessary and putting it into the
|
|
// component's DOM.
|
|
//
|
|
// Requires `component` is not destroyed.
|
|
add: function (child) {
|
|
var self = this;
|
|
|
|
if (self.isDestroyed)
|
|
throw new Error("Can't add child to a destroyed component");
|
|
if (! self.isInited)
|
|
throw new Error("Component must be inited already to add a child");
|
|
|
|
var guid = child.guid;
|
|
|
|
if (self.children[guid])
|
|
throw new Error("Child already added to this component!");
|
|
|
|
if (child.isInited)
|
|
throw new Error("Child already inited, can't add to a different parent");
|
|
|
|
// allocate a new dictionary to hold children if necessary
|
|
if (self.children === SEALED_EMPTY_OBJECT)
|
|
self.children = {};
|
|
|
|
self.children[guid] = child;
|
|
|
|
child.parent = self;
|
|
|
|
// Note on ordering of these two lines: You see `isInited`
|
|
// as `true` from `init` callbacks, even though
|
|
// linguistically it seems odd that you are marked
|
|
// inited before `init` is called. What's really going
|
|
// on is `init` is a callback which would normally be
|
|
// named in the past tense; for example, we'd set
|
|
// `isAdded` to true and then call the `added` callback.
|
|
//
|
|
// `isInited` in fact means essentially "has been added/
|
|
// instantiated", and `init` is the callback you get
|
|
// when that happens.
|
|
child.isInited = true;
|
|
Deps.nonreactive(function () {
|
|
callChainedCallback(child, 'init');
|
|
});
|
|
|
|
// useful in: `this.foo = this.add(Foo.extend())`
|
|
return child;
|
|
},
|
|
|
|
hasChild: function (comp) {
|
|
this._requireNotDestroyed();
|
|
this._requireInited();
|
|
|
|
return this.children[comp.guid] === comp;
|
|
},
|
|
|
|
// Init this Component without giving it a parent; it will
|
|
// never have a parent and always be the root of its own
|
|
// parent/child hierarchy.
|
|
//
|
|
// This is primarily intended for unit testing, or embedding
|
|
// Meteor UI.
|
|
makeRoot: function (comp) {
|
|
if (this.isInited) {
|
|
if (this.parent)
|
|
throw new Error("Component already parented");
|
|
throw new Error("Component already inited as a root");
|
|
}
|
|
|
|
this.isInited = true;
|
|
callChainedCallback(this, 'init');
|
|
},
|
|
|
|
remove: function (child) {
|
|
var self = this;
|
|
|
|
self._requireNotDestroyed();
|
|
|
|
if (! child) {
|
|
// Support `()` form of args; remove self.
|
|
// Can't `remove()` if we are a root or haven't been
|
|
// inited.
|
|
if (! self.isInited || ! self.parent)
|
|
throw new Error("Component to remove must have a parent");
|
|
self.parent.remove(self);
|
|
return;
|
|
}
|
|
|
|
// Child is inited but may or may not be built.
|
|
// Child may be destroyed.
|
|
|
|
// Note that child is not removed from the DOM if it is already
|
|
// destroyed. This is used when a Component is rebuilt -- the
|
|
// children are first destroyed, then removed as children, then
|
|
// removed from the DOM wholesale in one operation.
|
|
if (child.isBuilt && ! child.isDestroyed &&
|
|
child.isAttached) {
|
|
|
|
child.detach(true); // _forDestruction = true
|
|
}
|
|
|
|
var guid = child.guid;
|
|
if (! self.children[guid])
|
|
throw new Error("Child not found (id " + guid + ")");
|
|
|
|
delete self.children[guid];
|
|
// (don't delete child.parent pointer, could be useful
|
|
// in destroyed callback?)
|
|
|
|
child.destroy();
|
|
|
|
}
|
|
});
|
|
|
|
_extend(UI.Component, {
|
|
// If the Component is built into DOM, `start` and `end`
|
|
// are the first and last *nodes or Components* in this
|
|
// Component's subtree of DOM nodes and Components.
|
|
start: null,
|
|
end: null,
|
|
|
|
firstNode: function () {
|
|
this._requireBuilt();
|
|
this._requireNotDestroyed();
|
|
|
|
return UI.isComponent(this.start) ?
|
|
this.start.firstNode() : this.start;
|
|
},
|
|
|
|
lastNode: function () {
|
|
this._requireBuilt();
|
|
this._requireNotDestroyed();
|
|
|
|
return UI.isComponent(this.end) ?
|
|
this.end.lastNode() : this.end;
|
|
},
|
|
|
|
parentNode: function () {
|
|
return this.firstNode().parentNode;
|
|
},
|
|
|
|
// Built Components are either attached or detached.
|
|
// An attached Component is assumed to be part of
|
|
// its parent's DOM tree (or for a root Component, the
|
|
// document). A detached Component lives in its own private
|
|
// offscreen DIV. Components start detached (offscreen) but
|
|
// are typically attached immediately. They may then be
|
|
// attached and detached at will, which inserts and removes
|
|
// them from the parent's DOM. A detached Component has
|
|
// a functional DOM and can have attached and detached
|
|
// children.
|
|
//
|
|
// Components should only be inserted into the DOM by calling
|
|
// `append`, `insertBefore`, et al. on their parent, or
|
|
// for a root using `attachRoot`.
|
|
isAttached: false,
|
|
|
|
// DIV holding offscreen content (when component is built and not attached).
|
|
// It's a DIV rather than a fragment so that jQuery can run against it.
|
|
_offscreen: null,
|
|
|
|
// `content` and `elseContent` must be Components or functions
|
|
// that return components.
|
|
content: Empty,
|
|
elseContent: Empty,
|
|
|
|
// The `render` method is overridden by compiled templates
|
|
// and other components to declare the component's
|
|
// constituent HTML/DOM and children. It's called during
|
|
// building on the client, and it can also be used on the
|
|
// client or server to generate initial HTML.
|
|
render: function (buf) {
|
|
buf.write(this.content);
|
|
},
|
|
|
|
_populate: function (div) {
|
|
var self = this;
|
|
|
|
var buf = makeRenderBuffer(self);
|
|
self.render(buf);
|
|
|
|
var html = buf.getHtml();
|
|
|
|
$(div).append(html);
|
|
|
|
// returns info object with {start, end}
|
|
return buf.wireUpDOM(div);
|
|
},
|
|
|
|
build: function () {
|
|
var self = this;
|
|
|
|
self._requireNotDestroyed();
|
|
if (self.isBuilt)
|
|
throw new Error("Component already built");
|
|
|
|
if (! self.isInited)
|
|
self.makeRoot();
|
|
|
|
self._rebuilder = self.autorun(function (c) {
|
|
// record set of children that existed before,
|
|
// or null (for efficiency)
|
|
var oldChildren = null;
|
|
for (var k in self.children)
|
|
(oldChildren || (oldChildren = {}))[k] = true;
|
|
|
|
if (c.firstRun) {
|
|
var div = makeSafeDiv();
|
|
// capture reactivity:
|
|
var info = self._populate(div);
|
|
|
|
if (! div.firstChild)
|
|
div.appendChild(createEmptyComment());
|
|
|
|
self._offscreen = div;
|
|
self.start = info.start || div.firstChild;
|
|
self.end = info.end || div.lastChild;
|
|
} else {
|
|
// capture reactivity:
|
|
self._rebuild(c.builtChildren);
|
|
}
|
|
|
|
var newChildren = null;
|
|
for (var k in self.children)
|
|
if (! (oldChildren && oldChildren[k]))
|
|
(newChildren || (newChildren = {}))[k] = self.children[k];
|
|
|
|
// `builtChildren` is actually children *added* during build
|
|
c.builtChildren = newChildren;
|
|
|
|
// don't capture dependencies, but provide a
|
|
// parent autorun (so that any autoruns created
|
|
// from a built callback are stopped on rebuild)
|
|
var x = Deps.autorun(function (c) {
|
|
if (c.firstRun) {
|
|
self.isBuilt = true;
|
|
self._callOnNextBuiltCallbacks();
|
|
|
|
// FAKE-ISH (NON-DELEGATED) EVENT MAP STUFF
|
|
if (self._events && self._events.length) {
|
|
_.each(self._events, function (info) {
|
|
$(self.firstNode().parentNode).find(info.selector).on(
|
|
info.type, function (evt) {
|
|
if (self.containsElement(evt.currentTarget))
|
|
info.handler(evt);
|
|
});
|
|
});
|
|
}
|
|
|
|
callChainedCallback(self, 'rendered');
|
|
}
|
|
});
|
|
Deps.onInvalidate(function () {
|
|
x.stop();
|
|
});
|
|
});
|
|
},
|
|
|
|
// Don't call this directly. It implements the re-run of the
|
|
// build autorun, so it assumes it's already inside the appropriate
|
|
// reactive computation.
|
|
//
|
|
// `builtChildren` is a map of children that were added during
|
|
// the previous build (as opposed to at some other time, such as
|
|
// earlier from an `init` callback).
|
|
_rebuild: function (builtChildren) {
|
|
var self = this;
|
|
|
|
if (! (self.isBuilt && ! self.isDestroyed))
|
|
throw new Error("Assertion failed in _rebuild");
|
|
|
|
// Should work whether this component is detached or attached!
|
|
// In other words, it may reside in an offscreen element.
|
|
|
|
var firstNode = self.firstNode();
|
|
var lastNode = self.lastNode();
|
|
var parentNode = lastNode.parentNode;
|
|
var nextNode = lastNode.nextSibling || null;
|
|
var prevNode = firstNode.previousSibling || null;
|
|
|
|
// for efficiency, do a quick check to see if we've *ever*
|
|
// had children or if we are still using the prototype's
|
|
// empty object.
|
|
if (self.children !== UI.Component.children) {
|
|
Deps.nonreactive(function () {
|
|
// kill children from last render, and also any
|
|
// attached children
|
|
var children = self.children;
|
|
for (var k in children) {
|
|
var child = children[k];
|
|
if (builtChildren && builtChildren[k]) {
|
|
// destroy first, then remove
|
|
// (which doesn't affect DOM, which we will
|
|
// remove all at once)
|
|
child.destroy();
|
|
self.remove(child);
|
|
} else if (child.isAttached) {
|
|
// detach the child; we don't have a good way
|
|
// of keeping this from affecting the DOM
|
|
child.detach();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
var oldNodes = [];
|
|
// must be careful as call to `detach` above may have
|
|
// must with firstNode or lastNode
|
|
for (var n = prevNode ? prevNode.nextSibling :
|
|
parentNode.firstChild;
|
|
n && n !== nextNode;
|
|
n = n.nextSibling)
|
|
oldNodes.push(n);
|
|
|
|
$(oldNodes).remove();
|
|
|
|
var div = makeSafeDiv();
|
|
// set `self.start` to null so that calls to `attach` from
|
|
// `_populate` don't try to do start/end pointer logic.
|
|
self.start = self.end = null;
|
|
var info = self._populate(div);
|
|
if (! div.firstChild)
|
|
div.appendChild(createEmptyComment());
|
|
|
|
self.start = info.start || div.firstChild;
|
|
self.end = info.end || div.lastChild;
|
|
insertNodesBefore(div.childNodes, parentNode, nextNode);
|
|
},
|
|
|
|
// Internal method used by insertBefore, render buffer,
|
|
// and attachRoot.
|
|
_attach: function (parentNode, beforeNode) {
|
|
var self = this;
|
|
|
|
self._requireNotDestroyed();
|
|
|
|
if (! self.isInited)
|
|
throw new Error("Component to attach must be inited");
|
|
|
|
if (! self.isBuilt)
|
|
self.build();
|
|
|
|
if (self.isAttached)
|
|
throw new Error("Component already attached; must be detached first");
|
|
|
|
if ((! parentNode) || ! parentNode.nodeType)
|
|
throw new Error("first argument to attach must be a Node");
|
|
if (beforeNode && ! beforeNode.nodeType)
|
|
throw new Error("second argument to attach must be a Node" +
|
|
" if given");
|
|
|
|
insertNodesBefore(self._offscreen.childNodes,
|
|
parentNode, beforeNode);
|
|
|
|
self._offscreen = null;
|
|
self.isAttached = true;
|
|
|
|
var parent = self.parent;
|
|
// We could be a root (and have no parent). Parent could
|
|
// theoretically be destroyed, or not yet built (if we
|
|
// are currently building).
|
|
//
|
|
// We use a falsy `parent.start` as a cue that this is a
|
|
// rebuild, another case where we skip the start/end adjustment
|
|
// logic.
|
|
//
|
|
// `attach` is special in that it is used during building
|
|
// and rebuilding; it is not required that the parent is
|
|
// completely built.
|
|
if (parent && parent.isBuilt && ! parent.isDestroyed &&
|
|
parent.start) {
|
|
if (parent.isEmpty()) {
|
|
var comment = parent.start;
|
|
parent.start = parent.end = self;
|
|
comment.parentNode.removeChild(comment);
|
|
} else {
|
|
if (parent.firstNode() === self.lastNode().nextSibling)
|
|
parent.start = self;
|
|
if (parent.lastNode() === self.firstNode().previousSibling)
|
|
parent.end = self;
|
|
}
|
|
}
|
|
|
|
callChainedCallback(self, 'attached');
|
|
},
|
|
|
|
isEmpty: function () {
|
|
this._requireBuilt();
|
|
this._requireNotDestroyed();
|
|
|
|
var start = this.start;
|
|
return start === this.end &&
|
|
! UI.isComponent(start) && isEmptyComment(start);
|
|
},
|
|
|
|
// # component.detach()
|
|
//
|
|
// Component must be built and attached. Removes this component's
|
|
// DOM and puts it into an offscreen storage. Updates the parent's
|
|
// `start` and `end` and populates it with a comment if it becomes
|
|
// empty.
|
|
detach: function (_forDestruction) {
|
|
var self = this;
|
|
|
|
self._requireBuilt();
|
|
self._requireNotDestroyed();
|
|
if (! self.isAttached)
|
|
throw new Error("Component not attached");
|
|
|
|
var parent = self.parent;
|
|
var A = self.firstNode();
|
|
var B = self.lastNode();
|
|
|
|
// We could be a root (and have no parent). Parent could
|
|
// theoretically be destroyed, or not yet built.
|
|
if (parent && parent.isBuilt && ! parent.isDestroyed) {
|
|
// Do some magic to update the
|
|
// firstNode and lastNode. The main issue is we need to
|
|
// know if the new firstNode or lastNode is part of a
|
|
// child component or not, because if it is, we need to
|
|
// set `start` or `end` to the component rather than the
|
|
// node. Since we don't have any pointers from the DOM
|
|
// and can't make any assumptions about the structure of
|
|
// the component, we have to do a search over our children.
|
|
// Repeatedly detaching the first or last of O(N) top-level
|
|
// components is asymptotically bad -- O(n^2).
|
|
//
|
|
// Components that manage large numbers of top-level components
|
|
// should override _findStartComponent and _findEndComponent.
|
|
if (parent.start === self) {
|
|
if (parent.end === self) {
|
|
// we're emptying the parent; populate it with a
|
|
// comment in an appropriate place (adjacent to
|
|
// the not-yet-extracted DOM) and set pointers.
|
|
var comment = createEmptyComment();
|
|
A.parentNode.insertBefore(comment, A);
|
|
parent.start = parent.end = comment;
|
|
} else {
|
|
// Removing component at the beginning of parent.
|
|
//
|
|
// Figure out if the following top-level node is the
|
|
// first node of a Component.
|
|
var newFirstNode = B.nextSibling;
|
|
parent.start = parent._findStartComponent(newFirstNode);
|
|
if (! (parent.start && parent.start.firstNode() === newFirstNode))
|
|
parent.start = newFirstNode;
|
|
}
|
|
} else if (parent.end === self) {
|
|
// Removing component at the end of parent.
|
|
//
|
|
// Figure out if the previous top-level node is the
|
|
// last node of a Component.
|
|
var newLastNode = A.previousSibling;
|
|
parent.end = parent._findEndComponent(newLastNode);
|
|
if (! (parent.end && parent.end.lastNode() === newLastNode))
|
|
parent.end = newLastNode;
|
|
}
|
|
}
|
|
|
|
var nodes = [];
|
|
for (var n = A; n !== B; n = n.nextSibling)
|
|
nodes.push(n);
|
|
nodes.push(B);
|
|
|
|
if (_forDestruction === true) {
|
|
$(nodes).remove();
|
|
} else {
|
|
// Move nodes into an offscreen div, preserving
|
|
// any event handlers and data associated with the nodes.
|
|
var div = makeSafeDiv();
|
|
$(div).append(nodes);
|
|
|
|
self._offscreen = div;
|
|
self.isAttached = false;
|
|
|
|
callChainedCallback(self, 'detached');
|
|
}
|
|
},
|
|
|
|
// # component.append(childOrDom)
|
|
//
|
|
// childOrDom is a Component, or node, or HTML string,
|
|
// or array of elements (various things a la jQuery).
|
|
//
|
|
// Given `child`: It must be a child of this component or addable
|
|
// as one. Builds it if necessary. Attaches it at the end of
|
|
// this component. Updates `start` and `end` of this component.
|
|
|
|
append: function (childOrDom) {
|
|
this.insertAfter(childOrDom, this.lastNode());
|
|
},
|
|
|
|
prepend: function (childOrDom) {
|
|
this.insertBefore(childOrDom, this.firstNode());
|
|
},
|
|
|
|
// # component.insertBefore(childOrDom, before, parentNode)
|
|
//
|
|
// `before` is a Component or node. parentNode is only used
|
|
// if `before` is null. It defaults to the Component's
|
|
// parentNode.
|
|
//
|
|
// See append.
|
|
|
|
insertBefore: function (childOrDom, before, parentNode) {
|
|
var self = this;
|
|
|
|
self._requireBuilt();
|
|
self._requireNotDestroyed();
|
|
|
|
if (UI.isComponent(before)) {
|
|
before = before.firstNode();
|
|
} else if (! before) {
|
|
if ((! parentNode) || (parentNode === self.parentNode())) {
|
|
before = self.lastNode().nextSibling;
|
|
parentNode = parentNode || self.parentNode();
|
|
}
|
|
}
|
|
parentNode = parentNode || before.parentNode;
|
|
|
|
if (UI.isComponent(childOrDom)) {
|
|
var child = childOrDom;
|
|
|
|
child._requireNotDestroyed();
|
|
|
|
if (! child.isInited) {
|
|
self.add(child);
|
|
} else if (child.parent !== self) {
|
|
throw new Error("Can only append/prepend/insert" +
|
|
" a child (or a component addable as one)");
|
|
}
|
|
|
|
child._attach(parentNode, before);
|
|
} else {
|
|
var nodes;
|
|
if (typeof childOrDom === 'string') {
|
|
nodes = $.parseHTML(childOrDom) || [];
|
|
} else if (childOrDom.nodeType) {
|
|
nodes = [childOrDom];
|
|
} else if (typeof childOrDom.length === 'number' &&
|
|
typeof childOrDom === 'object') {
|
|
nodes = Array.prototype.slice.call(childOrDom);
|
|
} else {
|
|
throw new Error(
|
|
"Expected HTML, DOM node, array, or Component, found " +
|
|
childOrDom);
|
|
}
|
|
|
|
if (nodes.length) {
|
|
insertNodesBefore(nodes, parentNode, before);
|
|
|
|
if (self.isEmpty()) {
|
|
var comment = self.start;
|
|
comment.parentNode.removeChild(comment);
|
|
self.start = nodes[0];
|
|
self.end = nodes[nodes.length - 1];
|
|
} else if (before === self.firstNode()) {
|
|
self.start = nodes[0];
|
|
} else if (nodes[0].previousSibling === self.lastNode()) {
|
|
self.end = nodes[nodes.length - 1];
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
insertAfter: function (childOrDom, after, parentNode) {
|
|
var self = this;
|
|
|
|
if (UI.isComponent(after)) {
|
|
after = after.lastNode();
|
|
} else if (! after) {
|
|
if ((! parentNode) || (parentNode === self.parentNode())) {
|
|
after = self.firstNode().previousSibling;
|
|
parentNode = parentNode || self.parentNode();
|
|
}
|
|
}
|
|
parentNode = parentNode || after.parentNode;
|
|
|
|
this.insertBefore(childOrDom, after.nextSibling, parentNode);
|
|
},
|
|
|
|
containsElement: function (elem) {
|
|
if (elem.nodeType !== 1)
|
|
throw new Error("containsElement requires an Element node");
|
|
|
|
var self = this;
|
|
self._requireBuilt();
|
|
self._requireNotDestroyed();
|
|
|
|
var firstNode = self.firstNode();
|
|
var prevNode = firstNode.previousSibling;
|
|
var nextNode = self.lastNode().nextSibling;
|
|
|
|
// because we can only do comparisons on elements, find
|
|
// some elements.
|
|
while (prevNode && prevNode.nodeType !== 1)
|
|
prevNode = prevNode.previousSibling;
|
|
while (nextNode && nextNode.nodeType !== 1)
|
|
nextNode = nextNode.nextSibling;
|
|
|
|
if (! elementContains(firstNode.parentNode, elem))
|
|
return false;
|
|
// element must not be "at or before" prevNode
|
|
if (prevNode && compareElementIndex(prevNode, elem) >= 0)
|
|
return false;
|
|
// element must not be "at or after" nextNode
|
|
if (nextNode && compareElementIndex(elem, nextNode) >= 0)
|
|
return false;
|
|
return true;
|
|
},
|
|
|
|
// Take element `elem` and find the innermost component containing
|
|
// it which is either this component or a descendent of this component.
|
|
findByElement: function (elem) {
|
|
if (elem.nodeType !== 1)
|
|
throw new Error("findByElement requires an Element node");
|
|
|
|
var self = this;
|
|
self._requireBuilt();
|
|
|
|
if (! self.containsElement(elem))
|
|
return null;
|
|
|
|
var children = self.children;
|
|
// XXX linear-time scan through all child components,
|
|
// running DOM comparison methods that may themselves
|
|
// be O(N). Not sure what the constants are like.
|
|
for (var k in children) {
|
|
var child = children[k];
|
|
if (child.isBuilt && (! child.isDestroyed) &&
|
|
child.isAttached) {
|
|
var found = child.findByElement(elem);
|
|
if (found)
|
|
return found;
|
|
}
|
|
}
|
|
|
|
return self;
|
|
},
|
|
|
|
$: function (selector) {
|
|
var self = this;
|
|
|
|
self._requireBuilt();
|
|
self._requireNotDestroyed();
|
|
|
|
var firstNode = self.firstNode();
|
|
var parentNode = firstNode.parentNode;
|
|
var prevNode = firstNode.previousSibling;
|
|
var nextNode = self.lastNode().nextSibling;
|
|
|
|
// Don't assume `results` has jQuery API; a plain array
|
|
// should do just as well. However, if we do have a jQuery
|
|
// array, we want to end up with one also.
|
|
var results = $(selector, self.parentNode());
|
|
|
|
// Function that selects only elements that are actually in this
|
|
// Component, out of elements that are descendents of the Component's
|
|
// parentNode in the DOM (but may be, or descend from, siblings of
|
|
// this Component's top-level nodes that aren't between `start` and
|
|
// `end` inclusive).
|
|
var filterFunc = function (elem) {
|
|
// handle jQuery's arguments to filter, where the node
|
|
// is in `this` and the index is the first argument.
|
|
if (typeof elem === 'number')
|
|
elem = this;
|
|
|
|
if (prevNode && compareElementIndex(prevNode, elem) >= 0)
|
|
return false;
|
|
if (nextNode && compareElementIndex(elem, nextNode) >= 0)
|
|
return false;
|
|
return true;
|
|
};
|
|
|
|
if (! results.filter) {
|
|
// not a jQuery array, and not a browser with
|
|
// Array.prototype.filter (e.g. IE <9)
|
|
var newResults = [];
|
|
for (var i = 0; i < results.length; i++) {
|
|
var x = results[i];
|
|
if (filterFunc(x))
|
|
newResults.push(x);
|
|
}
|
|
results = newResults;
|
|
} else {
|
|
// `results.filter` is either jQuery's or ECMAScript's `filter`
|
|
results = results.filter(filterFunc);
|
|
}
|
|
|
|
return results;
|
|
},
|
|
|
|
autorun: function (compFunc) {
|
|
var self = this;
|
|
|
|
self._requireNotDestroyed();
|
|
|
|
// XXX so many nested functions... Deps.nonreactive here
|
|
// feels heavyweight, but we don't want building a child
|
|
// while building a parent to mean that when the parent
|
|
// rebuilds, the child automatically does.
|
|
var c = Deps.nonreactive(function () {
|
|
return Deps.autorun(compFunc);
|
|
});
|
|
|
|
self._computations = self._computations || [];
|
|
self._computations.push(c);
|
|
|
|
return c;
|
|
},
|
|
|
|
replaceChild: function (oldChild, newChild) {
|
|
var self = this;
|
|
|
|
self._requireBuilt();
|
|
self._requireNotDestroyed();
|
|
oldChild._requireBuilt();
|
|
oldChild._requireNotDestroyed();
|
|
if (! oldChild.isAttached)
|
|
throw new Error("Child to replace must be attached");
|
|
|
|
var lastNode = oldChild.lastNode();
|
|
var parentNode = lastNode.parentNode;
|
|
var nextNode = lastNode.nextSibling;
|
|
|
|
oldChild.remove();
|
|
self.insertBefore(newChild, nextNode, parentNode);
|
|
},
|
|
|
|
swapChild: function (oldChild, newChild) {
|
|
var self = this;
|
|
|
|
self._requireBuilt();
|
|
self._requireNotDestroyed();
|
|
oldChild._requireBuilt();
|
|
oldChild._requireNotDestroyed();
|
|
if (! oldChild.isAttached)
|
|
throw new Error("Child to swap out must be attached");
|
|
|
|
var lastNode = oldChild.lastNode();
|
|
var parentNode = lastNode.parentNode;
|
|
var nextNode = lastNode.nextSibling;
|
|
|
|
oldChild.detach();
|
|
self.insertBefore(newChild, nextNode, parentNode);
|
|
},
|
|
|
|
_onNextBuilt: function (cb) {
|
|
var self = this;
|
|
var cbs = self._builtCallbacks;
|
|
if (! cbs)
|
|
cbs = self._builtCallbacks = [];
|
|
cbs.push(cb);
|
|
},
|
|
|
|
_callOnNextBuiltCallbacks: function () {
|
|
var self = this;
|
|
var cbs = self._builtCallbacks;
|
|
if (cbs) {
|
|
for (var i = 0, N = cbs.length; i < N; i++)
|
|
cbs[i](self);
|
|
self._builtCallbacks.length = 0;
|
|
}
|
|
},
|
|
|
|
// Return a child whose firstNode() may be `firstNode`.
|
|
// If such a child exists, it must be found by this function.
|
|
// If no such child exists, this function may return null
|
|
// or a wrong guess at a child. Subclasses that know,
|
|
// for example, the earliest child component in the DOM
|
|
// at all times can supply that as a guess.
|
|
_findStartComponent: function (firstNode) {
|
|
var children = this.children;
|
|
// linear-time scan until found
|
|
for (var k in children) {
|
|
var c = children[k];
|
|
if (c.isBuilt && c.isAttached &&
|
|
c.firstNode() === firstNode)
|
|
return c;
|
|
}
|
|
return null;
|
|
},
|
|
|
|
_findEndComponent: function (lastNode) {
|
|
var children = this.children;
|
|
// linear-time scan until found
|
|
for (var k in children) {
|
|
var c = children[k];
|
|
if (c.isBuilt && c.isAttached &&
|
|
c.lastNode() === lastNode)
|
|
return c;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// If Component is ever emptied, it gets an empty comment node.
|
|
// This case is treated specially and the comment is removed
|
|
// if you then, say, append a node or component. However,
|
|
// the developer doing advanced things needs to be aware of
|
|
// this case or they may be surprised there is a node there
|
|
// that they didn't put there, e.g. if they call remove() on
|
|
// the last component and then start inserting DOM nodes
|
|
// manually.
|
|
|
|
// You are free to manipulate the DOM of your component, excluding
|
|
// the regions that belong to child components, though if you do it
|
|
// using jQuery or any other means besides the methods here
|
|
// (attach, detach, append, prepend, insert), you are responsible
|
|
// for ensuring that `start` and `end` point to the first and last
|
|
// *node or Component* at the top level of the component's DOM,
|
|
// and that the component does not become empty
|
|
|
|
});
|
|
|
|
|
|
var emptyCommentProp = 'meteor-ui-empty';
|
|
var createEmptyComment = function (beforeNode) {
|
|
var x = document.createComment("empty");
|
|
x[emptyCommentProp] = true;
|
|
return x;
|
|
};
|
|
var isEmptyComment = function (node) {
|
|
return node.nodeType === 8 && node[emptyCommentProp] === true;
|
|
};
|
|
|
|
// Returns 0 if the nodes are the same or either one contains the other;
|
|
// otherwise, -1 if a comes before b, or else 1 if b comes before a in
|
|
// document order.
|
|
// Requires: `a` and `b` are element nodes in the same document tree.
|
|
var compareElementIndex = function (a, b) {
|
|
// See http://ejohn.org/blog/comparing-document-position/
|
|
if (a === b)
|
|
return 0;
|
|
if (a.compareDocumentPosition) {
|
|
var n = a.compareDocumentPosition(b);
|
|
return ((n & 0x18) ? 0 : ((n & 0x4) ? -1 : 1));
|
|
} else {
|
|
// Only old IE is known to not have compareDocumentPosition (though Safari
|
|
// originally lacked it). Thankfully, IE gives us a way of comparing elements
|
|
// via the "sourceIndex" property.
|
|
if (a.contains(b) || b.contains(a))
|
|
return 0;
|
|
return (a.sourceIndex < b.sourceIndex ? -1 : 1);
|
|
}
|
|
};
|
|
|
|
// Returns true if element a contains node b and is not node b.
|
|
var elementContains = function (a, b) {
|
|
if (a.nodeType !== 1) /* ELEMENT */
|
|
return false;
|
|
if (a === b)
|
|
return false;
|
|
|
|
if (a.compareDocumentPosition) {
|
|
return a.compareDocumentPosition(b) & 0x10;
|
|
} else {
|
|
// Should be only old IE and maybe other old browsers here.
|
|
// Modern Safari has both functions but seems to get contains() wrong.
|
|
// IE can't handle b being a text node. We work around this
|
|
// by doing a direct parent test now.
|
|
b = b.parentNode;
|
|
if (! (b && b.nodeType === 1)) /* ELEMENT */
|
|
return false;
|
|
if (a === b)
|
|
return true;
|
|
|
|
return a.contains(b);
|
|
}
|
|
};
|
|
|
|
var insertNodesBefore = function (nodes, parentNode, beforeNode) {
|
|
if (beforeNode) {
|
|
$(nodes).insertBefore(beforeNode);
|
|
} else {
|
|
$(nodes).appendTo(parentNode);
|
|
}
|
|
};
|
|
|
|
var makeSafeDiv = function () {
|
|
// create a DIV in a DocumentFragment, where the DocumentFragment
|
|
// is created by jQuery, which uses tricks to create a "safe"
|
|
// fragment for HTML5 tags in IE <9.
|
|
var div = document.createElement("DIV");
|
|
var frag = $.buildFragment([div], document);
|
|
return div;
|
|
};
|
|
|
|
UI.body = UI.Component.extend({
|
|
typeName: 'body',
|
|
contentParts: [],
|
|
render: function (buf) {
|
|
for (var i = 0; i < this.contentParts.length; i++)
|
|
buf.write(this.contentParts[i]);
|
|
}
|
|
});
|