Skip to content

Commit 7fd29b4

Browse files
misteroneillgkatsev
authored andcommitted
fix: Allow evented objects, such as components and plugins, to listen to the window object in addition to DOM objects. (videojs#5255)
the bug is that objects using the new-ish evented mixin cannot listen to the window object, preventing things like: ``` component.on(window, 'scroll', throttledListener); ``` This fixes that so anything that's a native EventTarget works.
1 parent 5b8d373 commit 7fd29b4

File tree

2 files changed

+237
-17
lines changed

2 files changed

+237
-17
lines changed

src/js/mixins/evented.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ import * as Fn from '../utils/fn';
99
import * as Obj from '../utils/obj';
1010
import EventTarget from '../event-target';
1111

12+
/**
13+
* Returns whether or not an object supports native events.
14+
*
15+
* @param {Object} object
16+
* An object to test.
17+
*
18+
* @return {boolean}
19+
* Whether or not an object supports native events.
20+
*/
21+
const supportsNativeEvents = (object) => typeof object.addEventListener === 'function';
22+
1223
/**
1324
* Returns whether or not an object has had the evented mixin applied.
1425
*
@@ -50,7 +61,7 @@ const isValidEventType = (type) =>
5061
* The object to test.
5162
*/
5263
const validateTarget = (target) => {
53-
if (!target.nodeName && !isEvented(target)) {
64+
if (!supportsNativeEvents(target) && !isEvented(target)) {
5465
throw new Error('Invalid target; must be a DOM node or evented object.');
5566
}
5667
};
@@ -154,7 +165,7 @@ const normalizeListenArgs = (self, args) => {
154165
const listen = (target, method, type, listener) => {
155166
validateTarget(target);
156167

157-
if (target.nodeName) {
168+
if (supportsNativeEvents(target)) {
158169
Events[method](target, type, listener);
159170
} else {
160171
target[method](type, listener);
@@ -307,7 +318,7 @@ const EventedMixin = {
307318
// the same guid as the event listener in on().
308319
this.off('dispose', listener);
309320

310-
if (target.nodeName) {
321+
if (supportsNativeEvents(target)) {
311322
Events.off(target, type, listener);
312323
Events.off(target, 'dispose', listener);
313324
} else if (isEvented(target)) {
@@ -346,7 +357,7 @@ const EventedMixin = {
346357
* @param {String} [options.eventBusKey]
347358
* By default, adds a `eventBusEl_` DOM element to the target object,
348359
* which is used as an event bus. If the target object already has a
349-
* DOM element that should be used, pass its key here.
360+
* DOM element which should be used, pass its key here.
350361
*
351362
* @return {Object}
352363
* The target object.

test/unit/mixins/evented.test.js

Lines changed: 222 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
/* eslint-env qunit */
2+
import document from 'global/document';
3+
import window from 'global/window';
24
import sinon from 'sinon';
35
import evented from '../../../src/js/mixins/evented';
46
import * as Dom from '../../../src/js/utils/dom';
@@ -11,6 +13,20 @@ const errors = {
1113
target: new Error('Invalid target; must be a DOM node or evented object.')
1214
};
1315

16+
// Cross-browser event creation.
17+
const createEvent = (type) => {
18+
let event;
19+
20+
try {
21+
event = new window.Event(type);
22+
} catch (x) {
23+
event = document.createEvent('Event');
24+
event.initEvent(type, true, true);
25+
}
26+
27+
return event;
28+
};
29+
1430
const validateListenerCall = (call, thisValue, eventExpectation) => {
1531
const eventActual = call.args[0];
1632

@@ -31,7 +47,13 @@ QUnit.module('mixins: evented', {
3147
},
3248

3349
afterEach() {
34-
Object.keys(this.targets).forEach(k => this.targets[k].trigger('dispose'));
50+
Object.keys(this.targets).forEach(k => {
51+
if (typeof this.targets[k].trigger === 'function') {
52+
this.targets[k].trigger('dispose');
53+
} else {
54+
this.targets[k] = null;
55+
}
56+
});
3557
}
3658
});
3759

@@ -61,7 +83,7 @@ QUnit.test('evented() with custom element', function(assert) {
6183
);
6284
});
6385

64-
QUnit.test('on() and one() errors', function(assert) {
86+
QUnit.test('on() and one() error states', function(assert) {
6587
const target = this.targets.a = evented({});
6688

6789
['on', 'one'].forEach(method => {
@@ -74,7 +96,7 @@ QUnit.test('on() and one() errors', function(assert) {
7496
});
7597
});
7698

77-
QUnit.test('off() errors', function(assert) {
99+
QUnit.test('off() error states', function(assert) {
78100
const target = this.targets.a = evented({});
79101

80102
// An invalid event actually causes an invalid target error because it
@@ -86,7 +108,7 @@ QUnit.test('off() errors', function(assert) {
86108
assert.throws(() => target.off(evented({}), 'x', null), errors.listener, 'the expected error is thrown');
87109
});
88110

89-
QUnit.test('on() can add a listener to one event type on this object', function(assert) {
111+
QUnit.test('on() can add a listener to one event type on itself', function(assert) {
90112
const a = this.targets.a = evented({});
91113
const spy = sinon.spy();
92114

@@ -101,7 +123,7 @@ QUnit.test('on() can add a listener to one event type on this object', function(
101123
});
102124
});
103125

104-
QUnit.test('on() can add a listener to an array of event types on this object', function(assert) {
126+
QUnit.test('on() can add a listener to an array of event types on itself', function(assert) {
105127
const a = this.targets.a = evented({});
106128
const spy = sinon.spy();
107129

@@ -122,7 +144,7 @@ QUnit.test('on() can add a listener to an array of event types on this object',
122144
});
123145
});
124146

125-
QUnit.test('one() can add a listener to one event type on this object', function(assert) {
147+
QUnit.test('one() can add a listener to one event type on itself', function(assert) {
126148
const a = this.targets.a = evented({});
127149
const spy = sinon.spy();
128150

@@ -138,7 +160,7 @@ QUnit.test('one() can add a listener to one event type on this object', function
138160
});
139161
});
140162

141-
QUnit.test('one() can add a listener to an array of event types on this object', function(assert) {
163+
QUnit.test('one() can add a listener to an array of event types on itself', function(assert) {
142164
const a = this.targets.a = evented({});
143165
const spy = sinon.spy();
144166

@@ -161,7 +183,7 @@ QUnit.test('one() can add a listener to an array of event types on this object',
161183
});
162184
});
163185

164-
QUnit.test('on() can add a listener to one event type on a different target object', function(assert) {
186+
QUnit.test('on() can add a listener to one event type on a different evented object', function(assert) {
165187
const a = this.targets.a = evented({});
166188
const b = this.targets.b = evented({});
167189
const spy = sinon.spy();
@@ -180,7 +202,47 @@ QUnit.test('on() can add a listener to one event type on a different target obje
180202
});
181203
});
182204

183-
QUnit.test('on() can add a listener to an array of event types on a different target object', function(assert) {
205+
QUnit.test('on() can add a listener to one event type on a node object', function(assert) {
206+
const a = this.targets.a = evented({});
207+
const b = this.targets.b = Dom.createEl('div');
208+
const spy = sinon.spy();
209+
const event = createEvent('x');
210+
211+
a.on(b, 'x', spy);
212+
b.dispatchEvent(event);
213+
214+
// Make sure we aren't magically binding a listener to "a".
215+
a.trigger('x');
216+
217+
assert.strictEqual(spy.callCount, 1, 'the listener was called the expected number of times');
218+
219+
validateListenerCall(spy.getCall(0), a, {
220+
type: 'x',
221+
target: b
222+
});
223+
});
224+
225+
QUnit.test('on() can add a listener to one event type on the window object', function(assert) {
226+
const a = this.targets.a = evented({});
227+
const b = this.targets.b = window;
228+
const spy = sinon.spy();
229+
const event = createEvent('x');
230+
231+
a.on(b, 'x', spy);
232+
b.dispatchEvent(event);
233+
234+
// Make sure we aren't magically binding a listener to "a".
235+
a.trigger('x');
236+
237+
assert.strictEqual(spy.callCount, 1, 'the listener was called the expected number of times');
238+
239+
validateListenerCall(spy.getCall(0), a, {
240+
type: 'x',
241+
target: b
242+
});
243+
});
244+
245+
QUnit.test('on() can add a listener to an array of event types on a different evented object', function(assert) {
184246
const a = this.targets.a = evented({});
185247
const b = this.targets.b = evented({});
186248
const spy = sinon.spy();
@@ -206,13 +268,70 @@ QUnit.test('on() can add a listener to an array of event types on a different ta
206268
});
207269
});
208270

209-
QUnit.test('one() can add a listener to one event type on a different target object', function(assert) {
271+
QUnit.test('on() can add a listener to an array of event types on a node object', function(assert) {
272+
const a = this.targets.a = evented({});
273+
const b = this.targets.b = Dom.createEl('div');
274+
const spy = sinon.spy();
275+
const x = createEvent('x');
276+
const y = createEvent('y');
277+
278+
a.on(b, ['x', 'y'], spy);
279+
b.dispatchEvent(x);
280+
b.dispatchEvent(y);
281+
282+
// Make sure we aren't magically binding a listener to "a".
283+
a.trigger('x');
284+
a.trigger('y');
285+
286+
assert.strictEqual(spy.callCount, 2, 'the listener was called the expected number of times');
287+
288+
validateListenerCall(spy.getCall(0), a, {
289+
type: 'x',
290+
target: b
291+
});
292+
293+
validateListenerCall(spy.getCall(1), a, {
294+
type: 'y',
295+
target: b
296+
});
297+
});
298+
299+
QUnit.test('on() can add a listener to an array of event types on the window object', function(assert) {
300+
const a = this.targets.a = evented({});
301+
const b = this.targets.b = window;
302+
const spy = sinon.spy();
303+
const x = createEvent('x');
304+
const y = createEvent('y');
305+
306+
a.on(b, ['x', 'y'], spy);
307+
b.dispatchEvent(x);
308+
b.dispatchEvent(y);
309+
310+
// Make sure we aren't magically binding a listener to "a".
311+
a.trigger('x');
312+
a.trigger('y');
313+
314+
assert.strictEqual(spy.callCount, 2, 'the listener was called the expected number of times');
315+
316+
validateListenerCall(spy.getCall(0), a, {
317+
type: 'x',
318+
target: b
319+
});
320+
321+
validateListenerCall(spy.getCall(1), a, {
322+
type: 'y',
323+
target: b
324+
});
325+
});
326+
327+
QUnit.test('one() can add a listener to one event type on a different evented object', function(assert) {
210328
const a = this.targets.a = evented({});
211329
const b = this.targets.b = evented({});
212330
const spy = sinon.spy();
213331

214332
a.one(b, 'x', spy);
215333
b.trigger('x');
334+
b.trigger('x');
216335

217336
// Make sure we aren't magically binding a listener to "a".
218337
a.trigger('x');
@@ -225,9 +344,49 @@ QUnit.test('one() can add a listener to one event type on a different target obj
225344
});
226345
});
227346

228-
// The behavior here unfortunately differs from the identical case where "a"
229-
// listens to itself. This is something that should be resolved...
230-
QUnit.test('one() can add a listener to an array of event types on a different target object', function(assert) {
347+
QUnit.test('one() can add a listener to one event type on a node object', function(assert) {
348+
const a = this.targets.a = evented({});
349+
const b = this.targets.b = Dom.createEl('div');
350+
const spy = sinon.spy();
351+
const event = createEvent('x');
352+
353+
a.one(b, 'x', spy);
354+
b.dispatchEvent(event);
355+
b.dispatchEvent(event);
356+
357+
// Make sure we aren't magically binding a listener to "a".
358+
a.trigger('x');
359+
360+
assert.strictEqual(spy.callCount, 1, 'the listener was called the expected number of times');
361+
362+
validateListenerCall(spy.getCall(0), a, {
363+
type: 'x',
364+
target: b
365+
});
366+
});
367+
368+
QUnit.test('one() can add a listener to one event type on the window object', function(assert) {
369+
const a = this.targets.a = evented({});
370+
const b = this.targets.b = window;
371+
const spy = sinon.spy();
372+
const event = createEvent('x');
373+
374+
a.one(b, 'x', spy);
375+
b.dispatchEvent(event);
376+
b.dispatchEvent(event);
377+
378+
// Make sure we aren't magically binding a listener to "a".
379+
a.trigger('x');
380+
381+
assert.strictEqual(spy.callCount, 1, 'the listener was called the expected number of times');
382+
383+
validateListenerCall(spy.getCall(0), a, {
384+
type: 'x',
385+
target: b
386+
});
387+
});
388+
389+
QUnit.test('one() can add a listener to an array of event types on a different evented object', function(assert) {
231390
const a = this.targets.a = evented({});
232391
const b = this.targets.b = evented({});
233392
const spy = sinon.spy();
@@ -250,6 +409,56 @@ QUnit.test('one() can add a listener to an array of event types on a different t
250409
});
251410
});
252411

412+
QUnit.test('one() can add a listener to an array of event types on a node object', function(assert) {
413+
const a = this.targets.a = evented({});
414+
const b = this.targets.b = Dom.createEl('div');
415+
const spy = sinon.spy();
416+
const x = createEvent('x');
417+
const y = createEvent('y');
418+
419+
a.one(b, ['x', 'y'], spy);
420+
b.dispatchEvent(x);
421+
b.dispatchEvent(y);
422+
b.dispatchEvent(x);
423+
b.dispatchEvent(y);
424+
425+
// Make sure we aren't magically binding a listener to "a".
426+
a.trigger('x');
427+
a.trigger('y');
428+
429+
assert.strictEqual(spy.callCount, 1, 'the listener was called the expected number of times');
430+
431+
validateListenerCall(spy.getCall(0), a, {
432+
type: 'x',
433+
target: b
434+
});
435+
});
436+
437+
QUnit.test('one() can add a listener to an array of event types on the window object', function(assert) {
438+
const a = this.targets.a = evented({});
439+
const b = this.targets.b = window;
440+
const spy = sinon.spy();
441+
const x = createEvent('x');
442+
const y = createEvent('y');
443+
444+
a.one(b, ['x', 'y'], spy);
445+
b.dispatchEvent(x);
446+
b.dispatchEvent(y);
447+
b.dispatchEvent(x);
448+
b.dispatchEvent(y);
449+
450+
// Make sure we aren't magically binding a listener to "a".
451+
a.trigger('x');
452+
a.trigger('y');
453+
454+
assert.strictEqual(spy.callCount, 1, 'the listener was called the expected number of times');
455+
456+
validateListenerCall(spy.getCall(0), a, {
457+
type: 'x',
458+
target: b
459+
});
460+
});
461+
253462
QUnit.test('off() with no arguments will remove all listeners from all events on this object', function(assert) {
254463
const a = this.targets.a = evented({});
255464
const spyX = sinon.spy();

0 commit comments

Comments
 (0)