Skip to content

Commit

Permalink
Add EventListenerOptions and passive event listener feature
Browse files Browse the repository at this point in the history
This introduces an EventListenerOptions dictionary which can
be used to explicitly specify options for addEventListener()
and removeEventListener().

This also introduces a "passive" option, which disables the
ability for a listener to cancel the event.

See https://github.com/RByers/EventListenerOptions/blob/gh-pages/explainer.md
for a high-level overview, and
https://github.com/RByers/EventListenerOptions/issues?q=is%3Aissue
for most of the debate that went into the design.

PR: #82
  • Loading branch information
RByers authored and annevk committed Jan 6, 2016
1 parent 5dbefd1 commit 253a21b
Show file tree
Hide file tree
Showing 2 changed files with 219 additions and 82 deletions.
157 changes: 116 additions & 41 deletions dom.bs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ urlPrefix: https://html.spec.whatwg.org/multipage/
text: effective script origin
text: origin alias; url: #concept-origin-alias
text: Unicode serialization of an origin; url: #unicode-serialisation-of-an-origin
urlPrefix: infrastructure.html
text: in parallel
urlPrefix: https://w3c.github.io/webcomponents/spec/shadow/
type: dfn; urlPrefix: #dfn-
text: shadow root
Expand Down Expand Up @@ -613,7 +615,7 @@ Lets look at an example of how <a>events</a> work in a <a>tree</a>:
function test(e) {
debug(e.target, e.currentTarget, e.eventPhase)
}
document.addEventListener("hey", test, true)
document.addEventListener("hey", test, {capture: true})
document.body.addEventListener("hey", test)
var ev = new Event("hey", {bubbles:true})
document.getElementById("x").dispatchEvent(ev)
Expand Down Expand Up @@ -727,17 +729,13 @@ inherits from the {{Event}} interface.
{{Event/preventDefault()}} method.

<dt><code><var>event</var> . <a method for=Event lt="preventDefault()">preventDefault</a>()</code>
<dd>If invoked when the
{{Event/cancelable}} attribute value is true,
signals to the operation that caused <var>event</var> to be
<a>dispatched</a> that it needs to be
canceled.
<dd>If invoked when the {{Event/cancelable}} attribute value is true, and while executing a
listener for the <var>event</var> with {{EventListenerOptions/passive}} set to false, signals to
the operation that caused <var>event</var> to be <a>dispatched</a> that it needs to be canceled.

<dt><code><var>event</var> . {{Event/defaultPrevented}}</code>
<dd>Returns true if
{{Event/preventDefault()}} was invoked
while the {{Event/cancelable}} attribute
value is true, and false otherwise.
<dd>Returns true if {{Event/preventDefault()}} was invoked successfully to indicate cancellation,
and false otherwise.

<dt><code><var>event</var> . {{Event/isTrusted}}</code>
<dd>Returns true if <var>event</var> was
Expand Down Expand Up @@ -799,6 +797,7 @@ flags that are all initially unset:
<li><dfn export for=Event>canceled flag</dfn>
<li><dfn export for=Event>initialized flag</dfn>
<li><dfn export for=Event>dispatch flag</dfn>
<li><dfn export for=Event>in passive listener flag</dfn>
</ul>

The
Expand All @@ -814,10 +813,13 @@ The <dfn attribute for=Event>bubbles</dfn> and
<dfn attribute for=Event>cancelable</dfn> attributes
must return the values they were initialized to.

The
<dfn method for=Event>preventDefault()</dfn>
method must set the <a>canceled flag</a> if the
{{Event/cancelable}} attribute value is true.
The <dfn method for=Event><code>preventDefault()</code></dfn> method, when invoked, must set the
<a>canceled flag</a> if the {{Event/cancelable}} attribute value is true and the
<a>in passive listener flag</a> is unset.

<p class="note no-backref">This means there are scenarios where invoking {{preventDefault()}} has no
effect. User agents are encouraged to log the precise cause in a developer console, to aid
debugging.

The
<dfn attribute for=Event>defaultPrevented</dfn>
Expand Down Expand Up @@ -972,14 +974,19 @@ for historical reasons.
<pre class=idl>
[Exposed=(Window,Worker)]
interface EventTarget {
void addEventListener(DOMString type, EventListener? callback, optional boolean capture = false);
void removeEventListener(DOMString type, EventListener? callback, optional boolean capture = false);
void addEventListener(DOMString type, EventListener? callback, optional (EventListenerOptions or boolean) options);
void removeEventListener(DOMString type, EventListener? callback, optional (EventListenerOptions or boolean) options);
boolean dispatchEvent(Event event);
};

callback interface EventListener {
void handleEvent(Event event);
};

dictionary EventListenerOptions {
boolean capture;
boolean passive;
};
</pre>

{{EventTarget}} is an object to which an
Expand All @@ -991,60 +998,98 @@ occurred. Each {{EventTarget}} has an associated list of
<p>An <dfn export id=concept-event-listener>event listener</dfn> can be used to observe a specific
<a>event</a>.

