From 661f14151df98214907a2bfa2d0594ae1f5b0df0 Mon Sep 17 00:00:00 2001 From: Andras Kemeny Date: Tue, 13 Feb 2018 12:59:51 +0100 Subject: [PATCH] release v1.1.0 --- README.md | 306 ++++++++++++++++++++++++-- index.js | 597 +++++++++++++++++++++++++++++++++++++++++++++------ package.json | 10 +- test.js | 155 +++++++++---- 4 files changed, 942 insertions(+), 126 deletions(-) diff --git a/README.md b/README.md index 48f1d8e..a818c7e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ A drop-in replacement for EventEmitter that can handle DOM-style events whose propagation can be stopped. -This module exports two classes: `.emitter` and `.event`. The `.emitter` can be used as a basis for `extend`ing your own class, and the `.event` class can be used by itself or be extended to serve your needs for an in-transit cancelable event. +This module exports two classes: `emitter` and `event`. The `emitter` can be used as a basis for `extend`ing your own class, and the `event` class can be used by itself or be extended to serve your needs for an in-transit cancelable event. + +From version 1.1.0 onwards, the `emitter` class also comes with a parent-child architecture that allows for event traversal along the hierarchy tree. ## Usage @@ -23,26 +25,252 @@ For a detailed example, see test.js in this package. ## Reference +### Event traversal + +1. `event` objects are emitted by `emitter` objects. +2. `emitter` objects can be chained up in a parent/children relationship, and thus forming a propagation tree. +3. `emitter` objects can be set up to emit events locally, sideways (siblings), upwards (parents) and downwards (children). The path and order of emitting is configurable. +4. `event` objects can be set up to be either allowed or disallowed to travel in certain directions. +5. `event` objects can also be set up to "saturate" the emitter tree. +6. Non-saturating events can only be emitted locally, and can travel sideways, stright up the parental chain, and downwards to all children, +relative to the `emitter` they were emitted at. When in non-saturating mode, when an `emitter` in the propagation chain is called to emit an `event`, +it will only do so locally. +7. "Saturating" events will be fired on all connected `emitter`s on a tree, regardless of direct lineage. The order of emission is: + - first the event is emitted on the `emitter` object where it was fired at (the *origin* emitter); + - then it is emitted on the siblings of the *origin*; + - then on all of the children of the *origin* (recursively down to the last items in the tree); + - then on all of the children of the *origin*'s siblings (recursively, of course); + - then it is emitted on the direct parent of the *origin*; + - then on all of the siblings of the direct parent of *origin*; + - then on all the children of the siblings of the direct parent of *origin*, recursively, down to all of their children; + - then recursively upward on the parent of the direct parent of the origin, and those parents' siblings as well, and so on. +8. An `event`'s propagation can be stopped at any time. `event.stopPropagation()` will allow the event +to finish up calling listeners on the current `emitter` before canceling the event propagation to other `emitter`s. +`event.stopImmediatePropagation()` will cancel the propagation right away. +9. `event.eventPhase` reflects on which propagation phase the event is currently in. In the case of "saturating" events, it is either _LOCAL or _SATURATING. +10. `event.target` is optionally filled by whatever object is creating the event object. `event.currentTarget` is always updated by the `emitter` that currently +emits the event. There is no `event.deepPath` as I'm afraid it might lead to circular reference hell.* + +* However, if you think it'd be a good idea, voice your opinion in the Issues of this package on GitHub. + ### `emitter` +#### Static properties + +`defaultFirstDirection` = `event.PROPAGATES_LOCAL` +`defaultSecondDirection` = `event.PROPAGATES_DOWN` +`defaultThirdDirection` = `event.PROPAGATES_UP` +`defaultThirdDirection` = `event.PROPAGATES_NONE` + +If you use the parent/child hierarchy extension, these constants set up the default behaviour of all created emitter objects. +This setting describes a setup where the emitted `eventObject` first propagates locally (to the listeners of the emitter instance), +then to the emitter object's siblings if any, then to the emitter object's children, if any (where they are propagated locally), and their children, recursively. Once that +propagation is done, the same `eventObject` then is propagated to the original emitter's parent where it once again propagates +locally, then, if it is allowed, proceeds upwards to the parent's parent, recursively. + +If all four directions are traversed, an event only finishes propagating when: +- all local-level listeners are called; +- *and* all children of the local emitter are called, and theirs, recursively, until the deepermost children are emitted; +- *and* the whole of the local emitter's parental chain is called; +- *or* the event is specified either not to traverse certain directions, or is cancelled by one of the listeners. + +Bear in mind, that event traversal at the moment* is a sync operation, therefore it may block your entire process if written carelessly. + +`defaultMaxListeners` = `0` + +This is left in only for compatibility reasons, it does not have any effect on the classes' operations. + +*: Yes, that means I'm working on an async event emission solution, but there's no ETA on that yet. + +#### As a drop-in-replacement for NodeJS's EventEmitter + This class provides a drop-in replacement functionality for Node's official `EventEmitter` class. It's API-compatible for the most part, but it has a few subtle differences. - It does not care about maximum listener counts (it's just an excuse to tolerate bad programming). - The class internals look decidedly different from the official `EventEmitter` implementation, so any hacks building on interals abuse will not work. -- This class does not optimize for single listeners (which are done without an `Array` in the official implementation), so it might lose a few cycles when compared to the original.* -- Neither does it optimize for single or multiple arguments; the algorithm I used for extracting arguments is one that avoids that famed V8 slowdown bug.* +- This class does not optimize for single listeners (which are done without an `Array` in the official implementation), so it might lose a few cycles when compared to the original.** +- Neither does it optimize for single or multiple arguments; the algorithm I used for extracting arguments is one that avoids that famed V8 slowdown bug.** - It supports an object-based (`.event`) event approach that allows for payload and result distribution among listeners (allowing the use of events for dynamically configurable filters and data modifiers) and cancelable events (that stop any subsequent listeners to be called). -*: If you experience significant slowdowns because of these optimization omissions, let me know in an issue and I'll reintroduce them to the codeline. +**: If you experience significant slowdowns because of these optimization omissions, let me know in an issue and I'll reintroduce them to the codeline. + +##### Methods + +`addListener(eventName,listener)` +`on(eventName,listener)` +- **eventName** ``: the event name for which to register this listener +- **listener** ``: a function that will be called +- returns the emitter object itself, allowing for chaining + +`emit(eventName[,payload1[,payload2...]]):` +`emit(eventObject):` +- **eventName** ``: the event name for which to register this listener +- **eventObject** ``: an event object that holds all necessary data +- returns false if there were no handlers, true otherwise + +`eventNames():` +- returns an array of all the event names that have handlers + +`getMaxListeners():0` +- a stub; this emitter does not support maximum numbers of listeners + +`listenerCount(eventName):` +- **eventName** ``: the event name +- returns the number of listeners registered for `eventName` + +`listeners(eventName):` +- **eventName** ``: the event name +- returns an array of listener functions (both normal and one-off) registered for `eventName` + +`off(eventName[,listener])` +`removeListener(eventName[,listener])` +- **eventName** ``: the event name from which to deregister this listener +- **listener** ``: a function that will be called; if not given, all listeners for `eventName` are removed +- returns the emitter object itself, allowing for chaining + +`once(eventName,listener)` +- **eventName** ``: the event name for which to register this listener +- **listener** ``: a function that will be called only once +- returns the emitter object itself, allowing for chaining + +`prependListener(eventName,listener)` +- **eventName** ``: the event name for which to register this listener +- **listener** ``: a function that will be called +- like `addListener()`, only it pushes `listener` to the front of the listeners +- returns the emitter object itself, allowing for chaining + +`prependOnceListener(eventName,listener)` +- **eventName** ``: the event name for which to register this listener +- **listener** ``: a function that will be called only once +- like `addListener()`, only it pushes `listener` to the front of the one-time listeners +- returns the emitter object itself, allowing for chaining + +`removeAllListeners([eventName])` +- **eventName** ``: if given, only `eventName` listeners will be expunged +- returns the emitter object itself, allowing for chaining + +`setMaxListeners(number)` +- a stub; this emitter does not support maximum numbers of listeners + +##### Events + +`newListener(eventName,listener)` +- **eventName** ``: the event name for which this listener is about to be registered +- **listener** ``: a function that will be called + +`removeListener(eventName,listener)` +- **eventName** ``: the event name for which this listener is about to be unregistered +- **listener** ``: a function that will be called + +#### As a hierarchy-aware (parent/child relations) advanced event emitter + +`addChild(childEmitter)` +- **childEmitter** ``: the emitter instance to add as a child +- returns the emitter object itself, allowing for chaining + +`callOnChildren(method[,params[,thisarg[,childArray]]])` +- **method** ``: the method name to call on children emitters +- **params** ``: an array of method call arguments, presented as an Array, defaults to [] +- **thisarg** ``: the *this* argument for the method call, defaults to `this` +- **childArray** ``: if only a selected list of children should be triggered, this can be a filtered list +- returns the result value from the chain of method calls + +Important: no matter how many `params` are given, there will always be an extra argument, +and that is the result of the previous child's method call. In the first called child, it will be `null`. + +`callOnParent(method[,params[,thisarg]])` +- **method** ``: the method name to call on children emitters +- **params** ``: an array of method call arguments, presented as an Array, defaults to [] +- **thisarg** ``: the *this* argument for the method call, defaults to `this` +- returns either the result of the method call or `false` if there is no parent defined + +`callOnSiblings(method[,params[,thisarg]])` +- **method** ``: the method name to call on children emitters +- **params** ``: an array of method call arguments, presented as an Array, defaults to [] +- **thisarg** ``: the *this* argument for the method call, defaults to `this` +- returns the result value from the chain of method calls + +`emitEvent(eventObject[,skipDirection])` +- **eventObject** ``: an event object to emit +- **skipDirection** ``: one of the `PROPAGATES_*` constants from the event class, or 0 for no boundaries. +- returns the emitter object itself, allowing for chaining + +Please be advised that using `emitEvent()` turns off the compatibility mode for `event.stopImmediatePropagation()`, and also `event.phase` is +set according to the current propagation phase. + +`filterChildren(filterFunction):` +- **filterFunction** ``: a callback that is executed to filter the children emitters of the current one; internally, `Array.prototype.filter()` is used +- returns a filtered child array that can be fed as the last argument for `callOnChildren()` to selectively call methods on current children -The only noticable signature difference is in `.emitter.emit()`, as it, beside the original signature, also supports the new signature for the `.event` objects: +`getAllChildren([dontDescend])` +- **dontDescend** an optional `` of `` objects that the function should not descend to +- returns an `` of `` objects which are registered as children of the current emitter or its children (recursively down to the last level) -`.emitter.emit()` : in this case, would be an `.event` object or one that extends it. +`getChildren()` +- returns an `` of `` objects which are registered as children of the current emitter + +`getParent()` +- returns either `null` or a `` which is a parent to the current emitter + +`getRelations([dontDescend])` +- **dontDescend** an optional `` of `` objects that the function should not descend to (only used in recursive calls) +- returns an `` of `` objects which are related to the current `emitter` (are in the same propagation tree), regardless of how removed they are + +`getSiblings()` +- returns an `` of ``s that share the same parent as this emitter + +`hasChild(childEmitter):` +- **childEmitter** ``: an emitter or derivative +- returns true if **childEmitter* is a direct descendant of the current emitter; false otherwise + +`hasParent():` +- returns true if this emitter has a parent + +`removeAllChildren()` +- returns the emitter object itself, allowing for chaining + +Removes all children of the current emitter instance. + +`removeChild(childObject|index)` +- returns the emitter object itself, allowing for chaining + +Removes a specified `childObject`, or the `emitter` that resides within the children at position held in `index`. + +`removeSelf()` +- returns the emitter object itself, allowing for chaining + +Removes the current emitter instance as a child from its parent. This is here instead of a `removeParent()` or similar. + +`setOrder(first,second,third,fourth)` +- **first** : the first route to take; one of the `PROPAGATES_*` constants from the event class, or 0 for "do nothing" +- **second** : the second route to take; --""-- +- **third** : the third route to take; --""-- +- **fourth** : the fourth route to take; --""-- +- returns the emitter object itself, allowing for chaining + +If the event comes with a `getPropagation()` that contains `PROPAGATES_SATURATING`, then the order is discarded. + +`shutdown()` +- returns the emitter object itself, allowing for chaining + +Safely shut down this emitter, by letting go of foreign references; this allows for easier cleanup. ### `event` This class can be used on its own as a basis for a generic event object, or it can be extended to suit your needs. +#### Constants + +`PROPAGATES_NONE` = `0` meaning no propagation +`PROPAGATES_LOCAL` = `1` meaning only current emitter object +`PROPAGATES_UP` = `2` meaning only current emitter's parent +`PROPAGATES_DOWN` = `4` meaning only current emitter's children +`PROPAGATES_SIBLINGS` = `8` meaning only current emitter's siblings +`PROPAGATES_SATURATING` = `16` meaning the entire emitter tree + +These constants can be `OR`ed, so for example a `PROPAGATES_UP`|`PROPAGATES_LOCAL` defines an event or an emitter that spreads events locally and up. +`PROPAGATES_SATURATING` is a special case which ignores the rest of the constants. + #### Methods `new event(eventName[,payload[,origin]])` - eventName ``: the event's name (or type, YMMV) @@ -51,33 +279,71 @@ This class can be used on its own as a basis for a generic event object, or it c Creates a new event object (constructor). -`event.canPropagate()` +`canPropagate([where])` +- **where** ``: + - if nothing is given (no arguments), returns true if this event is allowed to propagate at all (not canceled); + - if one of the `event` constants are given, then returns true if the event can propagate in that direction. - returns `` -Returns `true` if the event is not canceled and can move on to the next listner, if there is one; `false` otherwise. - -`event.stopPropagation()` -`event.stopImmediatePropagation()` -`event.cancelEvent()` +`cancelEvent()` +`stopPropagation()` - returns the event object itself, allowing for chaining -These cancel the current event, so it won't get to any subsequent listeners. They all do the same, the aliases are there for convenience. +In *compatibility mode* (mainline 1.0.x), which is the default, these cancel the event, so they're equal to `stopImmediatePropagation()`. +In *native mode* (if the event is emitted with `emitter.emitEvent()`), these only cancel upward or downward propagation, leaving the local level intact. -`event.getPayload()` -`event.setPayload(newValue)` -`event.getResult()` -`event.setResult(newValue)` +In both modes, stops event propagation even on the same level -- ie. no more listeners are called. + +`getPayload()` +`setPayload(newValue)` +`getResult()` +`setResult(newValue)` - **newvValue** `` - **set\*** methods return the event object itself, allowing for chaining Accessor methods for the event's built-in payload and result containers. +`getPropagation()` +`setPropagation(newValue)` +- **newValue** ``: an ORed list of `PROPAGATES_*` constants which tells the emitter which directions are allowed for propagation in this event +- **setPropagation()** returns the event object itself + +Setting this and `emitter.setOrder()` will jointly decide how an event can propagate. In extreme situations, like where the emitter is set up to emit +only locally and upwards, and the event set up so that it would only propagates downwards, no event is actually emitted even though there are no +stop*Propagation() calls and the `emitter.emitEvent()` is called. + +`stopImmediatePropagation()` +- returns the event object itself, allowing for chaining + +#### Static properties + +`event.defaultPropagation`: `` + +The value (an ORed list of `PROPAGATES_*` constants) which newly created event objects use as their setPropagation() defaults. + #### Properties -`event.type`: `` +`bubbles`: `` -Contains the event name/type. +If true, the event can traverse outside its local boundary. -`event.target`: `` +`currentTarget`: `` + +The current emitter that is emitting this event object. + +`eventPhase`: `` + +One of the `PROPAGATES_*` constants, depending on current traversal status. In "saturating" propagation, +it's set to `PROPAGATES_LOCAL` only at the start, then for the rest of propagation it's `PROPAGATES_SATURATING`. + +`timeStamp`: `` + +The millisecond-precise epochal time when the event object came to be. + +`target`: `` The event's origin function or object, as set by the third argument of the constructor, defaults to `null`. + +`type`: `` + +Contains the event name/type. diff --git a/index.js b/index.js index 4513346..97ca0d6 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,9 @@ /** - * TODO + * swatk6/emitter + * @version v1.1.0 + * @author Andras Kemeny + * + * A drop-in replacement for EventEmitter that can handle DOM-style events whose propagation can be stopped. * * LICENSE: MIT * (c) Andras Kemeny, subpardaemon@gmail.com @@ -26,18 +30,37 @@ function swatk6_event(eventType,payload,target) { if (typeof target==='undefined') { target = null; } + /** @type {String} */ this.type = eventType; + /** @type {*} */ this.target = target; - this._propagates = true; - this._payload = payload; - this._result = null; + this._propagates = swatk6_event.defaultPropagation; + this.bubbles = (this._propagation&swatk6_event.PROPAGATES_DOWN || this._propagation&swatk6_event.PROPAGATES_UP); + /** @type {*} */ + this.payload = payload; + /** @type {*} */ + this.result = null; + /** @type {swatk6_emitter} */ + this.currentTarget = null; + /** @type {Number} */ + this.eventPhase = swatk6_event.PROPAGATES_NONE; + /** @type {Number} */ + this.timeStamp = new Date().getTime(); + this._bkwcmpt = true; } +swatk6_event.PROPAGATES_NONE = 0; +swatk6_event.PROPAGATES_LOCAL = 1; +swatk6_event.PROPAGATES_UP = 2; +swatk6_event.PROPAGATES_DOWN = 4; +swatk6_event.PROPAGATES_SIBLINGS = 8; +swatk6_event.PROPAGATES_SATURATING = 16; +swatk6_event.defaultPropagation = swatk6_event.PROPAGATES_LOCAL|swatk6_event.PROPAGATES_DOWN|swatk6_event.PROPAGATES_UP; /** * Get the current result value. * @returns {swatk6_event._result} */ swatk6_event.prototype.getResult = function() { - return this._result; + return this.result; }; /** * Set the current result value. @@ -45,7 +68,7 @@ swatk6_event.prototype.getResult = function() { * @returns {swatk6_event} */ swatk6_event.prototype.setResult = function(v) { - this._result = v; + this.result = v; return this; }; /** @@ -53,7 +76,7 @@ swatk6_event.prototype.setResult = function(v) { * @returns {swatk6_event.payload} */ swatk6_event.prototype.getPayload = function() { - return this._payload; + return this.payload; }; /** * Sets the current payload. @@ -61,7 +84,41 @@ swatk6_event.prototype.getPayload = function() { * @returns {swatk6_event} */ swatk6_event.prototype.setPayload = function(v) { - this._payload = v; + this.payload = v; + return this; +}; +/** + * Gets the current propagation setting; cf. PROPAGATES_* constants. + * @returns {Number} + * @since v1.1.0 + */ +swatk6_event.prototype.getPropagation = function() { + return this.propagates; +}; +/** + * Set the new propagation model. + * + * Valid values (can be ORed): + * - swatk6_event.PROPAGATES_LOCAL (1): propagates on the level (emitter) it started at + * - swatk6_event.PROPAGATES_UP (2): propagates to the current emitter's parents, if any + * - swatk6_event.PROPAGATES_DOWN (4): propagates to the current emitter's children, if any + * - swatk6_event.PROPAGATES_SIBLINGS (8): propagates to the current emitter's siblings, if any + * - swatk6_event.PROPAGATES_SATURATING (16): propagates to the whole emitter tree, and ignores any other PROPAGATES_* setting + * - or a big fat 0, but what's the point of that? Actually, that acts as a stopImmediatePropagation() before even the first listener would be called. + * + * If the new value is either LOCAL or none, it sets this.bubbles to false, otherwise to true. + * + * @param {Number} v the new setting (cf. PROPAGATES_* constants) + * @returns {swatk6_event} + * @since v1.1.0 + */ +swatk6_event.prototype.setPropagation = function(v) { + this._propagates = v; + if (this._propagates<=1) { + this.bubbles = false; + } else { + this.bubbles = true; + } return this; }; /** @@ -69,29 +126,55 @@ swatk6_event.prototype.setPayload = function(v) { * @returns {swatk6_event} */ swatk6_event.prototype.stopPropagation = function() { - this._propagates = false; + if (this._bkwcmpt===true) { + this._propagates = 0; + } else { + this._propagates = this._propagates & 1; + } + this.bubbles = false; return this; }; /** - * Alias to stopPropagation(). + * Stop the propagation of this event even on the current level, and on any further level. * @returns {swatk6_event} */ swatk6_event.prototype.stopImmediatePropagation = function() { - return this.stopPropagation(); + this._propagates = 0; + this.bubbles = false; + return this; }; /** - * Alias to stopPropagation(). + * Alias to stopImmediatePropagation(). * @returns {swatk6_event} */ swatk6_event.prototype.cancelEvent = function() { - return this.stopPropagation(); + return this.stopImmediatePropagation(); }; /** - * True if the event can propagate (event listneres can be called). + * Returns true if the event can propagate (event listneres can be called) in general or in a specific direction. + * @param {Number} [where=undefined] if undefined, returns true if the event can propagate at all; if any of the PROPAGATES_* constants, then return true if the event can propagate in that direction * @returns {Boolean} */ -swatk6_event.prototype.canPropagate = function() { - return this._propagates; +swatk6_event.prototype.canPropagate = function(where) { + if (typeof where==='undefined') { + return this._propagates>0; + } + else if (where===swatk6_event.PROPAGATES_LOCAL) { + return ((this._propagates&swatk6_event.PROPAGATES_LOCAL)>0); + } + else if (where===swatk6_event.PROPAGATES_UP) { + return ((this._propagates&swatk6_event.PROPAGATES_UP)>0 && this.bubbles); + } + else if (where===swatk6_event.PROPAGATES_DOWN) { + return ((this._propagates&swatk6_event.PROPAGATES_DOWN)>0 && this.bubbles); + } + else if (where===swatk6_event.PROPAGATES_SIBLINGS) { + return ((this._propagates&swatk6_event.PROPAGATES_SIBLINGS)>0 && this.bubbles); + } + else if (where===swatk6_event.PROPAGATES_SATURATING) { + return ((this._propagates&swatk6_event.PROPAGATES_SATURATING)>0 && this.bubbles); + } + return this._propagates>0; }; /* @@ -105,10 +188,13 @@ swatk6_event.prototype.canPropagate = function() { * @returns {swatk6_emitter} */ function swatk6_emitter() { - this._listeners = {}; - this._listenersOnce = {}; + this._setup(); } swatk6_emitter.defaultMaxListeners = 0; +swatk6_emitter.defaultFirstDirection = swatk6_event.PROPAGATES_LOCAL; +swatk6_emitter.defaultSecondDirection = swatk6_event.PROPAGATES_DOWN; +swatk6_emitter.defaultThirdDirection = swatk6_event.PROPAGATES_UP; +swatk6_emitter.defaultFourthDirection = swatk6_event.PROPAGATES_NONE; /** * This is called before any on and off handlers so that there is no need for a constructor. * @inner @@ -117,8 +203,293 @@ swatk6_emitter.prototype._setup = function() { if (typeof this._listeners==='undefined') { this._listeners = {}; this._listenersOnce = {}; + /** @type {swatk6_emitter} */ + this._parent = null; + /** @type {swatk6_emitter[]} */ + this._children = []; + this.setOrder(swatk6_emitter.defaultFirstDirection,swatk6_emitter.defaultSecondDirection,swatk6_emitter.defaultThirdDirection,swatk6_emitter.defaultFourthDirection); + } +}; +/** + * Set the order of event propagation. + * + * Default is PROPAGATES_LOCAL -> PROPAGATES_DOWN -> PROPAGATES_UP. + * + * @param {Number} first - one of the swatk6_event.PROPAGATES_* constants + * @param {Number} second - one of the swatk6_event.PROPAGATES_* constants + * @param {Number} third - one of the swatk6_event.PROPAGATES_* constants + * @param {Number} fourth - one of the swatk6_event.PROPAGATES_* constants + * @returns {swatk6_emitter} + */ +swatk6_emitter.prototype.setOrder = function(first,second,third,fourth) { + var d = [first]; + d.push((second!==first) ? second : 0); + d.push((third!==second && third!==first) ? third : 0); + d.push((fourth!==third && fourth!==second && fourth!==first) ? fourth : 0); + this._direction = d; + return this; +}; +/** + * Shut down the emitter and release all references. + * @returns {swatk6_emitter} + */ +swatk6_emitter.prototype.shutdown = function() { + this._listeners = {}; + this._listenersOnce = {}; + /* let's not keep circular links around */ + this.removeSelf(); + this.removeAllChildren(); + return this; +}; +/* + * ---------------------------------------------------------------------------- + * PARENT/CHILD HIERARCHY STUFF + * ---------------------------------------------------------------------------- + */ +/** + * Get the parent of the emitter. + * @returns {swatk6_emitter} + */ +swatk6_emitter.prototype.getParent = function() { + return this._parent; +}; +/** + * Returns true if this emitter has a parent. + * @returns {Boolean} + */ +swatk6_emitter.prototype.hasParent = function() { + return this._parent!==null; +}; +/** + * Oy vey + * @private + * @param {Array} list + * @returns {undefined} + */ +swatk6_emitter.prototype.abcug = function(list) { + console.log('!!!!!',this.origi); + for(var i=0;i0) { + for(var i=0;i-1); +}; +/** + * Add a child to the emitter. + * @param {swatk6_emitter} childObj + * @returns {swatk6_emitter} + */ +swatk6_emitter.prototype.addChild = function(childObj) { + if (this._children.indexOf(childObj)===-1) { + var cpar = childObj.getParent(); + if (cpar!==null) { + cpar.removeChild(childObj); + } + this._children.push(childObj); + childObj._parent = this; + this.emit('childAdded',childObj); + } + return this; +}; +/** + * Remove a particular child of this emitter. + * @param {swatk6_emitter} childObj + * @returns {swatk6_emitter} + */ +swatk6_emitter.prototype.removeChild = function(childObj) { + var x; + if (typeof childObj==='number' && x-1) { + childObj = this._children[x]; + childObj.emit('beforeChildRemoved',childObj); + this.emit('beforeChildRemoved',childObj); + this._children.splice(x,1); + childObj._parent = null; + this.emit('childRemoved',childObj); + } + return this; +}; +/** + * Get the siblings of this emitter. + * @returns {swatk6_emitter[]} + */ +swatk6_emitter.prototype.getSiblings = function() { + if (!this.hasParent()) { + return []; + } + var sibl = this.callOnParent('getChildren'); + sibl = sibl.filter(function(sibling) { + return sibling!==this; + }.bind(this)); + //this.abcug(sibl); + return sibl; +}; +/** + * If this has a parent, removes iself from that parent's children list. + * @returns {swatk6_emitter} + */ +swatk6_emitter.prototype.removeSelf = function() { + if (this._parent!==null) { + this._parent.removeChild(this); + } + return this; +}; +/** + * Remove all children of this emitter. + * @returns {swatk6_emitter} + */ +swatk6_emitter.prototype.removeAllChildren = function() { + for(var i=0;i", "license": "MIT", "bugs": { "url": "https://github.com/subpardaemon/swatk6-emitter/issues" + }, + "devDependencies": { + "@swatk6/tester": "^1.1.0" } } diff --git a/test.js b/test.js index b480f8d..ee6fc99 100644 --- a/test.js +++ b/test.js @@ -1,61 +1,134 @@ -var responses = []; -function addResponse(resp) { - responses.push(resp); -} -function matchResponses(expected) { - var haderrors = false; - for(var i=0;iexpected.length) { - console.error('mismatch: more responses than expected, superflous part:',responses.slice(expected.length)); - haderrors = true; - } - if (haderrors===true) { - process.exit(1); - } else { - console.info('all went as expected'); - process.exit(0); - } -} +const tstr = require('@swatk6/tester'); + +var tester = new tstr(); try { - const emitta = require('./index.js').emitter; - const evan = require('./index.js').event; + var emitta = require('./index.js').emitter; + var evan = require('./index.js').event; class testemitter extends emitta { - constructor() { + constructor(origi) { super(); + this.origi = origi; } } - var tr = new testemitter(); + var tr = new testemitter('base'); tr.on('testevent',function(evobj) { - addResponse('event1'); - addResponse(evobj.getPayload()); + tester.addResponse('event1'); + tester.addResponse(evobj.getPayload()); evobj.setPayload(null); evobj.setResult('hurray'); }); tr.on('testevent',function(evobj) { - addResponse('event2'); - addResponse(evobj.getPayload()); - addResponse(evobj.getResult()); + tester.addResponse('event2'); + tester.addResponse(evobj.getPayload()); + tester.addResponse(evobj.getResult()); evobj.cancelEvent(); }); tr.on('testevent',function(evobj) { - addResponse('event3'); + tester.addResponse('event3'); }); - addResponse(tr.listeners('testevent').length); - addResponse(tr.eventNames().pop()); + tester.addResponse(tr.listeners('testevent').length); + tester.addResponse(tr.eventNames().pop()); tr.emit(new evan('testevent','testpayload')); } catch(e) { - addResponse(e); + tester.addResponse(e,true,'exception (base block)'); +} + +if (tester.matchResponses([3,'testevent','event1','testpayload','event2',null,'hurray'])===false) { + process.exit(1); +} + +tester = new tstr(); + +try { + class testemitter extends emitta { + constructor(origi) { + super(); + this.origi = origi; + } + } + tr = new testemitter('base'); + var callorder = 0, callorder2 = 0; + tr.setOrder(evan.PROPAGATES_DOWN,evan.PROPAGATES_LOCAL,evan.PROPAGATES_UP,evan.PROPAGATES_SIBLINGS); + var child01 = new testemitter('child0-1'); + child01.setOrder(evan.PROPAGATES_DOWN,evan.PROPAGATES_UP,0,0); + var child11 = new testemitter('child1-1'); + var child12 = new testemitter('child1-2'); + var parent01 = new testemitter('parent0-1'); + var sibling1 = new testemitter('sibling-1'); + tr.addChild(child01); + child01.addChild(child11); + child01.addChild(child12); + parent01.addChild(tr); + parent01.addChild(sibling1); + // normal spread test + child01.on('testevent2',function(evobj) { + ++callorder; + tester.addResponse(callorder,false,'child spread 0-1, should never happen :: '+this.origi); + }); + child11.on('testevent2',function(evobj) { + ++callorder; + tester.addResponse(callorder,1,'child spread 1-1 :: '+this.origi); + }); + child12.on('testevent2',function(evobj) { + ++callorder; + tester.addResponse(callorder,2,'child spread 1-2 :: '+this.origi); + }); + tr.on('testevent2',function(evobj) { + ++callorder; + tester.addResponse(callorder,3,'local spread 1 :: '+this.origi); + }); + tr.on('testevent2',function(evobj) { + ++callorder; + tester.addResponse(callorder,4,'local spread 2 :: '+this.origi); + }); + parent01.on('testevent2',function(evobj) { + ++callorder; + tester.addResponse(callorder,5,'parent spread 1 :: '+this.origi); + }); + sibling1.on('testevent2',function(evobj) { + ++callorder; + tester.addResponse(callorder,6,'sibling spread 1 :: '+this.origi); + }); + // saturation test + tr.on('satevent',function(evobj) { + ++callorder2; + tester.addResponse(callorder2,1,'saturation: local spread 1 :: '+this.origi); + }); + sibling1.on('satevent',function(evobj) { + ++callorder2; + tester.addResponse(callorder2,2,'saturation: sibling spread 1 :: '+this.origi); + }); + child01.on('satevent',function(evobj) { + ++callorder2; + tester.addResponse(callorder2,3,'saturation: child spread 0-1 :: '+this.origi); + }); + child11.on('satevent',function(evobj) { + ++callorder2; + tester.addResponse(callorder2,4,'saturation: child spread 1-1 :: '+this.origi); + }); + child12.on('satevent',function(evobj) { + ++callorder2; + tester.addResponse(callorder2,5,'saturation: child spread 1-2 :: '+this.origi); + }); + parent01.on('satevent',function(evobj) { + ++callorder2; + tester.addResponse(callorder2,6,'saturation: parent spread 1 :: '+this.origi); + }); + var evv = new evan('testevent2'); + evv.setPropagation(evan.PROPAGATES_DOWN|evan.PROPAGATES_LOCAL|evan.PROPAGATES_UP|evan.PROPAGATES_SIBLINGS); + tr.emitEvent(evv); + var evv2 = new evan('satevent'); + evv2.setPropagation(evan.PROPAGATES_LOCAL|evan.PROPAGATES_SATURATING); + tr.emitEvent(evv2); +} +catch(e) { + tester.addResponse(e,true,'exception (hierarchy block)'); +} + +if (tester.matchResponses([1,2,3,4,5,6,1,2,3,4,5,6])===false) { + process.exit(1); } -matchResponses([3,'testevent','event1','testpayload','event2',null,'hurray']); +process.exit(0);