summaryrefslogtreecommitdiff
path: root/cli/js/event_target.ts
diff options
context:
space:
mode:
Diffstat (limited to 'cli/js/event_target.ts')
-rw-r--r--cli/js/event_target.ts503
1 files changed, 503 insertions, 0 deletions
diff --git a/cli/js/event_target.ts b/cli/js/event_target.ts
new file mode 100644
index 000000000..08c39544c
--- /dev/null
+++ b/cli/js/event_target.ts
@@ -0,0 +1,503 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import * as domTypes from "./dom_types.ts";
+import { DenoError, ErrorKind } from "./errors.ts";
+import { hasOwnProperty, requiredArguments } from "./util.ts";
+import {
+ getRoot,
+ isNode,
+ isShadowRoot,
+ isShadowInclusiveAncestor,
+ isSlotable,
+ retarget
+} from "./dom_util.ts";
+import { window } from "./window.ts";
+
+// https://dom.spec.whatwg.org/#get-the-parent
+// Note: Nodes, shadow roots, and documents override this algorithm so we set it to null.
+function getEventTargetParent(
+ _eventTarget: domTypes.EventTarget,
+ _event: domTypes.Event
+): null {
+ return null;
+}
+
+export const eventTargetAssignedSlot: unique symbol = Symbol();
+export const eventTargetHasActivationBehavior: unique symbol = Symbol();
+
+export class EventTarget implements domTypes.EventTarget {
+ public [domTypes.eventTargetHost]: domTypes.EventTarget | null = null;
+ public [domTypes.eventTargetListeners]: {
+ [type in string]: domTypes.EventListener[]
+ } = {};
+ public [domTypes.eventTargetMode] = "";
+ public [domTypes.eventTargetNodeType]: domTypes.NodeType =
+ domTypes.NodeType.DOCUMENT_FRAGMENT_NODE;
+ private [eventTargetAssignedSlot] = false;
+ private [eventTargetHasActivationBehavior] = false;
+
+ public addEventListener(
+ type: string,
+ callback: (event: domTypes.Event) => void | null,
+ options?: domTypes.AddEventListenerOptions | boolean
+ ): void {
+ const this_ = this || window;
+
+ requiredArguments("EventTarget.addEventListener", arguments.length, 2);
+ const normalizedOptions: domTypes.AddEventListenerOptions = eventTargetHelpers.normalizeAddEventHandlerOptions(
+ options
+ );
+
+ if (callback === null) {
+ return;
+ }
+
+ const listeners = this_[domTypes.eventTargetListeners];
+
+ if (!hasOwnProperty(listeners, type)) {
+ listeners[type] = [];
+ }
+
+ for (let i = 0; i < listeners[type].length; ++i) {
+ const listener = listeners[type][i];
+ if (
+ ((typeof listener.options === "boolean" &&
+ listener.options === normalizedOptions.capture) ||
+ (typeof listener.options === "object" &&
+ listener.options.capture === normalizedOptions.capture)) &&
+ listener.callback === callback
+ ) {
+ return;
+ }
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const eventTarget = this;
+ listeners[type].push({
+ callback,
+ options: normalizedOptions,
+ handleEvent(event: domTypes.Event): void {
+ this.callback.call(eventTarget, event);
+ }
+ } as domTypes.EventListener);
+ }
+
+ public removeEventListener(
+ type: string,
+ callback: (event: domTypes.Event) => void | null,
+ options?: domTypes.EventListenerOptions | boolean
+ ): void {
+ const this_ = this || window;
+
+ requiredArguments("EventTarget.removeEventListener", arguments.length, 2);
+ const listeners = this_[domTypes.eventTargetListeners];
+ if (hasOwnProperty(listeners, type) && callback !== null) {
+ listeners[type] = listeners[type].filter(
+ (listener): boolean => listener.callback !== callback
+ );
+ }
+
+ const normalizedOptions: domTypes.EventListenerOptions = eventTargetHelpers.normalizeEventHandlerOptions(
+ options
+ );
+
+ if (callback === null) {
+ // Optimization, not in the spec.
+ return;
+ }
+
+ if (!listeners[type]) {
+ return;
+ }
+
+ for (let i = 0; i < listeners[type].length; ++i) {
+ const listener = listeners[type][i];
+
+ if (
+ ((typeof listener.options === "boolean" &&
+ listener.options === normalizedOptions.capture) ||
+ (typeof listener.options === "object" &&
+ listener.options.capture === normalizedOptions.capture)) &&
+ listener.callback === callback
+ ) {
+ listeners[type].splice(i, 1);
+ break;
+ }
+ }
+ }
+
+ public dispatchEvent(event: domTypes.Event): boolean {
+ const this_ = this || window;
+
+ requiredArguments("EventTarget.dispatchEvent", arguments.length, 1);
+ const listeners = this_[domTypes.eventTargetListeners];
+ if (!hasOwnProperty(listeners, event.type)) {
+ return true;
+ }
+
+ if (event.dispatched || !event.initialized) {
+ throw new DenoError(
+ ErrorKind.InvalidData,
+ "Tried to dispatch an uninitialized event"
+ );
+ }
+
+ if (event.eventPhase !== domTypes.EventPhase.NONE) {
+ throw new DenoError(
+ ErrorKind.InvalidData,
+ "Tried to dispatch a dispatching event"
+ );
+ }
+
+ return eventTargetHelpers.dispatch(this_, event);
+ }
+
+ get [Symbol.toStringTag](): string {
+ return "EventTarget";
+ }
+}
+
+const eventTargetHelpers = {
+ // https://dom.spec.whatwg.org/#concept-event-dispatch
+ dispatch(
+ targetImpl: EventTarget,
+ eventImpl: domTypes.Event,
+ targetOverride?: domTypes.EventTarget
+ ): boolean {
+ let clearTargets = false;
+ let activationTarget = null;
+
+ eventImpl.dispatched = true;
+
+ targetOverride = targetOverride || targetImpl;
+ let relatedTarget = retarget(eventImpl.relatedTarget, targetImpl);
+
+ if (
+ targetImpl !== relatedTarget ||
+ targetImpl === eventImpl.relatedTarget
+ ) {
+ const touchTargets: domTypes.EventTarget[] = [];
+
+ eventTargetHelpers.appendToEventPath(
+ eventImpl,
+ targetImpl,
+ targetOverride,
+ relatedTarget,
+ touchTargets,
+ false
+ );
+
+ const isActivationEvent = eventImpl.type === "click";
+
+ if (isActivationEvent && targetImpl[eventTargetHasActivationBehavior]) {
+ activationTarget = targetImpl;
+ }
+
+ let slotInClosedTree = false;
+ let slotable =
+ isSlotable(targetImpl) && targetImpl[eventTargetAssignedSlot]
+ ? targetImpl
+ : null;
+ let parent = getEventTargetParent(targetImpl, eventImpl);
+
+ // Populate event path
+ // https://dom.spec.whatwg.org/#event-path
+ while (parent !== null) {
+ if (slotable !== null) {
+ slotable = null;
+
+ const parentRoot = getRoot(parent);
+ if (
+ isShadowRoot(parentRoot) &&
+ parentRoot &&
+ parentRoot[domTypes.eventTargetMode] === "closed"
+ ) {
+ slotInClosedTree = true;
+ }
+ }
+
+ relatedTarget = retarget(eventImpl.relatedTarget, parent);
+
+ if (
+ isNode(parent) &&
+ isShadowInclusiveAncestor(getRoot(targetImpl), parent)
+ ) {
+ eventTargetHelpers.appendToEventPath(
+ eventImpl,
+ parent,
+ null,
+ relatedTarget,
+ touchTargets,
+ slotInClosedTree
+ );
+ } else if (parent === relatedTarget) {
+ parent = null;
+ } else {
+ targetImpl = parent;
+
+ if (
+ isActivationEvent &&
+ activationTarget === null &&
+ targetImpl[eventTargetHasActivationBehavior]
+ ) {
+ activationTarget = targetImpl;
+ }
+
+ eventTargetHelpers.appendToEventPath(
+ eventImpl,
+ parent,
+ targetImpl,
+ relatedTarget,
+ touchTargets,
+ slotInClosedTree
+ );
+ }
+
+ if (parent !== null) {
+ parent = getEventTargetParent(parent, eventImpl);
+ }
+
+ slotInClosedTree = false;
+ }
+
+ let clearTargetsTupleIndex = -1;
+ for (
+ let i = eventImpl.path.length - 1;
+ i >= 0 && clearTargetsTupleIndex === -1;
+ i--
+ ) {
+ if (eventImpl.path[i].target !== null) {
+ clearTargetsTupleIndex = i;
+ }
+ }
+ const clearTargetsTuple = eventImpl.path[clearTargetsTupleIndex];
+
+ clearTargets =
+ (isNode(clearTargetsTuple.target) &&
+ isShadowRoot(getRoot(clearTargetsTuple.target))) ||
+ (isNode(clearTargetsTuple.relatedTarget) &&
+ isShadowRoot(getRoot(clearTargetsTuple.relatedTarget)));
+
+ eventImpl.eventPhase = domTypes.EventPhase.CAPTURING_PHASE;
+
+ for (let i = eventImpl.path.length - 1; i >= 0; --i) {
+ const tuple = eventImpl.path[i];
+
+ if (tuple.target === null) {
+ eventTargetHelpers.invokeEventListeners(targetImpl, tuple, eventImpl);
+ }
+ }
+
+ for (let i = 0; i < eventImpl.path.length; i++) {
+ const tuple = eventImpl.path[i];
+
+ if (tuple.target !== null) {
+ eventImpl.eventPhase = domTypes.EventPhase.AT_TARGET;
+ } else {
+ eventImpl.eventPhase = domTypes.EventPhase.BUBBLING_PHASE;
+ }
+
+ if (
+ (eventImpl.eventPhase === domTypes.EventPhase.BUBBLING_PHASE &&
+ eventImpl.bubbles) ||
+ eventImpl.eventPhase === domTypes.EventPhase.AT_TARGET
+ ) {
+ eventTargetHelpers.invokeEventListeners(targetImpl, tuple, eventImpl);
+ }
+ }
+ }
+
+ eventImpl.eventPhase = domTypes.EventPhase.NONE;
+
+ eventImpl.currentTarget = null;
+ eventImpl.path = [];
+ eventImpl.dispatched = false;
+ eventImpl.cancelBubble = false;
+ eventImpl.cancelBubbleImmediately = false;
+
+ if (clearTargets) {
+ eventImpl.target = null;
+ eventImpl.relatedTarget = null;
+ }
+
+ // TODO: invoke activation targets if HTML nodes will be implemented
+ // if (activationTarget !== null) {
+ // if (!eventImpl.defaultPrevented) {
+ // activationTarget._activationBehavior();
+ // }
+ // }
+
+ return !eventImpl.defaultPrevented;
+ },
+
+ // https://dom.spec.whatwg.org/#concept-event-listener-invoke
+ invokeEventListeners(
+ targetImpl: EventTarget,
+ tuple: domTypes.EventPath,
+ eventImpl: domTypes.Event
+ ): void {
+ const tupleIndex = eventImpl.path.indexOf(tuple);
+ for (let i = tupleIndex; i >= 0; i--) {
+ const t = eventImpl.path[i];
+ if (t.target) {
+ eventImpl.target = t.target;
+ break;
+ }
+ }
+
+ eventImpl.relatedTarget = tuple.relatedTarget;
+
+ if (eventImpl.cancelBubble) {
+ return;
+ }
+
+ eventImpl.currentTarget = tuple.item;
+
+ eventTargetHelpers.innerInvokeEventListeners(
+ targetImpl,
+ eventImpl,
+ tuple.item[domTypes.eventTargetListeners]
+ );
+ },
+
+ // https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke
+ innerInvokeEventListeners(
+ targetImpl: EventTarget,
+ eventImpl: domTypes.Event,
+ targetListeners: { [type in string]: domTypes.EventListener[] }
+ ): boolean {
+ let found = false;
+
+ const { type } = eventImpl;
+
+ if (!targetListeners || !targetListeners[type]) {
+ return found;
+ }
+
+ // Copy event listeners before iterating since the list can be modified during the iteration.
+ const handlers = targetListeners[type].slice();
+
+ for (let i = 0; i < handlers.length; i++) {
+ const listener = handlers[i];
+
+ let capture, once, passive;
+ if (typeof listener.options === "boolean") {
+ capture = listener.options;
+ once = false;
+ passive = false;
+ } else {
+ capture = listener.options.capture;
+ once = listener.options.once;
+ passive = listener.options.passive;
+ }
+
+ // Check if the event listener has been removed since the listeners has been cloned.
+ if (!targetListeners[type].includes(listener)) {
+ continue;
+ }
+
+ found = true;
+
+ if (
+ (eventImpl.eventPhase === domTypes.EventPhase.CAPTURING_PHASE &&
+ !capture) ||
+ (eventImpl.eventPhase === domTypes.EventPhase.BUBBLING_PHASE && capture)
+ ) {
+ continue;
+ }
+
+ if (once) {
+ targetListeners[type].splice(
+ targetListeners[type].indexOf(listener),
+ 1
+ );
+ }
+
+ if (passive) {
+ eventImpl.inPassiveListener = true;
+ }
+
+ try {
+ if (listener.callback) {
+ listener.handleEvent(eventImpl);
+ }
+ } catch (error) {
+ throw new DenoError(ErrorKind.Interrupted, error.message);
+ }
+
+ eventImpl.inPassiveListener = false;
+
+ if (eventImpl.cancelBubbleImmediately) {
+ return found;
+ }
+ }
+
+ return found;
+ },
+
+ normalizeAddEventHandlerOptions(
+ options: boolean | domTypes.AddEventListenerOptions | undefined
+ ): domTypes.AddEventListenerOptions {
+ if (typeof options === "boolean" || typeof options === "undefined") {
+ const returnValue: domTypes.AddEventListenerOptions = {
+ capture: Boolean(options),
+ once: false,
+ passive: false
+ };
+
+ return returnValue;
+ } else {
+ return options;
+ }
+ },
+
+ normalizeEventHandlerOptions(
+ options: boolean | domTypes.EventListenerOptions | undefined
+ ): domTypes.EventListenerOptions {
+ if (typeof options === "boolean" || typeof options === "undefined") {
+ const returnValue: domTypes.EventListenerOptions = {
+ capture: Boolean(options)
+ };
+
+ return returnValue;
+ } else {
+ return options;
+ }
+ },
+
+ // https://dom.spec.whatwg.org/#concept-event-path-append
+ appendToEventPath(
+ eventImpl: domTypes.Event,
+ target: domTypes.EventTarget,
+ targetOverride: domTypes.EventTarget | null,
+ relatedTarget: domTypes.EventTarget | null,
+ touchTargets: domTypes.EventTarget[],
+ slotInClosedTree: boolean
+ ): void {
+ const itemInShadowTree = isNode(target) && isShadowRoot(getRoot(target));
+ const rootOfClosedTree =
+ isShadowRoot(target) && target[domTypes.eventTargetMode] === "closed";
+
+ eventImpl.path.push({
+ item: target,
+ itemInShadowTree,
+ target: targetOverride,
+ relatedTarget,
+ touchTargetList: touchTargets,
+ rootOfClosedTree,
+ slotInClosedTree
+ });
+ }
+};
+
+/** Built-in objects providing `get` methods for our
+ * interceptable JavaScript operations.
+ */
+Reflect.defineProperty(EventTarget.prototype, "addEventListener", {
+ enumerable: true
+});
+Reflect.defineProperty(EventTarget.prototype, "removeEventListener", {
+ enumerable: true
+});
+Reflect.defineProperty(EventTarget.prototype, "dispatchEvent", {
+ enumerable: true
+});