Skip to content

Add EventListenerOptions and passive event listeners #82

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 117 additions & 39 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 @@ -816,8 +815,13 @@ 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.
method 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.
</p>

The
<dfn attribute for=Event>defaultPrevented</dfn>
Expand Down Expand Up @@ -972,14 +976,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 +1000,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>Let <var>capture</var> and <var>passive</var> be false.

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

<li>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>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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these four steps are repeated below I feel like we should introduce an abstract operation that returns two values for capture and passive given a dictionary or boolean.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I attempted that here and I agree it looks cleaner, thanks! I couldn't find another example of returning multiple values, is what I've done precise enough? Any suggestion for a term better than "normalize" (which could perhaps be confused with Node.normalize)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"flatten" perhaps? What you did seems great. Returning multiple values in English feels quite natural and I've done it before without confusing anybody.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Sure "flatten" sounds better to me - done.


<li>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>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>Let <var>capture</var> and <var>passive</var> be the result of <a>flattening</a> <var>options</var>.

<li>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 +1106,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 +1210,15 @@ 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>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>Clear <var>event</var>'s <a>in passive listener flag</a>.

</ol>
</ol>

Expand Down Expand Up @@ -9073,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