Skip to content

Commit

Permalink
feat: [IE11] polyfill capture option
Browse files Browse the repository at this point in the history
  • Loading branch information
buschtoens committed Apr 13, 2019
1 parent 956af08 commit 53de1b3
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 81 deletions.
17 changes: 11 additions & 6 deletions addon/modifiers/on.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint no-param-reassign: "off" */

import Ember from 'ember';
import addEventListener from '../utils/add-event-listener';
import { addEventListener, removeEventListener } from '../utils/event-listener';
import { assert } from '@ember/debug';
import { DEBUG } from '@glimmer/env';

Expand Down Expand Up @@ -44,9 +44,9 @@ function setupListener(element, eventName, callback, eventOptions, params) {
return callback;
}

function destroyListener(element, eventName, callback) {
function destroyListener(element, eventName, callback, eventOptions) {
if (typeof eventName === 'string' && typeof callback === 'function')
element.removeEventListener(eventName, callback);
removeEventListener(element, eventName, callback, eventOptions);
}

export default Ember._setModifierManager(
Expand Down Expand Up @@ -89,7 +89,12 @@ export default Ember._setModifierManager(
named: eventOptions
}
) {
destroyListener(state.element, state.eventName, state.callback);
destroyListener(
state.element,
state.eventName,
state.callback,
state.eventOptions
);
state.callback = setupListener(
state.element,
eventName,
Expand All @@ -103,8 +108,8 @@ export default Ember._setModifierManager(
state.eventOptions = eventOptions;
},

destroyModifier({ element, eventName, callback }) {
destroyListener(element, eventName, callback);
destroyModifier({ element, eventName, callback, eventOptions }) {
destroyListener(element, eventName, callback, eventOptions);
}
}),
class OnModifier {}
Expand Down
74 changes: 0 additions & 74 deletions addon/utils/add-event-listener.js

This file was deleted.

120 changes: 120 additions & 0 deletions addon/utils/event-listener.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* Internet Explorer 11 does not support `once` and also does not support
* passing `eventOptions`. In some situations it then throws a weird script
* error, like:
*
* ```
* Could not complete the operation due to error 80020101
* ```
*
* This flag determines, whether `{ once: true }` and thus also event options in
* general are supported.
*/
export const SUPPORTS_EVENT_OPTIONS = (() => {
try {
const div = document.createElement('div');
let counter = 0;
div.addEventListener('click', () => counter++);

let event;
if (typeof Event === 'function') {
event = new Event('click');
} else {
event = document.createEvent('Event');
event.initEvent('click', true, true);
}

div.dispatchEvent(event);
div.dispatchEvent(event);

return counter === 1;
} catch (error) {
return false;
}
})();

/**
* Registers an event for an `element` that is called exactly once and then
* unregistered again. This is effectively a polyfill for `{ once: true }`.
*
* It also accepts a fourth optional argument `useCapture`, that will be passed
* through to `addEventListener`.
*
* @param {Element} element
* @param {string} eventName
* @param {Function} callback
* @param {boolean} [useCapture=false]
*/
export function addEventListenerOnce(
element,
eventName,
callback,
useCapture = false
) {
function listener() {
element.removeEventListener(eventName, listener, useCapture);
callback();
}
element.addEventListener(eventName, listener, useCapture);
}

/**
* Safely invokes `addEventListener` for IE11 and also polyfills the
* `{ once: true }` and `{ capture: true }` options.
*
* All other options are discarded for IE11. Currently this is only `passive`.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
*
* @param {Element} element
* @param {string} eventName
* @param {Function} callback
* @param {object} [eventOptions]
*/
export function addEventListener(element, eventName, callback, eventOptions) {
if (SUPPORTS_EVENT_OPTIONS) {
element.addEventListener(eventName, callback, eventOptions);
} else if (eventOptions && eventOptions.once) {
addEventListenerOnce(
element,
eventName,
callback,
Boolean(eventOptions.capture)
);
} else {
element.addEventListener(
eventName,
callback,
Boolean(eventOptions && eventOptions.capture)
);
}
}

/**
* Since the same `capture` event option that was used to add the event listener
* needs to be used when removing the listener, it needs to be polyfilled as
* `useCapture` for IE11.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener
*
* @param {Element} element
* @param {string} eventName
* @param {Function} callback
* @param {object} [eventOptions]
*/
export function removeEventListener(
element,
eventName,
callback,
eventOptions
) {
if (SUPPORTS_EVENT_OPTIONS) {
element.removeEventListener(eventName, callback, eventOptions);
} else {
element.removeEventListener(
eventName,
callback,
Boolean(eventOptions && eventOptions.capture)
);
}
}
80 changes: 79 additions & 1 deletion tests/integration/modifiers/on-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ module('Integration | Modifier | on', function(hooks) {
await click('button');
});

test('it can accept event options', async function(assert) {
test('it can accept the `once` option', async function(assert) {
assert.expect(1);

let n = 0;
Expand All @@ -59,6 +59,51 @@ module('Integration | Modifier | on', function(hooks) {
assert.strictEqual(n, 1, 'callback has only been called once');
});

test('it can accept the `capture` option', async function(assert) {
assert.expect(1);

const calls = [];
this.outerListener = () => calls.push('outer');
this.innerListener = () => calls.push('inner');

await render(hbs`
<div {{on "click" this.outerListener capture=true}}>
<button {{on "click" this.innerListener}}>inner</button>
</div>
`);

await click('button');

assert.deepEqual(
calls,
['outer', 'inner'],
'outer capture listener was called first'
);
});

test('it can accept the `once` & `capture` option combined', async function(assert) {
assert.expect(1);

const calls = [];
this.outerListener = () => calls.push('outer');
this.innerListener = () => calls.push('inner');

await render(hbs`
<div {{on "click" this.outerListener once=true capture=true}}>
<button {{on "click" this.innerListener}}>inner</button>
</div>
`);

await click('button');
await click('button');

assert.deepEqual(
calls,
['outer', 'inner', 'inner'],
'outer capture listener was called first and was then unregistered'
);
});

(gte('3.0.0') // I have no clue how to catch the error in Ember 2.13
? test
: skip)('it raises an assertion if an invalid event option is passed in', async function(assert) {
Expand Down Expand Up @@ -129,6 +174,39 @@ module('Integration | Modifier | on', function(hooks) {
assert.strictEqual(b, 1);
});

test('it is re-registered, when the callback changes and `capture` is used', async function(assert) {
assert.expect(3);

let a = 0;
this.someMethod = () => a++;
this.capture = true;

await render(
hbs`<button {{on "click" this.someMethod capture=this.capture}}></button>`
);

await click('button');

let b = 0;
run(() => set(this, 'someMethod', () => b++));
await settled();

await click('button');

let c = 0;
run(() => {
set(this, 'someMethod', () => c++);
set(this, 'capture', false);
});
await settled();

await click('button');

assert.strictEqual(a, 1);
assert.strictEqual(b, 1);
assert.strictEqual(c, 1);
});

test('it does nothing if the callback or event name is `null` or `undefined`', async function(assert) {
assert.expect(0);

Expand Down

0 comments on commit 53de1b3

Please sign in to comment.