Files
2025-02-26 14:49:25 +07:00

3895 lines
130 KiB
JavaScript

//////////////////////////////////////////////////////////////////////////
// //
// This is a generated file. You can view the original //
// source in your browser if your browser supports source maps. //
// Source maps are supported by all recent versions of Chrome, Safari, //
// and Firefox, and by Internet Explorer 11. //
// //
//////////////////////////////////////////////////////////////////////////
(function () {
/* Imports */
var Meteor = Package.meteor.Meteor;
var global = Package.meteor.global;
var meteorEnv = Package.meteor.meteorEnv;
var $ = Package.jquery.$;
var jQuery = Package.jquery.jQuery;
var Tracker = Package.tracker.Tracker;
var Deps = Package.tracker.Deps;
var check = Package.check.check;
var Match = Package.check.Match;
var _ = Package.underscore._;
var ObserveSequence = Package['observe-sequence'].ObserveSequence;
var ReactiveVar = Package['reactive-var'].ReactiveVar;
var OrderedDict = Package['ordered-dict'].OrderedDict;
var HTML = Package.htmljs.HTML;
/* Package-scope variables */
var Blaze, AttributeHandler, ElementAttributesUpdater, UI, Handlebars;
(function(){
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/blaze/preamble.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
/**
* @namespace Blaze
* @summary The namespace for all Blaze-related methods and classes.
*/
Blaze = {};
// Utility to HTML-escape a string. Included for legacy reasons.
// TODO: Should be replaced with _.escape once underscore is upgraded to a newer
// version which escapes ` (backtick) as well. Underscore 1.5.2 does not.
Blaze._escape = (function() {
var escape_map = {
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#x27;",
"/": "&#x2F;",
"`": "&#x60;", /* IE allows backtick-delimited attributes?? */
"&": "&amp;"
};
var escape_one = function(c) {
return escape_map[c];
};
return function (x) {
return x.replace(/[&<>"'`]/g, escape_one);
};
})();
Blaze._warn = function (msg) {
msg = 'Warning: ' + msg;
if ((typeof console !== 'undefined') && console.warn) {
console.warn(msg);
}
};
var nativeBind = Function.prototype.bind;
// An implementation of _.bind which allows better optimization.
// See: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments
if (nativeBind) {
Blaze._bind = function (func, obj) {
if (arguments.length === 2) {
return nativeBind.call(func, obj);
}
// Copy the arguments so this function can be optimized.
var args = new Array(arguments.length);
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
return nativeBind.apply(func, args.slice(1));
};
}
else {
// A slower but backwards compatible version.
Blaze._bind = _.bind;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/blaze/dombackend.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
var DOMBackend = {};
Blaze._DOMBackend = DOMBackend;
var $jq = (typeof jQuery !== 'undefined' ? jQuery :
(typeof Package !== 'undefined' ?
Package.jquery && Package.jquery.jQuery : null));
if (! $jq)
throw new Error("jQuery not found");
DOMBackend._$jq = $jq;
DOMBackend.parseHTML = function (html) {
// Return an array of nodes.
//
// jQuery does fancy stuff like creating an appropriate
// container element and setting innerHTML on it, as well
// as working around various IE quirks.
return $jq.parseHTML(html) || [];
};
DOMBackend.Events = {
// `selector` is non-null. `type` is one type (but
// may be in backend-specific form, e.g. have namespaces).
// Order fired must be order bound.
delegateEvents: function (elem, type, selector, handler) {
$jq(elem).on(type, selector, handler);
},
undelegateEvents: function (elem, type, handler) {
$jq(elem).off(type, '**', handler);
},
bindEventCapturer: function (elem, type, selector, handler) {
var $elem = $jq(elem);
var wrapper = function (event) {
event = $jq.event.fix(event);
event.currentTarget = event.target;
// Note: It might improve jQuery interop if we called into jQuery
// here somehow. Since we don't use jQuery to dispatch the event,
// we don't fire any of jQuery's event hooks or anything. However,
// since jQuery can't bind capturing handlers, it's not clear
// where we would hook in. Internal jQuery functions like `dispatch`
// are too high-level.
var $target = $jq(event.currentTarget);
if ($target.is($elem.find(selector)))
handler.call(elem, event);
};
handler._meteorui_wrapper = wrapper;
type = DOMBackend.Events.parseEventType(type);
// add *capturing* event listener
elem.addEventListener(type, wrapper, true);
},
unbindEventCapturer: function (elem, type, handler) {
type = DOMBackend.Events.parseEventType(type);
elem.removeEventListener(type, handler._meteorui_wrapper, true);
},
parseEventType: function (type) {
// strip off namespaces
var dotLoc = type.indexOf('.');
if (dotLoc >= 0)
return type.slice(0, dotLoc);
return type;
}
};
///// Removal detection and interoperability.
// For an explanation of this technique, see:
// http://bugs.jquery.com/ticket/12213#comment:23 .
//
// In short, an element is considered "removed" when jQuery
// cleans up its *private* userdata on the element,
// which we can detect using a custom event with a teardown
// hook.
var NOOP = function () {};
// Circular doubly-linked list
var TeardownCallback = function (func) {
this.next = this;
this.prev = this;
this.func = func;
};
// Insert newElt before oldElt in the circular list
TeardownCallback.prototype.linkBefore = function(oldElt) {
this.prev = oldElt.prev;
this.next = oldElt;
oldElt.prev.next = this;
oldElt.prev = this;
};
TeardownCallback.prototype.unlink = function () {
this.prev.next = this.next;
this.next.prev = this.prev;
};
TeardownCallback.prototype.go = function () {
var func = this.func;
func && func();
};
TeardownCallback.prototype.stop = TeardownCallback.prototype.unlink;
DOMBackend.Teardown = {
_JQUERY_EVENT_NAME: 'blaze_teardown_watcher',
_CB_PROP: '$blaze_teardown_callbacks',
// Registers a callback function to be called when the given element or
// one of its ancestors is removed from the DOM via the backend library.
// The callback function is called at most once, and it receives the element
// in question as an argument.
onElementTeardown: function (elem, func) {
var elt = new TeardownCallback(func);
var propName = DOMBackend.Teardown._CB_PROP;
if (! elem[propName]) {
// create an empty node that is never unlinked
elem[propName] = new TeardownCallback;
// Set up the event, only the first time.
$jq(elem).on(DOMBackend.Teardown._JQUERY_EVENT_NAME, NOOP);
}
elt.linkBefore(elem[propName]);
return elt; // so caller can call stop()
},
// Recursively call all teardown hooks, in the backend and registered
// through DOMBackend.onElementTeardown.
tearDownElement: function (elem) {
var elems = [];
// Array.prototype.slice.call doesn't work when given a NodeList in
// IE8 ("JScript object expected").
var nodeList = elem.getElementsByTagName('*');
for (var i = 0; i < nodeList.length; i++) {
elems.push(nodeList[i]);
}
elems.push(elem);
$jq.cleanData(elems);
}
};
$jq.event.special[DOMBackend.Teardown._JQUERY_EVENT_NAME] = {
setup: function () {
// This "setup" callback is important even though it is empty!
// Without it, jQuery will call addEventListener, which is a
// performance hit, especially with Chrome's async stack trace
// feature enabled.
},
teardown: function() {
var elem = this;
var callbacks = elem[DOMBackend.Teardown._CB_PROP];
if (callbacks) {
var elt = callbacks.next;
while (elt !== callbacks) {
elt.go();
elt = elt.next;
}
callbacks.go();
elem[DOMBackend.Teardown._CB_PROP] = null;
}
}
};
// Must use jQuery semantics for `context`, not
// querySelectorAll's. In other words, all the parts
// of `selector` must be found under `context`.
DOMBackend.findBySelector = function (selector, context) {
return $jq(selector, context);
};
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/blaze/domrange.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// A constant empty array (frozen if the JS engine supports it).
var _emptyArray = Object.freeze ? Object.freeze([]) : [];
// `[new] Blaze._DOMRange([nodeAndRangeArray])`
//
// A DOMRange consists of an array of consecutive nodes and DOMRanges,
// which may be replaced at any time with a new array. If the DOMRange
// has been attached to the DOM at some location, then updating
// the array will cause the DOM to be updated at that location.
Blaze._DOMRange = function (nodeAndRangeArray) {
if (! (this instanceof DOMRange))
// called without `new`
return new DOMRange(nodeAndRangeArray);
var members = (nodeAndRangeArray || _emptyArray);
if (! (members && (typeof members.length) === 'number'))
throw new Error("Expected array");
for (var i = 0; i < members.length; i++)
this._memberIn(members[i]);
this.members = members;
this.emptyRangePlaceholder = null;
this.attached = false;
this.parentElement = null;
this.parentRange = null;
this.attachedCallbacks = _emptyArray;
};
var DOMRange = Blaze._DOMRange;
// In IE 8, don't use empty text nodes as placeholders
// in empty DOMRanges, use comment nodes instead. Using
// empty text nodes in modern browsers is great because
// it doesn't clutter the web inspector. In IE 8, however,
// it seems to lead in some roundabout way to the OAuth
// pop-up crashing the browser completely. In the past,
// we didn't use empty text nodes on IE 8 because they
// don't accept JS properties, so just use the same logic
// even though we don't need to set properties on the
// placeholder anymore.
DOMRange._USE_COMMENT_PLACEHOLDERS = (function () {
var result = false;
var textNode = document.createTextNode("");
try {
textNode.someProp = true;
} catch (e) {
// IE 8
result = true;
}
return result;
})();
// static methods
DOMRange._insert = function (rangeOrNode, parentElement, nextNode, _isMove) {
var m = rangeOrNode;
if (m instanceof DOMRange) {
m.attach(parentElement, nextNode, _isMove);
} else {
if (_isMove)
DOMRange._moveNodeWithHooks(m, parentElement, nextNode);
else
DOMRange._insertNodeWithHooks(m, parentElement, nextNode);
}
};
DOMRange._remove = function (rangeOrNode) {
var m = rangeOrNode;
if (m instanceof DOMRange) {
m.detach();
} else {
DOMRange._removeNodeWithHooks(m);
}
};
DOMRange._removeNodeWithHooks = function (n) {
if (! n.parentNode)
return;
if (n.nodeType === 1 &&
n.parentNode._uihooks && n.parentNode._uihooks.removeElement) {
n.parentNode._uihooks.removeElement(n);
} else {
n.parentNode.removeChild(n);
}
};
DOMRange._insertNodeWithHooks = function (n, parent, next) {
// `|| null` because IE throws an error if 'next' is undefined
next = next || null;
if (n.nodeType === 1 &&
parent._uihooks && parent._uihooks.insertElement) {
parent._uihooks.insertElement(n, next);
} else {
parent.insertBefore(n, next);
}
};
DOMRange._moveNodeWithHooks = function (n, parent, next) {
if (n.parentNode !== parent)
return;
// `|| null` because IE throws an error if 'next' is undefined
next = next || null;
if (n.nodeType === 1 &&
parent._uihooks && parent._uihooks.moveElement) {
parent._uihooks.moveElement(n, next);
} else {
parent.insertBefore(n, next);
}
};
DOMRange.forElement = function (elem) {
if (elem.nodeType !== 1)
throw new Error("Expected element, found: " + elem);
var range = null;
while (elem && ! range) {
range = (elem.$blaze_range || null);
if (! range)
elem = elem.parentNode;
}
return range;
};
DOMRange.prototype.attach = function (parentElement, nextNode, _isMove, _isReplace) {
// This method is called to insert the DOMRange into the DOM for
// the first time, but it's also used internally when
// updating the DOM.
//
// If _isMove is true, move this attached range to a different
// location under the same parentElement.
if (_isMove || _isReplace) {
if (! (this.parentElement === parentElement &&
this.attached))
throw new Error("Can only move or replace an attached DOMRange, and only under the same parent element");
}
var members = this.members;
if (members.length) {
this.emptyRangePlaceholder = null;
for (var i = 0; i < members.length; i++) {
DOMRange._insert(members[i], parentElement, nextNode, _isMove);
}
} else {
var placeholder = (
DOMRange._USE_COMMENT_PLACEHOLDERS ?
document.createComment("") :
document.createTextNode(""));
this.emptyRangePlaceholder = placeholder;
parentElement.insertBefore(placeholder, nextNode || null);
}
this.attached = true;
this.parentElement = parentElement;
if (! (_isMove || _isReplace)) {
for(var i = 0; i < this.attachedCallbacks.length; i++) {
var obj = this.attachedCallbacks[i];
obj.attached && obj.attached(this, parentElement);
}
}
};
DOMRange.prototype.setMembers = function (newNodeAndRangeArray) {
var newMembers = newNodeAndRangeArray;
if (! (newMembers && (typeof newMembers.length) === 'number'))
throw new Error("Expected array");
var oldMembers = this.members;
for (var i = 0; i < oldMembers.length; i++)
this._memberOut(oldMembers[i]);
for (var i = 0; i < newMembers.length; i++)
this._memberIn(newMembers[i]);
if (! this.attached) {
this.members = newMembers;
} else {
// don't do anything if we're going from empty to empty
if (newMembers.length || oldMembers.length) {
// detach the old members and insert the new members
var nextNode = this.lastNode().nextSibling;
var parentElement = this.parentElement;
// Use detach/attach, but don't fire attached/detached hooks
this.detach(true /*_isReplace*/);
this.members = newMembers;
this.attach(parentElement, nextNode, false, true /*_isReplace*/);
}
}
};
DOMRange.prototype.firstNode = function () {
if (! this.attached)
throw new Error("Must be attached");
if (! this.members.length)
return this.emptyRangePlaceholder;
var m = this.members[0];
return (m instanceof DOMRange) ? m.firstNode() : m;
};
DOMRange.prototype.lastNode = function () {
if (! this.attached)
throw new Error("Must be attached");
if (! this.members.length)
return this.emptyRangePlaceholder;
var m = this.members[this.members.length - 1];
return (m instanceof DOMRange) ? m.lastNode() : m;
};
DOMRange.prototype.detach = function (_isReplace) {
if (! this.attached)
throw new Error("Must be attached");
var oldParentElement = this.parentElement;
var members = this.members;
if (members.length) {
for (var i = 0; i < members.length; i++) {
DOMRange._remove(members[i]);
}
} else {
var placeholder = this.emptyRangePlaceholder;
this.parentElement.removeChild(placeholder);
this.emptyRangePlaceholder = null;
}
if (! _isReplace) {
this.attached = false;
this.parentElement = null;
for(var i = 0; i < this.attachedCallbacks.length; i++) {
var obj = this.attachedCallbacks[i];
obj.detached && obj.detached(this, oldParentElement);
}
}
};
DOMRange.prototype.addMember = function (newMember, atIndex, _isMove) {
var members = this.members;
if (! (atIndex >= 0 && atIndex <= members.length))
throw new Error("Bad index in range.addMember: " + atIndex);
if (! _isMove)
this._memberIn(newMember);
if (! this.attached) {
// currently detached; just updated members
members.splice(atIndex, 0, newMember);
} else if (members.length === 0) {
// empty; use the empty-to-nonempty handling of setMembers
this.setMembers([newMember]);
} else {
var nextNode;
if (atIndex === members.length) {
// insert at end
nextNode = this.lastNode().nextSibling;
} else {
var m = members[atIndex];
nextNode = (m instanceof DOMRange) ? m.firstNode() : m;
}
members.splice(atIndex, 0, newMember);
DOMRange._insert(newMember, this.parentElement, nextNode, _isMove);
}
};
DOMRange.prototype.removeMember = function (atIndex, _isMove) {
var members = this.members;
if (! (atIndex >= 0 && atIndex < members.length))
throw new Error("Bad index in range.removeMember: " + atIndex);
if (_isMove) {
members.splice(atIndex, 1);
} else {
var oldMember = members[atIndex];
this._memberOut(oldMember);
if (members.length === 1) {
// becoming empty; use the logic in setMembers
this.setMembers(_emptyArray);
} else {
members.splice(atIndex, 1);
if (this.attached)
DOMRange._remove(oldMember);
}
}
};
DOMRange.prototype.moveMember = function (oldIndex, newIndex) {
var member = this.members[oldIndex];
this.removeMember(oldIndex, true /*_isMove*/);
this.addMember(member, newIndex, true /*_isMove*/);
};
DOMRange.prototype.getMember = function (atIndex) {
var members = this.members;
if (! (atIndex >= 0 && atIndex < members.length))
throw new Error("Bad index in range.getMember: " + atIndex);
return this.members[atIndex];
};
DOMRange.prototype._memberIn = function (m) {
if (m instanceof DOMRange)
m.parentRange = this;
else if (m.nodeType === 1) // DOM Element
m.$blaze_range = this;
};
DOMRange._destroy = function (m, _skipNodes) {
if (m instanceof DOMRange) {
if (m.view)
Blaze._destroyView(m.view, _skipNodes);
} else if ((! _skipNodes) && m.nodeType === 1) {
// DOM Element
if (m.$blaze_range) {
Blaze._destroyNode(m);
m.$blaze_range = null;
}
}
};
DOMRange.prototype._memberOut = DOMRange._destroy;
// Tear down, but don't remove, the members. Used when chunks
// of DOM are being torn down or replaced.
DOMRange.prototype.destroyMembers = function (_skipNodes) {
var members = this.members;
for (var i = 0; i < members.length; i++)
this._memberOut(members[i], _skipNodes);
};
DOMRange.prototype.destroy = function (_skipNodes) {
DOMRange._destroy(this, _skipNodes);
};
DOMRange.prototype.containsElement = function (elem) {
if (! this.attached)
throw new Error("Must be attached");
// An element is contained in this DOMRange if it's possible to
// reach it by walking parent pointers, first through the DOM and
// then parentRange pointers. In other words, the element or some
// ancestor of it is at our level of the DOM (a child of our
// parentElement), and this element is one of our members or
// is a member of a descendant Range.
// First check that elem is a descendant of this.parentElement,
// according to the DOM.
if (! Blaze._elementContains(this.parentElement, elem))
return false;
// If elem is not an immediate child of this.parentElement,
// walk up to its ancestor that is.
while (elem.parentNode !== this.parentElement)
elem = elem.parentNode;
var range = elem.$blaze_range;
while (range && range !== this)
range = range.parentRange;
return range === this;
};
DOMRange.prototype.containsRange = function (range) {
if (! this.attached)
throw new Error("Must be attached");
if (! range.attached)
return false;
// A DOMRange is contained in this DOMRange if it's possible
// to reach this range by following parent pointers. If the
// DOMRange has the same parentElement, then it should be
// a member, or a member of a member etc. Otherwise, we must
// contain its parentElement.
if (range.parentElement !== this.parentElement)
return this.containsElement(range.parentElement);
if (range === this)
return false; // don't contain self
while (range && range !== this)
range = range.parentRange;
return range === this;
};
DOMRange.prototype.onAttached = function (attached) {
this.onAttachedDetached({ attached: attached });
};
// callbacks are `attached(range, element)` and
// `detached(range, element)`, and they may
// access the `callbacks` object in `this`.
// The arguments to `detached` are the same
// range and element that were passed to `attached`.
DOMRange.prototype.onAttachedDetached = function (callbacks) {
if (this.attachedCallbacks === _emptyArray)
this.attachedCallbacks = [];
this.attachedCallbacks.push(callbacks);
};
DOMRange.prototype.$ = function (selector) {
var self = this;
var parentNode = this.parentElement;
if (! parentNode)
throw new Error("Can't select in removed DomRange");
// Strategy: Find all selector matches under parentNode,
// then filter out the ones that aren't in this DomRange
// using `DOMRange#containsElement`. This is
// asymptotically slow in the presence of O(N) sibling
// content that is under parentNode but not in our range,
// so if performance is an issue, the selector should be
// run on a child element.
// Since jQuery can't run selectors on a DocumentFragment,
// we don't expect findBySelector to work.
if (parentNode.nodeType === 11 /* DocumentFragment */)
throw new Error("Can't use $ on an offscreen range");
var results = Blaze._DOMBackend.findBySelector(selector, parentNode);
// We 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, so we use
// `.filter`.
// Function that selects only elements that are actually
// in this DomRange, rather than simply descending from
// `parentNode`.
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;
return self.containsElement(elem);
};
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;
};
// Returns true if element a contains node b and is not node b.
//
// The restriction that `a` be an element (not a document fragment,
// say) is based on what's easy to implement cross-browser.
Blaze._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);
}
};
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/blaze/events.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
var EventSupport = Blaze._EventSupport = {};
var DOMBackend = Blaze._DOMBackend;
// List of events to always delegate, never capture.
// Since jQuery fakes bubbling for certain events in
// certain browsers (like `submit`), we don't want to
// get in its way.
//
// We could list all known bubbling
// events here to avoid creating speculative capturers
// for them, but it would only be an optimization.
var eventsToDelegate = EventSupport.eventsToDelegate = {
blur: 1, change: 1, click: 1, focus: 1, focusin: 1,
focusout: 1, reset: 1, submit: 1
};
var EVENT_MODE = EventSupport.EVENT_MODE = {
TBD: 0,
BUBBLING: 1,
CAPTURING: 2
};
var NEXT_HANDLERREC_ID = 1;
var HandlerRec = function (elem, type, selector, handler, recipient) {
this.elem = elem;
this.type = type;
this.selector = selector;
this.handler = handler;
this.recipient = recipient;
this.id = (NEXT_HANDLERREC_ID++);
this.mode = EVENT_MODE.TBD;
// It's important that delegatedHandler be a different
// instance for each handlerRecord, because its identity
// is used to remove it.
//
// It's also important that the closure have access to
// `this` when it is not called with it set.
this.delegatedHandler = (function (h) {
return function (evt) {
if ((! h.selector) && evt.currentTarget !== evt.target)
// no selector means only fire on target
return;
return h.handler.apply(h.recipient, arguments);
};
})(this);
// WHY CAPTURE AND DELEGATE: jQuery can't delegate
// non-bubbling events, because
// event capture doesn't work in IE 8. However, there
// are all sorts of new-fangled non-bubbling events
// like "play" and "touchenter". We delegate these
// events using capture in all browsers except IE 8.
// IE 8 doesn't support these events anyway.
var tryCapturing = elem.addEventListener &&
(! _.has(eventsToDelegate,
DOMBackend.Events.parseEventType(type)));
if (tryCapturing) {
this.capturingHandler = (function (h) {
return function (evt) {
if (h.mode === EVENT_MODE.TBD) {
// must be first time we're called.
if (evt.bubbles) {
// this type of event bubbles, so don't
// get called again.
h.mode = EVENT_MODE.BUBBLING;
DOMBackend.Events.unbindEventCapturer(
h.elem, h.type, h.capturingHandler);
return;
} else {
// this type of event doesn't bubble,
// so unbind the delegation, preventing
// it from ever firing.
h.mode = EVENT_MODE.CAPTURING;
DOMBackend.Events.undelegateEvents(
h.elem, h.type, h.delegatedHandler);
}
}
h.delegatedHandler(evt);
};
})(this);
} else {
this.mode = EVENT_MODE.BUBBLING;
}
};
EventSupport.HandlerRec = HandlerRec;
HandlerRec.prototype.bind = function () {
// `this.mode` may be EVENT_MODE_TBD, in which case we bind both. in
// this case, 'capturingHandler' is in charge of detecting the
// correct mode and turning off one or the other handlers.
if (this.mode !== EVENT_MODE.BUBBLING) {
DOMBackend.Events.bindEventCapturer(
this.elem, this.type, this.selector || '*',
this.capturingHandler);
}
if (this.mode !== EVENT_MODE.CAPTURING)
DOMBackend.Events.delegateEvents(
this.elem, this.type,
this.selector || '*', this.delegatedHandler);
};
HandlerRec.prototype.unbind = function () {
if (this.mode !== EVENT_MODE.BUBBLING)
DOMBackend.Events.unbindEventCapturer(this.elem, this.type,
this.capturingHandler);
if (this.mode !== EVENT_MODE.CAPTURING)
DOMBackend.Events.undelegateEvents(this.elem, this.type,
this.delegatedHandler);
};
EventSupport.listen = function (element, events, selector, handler, recipient, getParentRecipient) {
// Prevent this method from being JITed by Safari. Due to a
// presumed JIT bug in Safari -- observed in Version 7.0.6
// (9537.78.2) -- this method may crash the Safari render process if
// it is JITed.
// Repro: https://github.com/dgreensp/public/tree/master/safari-crash
try { element = element; } finally {}
var eventTypes = [];
events.replace(/[^ /]+/g, function (e) {
eventTypes.push(e);
});
var newHandlerRecs = [];
for (var i = 0, N = eventTypes.length; i < N; i++) {
var type = eventTypes[i];
var eventDict = element.$blaze_events;
if (! eventDict)
eventDict = (element.$blaze_events = {});
var info = eventDict[type];
if (! info) {
info = eventDict[type] = {};
info.handlers = [];
}
var handlerList = info.handlers;
var handlerRec = new HandlerRec(
element, type, selector, handler, recipient);
newHandlerRecs.push(handlerRec);
handlerRec.bind();
handlerList.push(handlerRec);
// Move handlers of enclosing ranges to end, by unbinding and rebinding
// them. In jQuery (or other DOMBackend) this causes them to fire
// later when the backend dispatches event handlers.
if (getParentRecipient) {
for (var r = getParentRecipient(recipient); r;
r = getParentRecipient(r)) {
// r is an enclosing range (recipient)
for (var j = 0, Nj = handlerList.length;
j < Nj; j++) {
var h = handlerList[j];
if (h.recipient === r) {
h.unbind();
h.bind();
handlerList.splice(j, 1); // remove handlerList[j]
handlerList.push(h);
j--; // account for removed handler
Nj--; // don't visit appended handlers
}
}
}
}
}
return {
// closes over just `element` and `newHandlerRecs`
stop: function () {
var eventDict = element.$blaze_events;
if (! eventDict)
return;
// newHandlerRecs has only one item unless you specify multiple
// event types. If this code is slow, it's because we have to
// iterate over handlerList here. Clearing a whole handlerList
// via stop() methods is O(N^2) in the number of handlers on
// an element.
for (var i = 0; i < newHandlerRecs.length; i++) {
var handlerToRemove = newHandlerRecs[i];
var info = eventDict[handlerToRemove.type];
if (! info)
continue;
var handlerList = info.handlers;
for (var j = handlerList.length - 1; j >= 0; j--) {
if (handlerList[j] === handlerToRemove) {
handlerToRemove.unbind();
handlerList.splice(j, 1); // remove handlerList[j]
}
}
}
newHandlerRecs.length = 0;
}
};
};
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/blaze/attrs.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
var jsUrlsAllowed = false;
Blaze._allowJavascriptUrls = function () {
jsUrlsAllowed = true;
};
Blaze._javascriptUrlsAllowed = function () {
return jsUrlsAllowed;
};
// An AttributeHandler object is responsible for updating a particular attribute
// of a particular element. AttributeHandler subclasses implement
// browser-specific logic for dealing with particular attributes across
// different browsers.
//
// To define a new type of AttributeHandler, use
// `var FooHandler = AttributeHandler.extend({ update: function ... })`
// where the `update` function takes arguments `(element, oldValue, value)`.
// The `element` argument is always the same between calls to `update` on
// the same instance. `oldValue` and `value` are each either `null` or
// a Unicode string of the type that might be passed to the value argument
// of `setAttribute` (i.e. not an HTML string with character references).
// When an AttributeHandler is installed, an initial call to `update` is
// always made with `oldValue = null`. The `update` method can access
// `this.name` if the AttributeHandler class is a generic one that applies
// to multiple attribute names.
//
// AttributeHandlers can store custom properties on `this`, as long as they
// don't use the names `element`, `name`, `value`, and `oldValue`.
//
// AttributeHandlers can't influence how attributes appear in rendered HTML,
// only how they are updated after materialization as DOM.
AttributeHandler = function (name, value) {
this.name = name;
this.value = value;
};
Blaze._AttributeHandler = AttributeHandler;
AttributeHandler.prototype.update = function (element, oldValue, value) {
if (value === null) {
if (oldValue !== null)
element.removeAttribute(this.name);
} else {
element.setAttribute(this.name, value);
}
};
AttributeHandler.extend = function (options) {
var curType = this;
var subType = function AttributeHandlerSubtype(/*arguments*/) {
AttributeHandler.apply(this, arguments);
};
subType.prototype = new curType;
subType.extend = curType.extend;
if (options)
_.extend(subType.prototype, options);
return subType;
};
/// Apply the diff between the attributes of "oldValue" and "value" to "element."
//
// Each subclass must implement a parseValue method which takes a string
// as an input and returns an ordered dict of attributes. The keys of the dict
// are unique identifiers (ie. css properties in the case of styles), and the
// values are the entire attribute which will be injected into the element.
//
// Extended below to support classes, SVG elements and styles.
Blaze._DiffingAttributeHandler = AttributeHandler.extend({
update: function (element, oldValue, value) {
if (!this.getCurrentValue || !this.setValue || !this.parseValue || !this.joinValues)
throw new Error("Missing methods in subclass of 'DiffingAttributeHandler'");
var oldAttrsMap = oldValue ? this.parseValue(oldValue) : new OrderedDict();
var attrsMap = value ? this.parseValue(value) : new OrderedDict();
// the current attributes on the element, which we will mutate.
var currentAttrString = this.getCurrentValue(element);
var currentAttrsMap = currentAttrString ? this.parseValue(currentAttrString) : new OrderedDict();
// Any outside changes to attributes we add at the end.
currentAttrsMap.forEach(function (value, key, i) {
// If the key already exists, we do not use the current value, but the new value.
if (attrsMap.has(key)) {
return;
}
// Key does not already exist, but it existed before. Which means it was explicitly
// removed, so we do not add it.
if (oldAttrsMap.has(key)) {
return;
}
attrsMap.append(key, value);
});
var values = [];
attrsMap.forEach(function (value, key, i) {
values.push(value);
});
this.setValue(element, this.joinValues(values));
}
});
var ClassHandler = Blaze._DiffingAttributeHandler.extend({
// @param rawValue {String}
getCurrentValue: function (element) {
return element.className;
},
setValue: function (element, className) {
element.className = className;
},
parseValue: function (attrString) {
var tokens = new OrderedDict();
_.each(attrString.split(' '), function(token) {
if (token) {
// Ordered dict requires unique keys.
if (! tokens.has(token)) {
tokens.append(token, token);
}
}
});
return tokens;
},
joinValues: function (values) {
return values.join(' ');
}
});
var SVGClassHandler = ClassHandler.extend({
getCurrentValue: function (element) {
return element.className.baseVal;
},
setValue: function (element, className) {
element.setAttribute('class', className);
}
});
var StyleHandler = Blaze._DiffingAttributeHandler.extend({
getCurrentValue: function (element) {
return element.getAttribute('style');
},
setValue: function (element, style) {
if (style === '') {
element.removeAttribute('style');
} else {
element.setAttribute('style', style);
}
},
// Parse a string to produce a map from property to attribute string.
//
// Example:
// "color:red; foo:12px" produces a token {color: "color:red", foo:"foo:12px"}
parseValue: function (attrString) {
var tokens = new OrderedDict();
// Regex for parsing a css attribute declaration, taken from css-parse:
// https://github.com/reworkcss/css-parse/blob/7cef3658d0bba872cde05a85339034b187cb3397/index.js#L219
var regex = /(\*?[-#\/\*\\\w]+(?:\[[0-9a-z_-]+\])?)\s*:\s*(?:\'(?:\\\'|.)*?\'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+[;\s]*/g;
var match = regex.exec(attrString);
while (match) {
// match[0] = entire matching string
// match[1] = css property
// Prefix the token to prevent conflicts with existing properties.
// We use the last value for the same key.
if (tokens.has(match[1])) {
tokens.remove(match[1]);
}
// XXX No `String.trim` on Safari 4. Swap out $.trim if we want to
// remove strong dep on jquery.
tokens.append(match[1], match[0].trim ? match[0].trim() : $.trim(match[0]));
match = regex.exec(attrString);
}
return tokens;
},
joinValues: function (values) {
// TODO: Assure that there is always ; between values. But what is an example where it breaks?
return values.join(' ');
}
});
var BooleanHandler = AttributeHandler.extend({
update: function (element, oldValue, value) {
var name = this.name;
if (value == null) {
if (oldValue != null)
element[name] = false;
} else {
element[name] = true;
}
}
});
var DOMPropertyHandler = AttributeHandler.extend({
update: function (element, oldValue, value) {
var name = this.name;
if (value !== element[name])
element[name] = value;
}
});
// attributes of the type 'xlink:something' should be set using
// the correct namespace in order to work
var XlinkHandler = AttributeHandler.extend({
update: function(element, oldValue, value) {
var NS = 'http://www.w3.org/1999/xlink';
if (value === null) {
if (oldValue !== null)
element.removeAttributeNS(NS, this.name);
} else {
element.setAttributeNS(NS, this.name, this.value);
}
}
});
// cross-browser version of `instanceof SVGElement`
var isSVGElement = function (elem) {
return 'ownerSVGElement' in elem;
};
var isUrlAttribute = function (tagName, attrName) {
// Compiled from http://www.w3.org/TR/REC-html40/index/attributes.html
// and
// http://www.w3.org/html/wg/drafts/html/master/index.html#attributes-1
var urlAttrs = {
FORM: ['action'],
BODY: ['background'],
BLOCKQUOTE: ['cite'],
Q: ['cite'],
DEL: ['cite'],
INS: ['cite'],
OBJECT: ['classid', 'codebase', 'data', 'usemap'],
APPLET: ['codebase'],
A: ['href'],
AREA: ['href'],
LINK: ['href'],
BASE: ['href'],
IMG: ['longdesc', 'src', 'usemap'],
FRAME: ['longdesc', 'src'],
IFRAME: ['longdesc', 'src'],
HEAD: ['profile'],
SCRIPT: ['src'],
INPUT: ['src', 'usemap', 'formaction'],
BUTTON: ['formaction'],
BASE: ['href'],
MENUITEM: ['icon'],
HTML: ['manifest'],
VIDEO: ['poster']
};
if (attrName === 'itemid') {
return true;
}
var urlAttrNames = urlAttrs[tagName] || [];
return _.contains(urlAttrNames, attrName);
};
// To get the protocol for a URL, we let the browser normalize it for
// us, by setting it as the href for an anchor tag and then reading out
// the 'protocol' property.
if (Meteor.isClient) {
var anchorForNormalization = document.createElement('A');
}
var getUrlProtocol = function (url) {
if (Meteor.isClient) {
anchorForNormalization.href = url;
return (anchorForNormalization.protocol || "").toLowerCase();
} else {
throw new Error('getUrlProtocol not implemented on the server');
}
};
// UrlHandler is an attribute handler for all HTML attributes that take
// URL values. It disallows javascript: URLs, unless
// Blaze._allowJavascriptUrls() has been called. To detect javascript:
// urls, we set the attribute on a dummy anchor element and then read
// out the 'protocol' property of the attribute.
var origUpdate = AttributeHandler.prototype.update;
var UrlHandler = AttributeHandler.extend({
update: function (element, oldValue, value) {
var self = this;
var args = arguments;
if (Blaze._javascriptUrlsAllowed()) {
origUpdate.apply(self, args);
} else {
var isJavascriptProtocol = (getUrlProtocol(value) === "javascript:");
var isVBScriptProtocol = (getUrlProtocol(value) === "vbscript:");
if (isJavascriptProtocol || isVBScriptProtocol) {
Blaze._warn("URLs that use the 'javascript:' or 'vbscript:' protocol are not " +
"allowed in URL attribute values. " +
"Call Blaze._allowJavascriptUrls() " +
"to enable them.");
origUpdate.apply(self, [element, oldValue, null]);
} else {
origUpdate.apply(self, args);
}
}
}
});
// XXX make it possible for users to register attribute handlers!
Blaze._makeAttributeHandler = function (elem, name, value) {
// generally, use setAttribute but certain attributes need to be set
// by directly setting a JavaScript property on the DOM element.
if (name === 'class') {
if (isSVGElement(elem)) {
return new SVGClassHandler(name, value);
} else {
return new ClassHandler(name, value);
}
} else if (name === 'style') {
return new StyleHandler(name, value);
} else if ((elem.tagName === 'OPTION' && name === 'selected') ||
(elem.tagName === 'INPUT' && name === 'checked') ||
(elem.tagName === 'VIDEO' && name === 'muted')) {
return new BooleanHandler(name, value);
} else if ((elem.tagName === 'TEXTAREA' || elem.tagName === 'INPUT')
&& name === 'value') {
// internally, TEXTAREAs tracks their value in the 'value'
// attribute just like INPUTs.
return new DOMPropertyHandler(name, value);
} else if (name.substring(0,6) === 'xlink:') {
return new XlinkHandler(name.substring(6), value);
} else if (isUrlAttribute(elem.tagName, name)) {
return new UrlHandler(name, value);
} else {
return new AttributeHandler(name, value);
}
// XXX will need one for 'style' on IE, though modern browsers
// seem to handle setAttribute ok.
};
ElementAttributesUpdater = function (elem) {
this.elem = elem;
this.handlers = {};
};
// Update attributes on `elem` to the dictionary `attrs`, whose
// values are strings.
ElementAttributesUpdater.prototype.update = function(newAttrs) {
var elem = this.elem;
var handlers = this.handlers;
for (var k in handlers) {
if (! _.has(newAttrs, k)) {
// remove attributes (and handlers) for attribute names
// that don't exist as keys of `newAttrs` and so won't
// be visited when traversing it. (Attributes that
// exist in the `newAttrs` object but are `null`
// are handled later.)
var handler = handlers[k];
var oldValue = handler.value;
handler.value = null;
handler.update(elem, oldValue, null);
delete handlers[k];
}
}
for (var k in newAttrs) {
var handler = null;
var oldValue = null;
var value = newAttrs[k];
if (! _.has(handlers, k)) {
if (value !== null) {
// make new handler
handler = Blaze._makeAttributeHandler(elem, k, value);
handlers[k] = handler;
}
} else {
handler = handlers[k];
oldValue = handler.value;
}
if (oldValue !== value) {
handler.value = value;
handler.update(elem, oldValue, value);
if (value === null)
delete handlers[k];
}
}
};
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/blaze/materializer.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// 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(Blaze._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) {
// map `null`, `undefined`, and `false` to null, which is important
// so that attributes with nully values are considered absent.
// stringify anything else (e.g. strings, booleans, numbers including 0).
if (flattenedAttrs[attrName] == null || flattenedAttrs[attrName] === false)
stringAttrs[attrName] = null;
else
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(Blaze._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);
};
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/blaze/exceptions.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
var debugFunc;
// We call into user code in many places, and it's nice to catch exceptions
// propagated from user code immediately so that the whole system doesn't just
// break. Catching exceptions is easy; reporting them is hard. This helper
// reports exceptions.
//
// Usage:
//
// ```
// try {
// // ... someStuff ...
// } catch (e) {
// reportUIException(e);
// }
// ```
//
// An optional second argument overrides the default message.
// Set this to `true` to cause `reportException` to throw
// the next exception rather than reporting it. This is
// useful in unit tests that test error messages.
Blaze._throwNextException = false;
Blaze._reportException = function (e, msg) {
if (Blaze._throwNextException) {
Blaze._throwNextException = false;
throw e;
}
if (! debugFunc)
// adapted from Tracker
debugFunc = function () {
return (typeof Meteor !== "undefined" ? Meteor._debug :
((typeof console !== "undefined") && console.log ? console.log :
function () {}));
};
// In Chrome, `e.stack` is a multiline string that starts with the message
// and contains a stack trace. Furthermore, `console.log` makes it clickable.
// `console.log` supplies the space between the two arguments.
debugFunc()(msg || 'Exception caught in template:', e.stack || e.message || e);
};
Blaze._wrapCatchingExceptions = function (f, where) {
if (typeof f !== 'function')
return f;
return function () {
try {
return f.apply(this, arguments);
} catch (e) {
Blaze._reportException(e, 'Exception in ' + where + ':');
}
};
};
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/blaze/view.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
/// [new] Blaze.View([name], renderMethod)
///
/// Blaze.View is the building block of reactive DOM. Views have
/// the following features:
///
/// * lifecycle callbacks - Views are created, rendered, and destroyed,
/// and callbacks can be registered to fire when these things happen.
///
/// * parent pointer - A View points to its parentView, which is the
/// View that caused it to be rendered. These pointers form a
/// hierarchy or tree of Views.
///
/// * render() method - A View's render() method specifies the DOM
/// (or HTML) content of the View. If the method establishes
/// reactive dependencies, it may be re-run.
///
/// * a DOMRange - If a View is rendered to DOM, its position and
/// extent in the DOM are tracked using a DOMRange object.
///
/// When a View is constructed by calling Blaze.View, the View is
/// not yet considered "created." It doesn't have a parentView yet,
/// and no logic has been run to initialize the View. All real
/// work is deferred until at least creation time, when the onViewCreated
/// callbacks are fired, which happens when the View is "used" in
/// some way that requires it to be rendered.
///
/// ...more lifecycle stuff
///
/// `name` is an optional string tag identifying the View. The only
/// time it's used is when looking in the View tree for a View of a
/// particular name; for example, data contexts are stored on Views
/// of name "with". Names are also useful when debugging, so in
/// general it's good for functions that create Views to set the name.
/// Views associated with templates have names of the form "Template.foo".
/**
* @class
* @summary Constructor for a View, which represents a reactive region of DOM.
* @locus Client
* @param {String} [name] Optional. A name for this type of View. See [`view.name`](#view_name).
* @param {Function} renderFunction A function that returns [*renderable content*](#renderable_content). In this function, `this` is bound to the View.
*/
Blaze.View = function (name, render) {
if (! (this instanceof Blaze.View))
// called without `new`
return new Blaze.View(name, render);
if (typeof name === 'function') {
// omitted "name" argument
render = name;
name = '';
}
this.name = name;
this._render = render;
this._callbacks = {
created: null,
rendered: null,
destroyed: null
};
// Setting all properties here is good for readability,
// and also may help Chrome optimize the code by keeping
// the View object from changing shape too much.
this.isCreated = false;
this._isCreatedForExpansion = false;
this.isRendered = false;
this._isAttached = false;
this.isDestroyed = false;
this._isInRender = false;
this.parentView = null;
this._domrange = null;
// This flag is normally set to false except for the cases when view's parent
// was generated as part of expanding some syntactic sugar expressions or
// methods.
// Ex.: Blaze.renderWithData is an equivalent to creating a view with regular
// Blaze.render and wrapping it into {{#with data}}{{/with}} view. Since the
// users don't know anything about these generated parent views, Blaze needs
// this information to be available on views to make smarter decisions. For
// example: removing the generated parent view with the view on Blaze.remove.
this._hasGeneratedParent = false;
// Bindings accessible to children views (via view.lookup('name')) within the
// closest template view.
this._scopeBindings = {};
this.renderCount = 0;
};
Blaze.View.prototype._render = function () { return null; };
Blaze.View.prototype.onViewCreated = function (cb) {
this._callbacks.created = this._callbacks.created || [];
this._callbacks.created.push(cb);
};
Blaze.View.prototype._onViewRendered = function (cb) {
this._callbacks.rendered = this._callbacks.rendered || [];
this._callbacks.rendered.push(cb);
};
Blaze.View.prototype.onViewReady = function (cb) {
var self = this;
var fire = function () {
Tracker.afterFlush(function () {
if (! self.isDestroyed) {
Blaze._withCurrentView(self, function () {
cb.call(self);
});
}
});
};
self._onViewRendered(function onViewRendered() {
if (self.isDestroyed)
return;
if (! self._domrange.attached)
self._domrange.onAttached(fire);
else
fire();
});
};
Blaze.View.prototype.onViewDestroyed = function (cb) {
this._callbacks.destroyed = this._callbacks.destroyed || [];
this._callbacks.destroyed.push(cb);
};
Blaze.View.prototype.removeViewDestroyedListener = function (cb) {
var destroyed = this._callbacks.destroyed;
if (! destroyed)
return;
var index = _.lastIndexOf(destroyed, cb);
if (index !== -1) {
// XXX You'd think the right thing to do would be splice, but _fireCallbacks
// gets sad if you remove callbacks while iterating over the list. Should
// change this to use callback-hook or EventEmitter or something else that
// properly supports removal.
destroyed[index] = null;
}
};
/// View#autorun(func)
///
/// Sets up a Tracker autorun that is "scoped" to this View in two
/// important ways: 1) Blaze.currentView is automatically set
/// on every re-run, and 2) the autorun is stopped when the
/// View is destroyed. As with Tracker.autorun, the first run of
/// the function is immediate, and a Computation object that can
/// be used to stop the autorun is returned.
///
/// View#autorun is meant to be called from View callbacks like
/// onViewCreated, or from outside the rendering process. It may not
/// be called before the onViewCreated callbacks are fired (too early),
/// or from a render() method (too confusing).
///
/// Typically, autoruns that update the state
/// of the View (as in Blaze.With) should be started from an onViewCreated
/// callback. Autoruns that update the DOM should be started
/// from either onViewCreated (guarded against the absence of
/// view._domrange), or onViewReady.
Blaze.View.prototype.autorun = function (f, _inViewScope, displayName) {
var self = this;
// The restrictions on when View#autorun can be called are in order
// to avoid bad patterns, like creating a Blaze.View and immediately
// calling autorun on it. A freshly created View is not ready to
// have logic run on it; it doesn't have a parentView, for example.
// It's when the View is materialized or expanded that the onViewCreated
// handlers are fired and the View starts up.
//
// Letting the render() method call `this.autorun()` is problematic
// because of re-render. The best we can do is to stop the old
// autorun and start a new one for each render, but that's a pattern
// we try to avoid internally because it leads to helpers being
// called extra times, in the case where the autorun causes the
// view to re-render (and thus the autorun to be torn down and a
// new one established).
//
// We could lift these restrictions in various ways. One interesting
// idea is to allow you to call `view.autorun` after instantiating
// `view`, and automatically wrap it in `view.onViewCreated`, deferring
// the autorun so that it starts at an appropriate time. However,
// then we can't return the Computation object to the caller, because
// it doesn't exist yet.
if (! self.isCreated) {
throw new Error("View#autorun must be called from the created callback at the earliest");
}
if (this._isInRender) {
throw new Error("Can't call View#autorun from inside render(); try calling it from the created or rendered callback");
}
var templateInstanceFunc = Blaze.Template._currentTemplateInstanceFunc;
var func = function viewAutorun(c) {
return Blaze._withCurrentView(_inViewScope || self, function () {
return Blaze.Template._withTemplateInstanceFunc(
templateInstanceFunc, function () {
return f.call(self, c);
});
});
};
// Give the autorun function a better name for debugging and profiling.
// The `displayName` property is not part of the spec but browsers like Chrome
// and Firefox prefer it in debuggers over the name function was declared by.
func.displayName =
(self.name || 'anonymous') + ':' + (displayName || 'anonymous');
var comp = Tracker.autorun(func);
var stopComputation = function () { comp.stop(); };
self.onViewDestroyed(stopComputation);
comp.onStop(function () {
self.removeViewDestroyedListener(stopComputation);
});
return comp;
};
Blaze.View.prototype._errorIfShouldntCallSubscribe = function () {
var self = this;
if (! self.isCreated) {
throw new Error("View#subscribe must be called from the created callback at the earliest");
}
if (self._isInRender) {
throw new Error("Can't call View#subscribe from inside render(); try calling it from the created or rendered callback");
}
if (self.isDestroyed) {
throw new Error("Can't call View#subscribe from inside the destroyed callback, try calling it inside created or rendered.");
}
};
/**
* Just like Blaze.View#autorun, but with Meteor.subscribe instead of
* Tracker.autorun. Stop the subscription when the view is destroyed.
* @return {SubscriptionHandle} A handle to the subscription so that you can
* see if it is ready, or stop it manually
*/
Blaze.View.prototype.subscribe = function (args, options) {
var self = this;
options = options || {};
self._errorIfShouldntCallSubscribe();
var subHandle;
if (options.connection) {
subHandle = options.connection.subscribe.apply(options.connection, args);
} else {
subHandle = Meteor.subscribe.apply(Meteor, args);
}
self.onViewDestroyed(function () {
subHandle.stop();
});
return subHandle;
};
Blaze.View.prototype.firstNode = function () {
if (! this._isAttached)
throw new Error("View must be attached before accessing its DOM");
return this._domrange.firstNode();
};
Blaze.View.prototype.lastNode = function () {
if (! this._isAttached)
throw new Error("View must be attached before accessing its DOM");
return this._domrange.lastNode();
};
Blaze._fireCallbacks = function (view, which) {
Blaze._withCurrentView(view, function () {
Tracker.nonreactive(function fireCallbacks() {
var cbs = view._callbacks[which];
for (var i = 0, N = (cbs && cbs.length); i < N; i++)
cbs[i] && cbs[i].call(view);
});
});
};
Blaze._createView = function (view, parentView, forExpansion) {
if (view.isCreated)
throw new Error("Can't render the same View twice");
view.parentView = (parentView || null);
view.isCreated = true;
if (forExpansion)
view._isCreatedForExpansion = true;
Blaze._fireCallbacks(view, 'created');
};
var doFirstRender = function (view, initialContent) {
var domrange = new Blaze._DOMRange(initialContent);
view._domrange = domrange;
domrange.view = view;
view.isRendered = true;
Blaze._fireCallbacks(view, 'rendered');
var teardownHook = null;
domrange.onAttached(function attached(range, element) {
view._isAttached = true;
teardownHook = Blaze._DOMBackend.Teardown.onElementTeardown(
element, function teardown() {
Blaze._destroyView(view, true /* _skipNodes */);
});
});
// tear down the teardown hook
view.onViewDestroyed(function () {
teardownHook && teardownHook.stop();
teardownHook = null;
});
return domrange;
};
// Take an uncreated View `view` and create and render it to DOM,
// setting up the autorun that updates the View. Returns a new
// DOMRange, which has been associated with the View.
//
// The private arguments `_workStack` and `_intoArray` are passed in
// by Blaze._materializeDOM and are only present for recursive calls
// (when there is some other _materializeView on the stack). If
// provided, then we avoid the mutual recursion of calling back into
// Blaze._materializeDOM so that deep View hierarchies don't blow the
// stack. Instead, we push tasks onto workStack for the initial
// rendering and subsequent setup of the View, and they are done after
// we return. When there is a _workStack, we do not return the new
// DOMRange, but instead push it into _intoArray from a _workStack
// task.
Blaze._materializeView = function (view, parentView, _workStack, _intoArray) {
Blaze._createView(view, parentView);
var domrange;
var lastHtmljs;
// We don't expect to be called in a Computation, but just in case,
// wrap in Tracker.nonreactive.
Tracker.nonreactive(function () {
view.autorun(function doRender(c) {
// `view.autorun` sets the current view.
view.renderCount++;
view._isInRender = true;
// Any dependencies that should invalidate this Computation come
// from this line:
var htmljs = view._render();
view._isInRender = false;
if (! c.firstRun && ! Blaze._isContentEqual(lastHtmljs, htmljs)) {
Tracker.nonreactive(function doMaterialize() {
// re-render
var rangesAndNodes = Blaze._materializeDOM(htmljs, [], view);
domrange.setMembers(rangesAndNodes);
Blaze._fireCallbacks(view, 'rendered');
});
}
lastHtmljs = htmljs;
// Causes any nested views to stop immediately, not when we call
// `setMembers` the next time around the autorun. Otherwise,
// helpers in the DOM tree to be replaced might be scheduled
// to re-run before we have a chance to stop them.
Tracker.onInvalidate(function () {
if (domrange) {
domrange.destroyMembers();
}
});
}, undefined, 'materialize');
// first render. lastHtmljs is the first htmljs.
var initialContents;
if (! _workStack) {
initialContents = Blaze._materializeDOM(lastHtmljs, [], view);
domrange = doFirstRender(view, initialContents);
initialContents = null; // help GC because we close over this scope a lot
} else {
// We're being called from Blaze._materializeDOM, so to avoid
// recursion and save stack space, provide a description of the
// work to be done instead of doing it. Tasks pushed onto
// _workStack will be done in LIFO order after we return.
// The work will still be done within a Tracker.nonreactive,
// because it will be done by some call to Blaze._materializeDOM
// (which is always called in a Tracker.nonreactive).
initialContents = [];
// push this function first so that it happens last
_workStack.push(function () {
domrange = doFirstRender(view, initialContents);
initialContents = null; // help GC because of all the closures here
_intoArray.push(domrange);
});
// now push the task that calculates initialContents
_workStack.push(Blaze._bind(Blaze._materializeDOM, null,
lastHtmljs, initialContents, view, _workStack));
}
});
if (! _workStack) {
return domrange;
} else {
return null;
}
};
// Expands a View to HTMLjs, calling `render` recursively on all
// Views and evaluating any dynamic attributes. Calls the `created`
// callback, but not the `materialized` or `rendered` callbacks.
// Destroys the view immediately, unless called in a Tracker Computation,
// in which case the view will be destroyed when the Computation is
// invalidated. If called in a Tracker Computation, the result is a
// reactive string; that is, the Computation will be invalidated
// if any changes are made to the view or subviews that might affect
// the HTML.
Blaze._expandView = function (view, parentView) {
Blaze._createView(view, parentView, true /*forExpansion*/);
view._isInRender = true;
var htmljs = Blaze._withCurrentView(view, function () {
return view._render();
});
view._isInRender = false;
var result = Blaze._expand(htmljs, view);
if (Tracker.active) {
Tracker.onInvalidate(function () {
Blaze._destroyView(view);
});
} else {
Blaze._destroyView(view);
}
return result;
};
// Options: `parentView`
Blaze._HTMLJSExpander = HTML.TransformingVisitor.extend();
Blaze._HTMLJSExpander.def({
visitObject: function (x) {
if (x instanceof Blaze.Template)
x = x.constructView();
if (x instanceof Blaze.View)
return Blaze._expandView(x, this.parentView);
// this will throw an error; other objects are not allowed!
return HTML.TransformingVisitor.prototype.visitObject.call(this, x);
},
visitAttributes: function (attrs) {
// expand dynamic attributes
if (typeof attrs === 'function')
attrs = Blaze._withCurrentView(this.parentView, attrs);
// call super (e.g. for case where `attrs` is an array)
return HTML.TransformingVisitor.prototype.visitAttributes.call(this, attrs);
},
visitAttribute: function (name, value, tag) {
// expand attribute values that are functions. Any attribute value
// that contains Views must be wrapped in a function.
if (typeof value === 'function')
value = Blaze._withCurrentView(this.parentView, value);
return HTML.TransformingVisitor.prototype.visitAttribute.call(
this, name, value, tag);
}
});
// Return Blaze.currentView, but only if it is being rendered
// (i.e. we are in its render() method).
var currentViewIfRendering = function () {
var view = Blaze.currentView;
return (view && view._isInRender) ? view : null;
};
Blaze._expand = function (htmljs, parentView) {
parentView = parentView || currentViewIfRendering();
return (new Blaze._HTMLJSExpander(
{parentView: parentView})).visit(htmljs);
};
Blaze._expandAttributes = function (attrs, parentView) {
parentView = parentView || currentViewIfRendering();
return (new Blaze._HTMLJSExpander(
{parentView: parentView})).visitAttributes(attrs);
};
Blaze._destroyView = function (view, _skipNodes) {
if (view.isDestroyed)
return;
view.isDestroyed = true;
Blaze._fireCallbacks(view, 'destroyed');
// Destroy views and elements recursively. If _skipNodes,
// only recurse up to views, not elements, for the case where
// the backend (jQuery) is recursing over the elements already.
if (view._domrange)
view._domrange.destroyMembers(_skipNodes);
};
Blaze._destroyNode = function (node) {
if (node.nodeType === 1)
Blaze._DOMBackend.Teardown.tearDownElement(node);
};
// Are the HTMLjs entities `a` and `b` the same? We could be
// more elaborate here but the point is to catch the most basic
// cases.
Blaze._isContentEqual = 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'));
}
};
/**
* @summary The View corresponding to the current template helper, event handler, callback, or autorun. If there isn't one, `null`.
* @locus Client
* @type {Blaze.View}
*/
Blaze.currentView = null;
Blaze._withCurrentView = function (view, func) {
var oldView = Blaze.currentView;
try {
Blaze.currentView = view;
return func();
} finally {
Blaze.currentView = oldView;
}
};
// Blaze.render publicly takes a View or a Template.
// Privately, it takes any HTMLJS (extended with Views and Templates)
// except null or undefined, or a function that returns any extended
// HTMLJS.
var checkRenderContent = function (content) {
if (content === null)
throw new Error("Can't render null");
if (typeof content === 'undefined')
throw new Error("Can't render undefined");
if ((content instanceof Blaze.View) ||
(content instanceof Blaze.Template) ||
(typeof content === 'function'))
return;
try {
// Throw if content doesn't look like HTMLJS at the top level
// (i.e. verify that this is an HTML.Tag, or an array,
// or a primitive, etc.)
(new HTML.Visitor).visit(content);
} catch (e) {
// Make error message suitable for public API
throw new Error("Expected Template or View");
}
};
// For Blaze.render and Blaze.toHTML, take content and
// wrap it in a View, unless it's a single View or
// Template already.
var contentAsView = function (content) {
checkRenderContent(content);
if (content instanceof Blaze.Template) {
return content.constructView();
} else if (content instanceof Blaze.View) {
return content;
} else {
var func = content;
if (typeof func !== 'function') {
func = function () {
return content;
};
}
return Blaze.View('render', func);
}
};
// For Blaze.renderWithData and Blaze.toHTMLWithData, wrap content
// in a function, if necessary, so it can be a content arg to
// a Blaze.With.
var contentAsFunc = function (content) {
checkRenderContent(content);
if (typeof content !== 'function') {
return function () {
return content;
};
} else {
return content;
}
};
/**
* @summary Renders a template or View to DOM nodes and inserts it into the DOM, returning a rendered [View](#blaze_view) which can be passed to [`Blaze.remove`](#blaze_remove).
* @locus Client
* @param {Template|Blaze.View} templateOrView The template (e.g. `Template.myTemplate`) or View object to render. If a template, a View object is [constructed](#template_constructview). If a View, it must be an unrendered View, which becomes a rendered View and is returned.
* @param {DOMNode} parentNode The node that will be the parent of the rendered template. It must be an Element node.
* @param {DOMNode} [nextNode] Optional. If provided, must be a child of <em>parentNode</em>; the template will be inserted before this node. If not provided, the template will be inserted as the last child of parentNode.
* @param {Blaze.View} [parentView] Optional. If provided, it will be set as the rendered View's [`parentView`](#view_parentview).
*/
Blaze.render = function (content, parentElement, nextNode, parentView) {
if (! parentElement) {
Blaze._warn("Blaze.render without a parent element is deprecated. " +
"You must specify where to insert the rendered content.");
}
if (nextNode instanceof Blaze.View) {
// handle omitted nextNode
parentView = nextNode;
nextNode = null;
}
// parentElement must be a DOM node. in particular, can't be the
// result of a call to `$`. Can't check if `parentElement instanceof
// Node` since 'Node' is undefined in IE8.
if (parentElement && typeof parentElement.nodeType !== 'number')
throw new Error("'parentElement' must be a DOM node");
if (nextNode && typeof nextNode.nodeType !== 'number') // 'nextNode' is optional
throw new Error("'nextNode' must be a DOM node");
parentView = parentView || currentViewIfRendering();
var view = contentAsView(content);
Blaze._materializeView(view, parentView);
if (parentElement) {
view._domrange.attach(parentElement, nextNode);
}
return view;
};
Blaze.insert = function (view, parentElement, nextNode) {
Blaze._warn("Blaze.insert has been deprecated. Specify where to insert the " +
"rendered content in the call to Blaze.render.");
if (! (view && (view._domrange instanceof Blaze._DOMRange)))
throw new Error("Expected template rendered with Blaze.render");
view._domrange.attach(parentElement, nextNode);
};
/**
* @summary Renders a template or View to DOM nodes with a data context. Otherwise identical to `Blaze.render`.
* @locus Client
* @param {Template|Blaze.View} templateOrView The template (e.g. `Template.myTemplate`) or View object to render.
* @param {Object|Function} data The data context to use, or a function returning a data context. If a function is provided, it will be reactively re-run.
* @param {DOMNode} parentNode The node that will be the parent of the rendered template. It must be an Element node.
* @param {DOMNode} [nextNode] Optional. If provided, must be a child of <em>parentNode</em>; the template will be inserted before this node. If not provided, the template will be inserted as the last child of parentNode.
* @param {Blaze.View} [parentView] Optional. If provided, it will be set as the rendered View's [`parentView`](#view_parentview).
*/
Blaze.renderWithData = function (content, data, parentElement, nextNode, parentView) {
// We defer the handling of optional arguments to Blaze.render. At this point,
// `nextNode` may actually be `parentView`.
return Blaze.render(Blaze._TemplateWith(data, contentAsFunc(content)),
parentElement, nextNode, parentView);
};
/**
* @summary Removes a rendered View from the DOM, stopping all reactive updates and event listeners on it. Also destroys the Blaze.Template instance associated with the view.
* @locus Client
* @param {Blaze.View} renderedView The return value from `Blaze.render` or `Blaze.renderWithData`, or the `view` property of a Blaze.Template instance. Calling `Blaze.remove(Template.instance().view)` from within a template event handler will destroy the view as well as that template and trigger the template's `onDestroyed` handlers.
*/
Blaze.remove = function (view) {
if (! (view && (view._domrange instanceof Blaze._DOMRange)))
throw new Error("Expected template rendered with Blaze.render");
while (view) {
if (! view.isDestroyed) {
var range = view._domrange;
if (range.attached && ! range.parentRange)
range.detach();
range.destroy();
}
view = view._hasGeneratedParent && view.parentView;
}
};
/**
* @summary Renders a template or View to a string of HTML.
* @locus Client
* @param {Template|Blaze.View} templateOrView The template (e.g. `Template.myTemplate`) or View object from which to generate HTML.
*/
Blaze.toHTML = function (content, parentView) {
parentView = parentView || currentViewIfRendering();
return HTML.toHTML(Blaze._expandView(contentAsView(content), parentView));
};
/**
* @summary Renders a template or View to HTML with a data context. Otherwise identical to `Blaze.toHTML`.
* @locus Client
* @param {Template|Blaze.View} templateOrView The template (e.g. `Template.myTemplate`) or View object from which to generate HTML.
* @param {Object|Function} data The data context to use, or a function returning a data context.
*/
Blaze.toHTMLWithData = function (content, data, parentView) {
parentView = parentView || currentViewIfRendering();
return HTML.toHTML(Blaze._expandView(Blaze._TemplateWith(
data, contentAsFunc(content)), parentView));
};
Blaze._toText = function (htmljs, parentView, textMode) {
if (typeof htmljs === 'function')
throw new Error("Blaze._toText doesn't take a function, just HTMLjs");
if ((parentView != null) && ! (parentView instanceof Blaze.View)) {
// omitted parentView argument
textMode = parentView;
parentView = null;
}
parentView = parentView || currentViewIfRendering();
if (! textMode)
throw new Error("textMode required");
if (! (textMode === HTML.TEXTMODE.STRING ||
textMode === HTML.TEXTMODE.RCDATA ||
textMode === HTML.TEXTMODE.ATTRIBUTE))
throw new Error("Unknown textMode: " + textMode);
return HTML.toText(Blaze._expand(htmljs, parentView), textMode);
};
/**
* @summary Returns the current data context, or the data context that was used when rendering a particular DOM element or View from a Meteor template.
* @locus Client
* @param {DOMElement|Blaze.View} [elementOrView] Optional. An element that was rendered by a Meteor, or a View.
*/
Blaze.getData = function (elementOrView) {
var theWith;
if (! elementOrView) {
theWith = Blaze.getView('with');
} else if (elementOrView instanceof Blaze.View) {
var view = elementOrView;
theWith = (view.name === 'with' ? view :
Blaze.getView(view, 'with'));
} else if (typeof elementOrView.nodeType === 'number') {
if (elementOrView.nodeType !== 1)
throw new Error("Expected DOM element");
theWith = Blaze.getView(elementOrView, 'with');
} else {
throw new Error("Expected DOM element or View");
}
return theWith ? theWith.dataVar.get() : null;
};
// For back-compat
Blaze.getElementData = function (element) {
Blaze._warn("Blaze.getElementData has been deprecated. Use " +
"Blaze.getData(element) instead.");
if (element.nodeType !== 1)
throw new Error("Expected DOM element");
return Blaze.getData(element);
};
// Both arguments are optional.
/**
* @summary Gets either the current View, or the View enclosing the given DOM element.
* @locus Client
* @param {DOMElement} [element] Optional. If specified, the View enclosing `element` is returned.
*/
Blaze.getView = function (elementOrView, _viewName) {
var viewName = _viewName;
if ((typeof elementOrView) === 'string') {
// omitted elementOrView; viewName present
viewName = elementOrView;
elementOrView = null;
}
// We could eventually shorten the code by folding the logic
// from the other methods into this method.
if (! elementOrView) {
return Blaze._getCurrentView(viewName);
} else if (elementOrView instanceof Blaze.View) {
return Blaze._getParentView(elementOrView, viewName);
} else if (typeof elementOrView.nodeType === 'number') {
return Blaze._getElementView(elementOrView, viewName);
} else {
throw new Error("Expected DOM element or View");
}
};
// Gets the current view or its nearest ancestor of name
// `name`.
Blaze._getCurrentView = function (name) {
var view = Blaze.currentView;
// Better to fail in cases where it doesn't make sense
// to use Blaze._getCurrentView(). There will be a current
// view anywhere it does. You can check Blaze.currentView
// if you want to know whether there is one or not.
if (! view)
throw new Error("There is no current view");
if (name) {
while (view && view.name !== name)
view = view.parentView;
return view || null;
} else {
// Blaze._getCurrentView() with no arguments just returns
// Blaze.currentView.
return view;
}
};
Blaze._getParentView = function (view, name) {
var v = view.parentView;
if (name) {
while (v && v.name !== name)
v = v.parentView;
}
return v || null;
};
Blaze._getElementView = function (elem, name) {
var range = Blaze._DOMRange.forElement(elem);
var view = null;
while (range && ! view) {
view = (range.view || null);
if (! view) {
if (range.parentRange)
range = range.parentRange;
else
range = Blaze._DOMRange.forElement(range.parentElement);
}
}
if (name) {
while (view && view.name !== name)
view = view.parentView;
return view || null;
} else {
return view;
}
};
Blaze._addEventMap = function (view, eventMap, thisInHandler) {
thisInHandler = (thisInHandler || null);
var handles = [];
if (! view._domrange)
throw new Error("View must have a DOMRange");
view._domrange.onAttached(function attached_eventMaps(range, element) {
_.each(eventMap, 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(' ');
handles.push(Blaze._EventSupport.listen(
element, newEvents, selector,
function (evt) {
if (! range.containsElement(evt.currentTarget))
return null;
var handlerThis = thisInHandler || this;
var handlerArgs = arguments;
return Blaze._withCurrentView(view, function () {
return handler.apply(handlerThis, handlerArgs);
});
},
range, function (r) {
return r.parentRange;
}));
});
});
});
view.onViewDestroyed(function () {
_.each(handles, function (h) {
h.stop();
});
handles.length = 0;
});
};
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/blaze/builtins.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
Blaze._calculateCondition = function (cond) {
if (cond instanceof Array && cond.length === 0)
cond = false;
return !! cond;
};
/**
* @summary Constructs a View that renders content with a data context.
* @locus Client
* @param {Object|Function} data An object to use as the data context, or a function returning such an object. If a function is provided, it will be reactively re-run.
* @param {Function} contentFunc A Function that returns [*renderable content*](#renderable_content).
*/
Blaze.With = function (data, contentFunc) {
var view = Blaze.View('with', contentFunc);
view.dataVar = new ReactiveVar;
view.onViewCreated(function () {
if (typeof data === 'function') {
// `data` is a reactive function
view.autorun(function () {
view.dataVar.set(data());
}, view.parentView, 'setData');
} else {
view.dataVar.set(data);
}
});
return view;
};
/**
* Attaches bindings to the instantiated view.
* @param {Object} bindings A dictionary of bindings, each binding name
* corresponds to a value or a function that will be reactively re-run.
* @param {View} view The target.
*/
Blaze._attachBindingsToView = function (bindings, view) {
view.onViewCreated(function () {
_.each(bindings, function (binding, name) {
view._scopeBindings[name] = new ReactiveVar;
if (typeof binding === 'function') {
view.autorun(function () {
view._scopeBindings[name].set(binding());
}, view.parentView);
} else {
view._scopeBindings[name].set(binding);
}
});
});
};
/**
* @summary Constructs a View setting the local lexical scope in the block.
* @param {Function} bindings Dictionary mapping names of bindings to
* values or computations to reactively re-run.
* @param {Function} contentFunc A Function that returns [*renderable content*](#renderable_content).
*/
Blaze.Let = function (bindings, contentFunc) {
var view = Blaze.View('let', contentFunc);
Blaze._attachBindingsToView(bindings, view);
return view;
};
/**
* @summary Constructs a View that renders content conditionally.
* @locus Client
* @param {Function} conditionFunc A function to reactively re-run. Whether the result is truthy or falsy determines whether `contentFunc` or `elseFunc` is shown. An empty array is considered falsy.
* @param {Function} contentFunc A Function that returns [*renderable content*](#renderable_content).
* @param {Function} [elseFunc] Optional. A Function that returns [*renderable content*](#renderable_content). If no `elseFunc` is supplied, no content is shown in the "else" case.
*/
Blaze.If = function (conditionFunc, contentFunc, elseFunc, _not) {
var conditionVar = new ReactiveVar;
var view = Blaze.View(_not ? 'unless' : 'if', function () {
return conditionVar.get() ? contentFunc() :
(elseFunc ? elseFunc() : null);
});
view.__conditionVar = conditionVar;
view.onViewCreated(function () {
this.autorun(function () {
var cond = Blaze._calculateCondition(conditionFunc());
conditionVar.set(_not ? (! cond) : cond);
}, this.parentView, 'condition');
});
return view;
};
/**
* @summary An inverted [`Blaze.If`](#blaze_if).
* @locus Client
* @param {Function} conditionFunc A function to reactively re-run. If the result is falsy, `contentFunc` is shown, otherwise `elseFunc` is shown. An empty array is considered falsy.
* @param {Function} contentFunc A Function that returns [*renderable content*](#renderable_content).
* @param {Function} [elseFunc] Optional. A Function that returns [*renderable content*](#renderable_content). If no `elseFunc` is supplied, no content is shown in the "else" case.
*/
Blaze.Unless = function (conditionFunc, contentFunc, elseFunc) {
return Blaze.If(conditionFunc, contentFunc, elseFunc, true /*_not*/);
};
/**
* @summary Constructs a View that renders `contentFunc` for each item in a sequence.
* @locus Client
* @param {Function} argFunc A function to reactively re-run. The function can
* return one of two options:
*
* 1. An object with two fields: '_variable' and '_sequence'. Each iterates over
* '_sequence', it may be a Cursor, an array, null, or undefined. Inside the
* Each body you will be able to get the current item from the sequence using
* the name specified in the '_variable' field.
*
* 2. Just a sequence (Cursor, array, null, or undefined) not wrapped into an
* object. Inside the Each body, the current item will be set as the data
* context.
* @param {Function} contentFunc A Function that returns [*renderable
* content*](#renderable_content).
* @param {Function} [elseFunc] A Function that returns [*renderable
* content*](#renderable_content) to display in the case when there are no items
* in the sequence.
*/
Blaze.Each = function (argFunc, contentFunc, elseFunc) {
var eachView = Blaze.View('each', function () {
var subviews = this.initialSubviews;
this.initialSubviews = null;
if (this._isCreatedForExpansion) {
this.expandedValueDep = new Tracker.Dependency;
this.expandedValueDep.depend();
}
return subviews;
});
eachView.initialSubviews = [];
eachView.numItems = 0;
eachView.inElseMode = false;
eachView.stopHandle = null;
eachView.contentFunc = contentFunc;
eachView.elseFunc = elseFunc;
eachView.argVar = new ReactiveVar;
eachView.variableName = null;
// update the @index value in the scope of all subviews in the range
var updateIndices = function (from, to) {
if (to === undefined) {
to = eachView.numItems - 1;
}
for (var i = from; i <= to; i++) {
var view = eachView._domrange.members[i].view;
view._scopeBindings['@index'].set(i);
}
};
eachView.onViewCreated(function () {
// We evaluate argFunc in an autorun to make sure
// Blaze.currentView is always set when it runs (rather than
// passing argFunc straight to ObserveSequence).
eachView.autorun(function () {
// argFunc can return either a sequence as is or a wrapper object with a
// _sequence and _variable fields set.
var arg = argFunc();
if (_.isObject(arg) && _.has(arg, '_sequence')) {
eachView.variableName = arg._variable || null;
arg = arg._sequence;
}
eachView.argVar.set(arg);
}, eachView.parentView, 'collection');
eachView.stopHandle = ObserveSequence.observe(function () {
return eachView.argVar.get();
}, {
addedAt: function (id, item, index) {
Tracker.nonreactive(function () {
var newItemView;
if (eachView.variableName) {
// new-style #each (as in {{#each item in items}})
// doesn't create a new data context
newItemView = Blaze.View('item', eachView.contentFunc);
} else {
newItemView = Blaze.With(item, eachView.contentFunc);
}
eachView.numItems++;
var bindings = {};
bindings['@index'] = index;
if (eachView.variableName) {
bindings[eachView.variableName] = item;
}
Blaze._attachBindingsToView(bindings, newItemView);
if (eachView.expandedValueDep) {
eachView.expandedValueDep.changed();
} else if (eachView._domrange) {
if (eachView.inElseMode) {
eachView._domrange.removeMember(0);
eachView.inElseMode = false;
}
var range = Blaze._materializeView(newItemView, eachView);
eachView._domrange.addMember(range, index);
updateIndices(index);
} else {
eachView.initialSubviews.splice(index, 0, newItemView);
}
});
},
removedAt: function (id, item, index) {
Tracker.nonreactive(function () {
eachView.numItems--;
if (eachView.expandedValueDep) {
eachView.expandedValueDep.changed();
} else if (eachView._domrange) {
eachView._domrange.removeMember(index);
updateIndices(index);
if (eachView.elseFunc && eachView.numItems === 0) {
eachView.inElseMode = true;
eachView._domrange.addMember(
Blaze._materializeView(
Blaze.View('each_else',eachView.elseFunc),
eachView), 0);
}
} else {
eachView.initialSubviews.splice(index, 1);
}
});
},
changedAt: function (id, newItem, oldItem, index) {
Tracker.nonreactive(function () {
if (eachView.expandedValueDep) {
eachView.expandedValueDep.changed();
} else {
var itemView;
if (eachView._domrange) {
itemView = eachView._domrange.getMember(index).view;
} else {
itemView = eachView.initialSubviews[index];
}
if (eachView.variableName) {
itemView._scopeBindings[eachView.variableName].set(newItem);
} else {
itemView.dataVar.set(newItem);
}
}
});
},
movedTo: function (id, item, fromIndex, toIndex) {
Tracker.nonreactive(function () {
if (eachView.expandedValueDep) {
eachView.expandedValueDep.changed();
} else if (eachView._domrange) {
eachView._domrange.moveMember(fromIndex, toIndex);
updateIndices(
Math.min(fromIndex, toIndex), Math.max(fromIndex, toIndex));
} else {
var subviews = eachView.initialSubviews;
var itemView = subviews[fromIndex];
subviews.splice(fromIndex, 1);
subviews.splice(toIndex, 0, itemView);
}
});
}
});
if (eachView.elseFunc && eachView.numItems === 0) {
eachView.inElseMode = true;
eachView.initialSubviews[0] =
Blaze.View('each_else', eachView.elseFunc);
}
});
eachView.onViewDestroyed(function () {
if (eachView.stopHandle)
eachView.stopHandle.stop();
});
return eachView;
};
Blaze._TemplateWith = function (arg, contentFunc) {
var w;
var argFunc = arg;
if (typeof arg !== 'function') {
argFunc = function () {
return arg;
};
}
// This is a little messy. When we compile `{{> Template.contentBlock}}`, we
// wrap it in Blaze._InOuterTemplateScope in order to skip the intermediate
// parent Views in the current template. However, when there's an argument
// (`{{> Template.contentBlock arg}}`), the argument needs to be evaluated
// in the original scope. There's no good order to nest
// Blaze._InOuterTemplateScope and Spacebars.TemplateWith to achieve this,
// so we wrap argFunc to run it in the "original parentView" of the
// Blaze._InOuterTemplateScope.
//
// To make this better, reconsider _InOuterTemplateScope as a primitive.
// Longer term, evaluate expressions in the proper lexical scope.
var wrappedArgFunc = function () {
var viewToEvaluateArg = null;
if (w.parentView && w.parentView.name === 'InOuterTemplateScope') {
viewToEvaluateArg = w.parentView.originalParentView;
}
if (viewToEvaluateArg) {
return Blaze._withCurrentView(viewToEvaluateArg, argFunc);
} else {
return argFunc();
}
};
var wrappedContentFunc = function () {
var content = contentFunc.call(this);
// Since we are generating the Blaze._TemplateWith view for the
// user, set the flag on the child view. If `content` is a template,
// construct the View so that we can set the flag.
if (content instanceof Blaze.Template) {
content = content.constructView();
}
if (content instanceof Blaze.View) {
content._hasGeneratedParent = true;
}
return content;
};
w = Blaze.With(wrappedArgFunc, wrappedContentFunc);
w.__isTemplateWith = true;
return w;
};
Blaze._InOuterTemplateScope = function (templateView, contentFunc) {
var view = Blaze.View('InOuterTemplateScope', contentFunc);
var parentView = templateView.parentView;
// Hack so that if you call `{{> foo bar}}` and it expands into
// `{{#with bar}}{{> foo}}{{/with}}`, and then `foo` is a template
// that inserts `{{> Template.contentBlock}}`, the data context for
// `Template.contentBlock` is not `bar` but the one enclosing that.
if (parentView.__isTemplateWith)
parentView = parentView.parentView;
view.onViewCreated(function () {
this.originalParentView = this.parentView;
this.parentView = parentView;
this.__childDoesntStartNewLexicalScope = true;
});
return view;
};
// XXX COMPAT WITH 0.9.0
Blaze.InOuterTemplateScope = Blaze._InOuterTemplateScope;
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/blaze/lookup.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
Blaze._globalHelpers = {};
// Documented as Template.registerHelper.
// This definition also provides back-compat for `UI.registerHelper`.
Blaze.registerHelper = function (name, func) {
Blaze._globalHelpers[name] = func;
};
// Also documented as Template.deregisterHelper
Blaze.deregisterHelper = function(name) {
delete Blaze._globalHelpers[name];
};
var bindIfIsFunction = function (x, target) {
if (typeof x !== 'function')
return x;
return Blaze._bind(x, target);
};
// If `x` is a function, binds the value of `this` for that function
// to the current data context.
var bindDataContext = function (x) {
if (typeof x === 'function') {
return function () {
var data = Blaze.getData();
if (data == null)
data = {};
return x.apply(data, arguments);
};
}
return x;
};
Blaze._OLDSTYLE_HELPER = {};
Blaze._getTemplateHelper = function (template, name, tmplInstanceFunc) {
// XXX COMPAT WITH 0.9.3
var isKnownOldStyleHelper = false;
if (template.__helpers.has(name)) {
var helper = template.__helpers.get(name);
if (helper === Blaze._OLDSTYLE_HELPER) {
isKnownOldStyleHelper = true;
} else if (helper != null) {
return wrapHelper(bindDataContext(helper), tmplInstanceFunc);
} else {
return null;
}
}
// old-style helper
if (name in template) {
// Only warn once per helper
if (! isKnownOldStyleHelper) {
template.__helpers.set(name, Blaze._OLDSTYLE_HELPER);
if (! template._NOWARN_OLDSTYLE_HELPERS) {
Blaze._warn('Assigning helper with `' + template.viewName + '.' +
name + ' = ...` is deprecated. Use `' + template.viewName +
'.helpers(...)` instead.');
}
}
if (template[name] != null) {
return wrapHelper(bindDataContext(template[name]), tmplInstanceFunc);
}
}
return null;
};
var wrapHelper = function (f, templateFunc) {
if (typeof f !== "function") {
return f;
}
return function () {
var self = this;
var args = arguments;
return Blaze.Template._withTemplateInstanceFunc(templateFunc, function () {
return Blaze._wrapCatchingExceptions(f, 'template helper').apply(self, args);
});
};
};
Blaze._lexicalBindingLookup = function (view, name) {
var currentView = view;
var blockHelpersStack = [];
// walk up the views stopping at a Spacebars.include or Template view that
// doesn't have an InOuterTemplateScope view as a parent
do {
// skip block helpers views
// if we found the binding on the scope, return it
if (_.has(currentView._scopeBindings, name)) {
var bindingReactiveVar = currentView._scopeBindings[name];
return function () {
return bindingReactiveVar.get();
};
}
} while (! (currentView.__startsNewLexicalScope &&
! (currentView.parentView &&
currentView.parentView.__childDoesntStartNewLexicalScope))
&& (currentView = currentView.parentView));
return null;
};
// templateInstance argument is provided to be available for possible
// alternative implementations of this function by 3rd party packages.
Blaze._getTemplate = function (name, templateInstance) {
if ((name in Blaze.Template) && (Blaze.Template[name] instanceof Blaze.Template)) {
return Blaze.Template[name];
}
return null;
};
Blaze._getGlobalHelper = function (name, templateInstance) {
if (Blaze._globalHelpers[name] != null) {
return wrapHelper(bindDataContext(Blaze._globalHelpers[name]), templateInstance);
}
return null;
};
// Looks up a name, like "foo" or "..", as a helper of the
// current template; the name of a template; a global helper;
// or a property of the data context. Called on the View of
// a template (i.e. a View with a `.template` property,
// where the helpers are). Used for the first name in a
// "path" in a template tag, like "foo" in `{{foo.bar}}` or
// ".." in `{{frobulate ../blah}}`.
//
// Returns a function, a non-function value, or null. If
// a function is found, it is bound appropriately.
//
// NOTE: This function must not establish any reactive
// dependencies itself. If there is any reactivity in the
// value, lookup should return a function.
Blaze.View.prototype.lookup = function (name, _options) {
var template = this.template;
var lookupTemplate = _options && _options.template;
var helper;
var binding;
var boundTmplInstance;
var foundTemplate;
if (this.templateInstance) {
boundTmplInstance = Blaze._bind(this.templateInstance, this);
}
// 0. looking up the parent data context with the special "../" syntax
if (/^\./.test(name)) {
// starts with a dot. must be a series of dots which maps to an
// ancestor of the appropriate height.
if (!/^(\.)+$/.test(name))
throw new Error("id starting with dot must be a series of dots");
return Blaze._parentData(name.length - 1, true /*_functionWrapped*/);
}
// 1. look up a helper on the current template
if (template && ((helper = Blaze._getTemplateHelper(template, name, boundTmplInstance)) != null)) {
return helper;
}
// 2. look up a binding by traversing the lexical view hierarchy inside the
// current template
if (template && (binding = Blaze._lexicalBindingLookup(Blaze.currentView, name)) != null) {
return binding;
}
// 3. look up a template by name
if (lookupTemplate && ((foundTemplate = Blaze._getTemplate(name, boundTmplInstance)) != null)) {
return foundTemplate;
}
// 4. look up a global helper
if ((helper = Blaze._getGlobalHelper(name, boundTmplInstance)) != null) {
return helper;
}
// 5. look up in a data context
return function () {
var isCalledAsFunction = (arguments.length > 0);
var data = Blaze.getData();
var x = data && data[name];
if (! x) {
if (lookupTemplate) {
throw new Error("No such template: " + name);
} else if (isCalledAsFunction) {
throw new Error("No such function: " + name);
} else if (name.charAt(0) === '@' && ((x === null) ||
(x === undefined))) {
// Throw an error if the user tries to use a `@directive`
// that doesn't exist. We don't implement all directives
// from Handlebars, so there's a potential for confusion
// if we fail silently. On the other hand, we want to
// throw late in case some app or package wants to provide
// a missing directive.
throw new Error("Unsupported directive: " + name);
}
}
if (! data) {
return null;
}
if (typeof x !== 'function') {
if (isCalledAsFunction) {
throw new Error("Can't call non-function: " + x);
}
return x;
}
return x.apply(data, arguments);
};
};
// Implement Spacebars' {{../..}}.
// @param height {Number} The number of '..'s
Blaze._parentData = function (height, _functionWrapped) {
// If height is null or undefined, we default to 1, the first parent.
if (height == null) {
height = 1;
}
var theWith = Blaze.getView('with');
for (var i = 0; (i < height) && theWith; i++) {
theWith = Blaze.getView(theWith, 'with');
}
if (! theWith)
return null;
if (_functionWrapped)
return function () { return theWith.dataVar.get(); };
return theWith.dataVar.get();
};
Blaze.View.prototype.lookupTemplate = function (name) {
return this.lookup(name, {template:true});
};
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/blaze/template.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// [new] Blaze.Template([viewName], renderFunction)
//
// `Blaze.Template` is the class of templates, like `Template.foo` in
// Meteor, which is `instanceof Template`.
//
// `viewKind` is a string that looks like "Template.foo" for templates
// defined by the compiler.
/**
* @class
* @summary Constructor for a Template, which is used to construct Views with particular name and content.
* @locus Client
* @param {String} [viewName] Optional. A name for Views constructed by this Template. See [`view.name`](#view_name).
* @param {Function} renderFunction A function that returns [*renderable content*](#renderable_content). This function is used as the `renderFunction` for Views constructed by this Template.
*/
Blaze.Template = function (viewName, renderFunction) {
if (! (this instanceof Blaze.Template))
// called without `new`
return new Blaze.Template(viewName, renderFunction);
if (typeof viewName === 'function') {
// omitted "viewName" argument
renderFunction = viewName;
viewName = '';
}
if (typeof viewName !== 'string')
throw new Error("viewName must be a String (or omitted)");
if (typeof renderFunction !== 'function')
throw new Error("renderFunction must be a function");
this.viewName = viewName;
this.renderFunction = renderFunction;
this.__helpers = new HelperMap;
this.__eventMaps = [];
this._callbacks = {
created: [],
rendered: [],
destroyed: []
};
};
var Template = Blaze.Template;
var HelperMap = function () {};
HelperMap.prototype.get = function (name) {
return this[' '+name];
};
HelperMap.prototype.set = function (name, helper) {
this[' '+name] = helper;
};
HelperMap.prototype.has = function (name) {
return (' '+name) in this;
};
/**
* @summary Returns true if `value` is a template object like `Template.myTemplate`.
* @locus Client
* @param {Any} value The value to test.
*/
Blaze.isTemplate = function (t) {
return (t instanceof Blaze.Template);
};
/**
* @name onCreated
* @instance
* @memberOf Template
* @summary Register a function to be called when an instance of this template is created.
* @param {Function} callback A function to be added as a callback.
* @locus Client
* @importFromPackage templating
*/
Template.prototype.onCreated = function (cb) {
this._callbacks.created.push(cb);
};
/**
* @name onRendered
* @instance
* @memberOf Template
* @summary Register a function to be called when an instance of this template is inserted into the DOM.
* @param {Function} callback A function to be added as a callback.
* @locus Client
* @importFromPackage templating
*/
Template.prototype.onRendered = function (cb) {
this._callbacks.rendered.push(cb);
};
/**
* @name onDestroyed
* @instance
* @memberOf Template
* @summary Register a function to be called when an instance of this template is removed from the DOM and destroyed.
* @param {Function} callback A function to be added as a callback.
* @locus Client
* @importFromPackage templating
*/
Template.prototype.onDestroyed = function (cb) {
this._callbacks.destroyed.push(cb);
};
Template.prototype._getCallbacks = function (which) {
var self = this;
var callbacks = self[which] ? [self[which]] : [];
// Fire all callbacks added with the new API (Template.onRendered())
// as well as the old-style callback (e.g. Template.rendered) for
// backwards-compatibility.
callbacks = callbacks.concat(self._callbacks[which]);
return callbacks;
};
var fireCallbacks = function (callbacks, template) {
Template._withTemplateInstanceFunc(
function () { return template; },
function () {
for (var i = 0, N = callbacks.length; i < N; i++) {
callbacks[i].call(template);
}
});
};
Template.prototype.constructView = function (contentFunc, elseFunc) {
var self = this;
var view = Blaze.View(self.viewName, self.renderFunction);
view.template = self;
view.templateContentBlock = (
contentFunc ? new Template('(contentBlock)', contentFunc) : null);
view.templateElseBlock = (
elseFunc ? new Template('(elseBlock)', elseFunc) : null);
if (self.__eventMaps || typeof self.events === 'object') {
view._onViewRendered(function () {
if (view.renderCount !== 1)
return;
if (! self.__eventMaps.length && typeof self.events === "object") {
// Provide limited back-compat support for `.events = {...}`
// syntax. Pass `template.events` to the original `.events(...)`
// function. This code must run only once per template, in
// order to not bind the handlers more than once, which is
// ensured by the fact that we only do this when `__eventMaps`
// is falsy, and we cause it to be set now.
Template.prototype.events.call(self, self.events);
}
_.each(self.__eventMaps, function (m) {
Blaze._addEventMap(view, m, view);
});
});
}
view._templateInstance = new Blaze.TemplateInstance(view);
view.templateInstance = function () {
// Update data, firstNode, and lastNode, and return the TemplateInstance
// object.
var inst = view._templateInstance;
/**
* @instance
* @memberOf Blaze.TemplateInstance
* @name data
* @summary The data context of this instance's latest invocation.
* @locus Client
*/
inst.data = Blaze.getData(view);
if (view._domrange && !view.isDestroyed) {
inst.firstNode = view._domrange.firstNode();
inst.lastNode = view._domrange.lastNode();
} else {
// on 'created' or 'destroyed' callbacks we don't have a DomRange
inst.firstNode = null;
inst.lastNode = null;
}
return inst;
};
/**
* @name created
* @instance
* @memberOf Template
* @summary Provide a callback when an instance of a template is created.
* @locus Client
* @deprecated in 1.1
*/
// To avoid situations when new callbacks are added in between view
// instantiation and event being fired, decide on all callbacks to fire
// immediately and then fire them on the event.
var createdCallbacks = self._getCallbacks('created');
view.onViewCreated(function () {
fireCallbacks(createdCallbacks, view.templateInstance());
});
/**
* @name rendered
* @instance
* @memberOf Template
* @summary Provide a callback when an instance of a template is rendered.
* @locus Client
* @deprecated in 1.1
*/
var renderedCallbacks = self._getCallbacks('rendered');
view.onViewReady(function () {
fireCallbacks(renderedCallbacks, view.templateInstance());
});
/**
* @name destroyed
* @instance
* @memberOf Template
* @summary Provide a callback when an instance of a template is destroyed.
* @locus Client
* @deprecated in 1.1
*/
var destroyedCallbacks = self._getCallbacks('destroyed');
view.onViewDestroyed(function () {
fireCallbacks(destroyedCallbacks, view.templateInstance());
});
return view;
};
/**
* @class
* @summary The class for template instances
* @param {Blaze.View} view
* @instanceName template
*/
Blaze.TemplateInstance = function (view) {
if (! (this instanceof Blaze.TemplateInstance))
// called without `new`
return new Blaze.TemplateInstance(view);
if (! (view instanceof Blaze.View))
throw new Error("View required");
view._templateInstance = this;
/**
* @name view
* @memberOf Blaze.TemplateInstance
* @instance
* @summary The [View](#blaze_view) object for this invocation of the template.
* @locus Client
* @type {Blaze.View}
*/
this.view = view;
this.data = null;
/**
* @name firstNode
* @memberOf Blaze.TemplateInstance
* @instance
* @summary The first top-level DOM node in this template instance.
* @locus Client
* @type {DOMNode}
*/
this.firstNode = null;
/**
* @name lastNode
* @memberOf Blaze.TemplateInstance
* @instance
* @summary The last top-level DOM node in this template instance.
* @locus Client
* @type {DOMNode}
*/
this.lastNode = null;
// This dependency is used to identify state transitions in
// _subscriptionHandles which could cause the result of
// TemplateInstance#subscriptionsReady to change. Basically this is triggered
// whenever a new subscription handle is added or when a subscription handle
// is removed and they are not ready.
this._allSubsReadyDep = new Tracker.Dependency();
this._allSubsReady = false;
this._subscriptionHandles = {};
};
/**
* @summary Find all elements matching `selector` in this template instance, and return them as a JQuery object.
* @locus Client
* @param {String} selector The CSS selector to match, scoped to the template contents.
* @returns {DOMNode[]}
*/
Blaze.TemplateInstance.prototype.$ = function (selector) {
var view = this.view;
if (! view._domrange)
throw new Error("Can't use $ on template instance with no DOM");
return view._domrange.$(selector);
};
/**
* @summary Find all elements matching `selector` in this template instance.
* @locus Client
* @param {String} selector The CSS selector to match, scoped to the template contents.
* @returns {DOMElement[]}
*/
Blaze.TemplateInstance.prototype.findAll = function (selector) {
return Array.prototype.slice.call(this.$(selector));
};
/**
* @summary Find one element matching `selector` in this template instance.
* @locus Client
* @param {String} selector The CSS selector to match, scoped to the template contents.
* @returns {DOMElement}
*/
Blaze.TemplateInstance.prototype.find = function (selector) {
var result = this.$(selector);
return result[0] || null;
};
/**
* @summary A version of [Tracker.autorun](#tracker_autorun) that is stopped when the template is destroyed.
* @locus Client
* @param {Function} runFunc The function to run. It receives one argument: a Tracker.Computation object.
*/
Blaze.TemplateInstance.prototype.autorun = function (f) {
return this.view.autorun(f);
};
/**
* @summary A version of [Meteor.subscribe](#meteor_subscribe) that is stopped
* when the template is destroyed.
* @return {SubscriptionHandle} The subscription handle to the newly made
* subscription. Call `handle.stop()` to manually stop the subscription, or
* `handle.ready()` to find out if this particular subscription has loaded all
* of its inital data.
* @locus Client
* @param {String} name Name of the subscription. Matches the name of the
* server's `publish()` call.
* @param {Any} [arg1,arg2...] Optional arguments passed to publisher function
* on server.
* @param {Function|Object} [options] If a function is passed instead of an
* object, it is interpreted as an `onReady` callback.
* @param {Function} [options.onReady] Passed to [`Meteor.subscribe`](#meteor_subscribe).
* @param {Function} [options.onStop] Passed to [`Meteor.subscribe`](#meteor_subscribe).
* @param {DDP.Connection} [options.connection] The connection on which to make the
* subscription.
*/
Blaze.TemplateInstance.prototype.subscribe = function (/* arguments */) {
var self = this;
var subHandles = self._subscriptionHandles;
var args = _.toArray(arguments);
// Duplicate logic from Meteor.subscribe
var options = {};
if (args.length) {
var lastParam = _.last(args);
// Match pattern to check if the last arg is an options argument
var lastParamOptionsPattern = {
onReady: Match.Optional(Function),
// XXX COMPAT WITH 1.0.3.1 onError used to exist, but now we use
// onStop with an error callback instead.
onError: Match.Optional(Function),
onStop: Match.Optional(Function),
connection: Match.Optional(Match.Any)
};
if (_.isFunction(lastParam)) {
options.onReady = args.pop();
} else if (lastParam && ! _.isEmpty(lastParam) && Match.test(lastParam, lastParamOptionsPattern)) {
options = args.pop();
}
}
var subHandle;
var oldStopped = options.onStop;
options.onStop = function (error) {
// When the subscription is stopped, remove it from the set of tracked
// subscriptions to avoid this list growing without bound
delete subHandles[subHandle.subscriptionId];
// Removing a subscription can only change the result of subscriptionsReady
// if we are not ready (that subscription could be the one blocking us being
// ready).
if (! self._allSubsReady) {
self._allSubsReadyDep.changed();
}
if (oldStopped) {
oldStopped(error);
}
};
var connection = options.connection;
var callbacks = _.pick(options, ["onReady", "onError", "onStop"]);
// The callbacks are passed as the last item in the arguments array passed to
// View#subscribe
args.push(callbacks);
// View#subscribe takes the connection as one of the options in the last
// argument
subHandle = self.view.subscribe.call(self.view, args, {
connection: connection
});
if (! _.has(subHandles, subHandle.subscriptionId)) {
subHandles[subHandle.subscriptionId] = subHandle;
// Adding a new subscription will always cause us to transition from ready
// to not ready, but if we are already not ready then this can't make us
// ready.
if (self._allSubsReady) {
self._allSubsReadyDep.changed();
}
}
return subHandle;
};
/**
* @summary A reactive function that returns true when all of the subscriptions
* called with [this.subscribe](#TemplateInstance-subscribe) are ready.
* @return {Boolean} True if all subscriptions on this template instance are
* ready.
*/
Blaze.TemplateInstance.prototype.subscriptionsReady = function () {
this._allSubsReadyDep.depend();
this._allSubsReady = _.all(this._subscriptionHandles, function (handle) {
return handle.ready();
});
return this._allSubsReady;
};
/**
* @summary Specify template helpers available to this template.
* @locus Client
* @param {Object} helpers Dictionary of helper functions by name.
* @importFromPackage templating
*/
Template.prototype.helpers = function (dict) {
if (! _.isObject(dict)) {
throw new Error("Helpers dictionary has to be an object");
}
for (var k in dict)
this.__helpers.set(k, dict[k]);
};
// Kind of like Blaze.currentView but for the template instance.
// This is a function, not a value -- so that not all helpers
// are implicitly dependent on the current template instance's `data` property,
// which would make them dependenct on the data context of the template
// inclusion.
Template._currentTemplateInstanceFunc = null;
Template._withTemplateInstanceFunc = function (templateInstanceFunc, func) {
if (typeof func !== 'function')
throw new Error("Expected function, got: " + func);
var oldTmplInstanceFunc = Template._currentTemplateInstanceFunc;
try {
Template._currentTemplateInstanceFunc = templateInstanceFunc;
return func();
} finally {
Template._currentTemplateInstanceFunc = oldTmplInstanceFunc;
}
};
/**
* @summary Specify event handlers for this template.
* @locus Client
* @param {EventMap} eventMap Event handlers to associate with this template.
* @importFromPackage templating
*/
Template.prototype.events = function (eventMap) {
if (! _.isObject(eventMap)) {
throw new Error("Event map has to be an object");
}
var template = this;
var eventMap2 = {};
for (var k in eventMap) {
eventMap2[k] = (function (k, v) {
return function (event/*, ...*/) {
var view = this; // passed by EventAugmenter
var data = Blaze.getData(event.currentTarget);
if (data == null)
data = {};
var args = Array.prototype.slice.call(arguments);
var tmplInstanceFunc = Blaze._bind(view.templateInstance, view);
args.splice(1, 0, tmplInstanceFunc());
return Template._withTemplateInstanceFunc(tmplInstanceFunc, function () {
return v.apply(data, args);
});
};
})(k, eventMap[k]);
}
template.__eventMaps.push(eventMap2);
};
/**
* @function
* @name instance
* @memberOf Template
* @summary The [template instance](#template_inst) corresponding to the current template helper, event handler, callback, or autorun. If there isn't one, `null`.
* @locus Client
* @returns {Blaze.TemplateInstance}
* @importFromPackage templating
*/
Template.instance = function () {
return Template._currentTemplateInstanceFunc
&& Template._currentTemplateInstanceFunc();
};
// Note: Template.currentData() is documented to take zero arguments,
// while Blaze.getData takes up to one.
/**
* @summary
*
* - Inside an `onCreated`, `onRendered`, or `onDestroyed` callback, returns
* the data context of the template.
* - Inside an event handler, returns the data context of the template on which
* this event handler was defined.
* - Inside a helper, returns the data context of the DOM node where the helper
* was used.
*
* Establishes a reactive dependency on the result.
* @locus Client
* @function
* @importFromPackage templating
*/
Template.currentData = Blaze.getData;
/**
* @summary Accesses other data contexts that enclose the current data context.
* @locus Client
* @function
* @param {Integer} [numLevels] The number of levels beyond the current data context to look. Defaults to 1.
* @importFromPackage templating
*/
Template.parentData = Blaze._parentData;
/**
* @summary Defines a [helper function](#template_helpers) which can be used from all templates.
* @locus Client
* @function
* @param {String} name The name of the helper function you are defining.
* @param {Function} function The helper function itself.
* @importFromPackage templating
*/
Template.registerHelper = Blaze.registerHelper;
/**
* @summary Removes a global [helper function](#template_helpers).
* @locus Client
* @function
* @param {String} name The name of the helper function you are defining.
* @importFromPackage templating
*/
Template.deregisterHelper = Blaze.deregisterHelper;
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/blaze/backcompat.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
UI = Blaze;
Blaze.ReactiveVar = ReactiveVar;
UI._templateInstance = Blaze.Template.instance;
Handlebars = {};
Handlebars.registerHelper = Blaze.registerHelper;
Handlebars._escape = Blaze._escape;
// Return these from {{...}} helpers to achieve the same as returning
// strings from {{{...}}} helpers
Handlebars.SafeString = function(string) {
this.string = string;
};
Handlebars.SafeString.prototype.toString = function() {
return this.string.toString();
};
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
/* Exports */
Package._define("blaze", {
Blaze: Blaze,
UI: UI,
Handlebars: Handlebars
});
})();