From 0ad4e562b638debc1709982743dfa38052a6f39f Mon Sep 17 00:00:00 2001 From: Rick Byers Date: Mon, 30 Nov 2015 09:21:28 -0500 Subject: [PATCH] Add EventListenerOptions and passive event listener feature This introduces an EventListenerOptions dictionary which can be used to explicitly specify options to 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. --- dom.bs | 156 +++++++++++++++++++++++++++++++++++++++++-------------- dom.html | 138 ++++++++++++++++++++++++++++++++++-------------- 2 files changed, 216 insertions(+), 78 deletions(-) diff --git a/dom.bs b/dom.bs index a7b134b1..7a439e79 100644 --- a/dom.bs +++ b/dom.bs @@ -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 @@ -613,7 +615,7 @@ Lets look at an example of how events work in a tree: 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) @@ -727,17 +729,13 @@ inherits from the {{Event}} interface. {{Event/preventDefault()}} method.
event . preventDefault() -
If invoked when the - {{Event/cancelable}} attribute value is true, - signals to the operation that caused event to be - dispatched that it needs to be - canceled. +
If invoked when the {{Event/cancelable}} attribute value is true, and while executing a + listener for the event with {{EventListenerOptions/passive}} set to false, signals to + the operation that caused event to be dispatched that it needs to be canceled.
event . {{Event/defaultPrevented}} -
Returns true if - {{Event/preventDefault()}} was invoked - while the {{Event/cancelable}} attribute - value is true, and false otherwise. +
Returns true if {{Event/preventDefault()}} was invoked successfully to indicate cancellation, + and false otherwise.
event . {{Event/isTrusted}}
Returns true if event was @@ -799,6 +797,7 @@ flags that are all initially unset:
  • canceled flag
  • initialized flag
  • dispatch flag +
  • in passive listener flag The @@ -816,8 +815,13 @@ must return the values they were initialized to. The preventDefault() -method must set the canceled flag if the -{{Event/cancelable}} attribute value is true. +method must set the canceled flag if the {{Event/cancelable}} attribute value is true and +the in passive listener flag is unset. + +

    + 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 defaultPrevented @@ -972,14 +976,19 @@ for historical reasons.
     [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;
    +};
     
    {{EventTarget}} is an object to which an @@ -991,34 +1000,45 @@ occurred. Each {{EventTarget}} has an associated list of

    An event listener can be used to observe a specific event. -

    An event listener consists of a type, callback, and capture. An -event listener also has an associated removed flag, which is initially unset. +

    An event listener consists of a type, callback, capture and +passive. An event listener also has an associated removed flag, which is +initially unset.

    The callback is named {{EventListener}} for historical reasons. As can be seen from the definition above, an event listener is a more broad concept.

    -
    target . addEventListener(type, callback [, capture = false]) +
    target . addEventListener(type, callback [, options])
    Appends an event listener for events whose {{Event/type}} attribute value is type. The callback argument sets the callback that will - be invoked when the event is dispatched. When set to true, - the capture argument prevents callback from being invoked when - the event's {{Event/eventPhase}} attribute value is {{Event/BUBBLING_PHASE}}. - When false, callback will not be invoked when event's {{Event/eventPhase}} - attribute value is {{Event/CAPTURING_PHASE}}. Either way, callback will be - invoked if event's {{Event/eventPhase}} attribute value is {{Event/AT_TARGET}}. - - The event listener is appended to target's list of - event listeners and is not appended if it is a duplicate, i.e., having the same - type, callback, and capture values. - -
    target . removeEventListener(type, callback [, capture = false]) + be invoked when the event is dispatched. + + The options 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 + options' capture member. + + When set to true, options' capture member prevents callback from + being invoked when the event's {{Event/eventPhase}} attribute value is + {{Event/BUBBLING_PHASE}}. When false (or not present), callback will not be invoked when + event's {{Event/eventPhase}} attribute value is {{Event/CAPTURING_PHASE}}. Either way, + callback will be invoked if event's {{Event/eventPhase}} attribute value is + {{Event/AT_TARGET}}. + + When set to true, options' passive member indicates that the + callback will not cancel the event by invoking {{preventDefault()}}. This is used to enable + performance optimizations described in [[#observing-event-listeners]]. + + The event listener is appended to target's list of event listeners and is + not appended if it is a duplicate, i.e., having the same type, callback, + capture and passive values. + +
    target . removeEventListener(type, callback [, options])
    Remove the event listener in target's list of event listeners with the same type, callback, and - capture. + options.
    target . dispatchEvent(event)
    Dispatches a synthetic event event to target and returns @@ -1026,25 +1046,52 @@ seen from the definition above, an event listener is a more broad concept {{Event/preventDefault()}} method was not invoked, and false otherwise.
    +

    To flatten options run these steps: + +

      +
    1. Let capture and passive be false. + +
    2. If options is a boolean, set capture to + options. + +
    3. If options is a dictionary and {{EventListenerOptions/capture}} is + present in options with value true, then set capture to true. + +
    4. If options is a dictionary and {{EventListenerOptions/passive}} is + present in options with value true, then set passive to true. + +
    5. Return capture and passive. +
    +

    The -addEventListener(type, callback, capture) +addEventListener(type, callback, options) method, when invoked, must run these steps:

    1. If callback is null, terminate these steps. +

    2. Let capture and passive be the result of flattening + options. +
    3. Append an event listener to the associated list of event listeners with - type set to type, callback set to callback, and capture - set to capture, unless there already is an event listener in that list with the - same type, callback, and capture. + type set to type, callback set to callback, capture + set to capture, and passive set to passive unless there already is an + event listener in that list with the same type, callback, capture, and + passive.

    The -removeEventListener(type, callback, capture) -method, when invoked, must, if there is an event listener in the associated list of -event listeners whose type is type, callback is callback, -and capture is capture, set that event listener's removed flag and -remove it from the associated list of event listeners. +removeEventListener(type, callback, options) +method, when invoked, must, run these steps + +

      +
    1. Let capture and passive be the result of flattening options. + +
    2. If there is an event listener in the associated list of event listeners whose + type is type, callback is callback, capture is + capture, and passive is passive then set that event listener's + removed flag and remove it from the associated list of event listeners. +

    The dispatchEvent(event) method, when invoked, must run these steps: @@ -1059,6 +1106,30 @@ invoked, must run these steps: +

    Observing event listeners

    + +

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

    Unfortunately, some event APIs have been designed such that implementing them efficiently + requires observing event listeners. 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 + in parallel 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. + +

    Ideally, any new event APIs are defined such that they do not need this property (use +public-scrip-coord@w3.org +for discussion). + +

    Dispatching events

    To dispatch an event to a @@ -1139,9 +1210,15 @@ invoked, must run these steps: listener's capture is true, terminate these substeps (and run them for the next event listener). +

  • If listener's passive is true, set event's in passive + listener flag. +
  • Call listener's callback's {{EventListener/handleEvent()}}, with event as argument and event's {{Event/currentTarget}} attribute value as callback this value. If this throws any exception, report the exception. + +

  • Clear event's in passive listener flag. + @@ -9073,6 +9150,7 @@ Peter Sharpe, Philip Jägenstedt, Philippe Le Hégaret, Rafael Weinstein, +Rick Byers, Rick Waldron, Robbert Broersma, Robin Berjon, diff --git a/dom.html b/dom.html index b46c78d4..d2ca7200 100644 --- a/dom.html +++ b/dom.html @@ -69,7 +69,7 @@

    DOM

    -

    Living Standard — Last Updated

    +

    Living Standard — Last Updated

    Participate: @@ -115,9 +115,10 @@

    3.4 Constructing events
  • 3.5 Defining event interfaces
  • 3.6 Interface EventTarget -
  • 3.7 Dispatching events -
  • 3.8 Firing events -
  • 3.9 Action versus occurrence +
  • 3.7 Observing event listeners +
  • 3.8 Dispatching events +
  • 3.9 Firing events +
  • 3.10 Action versus occurrence
  • 4 Nodes @@ -451,7 +452,7 @@

    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) @@ -531,13 +532,12 @@

    dispatched, can be canceled by invoking the preventDefault() method.
    event . preventDefault() -
    If invoked when the cancelable attribute value is true, +
    If invoked when the cancelable attribute value is true, and while executing a listener + for the event with passive set to false, signals to the operation that caused event to be dispatched that it needs to be canceled.
    event . defaultPrevented -
    Returns true if preventDefault() was invoked - while the cancelable attribute - value is true, and false otherwise. +
    Returns true if preventDefault() was invoked successfully to indicate cancellation, and false otherwise.
    event . isTrusted
    Returns true if event was dispatched by the user agent, and false otherwise. @@ -575,12 +575,17 @@

    canceled flag
  • initialized flag
  • dispatch flag +
  • in passive listener flag

    The stopPropagation() method must set the stop propagation flag.

    The stopImmediatePropagation() method must set both the stop propagation flag and stop immediate propagation flag.

    The bubbles and cancelable attributes must return the values they were initialized to.

    -

    The preventDefault() method must set the canceled flag if the cancelable attribute value is true.

    +

    The preventDefault() method must set the canceled flag if the cancelable attribute value is true and +the in passive listener flag is unset.

    +

    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 defaultPrevented attribute must return true if the canceled flag is set and false otherwise.


    @@ -672,49 +677,74 @@

    3.6. Interface EventTarget

    [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;
    +};
     

    EventTarget is an object to which an event is dispatched when something has occurred. Each EventTarget has an associated list of event listeners.

    An event listener can be used to observe a specific event.

    -

    An event listener consists of a type, callback, and capture. An event listener also has an associated removed flag, which is initially unset.

    +

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

    The callback is named EventListener for historical reasons. As can be seen from the definition above, an event listener is a more broad concept.

    -
    target . addEventListener(type, callback [, capture = false]) +
    target . addEventListener(type, callback [, options])
    Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will - be invoked when the event is dispatched. When set to true, - the capture argument prevents callback from being invoked when + be invoked when the event is dispatched. +

    The options 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 optionscapture member.

    +

    When set to true, optionscapture member prevents callback from being invoked when the event’s eventPhase attribute value is BUBBLING_PHASE. - When false, callback will not be invoked when event’s eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be - invoked if event’s eventPhase attribute value is AT_TARGET. -

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

    -
    target . removeEventListener(type, callback [, capture = false]) -
    Remove the event listener in target’s list of event listeners with the same type, callback, and capture. + When false (or not present), callback will not be invoked when event’s eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be + invoked if event’s eventPhase attribute value is AT_TARGET.

    +

    When set to true, optionspassive member indicates that the callback will not cancel the event by invoking preventDefault(). + This is used to enable performance optimizations described in §3.7 Observing event listeners.

    +

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

    +
    target . removeEventListener(type, callback [, options]) +
    Remove the event listener in target’s list of event listeners with the same type, callback, and options.
    target . dispatchEvent(event)
    Dispatches a synthetic event event to target and returns true if either event’s cancelable attribute value is false or its preventDefault() method was not invoked, and false otherwise.
    -

    The addEventListener(type, callback, capture) method, when invoked, must run these steps:

    +

    To flatten options run these steps:

    +
      +
    1. Let capture and passive be false. +
    2. If options is a boolean, set capture to options. +
    3. If options is a dictionary and capture is + present in options with value true, then set capture to true. +
    4. If options is a dictionary and passive is + present in options with value true, then set passive to true. +
    5. Return capture and passive. +
    +

    The addEventListener(type, callback, options) method, when invoked, must run these steps:

    1. If callback is null, terminate these steps.

      +
    2. Let capture and passive be the result of flattening options.
    3. -

      Append an event listener to the associated list of event listeners with type set to type, callback set to callback, and capture set to capture, unless there already is an event listener in that list with the - same type, callback, and capture.

      +

      Append an event listener to the associated list of event listeners with type set to type, callback set to callback, capture set to capture, and passive set to passive unless there + already is an event listener in that list with the same type, callback, capture, and passive.

      +
    +

    The removeEventListener(type, callback, options) method, when invoked, must, run these steps

    +
      +
    1. Let capture and passive be the result of flattening options. +
    2. If there is an event listener in the associated list of event listeners whose type is type, callback is callback, capture is capture, and passive is passive then + set that event listener’s removed flag and remove it from the + associated list of event listeners.
    -

    The removeEventListener(type, callback, capture) method, when invoked, must, if there is an event listener in the associated list of event listeners whose type is type, callback is callback, -and capture is capture, set that event listener’s removed flag and -remove it from the associated list of event listeners.

    The dispatchEvent(event) method, when invoked, must run these steps:

      @@ -726,7 +756,24 @@

      Dispatch the event and return the value that returns.

    -

    3.7. Dispatching events

    +

    3.7. Observing event listeners

    +

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

    +

    Unfortunately, some event APIs have been designed such that implementing them +efficiently requires observing event listeners. 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 cancelable only when there is at least one +non-passive listener. For example, non-passive TouchEvent listeners must block scrolling, but if all listeners are passive then +scrolling can be allowed to start in parallel by making the TouchEvent uncancelable (so that calls to preventDefault() are ignored). So code +dispatching an event is able to observe the absence of non-passive listeners, and use that to clear the cancelable property of the event +being dispatched.

    +

    Ideally, any new event APIs are defined such that they do not need this +property (use public-scrip-coord@w3.org for discussion).

    +

    3.8. Dispatching events

    To dispatch an event to a target, with an optional target override, run these steps:

    1. @@ -785,18 +832,20 @@

      event listener).

    2. If event’s eventPhase attribute value is BUBBLING_PHASE and listener’s capture is true, terminate these substeps (and run them for the next event listener).

      +
    3. If listener’s passive is true, set event’s in passive listener flag.
    4. Call listener’s callback’s handleEvent(), with event as argument and event’s currentTarget attribute value as callback this value. If this throws any exception, report the exception.

      +
    5. Clear event’s in passive listener flag.
    -

    3.8. Firing events

    +

    3.9. Firing events

    To fire an event named e means that a new event using the Event interface, with its type attribute initialized to e, and its isTrusted attribute initialized to true, is to be dispatched to the given object.

    Fire in the context of DOM is short for creating, initializing, and dispatching an event. Fire an event makes that process easier to write down. If the event needs its bubbles or cancelable attribute initialized, one could write "fire an event named submit with its cancelable attribute initialized to true".

    -

    3.9. Action versus occurrence

    +

    3.10. Action versus occurrence

    An event signifies an occurrence, not an action. Phrased differently, it represents a notification from an algorithm and can be used to influence the future course of that algorithm (e.g., through invoking preventDefault()). Events must not be @@ -4513,6 +4562,7 @@

    Acknowledgmen Philip Jägenstedt, Philippe Le Hégaret, Rafael Weinstein, +Rick Byers, Rick Waldron, Robbert Broersma, Robin Berjon, @@ -4551,7 +4601,7 @@

    add(), in §7.1
  • addedNodes, in §4.3.3
  • addEventListener(type, callback), in §3.6 -
  • addEventListener(type, callback, capture), in §3.6 +
  • addEventListener(type, callback, options), in §3.6
  • add(tokens), in §7.1
  • adopt, in §4.5
  • adopting steps, in §4.5 @@ -4614,6 +4664,7 @@

    attribute for Event, in §3.2
  • canceled flag, in §3.2 +
  • capture, in §3.6
  • CAPTURING_PHASE, in §3.2
  • case-sensitive, in §2.2
  • case-sensitively, in §2.2 @@ -4723,7 +4774,7 @@

    attribute for CustomEvent, in §3.3
  • disconnect(), in §4.3.1 -
  • dispatch, in §3.7 +
  • dispatch, in §3.8
  • dispatchEvent(event), in §3.6
  • dispatch flag, in §3.2
  • @@ -4796,6 +4847,7 @@

    EventInit, in §3.2
  • event listener, in §3.6
  • EventListener, in §3.6 +
  • EventListenerOptions, in §3.6
  • eventPhase, in §3.2
  • EventTarget, in §3.6
  • Event(type, eventInitDict), in §3.2 @@ -4814,11 +4866,12 @@

    FILTER_ACCEPT, in §6.3
  • FILTER_REJECT, in §6.3
  • FILTER_SKIP, in §6.3 -
  • fire an event, in §3.8 +
  • fire an event, in §3.9
  • firstChild(), in §6.2
  • firstChild, in §4.4
  • first child, in §2.1
  • firstElementChild, in §4.2.3 +
  • flatten, in §3.6
  • following, in §2.1
  • get an attribute by name, in §4.8
  • get an attribute by namespace and local name, in §4.8 @@ -4886,6 +4939,7 @@

    initEvent(type, bubbles, cancelable), in §3.2
  • initialize, in §3.2
  • initialized flag, in §3.2 +
  • in passive listener flag, in §3.2
  • inputEncoding, in §4.5
  • insert @@ -4899,7 +4953,7 @@

    insertNode(node), in §5.2
  • internalSubset, in §8.2
  • intersectsNode(node), in §5.2 -
  • invoke, in §3.7 +
  • invoke, in §3.8
  • isDefaultNamespace(namespace), in §4.4
  • isElementContentWhitespace, in §8.2
  • isEqualNode(otherNode), in §4.4 @@ -5054,6 +5108,7 @@

    parentNode(), in §6.2
  • partially contained, in §5.2
  • participate, in §2.1 +
  • passive, in §3.6
  • pointerBeforeReferenceNode, in §6.1
  • position, in §5.2
  • preceding, in §2.1 @@ -5109,8 +5164,8 @@

    queue a mutation observer compound microtask, in §4.3
  • queue a mutation record, in §4.3.2
  • quirks mode, in §4.5 -
  • Range, in §5.2
  • Range(), in §5.2 +
  • Range, in §5.2
  • range, in §5.2
  • RangeException, in §8.3
  • record queue, in §4.3.1 @@ -5133,7 +5188,7 @@

    removeChild(child), in §4.4
  • removedNodes, in §4.3.3
  • removeEventListener(type, callback), in §3.6 -
  • removeEventListener(type, callback, capture), in §3.6 +
  • removeEventListener(type, callback, options), in §3.6
  • removeNamedItemNS(namespace, localName), in §4.8.1
  • removeNamedItem(qualifiedName), in §4.8.1
  • remove(tokens), in §7.1 @@ -5206,8 +5261,8 @@

    start, in §5.2
  • startContainer, in §5.2
  • start node, in §5.2 -
  • start offset, in §5.2
  • startOffset, in §5.2 +
  • start offset, in §5.2
  • START_TO_END, in §5.2
  • START_TO_START, in §5.2
  • static collection, in §4.2.7 @@ -5439,8 +5494,8 @@

    IDL Inde [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); }; @@ -5448,6 +5503,11 @@

    IDL Inde void handleEvent(Event event); }; +dictionary EventListenerOptions { + boolean capture; + boolean passive; +}; + [NoInterfaceObject, Exposed=Window] interface NonElementParentNode {