evan
Version:
Universal events
1,417 lines (1,251 loc) • 56 kB
JavaScript
/*! 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,