Simplify FakeEvent and FakeEventTarget

We now avoid tricky things like CustomEvent and setting properties on
native Events.  This gives us better cross-browser compatibility and
less complexity.

Change-Id: Idc9fcc69c33257e4540d956bcbc949de6d992cf0
This commit is contained in:
Joey Parrish
2016-02-28 11:31:23 -08:00
parent 303ddaef7f
commit 9d70cad0ea
10 changed files with 247 additions and 258 deletions
-1
View File
@@ -1,6 +1,5 @@
# Polyfills used to emulate missing browsers features.
+../../lib/polyfill/customevent.js
+../../lib/polyfill/fullscreen.js
+../../lib/polyfill/mediakeys.js
+../../lib/polyfill/patchedmediakeys_20140218.js
-68
View File
@@ -1,68 +0,0 @@
/**
* @license
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
goog.provide('shaka.polyfill.CustomEvent');
goog.require('shaka.polyfill.register');
/**
* @namespace shaka.polyfill.CustomEvent
* @exportDoc
*
* @summary A polyfill to implement the CustomEvent constructor on browsers
* which don't have one or don't allow its direct use.
*/
/**
* Install the polyfill if needed.
*/
shaka.polyfill.CustomEvent.install = function() {
var present = 'CustomEvent' in window;
if (present) {
try {
new CustomEvent('');
} catch (exception) {
present = false;
}
}
if (!present) {
window['CustomEvent'] = shaka.polyfill.CustomEvent.ctor_;
}
};
/**
* @this {CustomEvent}
* @constructor
* @param {string} type
* @param {CustomEventInit=} opt_init
* @private
*/
shaka.polyfill.CustomEvent.ctor_ = function(type, opt_init) {
var event = /** @type {!CustomEvent} */(document.createEvent('CustomEvent'));
var init = opt_init || { bubbles: false, cancelable: false, detail: null };
event.initCustomEvent(type, !!init.bubbles, !!init.cancelable, init.detail);
return event;
};
shaka.polyfill.register(shaka.polyfill.CustomEvent.install);
+4 -6
View File
@@ -369,7 +369,7 @@ shaka.polyfill.PatchedMediaKeys.v20140218.MediaKeys.prototype.
shaka.polyfill.PatchedMediaKeys.v20140218.
MediaKeySession = function(nativeMediaKeys, sessionType) {
shaka.log.debug('v20140218.MediaKeySession');
shaka.util.FakeEventTarget.call(this, null);
shaka.util.FakeEventTarget.call(this);
// Native MediaKeySession, which will be created in generateRequest
/** @private {MSMediaKeySession} */
@@ -520,8 +520,7 @@ shaka.polyfill.PatchedMediaKeys.v20140218.onMsNeedKey_ = function(event) {
// Alias
var v20140218 = shaka.polyfill.PatchedMediaKeys.v20140218;
var event2 = shaka.util.FakeEvent.create({
type: 'encrypted',
var event2 = new shaka.util.FakeEvent('encrypted', {
initDataType: 'cenc',
initData: v20140218.NormaliseInitData_(event.initData)
});
@@ -615,8 +614,7 @@ shaka.polyfill.PatchedMediaKeys.v20140218.MediaKeySession.prototype.
var isNew = this.keyStatuses.getStatus() == undefined;
var event2 = shaka.util.FakeEvent.create({
type: 'message',
var event2 = new shaka.util.FakeEvent('message', {
messageType: isNew ? 'licenserequest' : 'licenserenewal',
message: event.message.buffer
});
@@ -711,7 +709,7 @@ shaka.polyfill.PatchedMediaKeys.v20140218.MediaKeySession.prototype.
shaka.polyfill.PatchedMediaKeys.v20140218.MediaKeySession.prototype.
updateKeyStatus_ = function(status) {
this.keyStatuses.setStatus(status);
var event = shaka.util.FakeEvent.create({type: 'keystatuseschange'});
var event = new shaka.util.FakeEvent('keystatuseschange');
this.dispatchEvent(event);
};
+4 -6
View File
@@ -404,8 +404,7 @@ shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.prototype.onWebkitNeedKey_ =
shaka.log.debug('v01b.onWebkitNeedKey_', event);
shaka.asserts.assert(this.media_, 'media_ not set in onWebkitNeedKey_');
var event2 = shaka.util.FakeEvent.create({
type: 'encrypted',
var event2 = new shaka.util.FakeEvent('encrypted', {
initDataType: 'webm', // not used by v0.1b EME, but given a valid value
initData: event.initData
});
@@ -430,8 +429,7 @@ shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.prototype.onWebkitKeyMessage_ =
var isNew = session.keyStatuses.getStatus() == undefined;
var event2 = shaka.util.FakeEvent.create({
type: 'message',
var event2 = new shaka.util.FakeEvent('message', {
messageType: isNew ? 'licenserequest' : 'licenserenewal',
message: event.message
});
@@ -514,7 +512,7 @@ shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.prototype.findSession_ =
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession =
function(media, keySystem, sessionType) {
shaka.log.debug('v01b.MediaKeySession');
shaka.util.FakeEventTarget.call(this, null);
shaka.util.FakeEventTarget.call(this);
/** @private {!HTMLMediaElement} */
this.media_ = media;
@@ -788,7 +786,7 @@ shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.update_ =
shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.
updateKeyStatus_ = function(status) {
this.keyStatuses.setStatus(status);
var event = shaka.util.FakeEvent.create({type: 'keystatuseschange'});
var event = new shaka.util.FakeEvent('keystatuseschange');
this.dispatchEvent(event);
};
+74 -24
View File
@@ -17,35 +17,85 @@
goog.provide('shaka.util.FakeEvent');
goog.require('shaka.asserts');
/**
* @namespace shaka.util.FakeEvent
* @summary A utility to simplify the creation of fake events.
*/
/**
* Return an Event object based on the dictionary.
* Create an Event work-alike object based on the dictionary.
* The event should contain all of the same properties from the dict.
* @param {!Object} dict
* @return {!Event}
*
* @param {string} type
* @param {Object=} opt_dict
* @constructor
* @extends {Event}
*/
shaka.util.FakeEvent.create = function(dict) {
var event = new CustomEvent(dict.type, {
detail: dict.detail,
bubbles: !!dict.bubbles
});
// NOTE: In strict mode, we can't overwrite existing properties, so we only
// set properties on "event" which don't exist yet. If a property does exist
// on "event", we assert that it has the correct value already.
shaka.util.FakeEvent = function(type, opt_dict) {
// Take properties from dict if present.
var dict = opt_dict || {};
for (var key in dict) {
if (key in event) {
shaka.asserts.assert(event[key] == dict[key], 'key = ' + key);
} else {
event[key] = dict[key];
}
this[key] = dict[key];
}
return event;
// These Properties below cannot be set by dict. They are all provided for
// compatibility with native events.
/** @const {boolean} */
this.bubbles = false;
/** @const {boolean} */
this.cancelable = false;
/** @const {boolean} */
this.defaultPrevented = false;
/**
* According to MDN, Chrome uses high-res timers instead of epoch time.
* Follow suit so that timeStamps on FakeEvents use the same base as
* on native Events.
* @const {number}
* @see https://developer.mozilla.org/en-US/docs/Web/API/Event/timeStamp
*/
this.timeStamp = window.performance ? window.performance.now() : Date.now();
/** @const {string} */
this.type = type;
/** @const {boolean} */
this.isTrusted = false;
/** @type {EventTarget} */
this.currentTarget = null;
/** @type {EventTarget} */
this.target = null;
/**
* Non-standard property read by FakeEventTarget to stop processing listeners.
* @type {boolean}
*/
this.stopped = false;
};
/**
* Does nothing, since FakeEvents have no default. Provided for compatibility
* with native Events.
*/
shaka.util.FakeEvent.prototype.preventDefault = function() {};
/**
* Stops processing event listeners for this event. Provided for compatibility
* with native Events.
*/
shaka.util.FakeEvent.prototype.stopImmediatePropagation = function() {
this.stopped = true;
};
/**
* Does nothing, since FakeEvents do not bubble. Provided for compatibility
* with native Events.
*/
shaka.util.FakeEvent.prototype.stopPropagation = function() {};
+20 -82
View File
@@ -19,6 +19,7 @@ goog.provide('shaka.util.FakeEventTarget');
goog.require('shaka.asserts');
goog.require('shaka.log');
goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.MultiMap');
@@ -26,23 +27,17 @@ goog.require('shaka.util.MultiMap');
/**
* A work-alike for EventTarget. Only DOM elements may be true EventTargets,
* but this can be used as a base class to provide event dispatch to non-DOM
* classes.
* classes. Only FakeEvents should be dispatched.
*
* @param {shaka.util.FakeEventTarget} parent The parent for the purposes of
* event bubbling. Note that events on a FakeEventTarget can only bubble
* to other FakeEventTargets.
* @struct
* @constructor
* @implements {EventTarget}
*/
shaka.util.FakeEventTarget = function(parent) {
shaka.util.FakeEventTarget = function() {
/**
* @private {!shaka.util.MultiMap.<shaka.util.FakeEventTarget.ListenerType>}
*/
this.listeners_ = new shaka.util.MultiMap();
/** @protected {shaka.util.FakeEventTarget} */
this.parent = parent;
};
@@ -59,18 +54,13 @@ shaka.util.FakeEventTarget.ListenerType;
* @param {string} type The event type to listen for.
* @param {shaka.util.FakeEventTarget.ListenerType} listener The callback or
* listener object to invoke.
* @param {boolean=} opt_capturing True to listen during the capturing phase,
* false to listen during the bubbling phase. Note that FakeEventTarget
* does not support the capturing phase from the standard event model.
* @param {boolean=} opt_capturing Ignored. FakeEventTargets do not have
* parents, so events neither capture nor bubble.
* @override
*/
shaka.util.FakeEventTarget.prototype.addEventListener =
function(type, listener, opt_capturing) {
// We don't support the capturing phase.
shaka.asserts.assert(!opt_capturing, 'Capturing phase unsupported');
if (!opt_capturing) {
this.listeners_.push(type, listener);
}
this.listeners_.push(type, listener);
};
@@ -80,19 +70,13 @@ shaka.util.FakeEventTarget.prototype.addEventListener =
* @param {string} type The event type for which you wish to remove a listener.
* @param {shaka.util.FakeEventTarget.ListenerType} listener The callback or
* listener object to remove.
* @param {boolean=} opt_capturing True to remove a listener for the capturing
* phase, false to remove a listener for the bubbling phase. Note that
* FakeEventTarget does not support the capturing phase from the standard
* event model.
* @param {boolean=} opt_capturing Ignored. FakeEventTargets do not have
* parents, so events neither capture nor bubble.
* @override
*/
shaka.util.FakeEventTarget.prototype.removeEventListener =
function(type, listener, opt_capturing) {
// We don't support the capturing phase.
shaka.asserts.assert(!opt_capturing, 'Capturing phase unsupported');
if (!opt_capturing) {
this.listeners_.remove(type, listener);
}
this.listeners_.remove(type, listener);
};
@@ -104,61 +88,18 @@ shaka.util.FakeEventTarget.prototype.removeEventListener =
* @override
*/
shaka.util.FakeEventTarget.prototype.dispatchEvent = function(event) {
// Overwrite the Event's properties if not already done so (events can be
// re-dispatched, so we may be have already done this, so in this case we
// can just update the values). Assignment doesn't work in most browsers.
// Object.defineProperty seems to work, although some browsers
// need the original properties deleted first.
if (!event.hasOwnProperty('srcElement')) {
delete event.srcElement;
Object.defineProperty(event, 'srcElement', {
get: function() { return null; }
});
}
if (event.hasOwnProperty('target')) {
event.target = this;
} else {
delete event.target;
var target = this;
Object.defineProperty(event, 'target', {
get: function() { return target; },
set: function(value) { target = value; }
});
}
if (event.hasOwnProperty('currentTarget')) {
event.currentTarget = null;
} else {
delete event.currentTarget;
var currentTarget = null;
Object.defineProperty(event, 'currentTarget', {
get: function() { return currentTarget; },
set: function(value) { currentTarget = value; }
});
}
return this.recursiveDispatch_(event);
};
/**
* Dispatches an event recursively without changing its original target.
*
* @param {!Event} event
* @return {boolean} True if the default action was prevented.
* @private
*/
shaka.util.FakeEventTarget.prototype.recursiveDispatch_ = function(event) {
event.currentTarget = this;
// In many browsers, it is complex to overwrite properties of actual Events.
// Here we expect only to dispatch FakeEvents, which are simpler.
shaka.asserts.assert(event instanceof shaka.util.FakeEvent,
'FakeEventTarget can only dispatch FakeEvents!');
var list = this.listeners_.get(event.type) || [];
for (var i = 0; i < list.length; ++i) {
// Do this every time, since events can be re-dispatched from handlers.
event.target = this;
event.currentTarget = this;
var listener = list[i];
try {
if (listener.handleEvent) {
@@ -166,20 +107,17 @@ shaka.util.FakeEventTarget.prototype.recursiveDispatch_ = function(event) {
} else {
listener.call(this, event);
}
// NOTE: If needed, stopImmediatePropagation() would be checked here.
} catch (exception) {
// Exceptions during event handlers should not affect the caller,
// but should appear on the console as uncaught, according to MDN:
// http://goo.gl/N6Ff27
shaka.log.error('Uncaught exception in event handler', exception);
}
}
// NOTE: If needed, stopPropagation() would be checked here.
if (this.parent && event.bubbles) {
this.parent.recursiveDispatch_(event);
if (event.stopped) {
break;
}
}
return event.defaultPrevented;
};
-1
View File
@@ -32,7 +32,6 @@ goog.require('shaka.media.VttTextParser');
goog.require('shaka.net.DataUriPlugin');
goog.require('shaka.net.HttpPlugin');
goog.require('shaka.polyfill.CustomEvent');
goog.require('shaka.polyfill.Fullscreen');
goog.require('shaka.polyfill.MediaKeys');
goog.require('shaka.polyfill.Promise');
+6 -2
View File
@@ -26,8 +26,12 @@ describe('EventManager', function() {
eventManager = new shaka.util.EventManager();
target1 = document.createElement('div');
target2 = document.createElement('div');
event1 = new CustomEvent('eventtype1');
event2 = new CustomEvent('eventtype2');
// new Event() is current, but document.createEvent() works back to IE11.
event1 = document.createEvent('Event');
event1.initEvent('eventtype1', false, false);
event2 = document.createEvent('Event');
event2.initEvent('eventtype2', false, false);
});
afterEach(function() {
+139
View File
@@ -0,0 +1,139 @@
/**
* @license
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
describe('FakeEventTarget', function() {
var target;
var logErrorSpy;
var originalLogError;
beforeAll(function() {
originalLogError = shaka.log.error;
logErrorSpy = jasmine.createSpy('shaka.log.error');
shaka.log.error = logErrorSpy;
});
afterAll(function() {
shaka.log.error = originalLogError;
});
beforeEach(function() {
target = new shaka.util.FakeEventTarget();
logErrorSpy.calls.reset();
logErrorSpy.and.callFake(fail);
});
it('sets target on dispatched events', function(done) {
target.addEventListener('event', function(event) {
expect(event.target).toBe(target);
expect(event.currentTarget).toBe(target);
done();
});
target.dispatchEvent(new shaka.util.FakeEvent('event'));
});
it('calls all event listeners', function(done) {
var listener1 = jasmine.createSpy('listener1');
var listener2 = jasmine.createSpy('listener2');
target.addEventListener('event', listener1);
target.addEventListener('event', listener2);
target.dispatchEvent(new shaka.util.FakeEvent('event'));
shaka.test.Util.delay(0.1).then(function() {
expect(listener1).toHaveBeenCalled();
expect(listener2).toHaveBeenCalled();
done();
});
});
it('stops processing on stopImmediatePropagation', function(done) {
var listener1 = jasmine.createSpy('listener1');
var listener2 = jasmine.createSpy('listener2');
target.addEventListener('event', listener1);
target.addEventListener('event', listener2);
listener1.and.callFake(function(event) {
event.stopImmediatePropagation();
});
target.dispatchEvent(new shaka.util.FakeEvent('event'));
shaka.test.Util.delay(0.1).then(function() {
expect(listener1).toHaveBeenCalled();
expect(listener2).not.toHaveBeenCalled();
done();
});
});
it('catches exceptions thrown from listeners', function(done) {
var listener1 = jasmine.createSpy('listener1');
var listener2 = jasmine.createSpy('listener2');
target.addEventListener('event', listener1);
target.addEventListener('event', listener2);
listener1.and.throwError('whoops');
logErrorSpy.and.stub();
target.dispatchEvent(new shaka.util.FakeEvent('event'));
shaka.test.Util.delay(0.1).then(function() {
expect(listener1).toHaveBeenCalled();
expect(logErrorSpy).toHaveBeenCalled();
expect(listener2).toHaveBeenCalled();
done();
});
});
it('allows events to be re-dispatched', function(done) {
var listener1 = jasmine.createSpy('listener1');
var listener2 = jasmine.createSpy('listener2');
target.addEventListener('event', listener1);
target.addEventListener('event', listener2);
var target2 = new shaka.util.FakeEventTarget();
var target2Listener = jasmine.createSpy('target2Listener');
target2.addEventListener('event', target2Listener);
listener1.and.callFake(function(event) {
expect(event.target).toBe(target);
target2.dispatchEvent(event);
});
target2Listener.and.callFake(function(event) {
expect(event.target).toBe(target2);
});
listener2.and.callFake(function(event) {
expect(event.target).toBe(target);
});
target.dispatchEvent(new shaka.util.FakeEvent('event'));
shaka.test.Util.delay(0.1).then(function() {
expect(listener1).toHaveBeenCalled();
expect(listener2).toHaveBeenCalled();
expect(target2Listener).toHaveBeenCalled();
done();
});
});
});
-68
View File
@@ -1,68 +0,0 @@
/**
* @license
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
describe('FakeEventTarget', function() {
it('sets target on dispatched events', function(done) {
var target = new shaka.util.FakeEventTarget(null);
target.addEventListener('event', function(event) {
expect(event.target).toBe(target);
done();
});
target.dispatchEvent(shaka.util.FakeEvent.create({
'type': 'event',
'bubbles': false
}));
});
it('sets currentTarget on dispatched events', function(done) {
var targetHigh = new shaka.util.FakeEventTarget(null);
var targetLow = new shaka.util.FakeEventTarget(targetHigh);
targetHigh.addEventListener('event', function(event) {
expect(event.target).toBe(targetLow);
expect(event.currentTarget).toBe(targetHigh);
done();
});
targetLow.dispatchEvent(shaka.util.FakeEvent.create({
'type': 'event',
'bubbles': true
}));
});
it('allows events to be re-dispatched', function(done) {
var targetHigh = new shaka.util.FakeEventTarget(null);
var targetLow = new shaka.util.FakeEventTarget(targetHigh);
targetLow.addEventListener('event', function(event) {
expect(event.target).toBe(targetLow);
targetHigh.dispatchEvent(event);
});
targetHigh.addEventListener('event', function(event) {
expect(event.target).toBe(targetHigh);
done();
});
targetLow.dispatchEvent(shaka.util.FakeEvent.create({
'type': 'event',
'bubbles': false
}));
});
});