diff --git a/build-system/dep-check-config.js b/build-system/dep-check-config.js index 3c33c9b0073f..adce76140d95 100644 --- a/build-system/dep-check-config.js +++ b/build-system/dep-check-config.js @@ -210,6 +210,7 @@ exports.rules = [ 'src/polyfills.js->src/polyfills/object-assign.js', 'src/polyfills.js->src/polyfills/promise.js', 'src/polyfills.js->src/polyfills/array-includes.js', + 'src/polyfills.js->src/polyfills/event.js', 'src/service/extensions-impl.js->src/polyfills/document-contains.js', 'src/service/extensions-impl.js->src/polyfills/domtokenlist-toggle.js', ], diff --git a/extensions/amp-access/0.1/test/test-amp-access.js b/extensions/amp-access/0.1/test/test-amp-access.js index c12b90bb05d6..c67798a70ca7 100644 --- a/extensions/amp-access/0.1/test/test-amp-access.js +++ b/extensions/amp-access/0.1/test/test-amp-access.js @@ -1030,14 +1030,10 @@ describes.fakeWin('AccessService pingback', { service.reportViewToServer_ = sandbox.spy(); const p = service.reportWhenViewed_(/* timeToView */ 2000); return Promise.resolve().then(() => { - let clickEvent; - if (document.createEvent) { - clickEvent = document.createEvent('MouseEvent'); - clickEvent.initMouseEvent('click', true, true, window, 1); - } else { - clickEvent = document.createEventObject(); - clickEvent.type = 'click'; - } + const clickEvent = new MouseEvent( + 'click', + {bubbles: true, cancelable: true, view: window, detail: 1} + ); document.documentElement.dispatchEvent(clickEvent); return p; }).then(() => {}, () => {}).then(() => { diff --git a/extensions/amp-bind/0.1/bind-impl.js b/extensions/amp-bind/0.1/bind-impl.js index 6e369e2d0d6c..1b27cbdba710 100644 --- a/extensions/amp-bind/0.1/bind-impl.js +++ b/extensions/amp-bind/0.1/bind-impl.js @@ -957,13 +957,7 @@ export class Bind { */ dispatchEventForTesting_(name) { if (getMode().test) { - let event; - if (typeof this.localWin_.Event === 'function') { - event = new Event(name, {bubbles: true, cancelable: true}); - } else { - event = this.localWin_.document.createEvent('Event'); - event.initEvent(name, /* bubbles */ true, /* cancelable */ true); - } + const event = new Event(name, {bubbles: true, cancelable: true}); this.localWin_.dispatchEvent(event); } } diff --git a/extensions/amp-live-list/0.1/amp-live-list.js b/extensions/amp-live-list/0.1/amp-live-list.js index 72d69f2dcd9b..b56eae355b0c 100644 --- a/extensions/amp-live-list/0.1/amp-live-list.js +++ b/extensions/amp-live-list/0.1/amp-live-list.js @@ -866,8 +866,10 @@ export class AmpLiveList extends AMP.BaseElement { } sendAmpDomUpdateEvent_() { - const event = this.win.document.createEvent('Event'); - event.initEvent(AmpEvents.DOM_UPDATE, true, true); + const event = new Event( + AmpEvents.DOM_UPDATE, + {bubbles: true, cancelable: true} + ); this.win.document.dispatchEvent(event); } } diff --git a/extensions/amp-sidebar/0.1/test/test-amp-sidebar.js b/extensions/amp-sidebar/0.1/test/test-amp-sidebar.js index d27d190102f0..19d0453a3e98 100644 --- a/extensions/amp-sidebar/0.1/test/test-amp-sidebar.js +++ b/extensions/amp-sidebar/0.1/test/test-amp-sidebar.js @@ -292,16 +292,14 @@ describes.realWin('amp-sidebar 0.1 version', { impl.open_(); expect(sidebarElement.hasAttribute('open')).to.be.true; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('false'); - const eventObj = document.createEventObject ? - document.createEventObject() : document.createEvent('Events'); - if (eventObj.initEvent) { - eventObj.initEvent('keydown', true, true); - } + const eventObj = new Event( + 'keydown', + {bubbles: true, cancelable: true} + ); eventObj.keyCode = KeyCodes.ESCAPE; eventObj.which = KeyCodes.ESCAPE; const el = iframe.doc.documentElement; - el.dispatchEvent ? - el.dispatchEvent(eventObj) : el.fireEvent('onkeydown', eventObj); + el.dispatchEvent(eventObj); expect(sidebarElement.hasAttribute('open')).to.be.false; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('true'); expect(sidebarElement.style.display).to.equal('none'); @@ -409,11 +407,7 @@ describes.realWin('amp-sidebar 0.1 version', { impl.open_(); expect(sidebarElement.hasAttribute('open')).to.be.true; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('false'); - const eventObj = document.createEventObject ? - document.createEventObject() : document.createEvent('Events'); - if (eventObj.initEvent) { - eventObj.initEvent('click', true, true); - } + const eventObj = new Event('click', {bubbles: true, cancelable: true}); sandbox.stub(sidebarElement, 'getAmpDoc', () => { return { win: { @@ -423,9 +417,7 @@ describes.realWin('amp-sidebar 0.1 version', { }, }; }); - anchor.dispatchEvent ? - anchor.dispatchEvent(eventObj) : - anchor.fireEvent('onkeydown', eventObj); + anchor.dispatchEvent(eventObj); expect(sidebarElement.hasAttribute('open')).to.be.false; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('true'); expect(sidebarElement.style.display).to.equal('none'); @@ -452,11 +444,7 @@ describes.realWin('amp-sidebar 0.1 version', { impl.open_(); expect(sidebarElement.hasAttribute('open')).to.be.true; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('false'); - const eventObj = document.createEventObject ? - document.createEventObject() : document.createEvent('Events'); - if (eventObj.initEvent) { - eventObj.initEvent('click', true, true); - } + const eventObj = new Event('click', {bubbles: true, cancelable: true}); sandbox.stub(sidebarElement, 'getAmpDoc', () => { return { win: { @@ -467,9 +455,7 @@ describes.realWin('amp-sidebar 0.1 version', { }, }; }); - anchor.dispatchEvent ? - anchor.dispatchEvent(eventObj) : - anchor.fireEvent('onkeydown', eventObj); + anchor.dispatchEvent(eventObj); expect(sidebarElement.hasAttribute('open')).to.be.true; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('false'); expect(sidebarElement.style.display).to.equal(''); @@ -496,11 +482,7 @@ describes.realWin('amp-sidebar 0.1 version', { impl.open_(); expect(sidebarElement.hasAttribute('open')).to.be.true; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('false'); - const eventObj = document.createEventObject ? - document.createEventObject() : document.createEvent('Events'); - if (eventObj.initEvent) { - eventObj.initEvent('click', true, true); - } + const eventObj = new Event('click', {bubbles: true, cancelable: true}); sandbox.stub(sidebarElement, 'getAmpDoc', () => { return { win: { @@ -512,9 +494,7 @@ describes.realWin('amp-sidebar 0.1 version', { }, }; }); - anchor.dispatchEvent ? - anchor.dispatchEvent(eventObj) : - anchor.fireEvent('onkeydown', eventObj); + anchor.dispatchEvent(eventObj); expect(sidebarElement.hasAttribute('open')).to.be.true; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('false'); expect(sidebarElement.style.display).to.equal(''); @@ -540,14 +520,8 @@ describes.realWin('amp-sidebar 0.1 version', { impl.open_(); expect(sidebarElement.hasAttribute('open')).to.be.true; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('false'); - const eventObj = document.createEventObject ? - document.createEventObject() : document.createEvent('Events'); - if (eventObj.initEvent) { - eventObj.initEvent('click', true, true); - } - li.dispatchEvent ? - li.dispatchEvent(eventObj) : - li.fireEvent('onkeydown', eventObj); + const eventObj = new Event('click', {bubbles: true, cancelable: true}); + li.dispatchEvent(eventObj); expect(sidebarElement.hasAttribute('open')).to.be.true; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('false'); expect(sidebarElement.style.display).to.equal(''); diff --git a/extensions/amp-sidebar/1.0/test/test-amp-sidebar.js b/extensions/amp-sidebar/1.0/test/test-amp-sidebar.js index 65375436882b..3f823ef674a5 100644 --- a/extensions/amp-sidebar/1.0/test/test-amp-sidebar.js +++ b/extensions/amp-sidebar/1.0/test/test-amp-sidebar.js @@ -345,16 +345,14 @@ impl.open_(); expect(sidebarElement.hasAttribute('open')).to.be.true; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('false'); - const eventObj = document.createEventObject ? - document.createEventObject() : document.createEvent('Events'); - if (eventObj.initEvent) { - eventObj.initEvent('keydown', true, true); - } + const eventObj = new Event( + 'keydown', + {bubbles: true, cancelable: true} + ); eventObj.keyCode = KeyCodes.ESCAPE; eventObj.which = KeyCodes.ESCAPE; const el = iframe.doc.documentElement; - el.dispatchEvent ? - el.dispatchEvent(eventObj) : el.fireEvent('onkeydown', eventObj); + el.dispatchEvent(eventObj); expect(sidebarElement.hasAttribute('open')).to.be.false; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('true'); expect(sidebarElement.style.display).to.equal('none'); @@ -462,11 +460,7 @@ impl.open_(); expect(sidebarElement.hasAttribute('open')).to.be.true; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('false'); - const eventObj = document.createEventObject ? - document.createEventObject() : document.createEvent('Events'); - if (eventObj.initEvent) { - eventObj.initEvent('click', true, true); - } + const eventObj = new Event('click', {bubbles: true, cancelable: true}); sandbox.stub(sidebarElement, 'getAmpDoc', () => { return { win: { @@ -476,9 +470,7 @@ }, }; }); - anchor.dispatchEvent ? - anchor.dispatchEvent(eventObj) : - anchor.fireEvent('onkeydown', eventObj); + anchor.dispatchEvent(eventObj); expect(sidebarElement.hasAttribute('open')).to.be.false; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('true'); expect(sidebarElement.style.display).to.equal('none'); @@ -506,11 +498,7 @@ impl.open_(); expect(sidebarElement.hasAttribute('open')).to.be.true; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('false'); - const eventObj = document.createEventObject ? - document.createEventObject() : document.createEvent('Events'); - if (eventObj.initEvent) { - eventObj.initEvent('click', true, true); - } + const eventObj = new Event('click', {bubbles: true, cancelable: true}); sandbox.stub(sidebarElement, 'getAmpDoc', () => { return { win: { @@ -521,9 +509,7 @@ }, }; }); - anchor.dispatchEvent ? - anchor.dispatchEvent(eventObj) : - anchor.fireEvent('onkeydown', eventObj); + anchor.dispatchEvent(eventObj); expect(sidebarElement.hasAttribute('open')).to.be.true; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('false'); expect(sidebarElement.style.display).to.equal(''); @@ -550,11 +536,7 @@ impl.open_(); expect(sidebarElement.hasAttribute('open')).to.be.true; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('false'); - const eventObj = document.createEventObject ? - document.createEventObject() : document.createEvent('Events'); - if (eventObj.initEvent) { - eventObj.initEvent('click', true, true); - } + const eventObj = new Event('click', {bubbles: true, cancelable: true}); sandbox.stub(sidebarElement, 'getAmpDoc', () => { return { win: { @@ -566,9 +548,7 @@ }, }; }); - anchor.dispatchEvent ? - anchor.dispatchEvent(eventObj) : - anchor.fireEvent('onkeydown', eventObj); + anchor.dispatchEvent(eventObj); expect(sidebarElement.hasAttribute('open')).to.be.true; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('false'); expect(sidebarElement.style.display).to.equal(''); @@ -594,14 +574,11 @@ impl.open_(); expect(sidebarElement.hasAttribute('open')).to.be.true; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('false'); - const eventObj = document.createEventObject ? - document.createEventObject() : document.createEvent('Events'); - if (eventObj.initEvent) { - eventObj.initEvent('click', true, true); - } - li.dispatchEvent ? - li.dispatchEvent(eventObj) : - li.fireEvent('onkeydown', eventObj); + const eventObj = new Event( + 'click', + {bubbles: true, cancelable: true} + ); + li.dispatchEvent(eventObj); expect(sidebarElement.hasAttribute('open')).to.be.true; expect(sidebarElement.getAttribute('aria-hidden')).to.equal('false'); expect(sidebarElement.style.display).to.equal(''); diff --git a/src/custom-element.js b/src/custom-element.js index 3c552111dea4..e59eb913e51d 100644 --- a/src/custom-element.js +++ b/src/custom-element.js @@ -1103,11 +1103,8 @@ function createBaseCustomElementClass(win) { */ dispatchCustomEvent(name, opt_data) { const data = opt_data || {}; - // Constructors of events need to come from the correct window. Sigh. - const win = this.ownerDocument.defaultView; - const event = win.document.createEvent('Event'); + const event = new Event(name, {bubbles: true, cancelable: true}); event.data = data; - event.initEvent(name, /* bubbles */ true, /* cancelable */ true); this.dispatchEvent(event); } diff --git a/src/polyfills.js b/src/polyfills.js index 958c1705c42b..1fb7bb69c516 100644 --- a/src/polyfills.js +++ b/src/polyfills.js @@ -16,17 +16,17 @@ // Importing the document-register-element module has the side effect // of installing the custom elements polyfill if necessary. -import installCustomElements from - 'document-register-element/build/document-register-element.node'; -import { - install as installDOMTokenListToggle, -} from './polyfills/domtokenlist-toggle'; +import installCustomElements + from 'document-register-element/build/document-register-element.node'; +import {getMode} from './mode'; +import {install as installArrayIncludes} from './polyfills/array-includes'; import {install as installDocContains} from './polyfills/document-contains'; +import {install as installDOMTokenListToggle} + from './polyfills/domtokenlist-toggle'; +import {install as installEvent} from './polyfills/event'; import {install as installMathSign} from './polyfills/math-sign'; import {install as installObjectAssign} from './polyfills/object-assign'; import {install as installPromise} from './polyfills/promise'; -import {install as installArrayIncludes} from './polyfills/array-includes'; -import {getMode} from './mode'; /** Only install in closure binary and not in babel/browserify binary, since in @@ -44,3 +44,4 @@ installObjectAssign(self); installPromise(self); installDocContains(self); installArrayIncludes(self); +installEvent(self); diff --git a/src/polyfills/event.js b/src/polyfills/event.js new file mode 100644 index 000000000000..f44dc5ebfd8d --- /dev/null +++ b/src/polyfills/event.js @@ -0,0 +1,57 @@ +/** + * Copyright 2017 The AMP HTML Authors. All Rights Reserved. + * + * 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. + */ + +/** + * @constructor + */ +function Event(name, params) { + if (!name) { + throw new TypeError( + "Failed to construct 'Event': 1 argument required but only 0 present", + 'event.js', + 20 + ); + } + params = params || {bubbles: false, cancelable: false}; + const evt = self.document.createEvent('Event'); + evt.initEvent( + name, + params.bubbles, + params.cancelable + ); + return evt; +} + +/** + * Sets the Event polyfill if it does not exist. + * @param {!Window} win + */ +export function install(win) { + // win.Event is a function on Edge, Chrome, FF, Safari but + // is an object on IE 11. + if (typeof win.Event !== 'function') { + + // supports >= IE 9. Below IE 9, window.Event.prototype is undefined + Event.prototype = win.Event.prototype; + + win.Object.defineProperty(win, 'Event', { + configurable: false, + enumerable: false, + value: Event, + writable: false, + }); + } +} diff --git a/test/functional/test-base-element.js b/test/functional/test-base-element.js index cf10307c6d44..93c5497255ac 100644 --- a/test/functional/test-base-element.js +++ b/test/functional/test-base-element.js @@ -177,11 +177,9 @@ describe('BaseElement', () => { const timer = Services.timerFor(element.win); target = document.createElement('div'); - event1 = document.createEvent('Event'); - event1.initEvent('event1', false, true); + event1 = new Event('event1', {bubbles: false, cancelable: true}); - event2 = document.createEvent('Event'); - event2.initEvent('event2', false, true); + event2 = new Event('event2', {bubbles: false, cancelable: true}); event1Promise = listenOncePromise(element.element, 'event1'); event1Promise = timer.timeoutPromise(TIMEOUT, event1Promise); diff --git a/test/functional/test-event-helper.js b/test/functional/test-event-helper.js index a7d34a1132cb..c571d5a5ecc5 100644 --- a/test/functional/test-event-helper.js +++ b/test/functional/test-event-helper.js @@ -28,8 +28,7 @@ import * as sinon from 'sinon'; describe('EventHelper', () => { function getEvent(name, target) { - const event = document.createEvent('Event'); - event.initEvent(name, true, true); + const event = new Event(name, {bubbles: true, cancelable: true}); event.testTarget = target; return event; } diff --git a/test/functional/test-polyfill-event.js b/test/functional/test-polyfill-event.js new file mode 100644 index 000000000000..8cb38fa9d9ac --- /dev/null +++ b/test/functional/test-polyfill-event.js @@ -0,0 +1,59 @@ +/** + * Copyright 2017 The AMP HTML Authors. All Rights Reserved. + * + * 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. + */ + +import {install} from '../../src/polyfills/event.js'; + +describes.fakeWin('Event', {}, env => { + + beforeEach(() => { + // makes the Event polyfill install by setting window.Event to an object + env.win.Event = Object.create(self.Event.prototype); + // makes the prototype available for assignment + env.win.Event.prototype = self.Event.prototype; + install(env.win); + }); + + it('should be a function', () => { + expect(env.win.Event).to.be.a('function'); + }); + + it('should require a type argument', () => { + expect(() => new env.win.Event()).to.throw(TypeError); + }); + + it('should not bubble or be cancelable by default', () => { + const defaultEvent = new env.win.Event('event'); + expect(defaultEvent.bubbles).to.be.false; + expect(defaultEvent.cancelable).to.be.false; + }); + + it('should be configurable', () => { + const ev = new env.win.Event('event', {bubbles: true, cancelable: true}); + expect(ev.bubbles).to.be.true; + expect(ev.cancelable).to.be.true; + }); + + it('should trigger listeners', () => { + const ev = new env.win.Event('event'); + const elm = document.createElement('p'); + let flag = false; + elm.addEventListener('event', () => { + flag = true; + }); + elm.dispatchEvent(ev); + expect(flag).to.be.true; + }); +}); diff --git a/testing/fake-dom.js b/testing/fake-dom.js index 823fa0109716..c6ee3e2f45d7 100644 --- a/testing/fake-dom.js +++ b/testing/fake-dom.js @@ -64,6 +64,8 @@ export class FakeWindow { this.DOMTokenList = window.DOMTokenList; /** @const */ this.Math = window.Math; + /** @const */ + this.Event = window.Event; // Parent Window points to itself if spec.parent was not passed. /** @const @type {!Window} */