mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
413 lines
13 KiB
JavaScript
413 lines
13 KiB
JavaScript
UI = {};
|
|
|
|
// 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;
|
|
};
|
|
|
|
// Defines a single non-enumerable, read-only property
|
|
// on `tgt`.
|
|
// It won't be non-enumerable in IE 8, so its
|
|
// non-enumerability can't be relied on for logic
|
|
// purposes, it just makes things prettier in
|
|
// the dev console.
|
|
var _defineNonEnum = function (tgt, name, value) {
|
|
try {
|
|
Object.defineProperty(tgt, name, {value: value});
|
|
} catch (e) {
|
|
// IE < 9
|
|
tgt[name] = value;
|
|
}
|
|
return tgt;
|
|
};
|
|
|
|
// Named function (like `function Component() {}` below) make
|
|
// inspection in debuggers more descriptive. In IE, this sets the
|
|
// value of the `Component` var in the function scope in which it's
|
|
// executed. We already have a top-level `Component` var so we create
|
|
// a new function scope to not write it over in IE.
|
|
(function () {
|
|
|
|
// Components and Component kinds 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).
|
|
UI.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.
|
|
var C = new constr;
|
|
_defineNonEnum(C, '_constr', constr);
|
|
_defineNonEnum(C, '_super', null);
|
|
return C;
|
|
})(function Component() {});
|
|
})();
|
|
|
|
_extend(UI, {
|
|
nextGuid: 2, // Component is 1!
|
|
|
|
isComponent: function (obj) {
|
|
return obj && UI.isKindOf(obj, UI.Component);
|
|
},
|
|
// `UI.isKindOf(a, b)` where `a` and `b` are Components
|
|
// (or kinds) asks if `a` is or descends from
|
|
// (transitively extends) `b`.
|
|
isKindOf: function (a, b) {
|
|
while (a) {
|
|
if (a === b)
|
|
return true;
|
|
a = a._super;
|
|
}
|
|
return false;
|
|
},
|
|
// use these to produce error messages for developers
|
|
// (though throwing a more specific error message is
|
|
// even better)
|
|
_requireNotDestroyed: function (c) {
|
|
if (c.isDestroyed)
|
|
throw new Error("Component has been destroyed; can't perform this operation");
|
|
},
|
|
_requireInited: function (c) {
|
|
if (! c.isInited)
|
|
throw new Error("Component must be inited to perform this operation");
|
|
},
|
|
_requireDom: function (c) {
|
|
if (! c.dom)
|
|
throw new Error("Component must be built into DOM to perform this operation");
|
|
}
|
|
});
|
|
|
|
Component = UI.Component;
|
|
|
|
_extend(UI.Component, {
|
|
kind: "Component",
|
|
guid: "1",
|
|
dom: null,
|
|
// Has this Component ever been inited?
|
|
isInited: false,
|
|
// Has this Component been destroyed? Only inited Components
|
|
// can be destroyed.
|
|
isDestroyed: false,
|
|
// Component that created this component (typically also
|
|
// the DOM containment parent).
|
|
// No child pointers (except in `dom`).
|
|
parent: null,
|
|
|
|
// create a new subkind or instance whose proto pointer
|
|
// points to this, with additional props set.
|
|
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;
|
|
if (props && props.kind) {
|
|
// If `kind` is different from super, set a constructor.
|
|
// We used to set the function name here so that components
|
|
// printed better in the console, but we took it out because
|
|
// of CSP (and in hopes that Chrome finally adds proper
|
|
// displayName support).
|
|
constr = function () {};
|
|
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.
|
|
_defineNonEnum(c, '_super', this);
|
|
c.guid = String(UI.nextGuid++);
|
|
|
|
return c;
|
|
}
|
|
});
|
|
|
|
//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);
|
|
//};
|
|
|
|
|
|
// 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);
|
|
}
|
|
};
|
|
|
|
findComponentWithProp = function (id, comp) {
|
|
while (comp) {
|
|
if (typeof comp[id] !== 'undefined')
|
|
return comp;
|
|
comp = comp.parent;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// Look up the component's chain of parents until we find one with
|
|
// `__helperHost` set (a component that can have helpers defined on it,
|
|
// i.e. a template).
|
|
var findHelperHostComponent = function (comp) {
|
|
while (comp) {
|
|
if (comp.__helperHost) {
|
|
return comp;
|
|
}
|
|
comp = comp.parent;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
findComponentWithHelper = function (id, comp) {
|
|
while (comp) {
|
|
if (comp.__helperHost) {
|
|
if (typeof comp[id] !== 'undefined')
|
|
return comp;
|
|
else
|
|
return null;
|
|
}
|
|
comp = comp.parent;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
getComponentData = function (comp) {
|
|
comp = findComponentWithProp('data', comp);
|
|
return (comp ?
|
|
(typeof comp.data === 'function' ?
|
|
comp.data() : comp.data) :
|
|
null);
|
|
};
|
|
|
|
updateTemplateInstance = function (comp) {
|
|
// Populate `comp.templateInstance.{firstNode,lastNode,data}`
|
|
// on demand.
|
|
var tmpl = comp.templateInstance;
|
|
tmpl.data = getComponentData(comp);
|
|
|
|
if (comp.dom && !comp.isDestroyed) {
|
|
tmpl.firstNode = comp.dom.startNode().nextSibling;
|
|
tmpl.lastNode = comp.dom.endNode().previousSibling;
|
|
// Catch the case where the DomRange is empty and we'd
|
|
// otherwise pass the out-of-order nodes (end, start)
|
|
// as (firstNode, lastNode).
|
|
if (tmpl.lastNode && tmpl.lastNode.nextSibling === tmpl.firstNode)
|
|
tmpl.lastNode = tmpl.firstNode;
|
|
} else {
|
|
// on 'created' or 'destroyed' callbacks we don't have a DomRange
|
|
tmpl.firstNode = null;
|
|
tmpl.lastNode = null;
|
|
}
|
|
};
|
|
|
|
_extend(UI.Component, {
|
|
// We implement the old APIs here, including how data is passed
|
|
// to helpers in `this`.
|
|
helpers: function (dict) {
|
|
_extend(this, dict);
|
|
},
|
|
events: function (dict) {
|
|
var events;
|
|
if (this.hasOwnProperty('_events'))
|
|
events = this._events;
|
|
else
|
|
events = (this._events = []);
|
|
|
|
_.each(dict, function (handler, spec) {
|
|
var clauses = spec.split(/,\s+/);
|
|
// iterate over clauses of spec, e.g. ['click .foo', 'click .bar']
|
|
_.each(clauses, function (clause) {
|
|
var parts = clause.split(/\s+/);
|
|
if (parts.length === 0)
|
|
return;
|
|
|
|
var newEvents = parts.shift();
|
|
var selector = parts.join(' ');
|
|
events.push({events: newEvents,
|
|
selector: selector,
|
|
handler: handler});
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
// XXX we don't really want this to be a user-visible callback,
|
|
// it's just a particular signal we need from DomRange.
|
|
UI.Component.notifyParented = function () {
|
|
var self = this;
|
|
for (var comp = self; comp; comp = comp._super) {
|
|
var events = (comp.hasOwnProperty('_events') && comp._events) || null;
|
|
if ((! events) && comp.hasOwnProperty('events') &&
|
|
typeof comp.events === 'object') {
|
|
// Provide limited back-compat support for `.events = {...}`
|
|
// syntax. Pass `comp.events` to the original `.events(...)`
|
|
// function. This code must run only once per component, in
|
|
// order to not bind the handlers more than once, which is
|
|
// ensured by the fact that we only do this when `comp._events`
|
|
// is falsy, and we cause it to be set now.
|
|
UI.Component.events.call(comp, comp.events);
|
|
events = comp._events;
|
|
}
|
|
_.each(events, function (esh) { // {events, selector, handler}
|
|
// wrap the handler here, per instance of the template that
|
|
// declares the event map, so we can pass the instance to
|
|
// the event handler.
|
|
var wrappedHandler = function (event) {
|
|
var comp = UI.DomRange.getContainingComponent(event.currentTarget);
|
|
var data = comp && getComponentData(comp);
|
|
var args = _.toArray(arguments);
|
|
updateTemplateInstance(self);
|
|
return Deps.nonreactive(function () {
|
|
// put self.templateInstance as the second argument
|
|
args.splice(1, 0, self.templateInstance);
|
|
// Don't want to be in a deps context, even if we were somehow
|
|
// triggered synchronously in an existing deps context
|
|
// (the `blur` event can do this).
|
|
// XXX we should probably do what Spark did and block all
|
|
// event handling during our DOM manip. Many apps had weird
|
|
// unanticipated bugs until we did that.
|
|
return esh.handler.apply(data === null ? {} : data, args);
|
|
});
|
|
};
|
|
|
|
self.dom.on(esh.events, esh.selector, wrappedHandler);
|
|
});
|
|
}
|
|
|
|
if (self.rendered) {
|
|
// Defer rendered callback until flush time.
|
|
Deps.afterFlush(function () {
|
|
if (! self.isDestroyed) {
|
|
updateTemplateInstance(self);
|
|
self.rendered.call(self.templateInstance);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// past compat
|
|
UI.Component.preserve = function () {
|
|
Meteor._debug("The 'preserve' method on templates is now unnecessary and deprecated.");
|
|
};
|
|
|
|
// Gets the data context of the enclosing component that rendered a
|
|
// given element
|
|
UI.getElementData = function (el) {
|
|
var comp = UI.DomRange.getContainingComponent(el);
|
|
return comp && getComponentData(comp);
|
|
};
|
|
|
|
var jsUrlsAllowed = false;
|
|
UI._allowJavascriptUrls = function () {
|
|
jsUrlsAllowed = true;
|
|
};
|
|
UI._javascriptUrlsAllowed = function () {
|
|
return jsUrlsAllowed;
|
|
};
|
|
|
|
UI._templateInstance = function () {
|
|
var currentComp = currentComponent.get();
|
|
if (! currentComp) {
|
|
throw new Error("You can only call UI._templateInstance() from within" +
|
|
" a helper function.");
|
|
}
|
|
|
|
// Find the enclosing component that is a template. (`currentComp`
|
|
// could be, for example, an #if or #with, and we want the component
|
|
// that is the surrounding template.)
|
|
var template = findHelperHostComponent(currentComp);
|
|
if (! template) {
|
|
throw new Error("Current component is not inside a template?");
|
|
}
|
|
|
|
// Lazily update the template instance for this helper, and do it only
|
|
// once.
|
|
if (! currentTemplateInstance) {
|
|
updateTemplateInstance(template);
|
|
currentTemplateInstance = template.templateInstance;
|
|
}
|
|
return currentTemplateInstance;
|
|
};
|
|
|
|
// Returns the data context of the parent which is 'numLevels' above the
|
|
// component. Same behavior as {{../..}} in a template, with 'numLevels'
|
|
// occurrences of '..'.
|
|
UI._parentData = function (numLevels) {
|
|
var component = currentComponent.get();
|
|
while (component && numLevels >= 0) {
|
|
// Decrement numLevels every time we find a new data context. Break
|
|
// once we have reached numLevels < 0.
|
|
if (component.data !== undefined && --numLevels < 0) {
|
|
break;
|
|
}
|
|
component = component.parent;
|
|
}
|
|
|
|
if (! component) {
|
|
return null;
|
|
}
|
|
|
|
return getComponentData(component);
|
|
};
|