<p>An <a>event listener</a> consists of a <b>type</b>, <b>callback</b>, and <b>capture</b>. An
<a>event listener</a> also has an associated <b>removed flag</b>, which is initially unset.
<p>An <a>event listener</a> consists of a <b>type</b>, <b>callback</b>, <b>capture</b> and
<b>passive</b>. An <a>event listener</a> also has an associated <b>removed flag</b>, which is
initially unset.

<p class="note no-backref">The callback is named {{EventListener}} for historical reasons. As can be
seen from the definition above, an <a>event listener</a> is a more broad concept.

<dl class=domintro>
<dt><code><var>target</var> . <a method lt="addEventListener()">addEventListener</a>(<var>type</var>, <var>callback</var> [, <var>capture</var> = false])</code>
<dt><code><var>target</var> . <a method lt="addEventListener()">addEventListener</a>(<var>type</var>, <var>callback</var> [, <var>options</var>])</code>
<dd>
Appends an <a>event listener</a> for <a>events</a> whose {{Event/type}} attribute value
is <var>type</var>. The <var>callback</var> argument sets the <b>callback</b> that will
be invoked when the <a>event</a> is <a>dispatched</a>. When set to true,
the <var>capture</var> argument prevents <b>callback</b> from being invoked when
the <a>event</a>'s {{Event/eventPhase}} attribute value is {{Event/BUBBLING_PHASE}}.
When false, <b>callback</b> will not be invoked when <a>event</a>'s {{Event/eventPhase}}
attribute value is {{Event/CAPTURING_PHASE}}. Either way, <b>callback</b> will be
invoked if <a>event</a>'s {{Event/eventPhase}} attribute value is {{Event/AT_TARGET}}.

The <a>event listener</a> is appended to <var>target</var>'s list of
<a>event listeners</a> and is not appended if it is a duplicate, i.e., having the same
<b>type</b>, <b>callback</b>, and <b>capture</b> values.

<dt><code><var>target</var> . <a method lt="removeEventListener()">removeEventListener</a>(<var>type</var>, <var>callback</var> [, <var>capture</var> = false])</code>
be invoked when the <a>event</a> is <a>dispatched</a>.

The <var>options</var> argument sets listener-specific options. For compatibility this can be just
a boolean, in which case the method behaves exactly as if the value was specified as
<var>options</var>' <code>capture</code> member.

When set to true, <var>options</var>' <code>capture</code> member prevents <b>callback</b> from
being invoked when the <a>event</a>'s {{Event/eventPhase}} attribute value is
{{Event/BUBBLING_PHASE}}. When false (or not present), <b>callback</b> will not be invoked when
<a>event</a>'s {{Event/eventPhase}} attribute value is {{Event/CAPTURING_PHASE}}. Either way,
<b>callback</b> will be invoked if <a>event</a>'s {{Event/eventPhase}} attribute value is
{{Event/AT_TARGET}}.

