-
Notifications
You must be signed in to change notification settings - Fork 3.6k
/
emittermixin.js
263 lines (234 loc) · 10.5 KB
/
emittermixin.js
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
/**
* @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module utils/dom/emittermixin
*/
import { default as EmitterMixin, _getEmitterListenedTo, _setEmitterId } from '../emittermixin';
import uid from '../uid';
import isNode from './isnode';
import isWindow from './iswindow';
import { extend } from 'lodash-es';
/**
* Mixin that injects the DOM events API into its host. It provides the API
* compatible with {@link module:utils/emittermixin~EmitterMixin}.
*
* DOM emitter mixin is by default available in the {@link module:ui/view~View} class,
* but it can also be mixed into any other class:
*
* import mix from '../utils/mix.js';
* import DomEmitterMixin from '../utils/dom/emittermixin.js';
*
* class SomeView {}
* mix( SomeView, DomEmitterMixin );
*
* const view = new SomeView();
* view.listenTo( domElement, ( evt, domEvt ) => {
* console.log( evt, domEvt );
* } );
*
* @mixin EmitterMixin
* @mixes module:utils/emittermixin~EmitterMixin
* @implements module:utils/dom/emittermixin~Emitter
*/
const DomEmitterMixin = extend( {}, EmitterMixin, {
/**
* Registers a callback function to be executed when an event is fired in a specific Emitter or DOM Node.
* It is backwards compatible with {@link module:utils/emittermixin~EmitterMixin#listenTo}.
*
* @param {module:utils/emittermixin~Emitter|Node} emitter The object that fires the event.
* @param {String} event The name of the event.
* @param {Function} callback The function to be called on event.
* @param {Object} [options={}] Additional options.
* @param {module:utils/priorities~PriorityString|Number} [options.priority='normal'] The priority of this event callback. The higher
* the priority value the sooner the callback will be fired. Events having the same priority are called in the
* order they were added.
* @param {Boolean} [options.useCapture=false] Indicates that events of this type will be dispatched to the registered
* listener before being dispatched to any EventTarget beneath it in the DOM tree.
*/
listenTo( emitter, ...rest ) {
// Check if emitter is an instance of DOM Node. If so, replace the argument with
// corresponding ProxyEmitter (or create one if not existing).
if ( isNode( emitter ) || isWindow( emitter ) ) {
const proxy = this._getProxyEmitter( emitter ) || new ProxyEmitter( emitter );
proxy.attach( ...rest );
emitter = proxy;
}
// Execute parent class method with Emitter (or ProxyEmitter) instance.
EmitterMixin.listenTo.call( this, emitter, ...rest );
},
/**
* Stops listening for events. It can be used at different levels:
* It is backwards compatible with {@link module:utils/emittermixin~EmitterMixin#listenTo}.
*
* * To stop listening to a specific callback.
* * To stop listening to a specific event.
* * To stop listening to all events fired by a specific object.
* * To stop listening to all events fired by all object.
*
* @param {module:utils/emittermixin~Emitter|Node} [emitter] The object to stop listening to. If omitted, stops it for all objects.
* @param {String} [event] (Requires the `emitter`) The name of the event to stop listening to. If omitted, stops it
* for all events from `emitter`.
* @param {Function} [callback] (Requires the `event`) The function to be removed from the call list for the given
* `event`.
*/
stopListening( emitter, event, callback ) {
// Check if emitter is an instance of DOM Node. If so, replace the argument with corresponding ProxyEmitter.
if ( isNode( emitter ) || isWindow( emitter ) ) {
const proxy = this._getProxyEmitter( emitter );
// Element has no listeners.
if ( !proxy ) {
return;
}
emitter = proxy;
}
// Execute parent class method with Emitter (or ProxyEmitter) instance.
EmitterMixin.stopListening.call( this, emitter, event, callback );
if ( emitter instanceof ProxyEmitter ) {
emitter.detach( event );
}
},
/**
* Retrieves ProxyEmitter instance for given DOM Node residing in this Host.
*
* @private
* @param {Node} node DOM Node of the ProxyEmitter.
* @returns {module:utils/dom/emittermixin~ProxyEmitter} ProxyEmitter instance or null.
*/
_getProxyEmitter( node ) {
return _getEmitterListenedTo( this, getNodeUID( node ) );
}
} );
export default DomEmitterMixin;
/**
* Creates a ProxyEmitter instance. Such an instance is a bridge between a DOM Node firing events
* and any Host listening to them. It is backwards compatible with {@link module:utils/emittermixin~EmitterMixin#on}.
*
* listenTo( click, ... )
* +-----------------------------------------+
* | stopListening( ... ) |
* +----------------------------+ | addEventListener( click, ... )
* | Host | | +---------------------------------------------+
* +----------------------------+ | | removeEventListener( click, ... ) |
* | _listeningTo: { | +----------v-------------+ |
* | UID: { | | ProxyEmitter | |
* | emitter: ProxyEmitter, | +------------------------+ +------------v----------+
* | callbacks: { | | events: { | | Node (HTMLElement) |
* | click: [ callbacks ] | | click: [ callbacks ] | +-----------------------+
* | } | | }, | | data-ck-expando: UID |
* | } | | _domNode: Node, | +-----------------------+
* | } | | _domListeners: {}, | |
* | +------------------------+ | | _emitterId: UID | |
* | | DomEmitterMixin | | +--------------^---------+ |
* | +------------------------+ | | | |
* +--------------^-------------+ | +---------------------------------------------+
* | | click (DOM Event)
* +-----------------------------------------+
* fire( click, DOM Event )
*
* @mixes module:utils/emittermixin~EmitterMixin
* @implements module:utils/dom/emittermixin~Emitter
* @private
*/
class ProxyEmitter {
/**
* @param {Node} node DOM Node that fires events.
* @returns {Object} ProxyEmitter instance bound to the DOM Node.
*/
constructor( node ) {
// Set emitter ID to match DOM Node "expando" property.
_setEmitterId( this, getNodeUID( node ) );
// Remember the DOM Node this ProxyEmitter is bound to.
this._domNode = node;
}
}
extend( ProxyEmitter.prototype, EmitterMixin, {
/**
* Collection of native DOM listeners.
*
* @private
* @member {Object} module:utils/dom/emittermixin~ProxyEmitter#_domListeners
*/
/**
* Registers a callback function to be executed when an event is fired.
*
* It attaches a native DOM listener to the DOM Node. When fired,
* a corresponding Emitter event will also fire with DOM Event object as an argument.
*
* @method module:utils/dom/emittermixin~ProxyEmitter#attach
* @param {String} event The name of the event.
* @param {Function} callback The function to be called on event.
* @param {Object} [options={}] Additional options.
* @param {Boolean} [options.useCapture=false] Indicates that events of this type will be dispatched to the registered
* listener before being dispatched to any EventTarget beneath it in the DOM tree.
*/
attach( event, callback, options = {} ) {
// If the DOM Listener for given event already exist it is pointless
// to attach another one.
if ( this._domListeners && this._domListeners[ event ] ) {
return;
}
const domListener = this._createDomListener( event, !!options.useCapture );
// Attach the native DOM listener to DOM Node.
this._domNode.addEventListener( event, domListener, !!options.useCapture );
if ( !this._domListeners ) {
this._domListeners = {};
}
// Store the native DOM listener in this ProxyEmitter. It will be helpful
// when stopping listening to the event.
this._domListeners[ event ] = domListener;
},
/**
* Stops executing the callback on the given event.
*
* @method module:utils/dom/emittermixin~ProxyEmitter#detach
* @param {String} event The name of the event.
*/
detach( event ) {
let events;
// Remove native DOM listeners which are orphans. If no callbacks
// are awaiting given event, detach native DOM listener from DOM Node.
// See: {@link attach}.
if ( this._domListeners[ event ] && ( !( events = this._events[ event ] ) || !events.callbacks.length ) ) {
this._domListeners[ event ].removeListener();
}
},
/**
* Creates a native DOM listener callback. When the native DOM event
* is fired it will fire corresponding event on this ProxyEmitter.
* Note: A native DOM Event is passed as an argument.
*
* @private
* @method module:utils/dom/emittermixin~ProxyEmitter#_createDomListener
* @param {String} event The name of the event.
* @param {Boolean} useCapture Indicates whether the listener was created for capturing event.
* @returns {Function} The DOM listener callback.
*/
_createDomListener( event, useCapture ) {
const domListener = domEvt => {
this.fire( event, domEvt );
};
// Supply the DOM listener callback with a function that will help
// detach it from the DOM Node, when it is no longer necessary.
// See: {@link detach}.
domListener.removeListener = () => {
this._domNode.removeEventListener( event, domListener, useCapture );
delete this._domListeners[ event ];
};
return domListener;
}
} );
// Gets an unique DOM Node identifier. The identifier will be set if not defined.
//
// @private
// @param {Node} node
// @returns {String} UID for given DOM Node.
function getNodeUID( node ) {
return node[ 'data-ck-expando' ] || ( node[ 'data-ck-expando' ] = uid() );
}
/**
* Interface representing classes which mix in {@link module:utils/dom/emittermixin~EmitterMixin}.
*
* @interface Emitter
*/