forked from humphd/brackets
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEventDispatcher.js
More file actions
303 lines (271 loc) · 13.1 KB
/
Copy pathEventDispatcher.js
File metadata and controls
303 lines (271 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
/*
* Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, $ */
/**
* Implements a jQuery-like event dispatch pattern for non-DOM objects:
* - Listeners are attached via on()/one() & detached via off()
* - Listeners can use namespaces for easy removal
* - Listeners can attach to multiple events at once via a space-separated list
* - Events are fired via trigger()
* - The same listener can be attached twice, and will be called twice; but off() will detach all
* duplicate copies at once ('duplicate' means '===' equality - see http://jsfiddle.net/bf4p29g5/1/)
*
* But it has some important differences from jQuery's non-DOM event mechanism:
* - More robust to listeners that throw exceptions (other listeners will still be called, and
* trigger() will still return control to its caller).
* - Events can be marked deprecated, causing on() to issue warnings
* - Easier to debug, since the dispatch code is much simpler
* - Faster, for the same reason
* - Uses less memory, since $(nonDOMObj).on() leaks memory in jQuery
* - API is simplified:
* - Event handlers do not have 'this' set to the event dispatcher object
* - Event object passed to handlers only has 'type' and 'target' fields
* - trigger() uses a simpler argument-list signature (like Promise APIs), rather than requiring
* an Array arg and ignoring additional args
* - trigger() does not support namespaces
* - For simplicity, on() does not accept a map of multiple events -> multiple handlers, nor a
* missing arg standing in for a bare 'return false' handler.
*
* For now, Brackets uses a jQuery patch to ensure $(obj).on() and obj.on() (etc.) are identical
* for any obj that has the EventDispatcher pattern. In the future, this may be deprecated.
*
* To add EventDispatcher methods to any object, call EventDispatcher.makeEventDispatcher(obj).
*/
define(function (require, exports, module) {
"use strict";
var _ = require("thirdparty/lodash");
/**
* Split "event.namespace" string into its two parts; both parts are optional.
* @param {string} eventName Event name and/or trailing ".namespace"
* @return {!{event:string, ns:string}} Uses "" for missing parts.
*/
function splitNs(eventStr) {
var dot = eventStr.indexOf(".");
if (dot === -1) {
return { eventName: eventStr };
} else {
return { eventName: eventStr.substring(0, dot), ns: eventStr.substring(dot) };
}
}
// These functions are added as mixins to any object by makeEventDispatcher()
/**
* Adds the given handler function to 'events': a space-separated list of one or more event names, each
* with an optional ".namespace" (used by off() - see below). If the handler is already listening to this
* event, a duplicate copy is added.
* @param {string} events
* @param {!function(!{type:string, target:!Object}, ...)} fn
*/
var on = function (events, fn) {
var eventsList = events.split(/\s+/).map(splitNs),
i;
// Check for deprecation warnings
if (this._deprecatedEvents) {
for (i = 0; i < eventsList.length; i++) {
var deprecation = this._deprecatedEvents[eventsList[i].eventName];
if (deprecation) {
var message = "Registering for deprecated event '" + eventsList[i].eventName + "'.";
if (typeof deprecation === "string") {
message += " Instead, use " + deprecation + ".";
}
console.warn(message, new Error().stack);
}
}
}
// Attach listener for each event clause
for (i = 0; i < eventsList.length; i++) {
var eventName = eventsList[i].eventName;
if (!this._eventHandlers) {
this._eventHandlers = {};
}
if (!this._eventHandlers[eventName]) {
this._eventHandlers[eventName] = [];
}
eventsList[i].handler = fn;
this._eventHandlers[eventName].push(eventsList[i]);
}
return this; // for chaining
};
/**
* Removes one or more handler functions based on the space-separated 'events' list. Each item in
* 'events' can be: bare event name, bare .namespace, or event.namespace pair. This yields a set of
* matching handlers. If 'fn' is ommitted, all these handlers are removed. If 'fn' is provided,
* only handlers exactly equal to 'fn' are removed (there may still be >1, if duplicates were added).
* @param {string} events
* @param {?function(!{type:string, target:!Object}, ...)} fn
*/
var off = function (events, fn) {
if (!this._eventHandlers) {
return this;
}
var eventsList = events.split(/\s+/).map(splitNs),
i;
var removeAllMatches = function (eventRec, eventName) {
var handlerList = this._eventHandlers[eventName],
k;
if (!handlerList) {
return;
}
// Walk backwards so it's easy to remove items
for (k = handlerList.length - 1; k >= 0; k--) {
// Look at ns & fn only - doRemove() has already taken care of eventName
if (!eventRec.ns || eventRec.ns === handlerList[k].ns) {
var handler = handlerList[k].handler;
if (!fn || fn === handler || fn._eventOnceWrapper === handler) {
handlerList.splice(k, 1);
}
}
}
if (!handlerList.length) {
delete this._eventHandlers[eventName];
}
}.bind(this);
var doRemove = function (eventRec) {
if (eventRec.eventName) {
// If arg calls out an event name, look at that handler list only
removeAllMatches(eventRec, eventRec.eventName);
} else {
// If arg only gives a namespace, look at handler lists for all events
_.forEach(this._eventHandlers, function (handlerList, eventName) {
removeAllMatches(eventRec, eventName);
});
}
}.bind(this);
// Detach listener for each event clause
// Each clause may be: bare eventname, bare .namespace, full eventname.namespace
for (i = 0; i < eventsList.length; i++) {
doRemove(eventsList[i]);
}
return this; // for chaining
};
/**
* Attaches a handler so it's only called once (per event in the 'events' list).
* @param {string} events
* @param {?function(!{type:string, target:!Object}, ...)} fn
*/
var one = function (events, fn) {
// Wrap fn in a self-detaching handler; saved on the original fn so off() can detect it later
if (!fn._eventOnceWrapper) {
fn._eventOnceWrapper = function (event) {
// Note: this wrapper is reused for all attachments of the same fn, so it shouldn't reference
// anything from the outer closure other than 'fn'
event.target.off(event.type, fn._eventOnceWrapper);
fn.apply(this, arguments);
};
}
return this.on(events, fn._eventOnceWrapper);
};
/**
* Invokes all handlers for the given event (in the order they were added).
* @param {string} eventName
* @param {*} ... Any additional args are passed to the event handler after the event object
*/
var trigger = function (eventName) {
var event = { type: eventName, target: this },
handlerList = this._eventHandlers && this._eventHandlers[eventName],
i;
if (!handlerList) {
return;
}
// Use a clone of the list in case handlers call on()/off() while we're still in the loop
handlerList = handlerList.slice();
// Pass 'event' object followed by any additional args trigger() was given
var applyArgs = Array.prototype.slice.call(arguments, 1);
applyArgs.unshift(event);
for (i = 0; i < handlerList.length; i++) {
try {
// Call one handler
handlerList[i].handler.apply(null, applyArgs);
} catch (err) {
console.error("Exception in '" + eventName + "' listener on", this, String(err), err.stack);
console.assert(); // causes dev tools to pause, just like an uncaught exception
}
}
};
/**
* Adds the EventDispatcher APIs to the given object: on(), one(), off(), and trigger(). May also be
* called on a prototype object - each instance will still behave independently.
* @param {!Object} obj Object to add event-dispatch methods to
*/
function makeEventDispatcher(obj) {
$.extend(obj, {
on: on,
off: off,
one: one,
trigger: trigger,
_EventDispatcher: true
});
// Later, on() may add _eventHandlers: Object.<string, Array.<{event:string, namespace:?string,
// handler:!function(!{type:string, target:!Object}, ...)}>> - map from eventName to an array
// of handler records
// Later, markDeprecated() may add _deprecatedEvents: Object.<string, string|boolean> - map from
// eventName to deprecation warning info
}
/**
* Utility for calling on() with an array of arguments to pass to event handlers (rather than a varargs
* list). makeEventDispatcher() must have previously been called on 'dispatcher'.
* @param {!Object} dispatcher
* @param {string} eventName
* @param {!Array.<*>} argsArray
*/
function triggerWithArray(dispatcher, eventName, argsArray) {
var triggerArgs = [eventName].concat(argsArray);
dispatcher.trigger.apply(dispatcher, triggerArgs);
}
/**
* Utility for attaching an event handler to an object that has not YET had makeEventDispatcher() called
* on it, but will in the future. Once 'futureDispatcher' becomes a real event dispatcher, any handlers
* attached here will be retained.
*
* Useful with core modules that have circular dependencies (one module initially gets an empty copy of the
* other, with no on() API present yet). Unlike other strategies like waiting for htmlReady(), this helper
* guarantees you won't miss any future events, regardless of how soon the other module finishes init and
* starts calling trigger().
*
* @param {!Object} futureDispatcher
* @param {string} events
* @param {?function(!{type:string, target:!Object}, ...)} fn
*/
function on_duringInit(futureDispatcher, events, fn) {
on.call(futureDispatcher, events, fn);
}
/**
* Mark a given event name as deprecated, such that on() will emit warnings when called with it.
* May be called before makeEventDispatcher(). May be called on a prototype where makeEventDispatcher()
* is called separately per instance (i.e. in the constructor). Should be called before clients have
* a chance to start calling on().
* @param {!Object} obj Event dispatcher object
* @param {string} eventName Name of deprecated event
* @param {string=} insteadStr Suggested thing to use instead
*/
function markDeprecated(obj, eventName, insteadStr) {
// Mark event as deprecated - on() will emit warnings when called with this event
if (!obj._deprecatedEvents) {
obj._deprecatedEvents = {};
}
obj._deprecatedEvents[eventName] = insteadStr || true;
}
exports.makeEventDispatcher = makeEventDispatcher;
exports.triggerWithArray = triggerWithArray;
exports.on_duringInit = on_duringInit;
exports.markDeprecated = markDeprecated;
});