When set to true, <var>options</var>' <code>passive</code> member indicates that the
<b>callback</b> will not cancel the event by invoking {{preventDefault()}}. This is used to enable
performance optimizations described in [[#observing-event-listeners]].

The <a>event listener</a> is appended to <var>target</var>'s list of <a>event listeners</a> and is
not appended if it is a duplicate, i.e., having the same <b>type</b>, <b>callback</b>,
<b>capture</b> and <b>passive</b> values.

<dt><code><var>target</var> . <a method lt="removeEventListener()">removeEventListener</a>(<var>type</var>, <var>callback</var> [, <var>options</var>])</code>
<dd>Remove the <a>event listener</a>
in <var>target</var>'s list of
<a>event listeners</a> with the same
<var>type</var>, <var>callback</var>, and
<var>capture</var>.
<var>options</var>.

<dt><code><var>target</var> . <a method lt="dispatchEvent()">dispatchEvent</a>(<var>event</var>)</code>
<dd><a>Dispatches</a> a synthetic event <var>event</var> to <var>target</var> and returns
true if either <var>event</var>'s {{Event/cancelable}} attribute value is false or its
{{Event/preventDefault()}} method was not invoked, and false otherwise.
</dl>

<p>To <dfn export for=Event id=concept-flatten-options>flatten</dfn> <var>options</var> run these steps:

<ol>
<li><p>Let <var>capture</var> and <var>passive</var> be false.

<li><p>If <var>options</var> is a boolean, set <var>capture</var> to <var>options</var>.

<li><p>If <var>options</var> is a dictionary and <code>{{EventListenerOptions/capture}}</code> is
present in <var>options</var> with value true, then set <var>capture</var> to true.

<li><p>If <var>options</var> is a dictionary and <code>{{EventListenerOptions/passive}}</code> is
present in <var>options</var> with value true, then set <var>passive</var> to true.

<li><p>Return <var>capture</var> and <var>passive</var>.
</ol>

<p>The
<dfn method for=EventTarget><code>addEventListener(<var>type</var>, <var>callback</var>, <var>capture</var>)</code></dfn>
<dfn method for=EventTarget><code>addEventListener(<var>type</var>, <var>callback</var>, <var>options</var>)</code></dfn>
method, when invoked, must run these steps:

<ol>
<li><p>If <var>callback</var> is null, terminate these steps.

<li><p>Let <var>capture</var> and <var>passive</var> be the result of <a>flattening</a>
<var>options</var>.

<li><p>Append an <a>event listener</a> to the associated list of <a>event listeners</a> with
<b>type</b> set to <var>type</var>, <b>callback</b> set to <var>callback</var>, and <b>capture</b>
set to <var>capture</var>, unless there already is an <a>event listener</a> in that list with the
same <b>type</b>, <b>callback</b>, and <b>capture</b>.
<b>type</b> set to <var>type</var>, <b>callback</b> set to <var>callback</var>, <b>capture</b>
set to <var>capture</var>, and <b>passive</b> set to <var>passive</var> unless there already is an
<a>event listener</a> in that list with the same <b>type</b>, <b>callback</b>, <b>capture</b>, and
<b>passive</b>.
</ol>

<p>The
<dfn method for=EventTarget><code>removeEventListener(<var>type</var>, <var>callback</var>, <var>capture</var>)</code></dfn>
method, when invoked, must, if there is an <a>event listener</a> in the associated list of
<a>event listeners</a> whose <b>type</b> is <var>type</var>, <b>callback</b> is <var>callback</var>,
and <b>capture</b> is <var>capture</var>, set that <a>event listener</a>'s <b>removed flag</b> and
remove it from the associated list of <a>event listeners</a>.
<dfn method for=EventTarget><code>removeEventListener(<var>type</var>, <var>callback</var>, <var>options</var>)</code></dfn>
method, when invoked, must, run these steps

<ol>
<li><p>Let <var>capture</var> and <var>passive</var> be the result of <a>flattening</a>
<var>options</var>.

<li><p>If there is an <a>event listener</a> in the associated list of <a>event listeners</a> whose
<b>type</b> is <var>type</var>, <b>callback</b> is <var>callback</var>, <b>capture</b> is
<var>capture</var>, and <b>passive</b> is <var>passive</var> then set that <a>event listener</a>'s
<b>removed flag</b> and remove it from the associated list of <a>event listeners</a>.
</ol>

<p>The <dfn method for=EventTarget><code>dispatchEvent(<var>event</var>)</code></dfn> method, when
invoked, must run these steps:
Expand All @@ -1059,6 +1104,30 @@ invoked, must run these steps:
</ol>


<h3 id=observing-event-listeners>Observing event listeners</h3>

<p>In general, developers do not expect the presence of an <a>event listener</a> to be observable.
The impact of an <a>event listener</a> is determined by its <b>callback</b>. That is, a developer
adding a no-op <a>event listener</a> would not expect it to have any side effects.

<p>Unfortunately, some event APIs have been designed such that implementing them efficiently
requires observing <a>event listeners</a>. This can make the presence of listeners observable in
that even empty listeners can have a dramatic performance impact on the behavior of the application.
For example, touch and wheel events which can be used to block asynchronous scrolling. In some cases
this problem can be mitigated by specifying the event to be {{Event/cancelable}} only when there is
at least one non-{{EventListenerOptions/passive}} listener. For example,
non-{{EventListenerOptions/passive}} {{TouchEvent}} listeners must block scrolling, but if all
listeners are {{EventListenerOptions/passive}} then scrolling can be allowed to start
<a>in parallel</a> by making the {{TouchEvent}} uncancelable (so that calls to
{{Event/preventDefault()}} are ignored). So code dispatching an event is able to observe the absence
of non-{{EventListenerOptions/passive}} listeners, and use that to clear the {{Event/cancelable}}
property of the event being dispatched.

<p>Ideally, any new event APIs are defined such that they do not need this property (use
<a href="https://lists.w3.org/Archives/Public/public-script-coord/">[email protected]</a>
for discussion).


<h3 id=dispatching-events>Dispatching events</h3>

<p>To <dfn export for=Event id=concept-event-dispatch>dispatch</dfn> an <var>event</var> to a
Expand Down Expand Up @@ -1139,9 +1208,14 @@ invoked, must run these steps:
<var>listener</var>'s <b>capture</b> is true, terminate these substeps (and run them for the next
<a>event listener</a>).

<li><p>If <var>listener</var>'s <b>passive</b> is true, set <var>event</var>'s
<a>in passive listener flag</a>.

<li><p>Call <var>listener</var>'s <b>callback</b>'s {{EventListener/handleEvent()}}, with
<var>event</var> as argument and <var>event</var>'s {{Event/currentTarget}} attribute value as
<a>callback this value</a>. If this throws any exception, <a>report the exception</a>.

<li><p>Clear <var>event</var>'s <a>in passive listener flag</a>.
</ol>
</ol>

Expand Down Expand Up @@ -9076,6 +9150,7 @@ Peter Sharpe,
Philip Jägenstedt,
Philippe Le Hégaret,
Rafael Weinstein,
Rick Byers,
Rick Waldron,
Robbert Broersma,
Robin Berjon,
Expand Down
Loading

0 comments on commit 253a21b

Please sign in to comment.