UNPKG

evan

Version:
1,417 lines (1,251 loc) 56 kB
/*! evan - v0.8.1 - 2015-08-18 - Dan Stocker <dan@kwaia.com> - Copyright (c) 2012-2015 Dan Stocker*/ /** * Top-Level Library Namespace */ /*global require */ /** @namespace */ var evan = {}, e$ = evan; /** * @class * @see https://github.com/production-minds/dessert */ var dessert = dessert || require('dessert'); /** * @namespace * @see https://github.com/danstocker/troop */ var troop = troop || require('troop'); /** * @namespace * @see https://github.com/danstocker/sntls */ var sntls = sntls || require('sntls'); /** * Interface that marks a class as an event spawner. Event spawners create and prepare instances of evan.Event. * @name evan.EventSpawner * @class * @extends troop.Base */ /** * Creates and prepares and event with the specified name. * @name evan.EventSpawner#spawnEvent * @function * @param {string} eventName * @returns {evan.Event} */ /** * Interface that marks a class as a source of events, ie. triggering and broadcasting events. * @name evan.EventSource * @class * @extends troop.Base */ /** * Triggers an event of a specific type on a specific path in a specific event space. * These parameters may be passed to this method, or defined elsewhere depending on the implementation. * Triggered events are unidirectional, optionally bubbling towards the root path. * @name evan.EventSource#triggerSync * @function * @returns {evan.EventSource} */ /** * Broadcasts an event of a specific type on a specific path in a specific event space. * These parameters may be passed to this method, or defined elsewhere depending on the implementation. * Broadcast events will call all handlers subscribed at a path relative to the broadcast path. * @name evan.EventSource#broadcastSync * @function * @returns {evan.EventSource} */ /** * Interface that marks a class as target for events. Event targets may subscribe to events. * @name evan.EventTarget * @class * @extends troop.Base */ /** * Subscribes a handler to the specified event, in a specific event space. * @name evan.EventTarget#subscribeTo * @function * @param {string} eventName * @returns {evan.EventTarget} */ /** * Unsubscribes a handler from the specified event, in a specific event space. * @name evan.EventTarget#unsubscribeFrom * @function * @param {string} eventName * @returns {evan.EventTarget} */ /** * Subscribes a handler to the specified event, in a specific event space, and unsubscribes after the first time it was triggered. * @name evan.EventTarget#subscribeToUntilTriggered * @function * @param {string} eventName * @returns {evan.EventTarget} */ /** * Subscribes a handler to the specified event, in a specific event space, but only if the event's original path matches a specified Query. * @name evan.EventTarget#delegateSubscriptionTo * @function * @param {string} eventName * @returns {evan.EventTarget} */ /*global dessert, troop, sntls, evan */ troop.postpone(evan, 'Link', function () { "use strict"; var base = troop.Base, self = base.extend(); /** * Creates a Link instance. * @name evan.Link.create * @function * @returns {evan.Link} */ /** * Basic link, can chain other links to it. * @class * @extends troop.Base */ evan.Link = self .addMethods(/** @lends evan.Link# */{ /** @ignore */ init: function () { /** * Link that comes before the current link in the chain. * @type {evan.Link} */ this.previousLink = undefined; /** * Link that comes after the current link in the chain. * @type {evan.Link} */ this.nextLink = undefined; /** * Chain instance the link is associated with. * @type {evan.OpenChain} */ this.parentChain = undefined; }, /** * Adds current unconnected link after the specified link. * @param {evan.Link} link * @returns {evan.Link} */ addAfter: function (link) { dessert.assert(!this.previousLink && !this.nextLink, "Attempted to connect already connected link"); // setting links on current link this.previousLink = link; this.nextLink = link.nextLink; this.parentChain = link.parentChain; // setting self as previous link on old next link if (link.nextLink) { link.nextLink.previousLink = this; } // setting self as next link on target link link.nextLink = this; return this; }, /** * Adds current link before the specified link. * @param {evan.Link} link * @returns {evan.Link} */ addBefore: function (link) { dessert.assert(!this.previousLink && !this.nextLink, "Attempted to connect already connected link"); // setting links on current link this.nextLink = link; this.previousLink = link.previousLink; this.parentChain = link.parentChain; // setting self as next link on old previous link if (link.previousLink) { link.previousLink.nextLink = this; } // setting self as previous link on target link link.previousLink = this; return this; }, /** * Removes link from the chain. * @returns {evan.Link} */ unLink: function () { var nextLink = this.nextLink, previousLink = this.previousLink; if (nextLink) { nextLink.previousLink = previousLink; } if (previousLink) { previousLink.nextLink = nextLink; } this.previousLink = undefined; this.nextLink = undefined; this.parentChain = undefined; return this; }, /** * Sets the parent chain on unconnected links. * Fails when called on connected links. * @param {evan.OpenChain} parentChain * @returns {evan.Link} */ setParentChain: function (parentChain) { dessert.assert(!this.previousLink && !this.nextLink, "Attempted to set parent chain on connected link"); this.parentChain = parentChain; return this; } }); }); /*global dessert, troop, sntls, evan */ troop.postpone(evan, 'ValueLink', function () { "use strict"; var base = evan.Link, self = base.extend(); /** * Creates a ValueLink instance. * @name evan.ValueLink.create * @function * @returns {evan.ValueLink} */ /** * Link that carries a value, and has the option to be unlinked. * @class * @extends evan.Link */ evan.ValueLink = self .addMethods(/** @lends evan.ValueLink# */{ /** @ignore */ init: function () { base.init.call(this); /** * Value associated with link. * @type {*} */ this.value = undefined; }, /** * Sets link value. * @param {*} value * @returns {evan.ValueLink} */ setValue: function (value) { this.value = value; return this; } }); }); /*global dessert, troop, sntls, evan */ troop.postpone(evan, 'OpenChain', function () { "use strict"; var base = troop.Base, self = base.extend(); /** * Creates an OpenChain instance. * @name evan.OpenChain.create * @function * @returns {evan.OpenChain} */ /** * Chain data structure with two fixed ends and value carrying links in between. * OpenChain behaves like a stack in that you may append and prepend the chain * using a stack-like API. (push, pop, etc.) * @class * @extends troop.Base */ evan.OpenChain = self .addMethods(/** @lends evan.OpenChain# */{ /** @ignore */ init: function () { /** * First (fixed) link in the chain. * @type {evan.ValueLink} */ this.firstLink = evan.Link.create() .setParentChain(this); /** * Last (fixed) link in the chain. * @type {evan.ValueLink} */ this.lastLink = evan.Link.create() .addAfter(this.firstLink); }, /** * Adds link at the end of the chain. * @param {evan.Link} link */ pushLink: function (link) { link.addBefore(this.lastLink); return this; }, /** * Removes link from the end of the chain and returns removed link. * @returns {evan.Link} */ popLink: function () { return this.lastLink.previousLink .unLink(); }, /** * Adds link at the start of the chain. * @param {evan.Link} link */ unshiftLink: function (link) { link.addAfter(this.firstLink); return this; }, /** * Removes link from the start of the chain and returns removed link. * @returns {evan.Link} */ shiftLink: function () { return this.firstLink.nextLink .unLink(); }, /** * Retrieves the values stored in the chain's links as an array. * O(n) complexity. * @returns {Array} */ getValues: function () { var link = this.firstLink.nextLink, result = []; while (link !== this.lastLink) { result.push(link.value); link = link.nextLink; } return result; } }); }); /*global dessert, troop, sntls, evan */ troop.postpone(evan, 'PathCollection', function () { "use strict"; /** * @name evan.PathCollection.create * @function * @param {object} [items] Initial contents. * @return {evan.PathCollection} */ /** * @name evan.PathCollection#asArray * @ignore */ /** * @class evan.PathCollection * @extends sntls.Collection * @extends sntls.Path */ evan.PathCollection = sntls.Collection.of(sntls.Path); }); /*global dessert, troop, sntls, evan */ troop.postpone(evan, 'Event', function () { "use strict"; var base = troop.Base, self = base.extend(); /** * Instantiates class. * @name evan.Event.create * @function * @param {string} eventName Event name * @param {evan.EventSpace} eventSpace Event space associated with event * @return {evan.Event} */ /** * An event is an object that may traverse in an event space. * Events carry all information regarding their position & properties. * @class * @extends troop.Base * @extends evan.EventSource */ evan.Event = self .addPrivateMethods(/** @lends evan.Event# */{ /** * Creates a new event instance and prepares it to be triggered. * @param {sntls.Path} targetPath * @return {evan.Event} * @private */ _spawnMainBroadcastEvent: function (targetPath) { return self.create(this.eventName, this.eventSpace) .setBroadcastPath(targetPath) .setTargetPath(targetPath); }, /** * Creates a new event instance and prepares it to be broadcast. * Broadcast events do not bubble. * @param {sntls.Path} broadcastPath * @param {sntls.Path} targetPath * @return {evan.Event} * @private */ _spawnBroadcastEvent: function (broadcastPath, targetPath) { return self.create(this.eventName, this.eventSpace) .allowBubbling(false) .setBroadcastPath(broadcastPath) .setTargetPath(targetPath); } }) .addMethods(/** @lends evan.Event# */{ /** * @param {string} eventName Event name * @param {evan.EventSpace} eventSpace Event space associated with event * @ignore */ init: function (eventName, eventSpace) { dessert .isString(eventName, "Invalid event name") .isEventSpace(eventSpace, "Invalid event space"); /** * @type {string} * @constant */ this.eventName = eventName; /** * @type {evan.EventSpace} * @constant */ this.eventSpace = eventSpace; /** * Whether the current event can bubble * @type {boolean} */ this.canBubble = true; /** * Evan event or DOM event that led to triggering the current event. * In most cases, this property is not set directly, but through * evan.pushOriginalEvent() * @type {evan.Event|*} * @see evan.pushOriginalEvent */ this.originalEvent = undefined; /** * Whether the event's default behavior was prevented. * @type {boolean} */ this.defaultPrevented = false; /** * Whether event was handled. (A subscribed handler ran.) * @type {boolean} */ this.handled = false; /** * Identifies the sender of the event. * @type {*} */ this.sender = undefined; /** * Custom payload to be carried by the event. * In most cases, this property is not modified directly, but through * evan.setNextPayloadItem() * @type {object} * @see evan.setNextPayloadItem */ this.payload = {}; /** * Path reflecting current state of bubbling * @type {evan.Path} */ this.currentPath = undefined; /** * Path on which the event was originally triggered * @type {sntls.Path} */ this.originalPath = undefined; /** * Reference to the original target path if * the event was triggered as part of a broadcast. * @type {sntls.Path} */ this.broadcastPath = undefined; }, /** * Clones event and optionally sets its currentPath property to * the one specified by the argument. * Override in subclasses to clone additional properties. * @param {sntls.Path} [currentPath] * @return {evan.Event} */ clone: function (currentPath) { dessert.isPathOptional(currentPath, "Invalid current event path"); var result = this.getBase().create(this.eventName, this.eventSpace); // transferring paths result.originalPath = this.originalPath; result.currentPath = currentPath ? currentPath.clone() : this.currentPath.clone(); result.broadcastPath = this.broadcastPath; // transferring event state result.originalEvent = this.originalEvent; result.defaultPrevented = this.defaultPrevented; result.handled = this.handled; // transferring load result.sender = this.sender; result.payload = this.payload; return result; }, /** * Sets whether the event can bubble * @param {boolean} value Bubbling flag * @return {evan.Event} */ allowBubbling: function (value) { dessert.isBoolean(value, "Invalid bubbling flag"); this.canBubble = value; return this; }, /** * Sets original event that led to triggering the current event. * @param {evan.Event|*} originalEvent * @returns {evan.Event} */ setOriginalEvent: function (originalEvent) { this.originalEvent = originalEvent; return this; }, /** * Retrieves event from chain of original events by type. * @param {function|troop.Base} eventType * @returns {evan.Event|*} Original event matching the specified type. */ getOriginalEventByType: function (eventType) { var that = this.originalEvent, result; if (typeof eventType === 'function') { while (that) { if (that instanceof eventType) { result = that; break; } else { that = that.originalEvent; } } } else if (troop.Base.isBaseOf(eventType)) { while (that) { if (eventType.isBaseOf(that)) { result = that; break; } else { that = that.originalEvent; } } } return result; }, /** * Retrieves event from chain of original events by the name of the event. * @param {string} eventName * @returns {evan.Event|*} Original event matching the specified name. */ getOriginalEventByName: function (eventName) { var that = this.originalEvent, result; while (that) { if (that.eventName === eventName) { result = that; break; } else { that = that.originalEvent; } } return result; }, /** * Sets flag for default behavior prevention to true. * @returns {evan.Event} */ preventDefault: function () { this.defaultPrevented = true; return this; }, /** * Assigns paths to the event. * @param {sntls.Path} targetPath Path on which to trigger event. * @return {evan.Event} */ setTargetPath: function (targetPath) { dessert.isPath(targetPath, "Invalid target path"); this.originalPath = targetPath; this.currentPath = targetPath.clone(); return this; }, /** * Assigns a broadcast path to the event. * @param {sntls.Path} broadcastPath Path associated with broadcasting. * @return {evan.Event} */ setBroadcastPath: function (broadcastPath) { dessert.isPath(broadcastPath, "Invalid broadcast path"); this.broadcastPath = broadcastPath; return this; }, /** * Sets event sender reference. * @param {*} sender * @returns {evan.Event} */ setSender: function (sender) { this.sender = sender; return this; }, /** * Sets an item on the event payload. * An event may carry multiple payload items set by multiple sources. * User payloads are usually set via evan.setNextPayloadItem. * @param {string} payloadName * @param {*} payloadValue * @return {evan.Event} * @see evan.EventSpace#setNextPayloadItem */ setPayloadItem: function (payloadName, payloadValue) { this.payload[payloadName] = payloadValue; return this; }, /** * Sets multiple payload items in the current event's payload. * An event may carry multiple payload items set by multiple sources. * User payloads are usually set via evan.setNextPayloadItems. * @param {object} payloadItems * @return {evan.Event} */ setPayloadItems: function (payloadItems) { var payload = this.payload, payloadNames = Object.keys(payloadItems), i, payloadName; for (i = 0; i < payloadNames.length; i++) { payloadName = payloadNames[i]; payload[payloadName] = payloadItems[payloadName]; } return this; }, /** * Triggers event. * Event handlers are assumed to be synchronous. Event properties change * between stages of bubbling, hence holding on to an event instance in an async handler * may not reflect the current paths and payload carried. * @param {sntls.Path} [targetPath] Path on which to trigger event. * @return {evan.Event} */ triggerSync: function (targetPath) { dessert.isPathOptional(targetPath, "Invalid target path"); // preparing event for trigger if (targetPath) { this.setTargetPath(targetPath); } var currentPath = this.currentPath, eventSpace = this.eventSpace, handlerCount; if (!this.canBubble || this.originalPath.isA(sntls.Query)) { // event can't bubble because it's not allowed to // or because path is a query and queries shouldn't bubble // calling subscribed handlers once eventSpace.callHandlers(this); } else { // bubbling and calling handlers while (currentPath.asArray.length) { handlerCount = eventSpace.callHandlers(this); if (handlerCount === false) { // bubbling was deliberately stopped // getting out of the bubbling loop break; } else { if (handlerCount > 0) { // setting handled flag this.handled = true; } currentPath.asArray.pop(); } } } return this; }, /** * Broadcasts the event to all subscribed paths branching from the specified path. * Events spawned by a broadcast do not bubble except for the one that is triggered * on the specified broadcast path. It is necessary for delegates to react to * broadcasts. * @param {sntls.Path} [broadcastPath] Target root for broadcast. * @return {evan.Event} */ broadcastSync: function (broadcastPath) { dessert.isPathOptional(broadcastPath, "Invalid broadcast path"); // defaulting to current path in case broadcast path was omitted broadcastPath = broadcastPath || this.currentPath; var mainEvent = this._spawnMainBroadcastEvent(broadcastPath), broadcastEvents = this.eventSpace // obtaining subscribed paths relative to broadcast path .getPathsRelativeTo(this.eventName, broadcastPath) // spawning an event for each subscribed path .passEachItemTo(this._spawnBroadcastEvent, this, 1, broadcastPath) .asType(evan.EventCollection) // adding main event .setItem('main', mainEvent); // triggering all affected events broadcastEvents .setSender(this.sender) .setPayloadItems(this.payload) .setOriginalEvent(this.originalEvent) .triggerSync(); return this; } }); }); (function () { "use strict"; dessert.addTypes(/** @lends dessert */{ /** @param {evan.Event} expr */ isEvent: function (expr) { return evan.Event.isBaseOf(expr); }, /** @param {evan.Event} expr */ isEventOptional: function (expr) { return typeof expr === 'undefined' || evan.Event.isBaseOf(expr); } }); }()); /*global dessert, troop, sntls, evan */ troop.postpone(evan, 'EventCollection', function () { "use strict"; /** * @name evan.EventCollection.create * @function * @param {object} [items] Initial contents. * @return {evan.EventCollection} */ /** * @name evan.EventCollection#eventName * @ignore */ /** * @name evan.EventCollection#eventSpace * @ignore */ /** * @name evan.EventCollection#canBubble * @ignore */ /** * @name evan.EventCollection#payload * @ignore */ /** * @name evan.EventCollection#currentPath * @ignore */ /** * @name evan.EventCollection#originalPath * @ignore */ /** * @name evan.EventCollection#broadcastPath * @ignore */ /** * @class evan.EventCollection * @extends sntls.Collection * @extends evan.Event */ evan.EventCollection = sntls.Collection.of(evan.Event); }); /*global dessert, troop, sntls, evan */ troop.postpone(evan, 'EventSpace', function () { "use strict"; var base = troop.Base, self = base.extend(); /** * Instantiates an EventSpace. * @name evan.EventSpace.create * @function * @return {evan.EventSpace} */ /** * Events traverse within a confined event space. * @class * @extends troop.Base * @extends evan.EventSpawner * @extends evan.EventTarget */ evan.EventSpace = self .addPrivateMethods(/** @lends evan.EventSpace */{ /** * Generates a stub for event handlers. (An empty array) * @return {Array} * @private */ _generateHandlersStub: function () { return []; }, /** * Prepares spawned event for triggering. * @param {evan.Event} event * @private */ _prepareEvent: function (event) { var nextPayloadItems = evan.nextPayloadStore.getPayload(event.eventName), nextOriginalEvent = evan.originalEventStack.getLastEvent(); if (nextPayloadItems) { // applying next payload on spawned event event.setPayloadItems(nextPayloadItems); } if (nextOriginalEvent) { // setting next original event on spawned event event.setOriginalEvent(nextOriginalEvent); } } }) .addMethods(/** @lends evan.EventSpace# */{ /** @ignore */ init: function () { /** * Lookup for subscribed event handlers. Indexed by event name, then event path (stringified), then handler index. * @type {sntls.Tree} * @constant * TODO: Rename to subscriptionRegistry. Breaking. */ this.eventRegistry = sntls.Tree.create(); }, /** * @param {string} eventName * @return {evan.Event} */ spawnEvent: function (eventName) { var event = evan.Event.create(eventName, this); this._prepareEvent(event); return event; }, /** * Subscribes to event. * TODO: Switch eventPath / eventName arguments. Breaking. * @param {string} eventName Name of event to be triggered. * @param {sntls.Path} eventPath Path we're listening to * @param {function} handler Event handler function that is called when the event * is triggered on (or bubbles to) the specified path. * @return {evan.EventSpace} */ subscribeTo: function (eventName, eventPath, handler) { dessert.isFunction(handler, "Invalid event handler function"); var eventRegistry = this.eventRegistry, eventPathString = eventPath.toString(), handlers = eventRegistry.getOrSetNode( [eventPathString, eventName].toPath(), this._generateHandlersStub); // adding handler to handlers handlers.push(handler); return this; }, /** * Unsubscribes from event. Removes entries associated with subscription * from event registry, both from the list of handlers and the list of * subscribed paths. * TODO: Switch eventPath / eventName arguments. Breaking. * TODO: Consider changing unsetKey to unsetPath. Measure performance impact. * @param {string} [eventName] Name of event to be triggered. * @param {sntls.Path} [eventPath] Path we're listening to * @param {function} [handler] Event handler function * @return {evan.EventSpace} */ unsubscribeFrom: function (eventName, eventPath, handler) { dessert.isFunctionOptional(handler, "Invalid event handler function"); var eventRegistry = this.eventRegistry, handlers, handlerIndex; if (eventPath) { if (eventName) { if (handler) { handlers = eventRegistry.getNode([eventPath, eventName].toPath()); if (handlers) { // there are subscriptions on event/path if (handlers.length > 1) { handlerIndex = handlers.indexOf(handler); if (handlerIndex > -1) { // specified handler is subscribed handlers.splice(handlerIndex, 1); } } else { // removing last handler eventRegistry.unsetKey([eventPath, eventName].toPath()); } } } else { // removing all handlers eventRegistry.unsetKey([eventPath, eventName].toPath()); } } else { // removing all handlers for specified path eventRegistry.unsetKey([eventPath].toPath()); } } else { // removing all event bindings this.eventRegistry.clear(); } return this; }, /** * Subscribes to event and unsubscribes after first trigger. * @param {string} eventName Name of event to be triggered. * @param {sntls.Path} eventPath Path we're listening to * @param {function} handler Event handler function that is called when the event * is triggered on (or bubbles to) the specified path. * @return {function} Event handler actually subscribed. Use this for unsubscribing. */ subscribeToUntilTriggered: function (eventName, eventPath, handler) { /** * Handler wrapper for events that automatically unsubscribe * after the first trigger. * @param {evan.Event} event * @param {*} data * @return {*} Whatever the user-defined handler returns (possibly a `false`) */ function oneHandler(event, data) { /*jshint validthis: true */ handler.call(this, event, data); return event.eventSpace.unsubscribeFrom(event.eventName, event.currentPath, oneHandler); } // subscribing delegate handler to capturing path this.subscribeTo(eventName, eventPath, oneHandler); return oneHandler; }, /** * Delegates event capturing to a path closer to the root. * Handlers subscribed this way CANNOT be unsubscribed individually. * @param {string} eventName * @param {sntls.Path} capturePath Path where the event will actually subscribe * @param {sntls.Path} delegatePath Path we're listening to. (Could be derived, eg. Query) * @param {function} handler Event handler function * @return {function} Event handler actually subscribed. Use this for unsubscribing. */ delegateSubscriptionTo: function (eventName, capturePath, delegatePath, handler) { dessert .assert(delegatePath.isRelativeTo(capturePath), "Delegate path is not relative to capture path") .isFunction(handler, "Invalid event handler function"); /** * Handler wrapper for subscribing delegates * @param {evan.Event} event Event object passed down by the triggering process * @param {*} data Custom event data * @return {*} Whatever the user-defined handler returns (possibly a `false`) */ function delegateHandler(event, data) { /*jshint validthis: true */ var originalPath = event.originalPath, broadcastPath = event.broadcastPath; if (delegatePath.isRootOf(originalPath) || broadcastPath && delegatePath.isRelativeTo(broadcastPath) ) { // triggering handler and passing forged current path set to delegatePath return handler.call(this, event.clone(delegatePath), data); } } // subscribing delegate handler to capturing path this.subscribeTo(eventName, capturePath, delegateHandler); return delegateHandler; }, /** * Calls handlers associated with an event name and path. * Handlers are assumed to be synchronous. * @param {evan.Event} event * @return {number|boolean} Number of handlers processed, or false when one handler returned false. * @see evan.Event#triggerSync */ callHandlers: function (event) { var handlersPath = [event.currentPath.toString(), event.eventName].toPath(), handlers = this.eventRegistry.getNode(handlersPath), i = 0, handler; if (handlers && handlers.length) { // making local copy of handlers // in case any of these handlers end up modifying the subscription registry handlers = handlers.concat(); for (; i < handlers.length; i++) { handler = handlers[i]; // calling handler, passing event and payload if (handler.call(this, event, event.payload) === false) { // stopping iteration when handler returns false // TODO: Add .stopPropagation() API to event. return false; } } } return i; }, /** * Retrieves subscribed paths that are relative to the specified path. * @param {string} eventName * @param {sntls.Path} path * @return {evan.PathCollection} Collection of paths relative to (not including) `path` * Question is which lib/class should delegate the method. */ getPathsRelativeTo: function (eventName, path) { // obtaining all paths associated with event name var pathsQuery = ['{|}'.toKVP(), eventName].toQuery(), paths = this.eventRegistry .queryKeysAsHash(pathsQuery) .toOrderedStringList(); if (paths) { // there are subscriptions matching eventName return /** @type evan.PathCollection */paths // querying collection of strings that are relative to `path` .getRangeByPrefixAsHash(path.toString(), true) .toStringCollection() // converting them to a collection of paths .toPathOrQuery().asType(evan.PathCollection); } else { // no subscriptions match eventName // returning empty path collection return evan.PathCollection.create([]); } } }); }); (function () { "use strict"; dessert.addTypes(/** @lends dessert */{ isEventSpace: function (expr) { return evan.EventSpace.isPrototypeOf(expr); }, isEventSpaceOptional: function (expr) { return typeof expr === 'undefined' || evan.EventSpace.isPrototypeOf(expr); } }); }()); /*global dessert, troop, sntls, evan */ troop.postpone(evan, 'Evented', function () { "use strict"; var base = troop.Base, self = base.extend(); /** * Trait. * Classes with this trait may trigger and capture * events on a specified event space directly. * @class * @extends troop.Base * @extends evan.EventSpawner * @extends evan.EventSource * @extends evan.EventTarget */ evan.Evented = self .addPrivateMethods(/** @lends evan.Evented# */{ /** * @param {sntls.Dictionary} dictionary * @returns {Array} * @private */ _flattenDictionary: function (dictionary) { var result = [], items = dictionary.items, keys = Object.keys(items), i, key, values, j; for (i = 0; i < keys.length; i++) { key = keys[i]; values = items[key]; if (values instanceof Array) { for (j = 0; j < values.length; j++) { result.push([key, values[j]]); } } else { result.push([key, values]); } } return result; }, /** * @param {sntls.Path} oldEventPath * @param {sntls.Path} newEventPath * @private */ _reSubscribe: function (oldEventPath, newEventPath) { var that = this; this._flattenDictionary(this.subscriptionRegistry) .toCollection() .forEachItem(function (keyValuePair) { var eventName = keyValuePair[0], handler = keyValuePair[1]; that.eventSpace .unsubscribeFrom(eventName, oldEventPath, handler) .subscribeTo(eventName, newEventPath, handler); }); } }) .addMethods(/** @lends evan.Evented# */{ /** @ignore */ init: function () { /** * Stores event name - handler associations for the current evented instance. * @type {sntls.Dictionary} */ this.subscriptionRegistry = undefined; }, /** * Spawns an event in the current event space, prepared with the current event path * as the target path. Returned event may be triggered without specifying a target path. * Current eventSpace and eventPath properties must not be undefined. * @param {string} eventName * @return {evan.Event} */ spawnEvent: function (eventName) { return this.eventSpace.spawnEvent(eventName) .setSender(this) .setTargetPath(this.eventPath); }, /** * Sets event space on current class or instance. * @param {evan.EventSpace} eventSpace * @returns {evan.Evented} * @memberOf {evan.Evented} */ setEventSpace: function (eventSpace) { dessert.isEventSpace(eventSpace, "Invalid event space"); this.eventSpace = eventSpace; return this; }, /** * Sets event path for the current class or instance. * @param {sntls.Path} eventPath * @returns {evan.Evented} * @memberOf {evan.Evented} */ setEventPath: function (eventPath) { var baseEventPath = this.getBase().eventPath, subscriptionRegistry = this.subscriptionRegistry; dessert .isPath(eventPath, "Invalid event path") .assert( !baseEventPath || eventPath.isRelativeTo(baseEventPath), "Specified event path is not relative to static event path"); if (!subscriptionRegistry) { // initializing subscription registry this.subscriptionRegistry = sntls.Dictionary.create(); } else if (subscriptionRegistry.getKeyCount()) { // re-subscribing events this._reSubscribe(this.eventPath, eventPath); } // storing new event path this.eventPath = eventPath; return this; }, /** * Subscribes to event. * @param {string} eventName Name of event to be triggered. * @param {function} handler Event handler function that is called when the event * is triggered on (or bubbles to) the specified path. * @return {evan.Evented} */ subscribeTo: function (eventName, handler) { this.eventSpace.subscribeTo(eventName, this.eventPath, handler); this.subscriptionRegistry.addItem(eventName, handler); return this; }, /** * Unsubscribes from event. * @param {string} [eventName] Name of event to be triggered. * @param {function} [handler] Event handler function * @return {evan.Evented} */ unsubscribeFrom: function (eventName, handler) { this.eventSpace.unsubscribeFrom(eventName, this.eventPath, handler); if (eventName) { this.subscriptionRegistry.removeItem(eventName, handler); } else { this.subscriptionRegistry.clear(); } return this; }, /** * Subscribes to event and unsubscribes after first trigger. * @param {string} eventName Name of event to be triggered. * @param {function} handler Event handler function that is called when the event * is triggered on (or bubbles to) the specified path. * @return {evan.Evented} */ subscribeToUntilTriggered: function (eventName, handler) { var oneHandler = this.eventSpace.subscribeToUntilTriggered(eventName, this.eventPath, handler); this.subscriptionRegistry.addItem(eventName, oneHandler); return this; }, /** * Delegates event capturing to a path closer to the root. * Handlers subscribed this way CANNOT be unsubscribed individually. * @param {string} eventName * @param {sntls.Path} delegatePath Path we're listening to. (Could be derived, eg. Query) * @param {function} handler Event handler function * @return {evan.Evented} */ delegateSubscriptionTo: function (eventName, delegatePath, handler) { var delegateHandler = this.eventSpace.delegateSubscriptionTo(eventName, this.eventPath, delegatePath, handler); this.subscriptionRegistry.addItem(eventName, delegateHandler); return this; }, /** * Shorthand for **triggering** an event in the event space * associated with the instance / class. * @param {string} eventName * @return {evan.Evented} */ triggerSync: function (eventName) { this.spawnEvent(eventName) .triggerSync(this.eventPath); return this; }, /** * Shorthand for **broadcasting** an event in the event space * associated with the instance / class. * @param {string} eventName * @return {evan.Evented} */ broadcastSync: function (eventName) { this.spawnEvent(eventName) .broadcastSync(this.eventPath); return this; } }); }); /*global dessert, troop, sntls, evan */ troop.postpone(evan, 'EventStack', function () { "use strict"; var base = troop.Base, self = base.extend(); /** * Creates an EventStack instance. * @name evan.EventStack.create * @function * @returns {evan.EventStack} */ /** * Stores events in a quasi-stack structure. * @class * @extends troop.Base */ evan.EventStack = self .addMethods(/** @lends evan.EventStack# */{ /** * @ignore */ init: function () { /** * Chain structure serving as the buffer for events. * @type {evan.OpenChain} */ this.events = evan.OpenChain.create(); }, /** * Adds an event to the stack. To remove the event from the stack, call .unLink() on the returned evan.ValueLink instance. * @param {evan.Event|*} event * @returns {evan.ValueLink} */ pushEvent: function (event) { var link = evan.ValueLink.create().setValue(event); this.events.pushLink(link); return link; }, /** * Retrieves the last event added to the stack. * @returns {evan.Event|*} */ getLastEvent: function () { return this.events.lastLink.previousLink.value; } }); }); /*global dessert,