////////////////////////////////////////////////////////////////////////// // // // 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 = { "<": "<", ">": ">", '"': """, "'": "'", "/": "/", "`": "`", /* IE allows backtick-delimited attributes?? */ "&": "&" }; 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 elements because // if "A" were in our list of known svg element names, then all // nodes would be created using // `document.createElementNS`. But in the special case of , we can at least detect that attribute and // create an SVG 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 parentNode; 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 parentNode; 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 }); })();