diff options
Diffstat (limited to 'cli/tsc/01_event.js')
-rw-r--r-- | cli/tsc/01_event.js | 1044 |
1 files changed, 1044 insertions, 0 deletions
diff --git a/cli/tsc/01_event.js b/cli/tsc/01_event.js new file mode 100644 index 000000000..35967e0a1 --- /dev/null +++ b/cli/tsc/01_event.js @@ -0,0 +1,1044 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +// This module follows most of the WHATWG Living Standard for the DOM logic. +// Many parts of the DOM are not implemented in Deno, but the logic for those +// parts still exists. This means you will observe a lot of strange structures +// and impossible logic branches based on what Deno currently supports. + +((window) => { + const eventData = new WeakMap(); + + function requiredArguments( + name, + length, + required, + ) { + if (length < required) { + const errMsg = `${name} requires at least ${required} argument${ + required === 1 ? "" : "s" + }, but only ${length} present`; + throw new TypeError(errMsg); + } + } + + // accessors for non runtime visible data + + function getDispatched(event) { + return Boolean(eventData.get(event)?.dispatched); + } + + function getPath(event) { + return eventData.get(event)?.path ?? []; + } + + function getStopImmediatePropagation(event) { + return Boolean(eventData.get(event)?.stopImmediatePropagation); + } + + function setCurrentTarget( + event, + value, + ) { + event.currentTarget = value; + } + + function setDispatched(event, value) { + const data = eventData.get(event); + if (data) { + data.dispatched = value; + } + } + + function setEventPhase(event, value) { + event.eventPhase = value; + } + + function setInPassiveListener(event, value) { + const data = eventData.get(event); + if (data) { + data.inPassiveListener = value; + } + } + + function setPath(event, value) { + const data = eventData.get(event); + if (data) { + data.path = value; + } + } + + function setRelatedTarget( + event, + value, + ) { + if ("relatedTarget" in event) { + event.relatedTarget = value; + } + } + + function setTarget(event, value) { + event.target = value; + } + + function setStopImmediatePropagation( + event, + value, + ) { + const data = eventData.get(event); + if (data) { + data.stopImmediatePropagation = value; + } + } + + // Type guards that widen the event type + + function hasRelatedTarget( + event, + ) { + return "relatedTarget" in event; + } + + function isTrusted() { + return eventData.get(this).isTrusted; + } + + class Event { + #canceledFlag = false; + #stopPropagationFlag = false; + #attributes = {}; + + constructor(type, eventInitDict = {}) { + requiredArguments("Event", arguments.length, 1); + type = String(type); + this.#attributes = { + type, + bubbles: eventInitDict.bubbles ?? false, + cancelable: eventInitDict.cancelable ?? false, + composed: eventInitDict.composed ?? false, + currentTarget: null, + eventPhase: Event.NONE, + target: null, + timeStamp: Date.now(), + }; + eventData.set(this, { + dispatched: false, + inPassiveListener: false, + isTrusted: false, + path: [], + stopImmediatePropagation: false, + }); + Reflect.defineProperty(this, "isTrusted", { + enumerable: true, + get: isTrusted, + }); + } + + get bubbles() { + return this.#attributes.bubbles; + } + + get cancelBubble() { + return this.#stopPropagationFlag; + } + + set cancelBubble(value) { + this.#stopPropagationFlag = value; + } + + get cancelable() { + return this.#attributes.cancelable; + } + + get composed() { + return this.#attributes.composed; + } + + get currentTarget() { + return this.#attributes.currentTarget; + } + + set currentTarget(value) { + this.#attributes = { + type: this.type, + bubbles: this.bubbles, + cancelable: this.cancelable, + composed: this.composed, + currentTarget: value, + eventPhase: this.eventPhase, + target: this.target, + timeStamp: this.timeStamp, + }; + } + + get defaultPrevented() { + return this.#canceledFlag; + } + + get eventPhase() { + return this.#attributes.eventPhase; + } + + set eventPhase(value) { + this.#attributes = { + type: this.type, + bubbles: this.bubbles, + cancelable: this.cancelable, + composed: this.composed, + currentTarget: this.currentTarget, + eventPhase: value, + target: this.target, + timeStamp: this.timeStamp, + }; + } + + get initialized() { + return true; + } + + get target() { + return this.#attributes.target; + } + + set target(value) { + this.#attributes = { + type: this.type, + bubbles: this.bubbles, + cancelable: this.cancelable, + composed: this.composed, + currentTarget: this.currentTarget, + eventPhase: this.eventPhase, + target: value, + timeStamp: this.timeStamp, + }; + } + + get timeStamp() { + return this.#attributes.timeStamp; + } + + get type() { + return this.#attributes.type; + } + + composedPath() { + const path = eventData.get(this).path; + if (path.length === 0) { + return []; + } + + if (!this.currentTarget) { + throw new Error("assertion error"); + } + const composedPath = [ + { + item: this.currentTarget, + itemInShadowTree: false, + relatedTarget: null, + rootOfClosedTree: false, + slotInClosedTree: false, + target: null, + touchTargetList: [], + }, + ]; + + let currentTargetIndex = 0; + let currentTargetHiddenSubtreeLevel = 0; + + for (let index = path.length - 1; index >= 0; index--) { + const { item, rootOfClosedTree, slotInClosedTree } = path[index]; + + if (rootOfClosedTree) { + currentTargetHiddenSubtreeLevel++; + } + + if (item === this.currentTarget) { + currentTargetIndex = index; + break; + } + + if (slotInClosedTree) { + currentTargetHiddenSubtreeLevel--; + } + } + + let currentHiddenLevel = currentTargetHiddenSubtreeLevel; + let maxHiddenLevel = currentTargetHiddenSubtreeLevel; + + for (let i = currentTargetIndex - 1; i >= 0; i--) { + const { item, rootOfClosedTree, slotInClosedTree } = path[i]; + + if (rootOfClosedTree) { + currentHiddenLevel++; + } + + if (currentHiddenLevel <= maxHiddenLevel) { + composedPath.unshift({ + item, + itemInShadowTree: false, + relatedTarget: null, + rootOfClosedTree: false, + slotInClosedTree: false, + target: null, + touchTargetList: [], + }); + } + + if (slotInClosedTree) { + currentHiddenLevel--; + + if (currentHiddenLevel < maxHiddenLevel) { + maxHiddenLevel = currentHiddenLevel; + } + } + } + + currentHiddenLevel = currentTargetHiddenSubtreeLevel; + maxHiddenLevel = currentTargetHiddenSubtreeLevel; + + for (let index = currentTargetIndex + 1; index < path.length; index++) { + const { item, rootOfClosedTree, slotInClosedTree } = path[index]; + + if (slotInClosedTree) { + currentHiddenLevel++; + } + + if (currentHiddenLevel <= maxHiddenLevel) { + composedPath.push({ + item, + itemInShadowTree: false, + relatedTarget: null, + rootOfClosedTree: false, + slotInClosedTree: false, + target: null, + touchTargetList: [], + }); + } + + if (rootOfClosedTree) { + currentHiddenLevel--; + + if (currentHiddenLevel < maxHiddenLevel) { + maxHiddenLevel = currentHiddenLevel; + } + } + } + return composedPath.map((p) => p.item); + } + + preventDefault() { + if (this.cancelable && !eventData.get(this).inPassiveListener) { + this.#canceledFlag = true; + } + } + + stopPropagation() { + this.#stopPropagationFlag = true; + } + + stopImmediatePropagation() { + this.#stopPropagationFlag = true; + eventData.get(this).stopImmediatePropagation = true; + } + + get NONE() { + return Event.NONE; + } + + get CAPTURING_PHASE() { + return Event.CAPTURING_PHASE; + } + + get AT_TARGET() { + return Event.AT_TARGET; + } + + get BUBBLING_PHASE() { + return Event.BUBBLING_PHASE; + } + + static get NONE() { + return 0; + } + + static get CAPTURING_PHASE() { + return 1; + } + + static get AT_TARGET() { + return 2; + } + + static get BUBBLING_PHASE() { + return 3; + } + } + + function defineEnumerableProps( + Ctor, + props, + ) { + for (const prop of props) { + Reflect.defineProperty(Ctor.prototype, prop, { enumerable: true }); + } + } + + defineEnumerableProps(Event, [ + "bubbles", + "cancelable", + "composed", + "currentTarget", + "defaultPrevented", + "eventPhase", + "target", + "timeStamp", + "type", + ]); + + // This is currently the only node type we are using, so instead of implementing + // the whole of the Node interface at the moment, this just gives us the one + // value to power the standards based logic + const DOCUMENT_FRAGMENT_NODE = 11; + + // DOM Logic Helper functions and type guards + + /** Get the parent node, for event targets that have a parent. + * + * Ref: https://dom.spec.whatwg.org/#get-the-parent */ + function getParent(eventTarget) { + return isNode(eventTarget) ? eventTarget.parentNode : null; + } + + function getRoot(eventTarget) { + return isNode(eventTarget) + ? eventTarget.getRootNode({ composed: true }) + : null; + } + + function isNode( + eventTarget, + ) { + return Boolean(eventTarget && "nodeType" in eventTarget); + } + + // https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-ancestor + function isShadowInclusiveAncestor( + ancestor, + node, + ) { + while (isNode(node)) { + if (node === ancestor) { + return true; + } + + if (isShadowRoot(node)) { + node = node && getHost(node); + } else { + node = getParent(node); + } + } + + return false; + } + + function isShadowRoot(nodeImpl) { + return Boolean( + nodeImpl && + isNode(nodeImpl) && + nodeImpl.nodeType === DOCUMENT_FRAGMENT_NODE && + getHost(nodeImpl) != null, + ); + } + + function isSlotable( + nodeImpl, + ) { + return Boolean(isNode(nodeImpl) && "assignedSlot" in nodeImpl); + } + + // DOM Logic functions + + /** Append a path item to an event's path. + * + * Ref: https://dom.spec.whatwg.org/#concept-event-path-append + */ + function appendToEventPath( + eventImpl, + target, + targetOverride, + relatedTarget, + touchTargets, + slotInClosedTree, + ) { + const itemInShadowTree = isNode(target) && isShadowRoot(getRoot(target)); + const rootOfClosedTree = isShadowRoot(target) && + getMode(target) === "closed"; + + getPath(eventImpl).push({ + item: target, + itemInShadowTree, + target: targetOverride, + relatedTarget, + touchTargetList: touchTargets, + rootOfClosedTree, + slotInClosedTree, + }); + } + + function dispatch( + targetImpl, + eventImpl, + targetOverride, + ) { + let clearTargets = false; + let activationTarget = null; + + setDispatched(eventImpl, true); + + targetOverride = targetOverride ?? targetImpl; + const eventRelatedTarget = hasRelatedTarget(eventImpl) + ? eventImpl.relatedTarget + : null; + let relatedTarget = retarget(eventRelatedTarget, targetImpl); + + if (targetImpl !== relatedTarget || targetImpl === eventRelatedTarget) { + const touchTargets = []; + + appendToEventPath( + eventImpl, + targetImpl, + targetOverride, + relatedTarget, + touchTargets, + false, + ); + + const isActivationEvent = eventImpl.type === "click"; + + if (isActivationEvent && getHasActivationBehavior(targetImpl)) { + activationTarget = targetImpl; + } + + let slotInClosedTree = false; + let slotable = isSlotable(targetImpl) && getAssignedSlot(targetImpl) + ? targetImpl + : null; + let parent = getParent(targetImpl); + + // 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 && + getMode(parentRoot) === "closed" + ) { + slotInClosedTree = true; + } + } + + relatedTarget = retarget(eventRelatedTarget, parent); + + if ( + isNode(parent) && + isShadowInclusiveAncestor(getRoot(targetImpl), parent) + ) { + appendToEventPath( + eventImpl, + parent, + null, + relatedTarget, + touchTargets, + slotInClosedTree, + ); + } else if (parent === relatedTarget) { + parent = null; + } else { + targetImpl = parent; + + if ( + isActivationEvent && + activationTarget === null && + getHasActivationBehavior(targetImpl) + ) { + activationTarget = targetImpl; + } + + appendToEventPath( + eventImpl, + parent, + targetImpl, + relatedTarget, + touchTargets, + slotInClosedTree, + ); + } + + if (parent !== null) { + parent = getParent(parent); + } + + slotInClosedTree = false; + } + + let clearTargetsTupleIndex = -1; + const path = getPath(eventImpl); + for ( + let i = path.length - 1; + i >= 0 && clearTargetsTupleIndex === -1; + i-- + ) { + if (path[i].target !== null) { + clearTargetsTupleIndex = i; + } + } + const clearTargetsTuple = path[clearTargetsTupleIndex]; + + clearTargets = (isNode(clearTargetsTuple.target) && + isShadowRoot(getRoot(clearTargetsTuple.target))) || + (isNode(clearTargetsTuple.relatedTarget) && + isShadowRoot(getRoot(clearTargetsTuple.relatedTarget))); + + setEventPhase(eventImpl, Event.CAPTURING_PHASE); + + for (let i = path.length - 1; i >= 0; --i) { + const tuple = path[i]; + + if (tuple.target === null) { + invokeEventListeners(tuple, eventImpl); + } + } + + for (let i = 0; i < path.length; i++) { + const tuple = path[i]; + + if (tuple.target !== null) { + setEventPhase(eventImpl, Event.AT_TARGET); + } else { + setEventPhase(eventImpl, Event.BUBBLING_PHASE); + } + + if ( + (eventImpl.eventPhase === Event.BUBBLING_PHASE && + eventImpl.bubbles) || + eventImpl.eventPhase === Event.AT_TARGET + ) { + invokeEventListeners(tuple, eventImpl); + } + } + } + + setEventPhase(eventImpl, Event.NONE); + setCurrentTarget(eventImpl, null); + setPath(eventImpl, []); + setDispatched(eventImpl, false); + eventImpl.cancelBubble = false; + setStopImmediatePropagation(eventImpl, false); + + if (clearTargets) { + setTarget(eventImpl, null); + setRelatedTarget(eventImpl, null); + } + + // TODO: invoke activation targets if HTML nodes will be implemented + // if (activationTarget !== null) { + // if (!eventImpl.defaultPrevented) { + // activationTarget._activationBehavior(); + // } + // } + + return !eventImpl.defaultPrevented; + } + + /** Inner invoking of the event listeners where the resolved listeners are + * called. + * + * Ref: https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke */ + function innerInvokeEventListeners( + eventImpl, + targetListeners, + ) { + 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 === Event.CAPTURING_PHASE && !capture) || + (eventImpl.eventPhase === Event.BUBBLING_PHASE && capture) + ) { + continue; + } + + if (once) { + targetListeners[type].splice( + targetListeners[type].indexOf(listener), + 1, + ); + } + + if (passive) { + setInPassiveListener(eventImpl, true); + } + + if (typeof listener.callback === "object") { + if (typeof listener.callback.handleEvent === "function") { + listener.callback.handleEvent(eventImpl); + } + } else { + listener.callback.call(eventImpl.currentTarget, eventImpl); + } + + setInPassiveListener(eventImpl, false); + + if (getStopImmediatePropagation(eventImpl)) { + return found; + } + } + + return found; + } + + /** Invokes the listeners on a given event path with the supplied event. + * + * Ref: https://dom.spec.whatwg.org/#concept-event-listener-invoke */ + function invokeEventListeners(tuple, eventImpl) { + const path = getPath(eventImpl); + const tupleIndex = path.indexOf(tuple); + for (let i = tupleIndex; i >= 0; i--) { + const t = path[i]; + if (t.target) { + setTarget(eventImpl, t.target); + break; + } + } + + setRelatedTarget(eventImpl, tuple.relatedTarget); + + if (eventImpl.cancelBubble) { + return; + } + + setCurrentTarget(eventImpl, tuple.item); + + innerInvokeEventListeners(eventImpl, getListeners(tuple.item)); + } + + function normalizeAddEventHandlerOptions( + options, + ) { + if (typeof options === "boolean" || typeof options === "undefined") { + return { + capture: Boolean(options), + once: false, + passive: false, + }; + } else { + return options; + } + } + + function normalizeEventHandlerOptions( + options, + ) { + if (typeof options === "boolean" || typeof options === "undefined") { + return { + capture: Boolean(options), + }; + } else { + return options; + } + } + + /** Retarget the target following the spec logic. + * + * Ref: https://dom.spec.whatwg.org/#retarget */ + function retarget(a, b) { + while (true) { + if (!isNode(a)) { + return a; + } + + const aRoot = a.getRootNode(); + + if (aRoot) { + if ( + !isShadowRoot(aRoot) || + (isNode(b) && isShadowInclusiveAncestor(aRoot, b)) + ) { + return a; + } + + a = getHost(aRoot); + } + } + } + + // Accessors for non-public data + + const eventTargetData = new WeakMap(); + + function setEventTargetData(value) { + eventTargetData.set(value, getDefaultTargetData()); + } + + function getAssignedSlot(target) { + return Boolean(eventTargetData.get(target)?.assignedSlot); + } + + function getHasActivationBehavior(target) { + return Boolean( + eventTargetData.get(target)?.hasActivationBehavior, + ); + } + + function getHost(target) { + return eventTargetData.get(target)?.host ?? null; + } + + function getListeners(target) { + return eventTargetData.get(target)?.listeners ?? {}; + } + + function getMode(target) { + return eventTargetData.get(target)?.mode ?? null; + } + + function getDefaultTargetData() { + return { + assignedSlot: false, + hasActivationBehavior: false, + host: null, + listeners: Object.create(null), + mode: "", + }; + } + + class EventTarget { + constructor() { + eventTargetData.set(this, getDefaultTargetData()); + } + + addEventListener( + type, + callback, + options, + ) { + requiredArguments("EventTarget.addEventListener", arguments.length, 2); + if (callback === null) { + return; + } + + options = normalizeAddEventHandlerOptions(options); + const { listeners } = eventTargetData.get(this ?? globalThis); + + if (!(type in listeners)) { + listeners[type] = []; + } + + for (const listener of listeners[type]) { + if ( + ((typeof listener.options === "boolean" && + listener.options === options.capture) || + (typeof listener.options === "object" && + listener.options.capture === options.capture)) && + listener.callback === callback + ) { + return; + } + } + + listeners[type].push({ callback, options }); + } + + removeEventListener( + type, + callback, + options, + ) { + requiredArguments("EventTarget.removeEventListener", arguments.length, 2); + + const listeners = eventTargetData.get(this ?? globalThis).listeners; + if (callback !== null && type in listeners) { + listeners[type] = listeners[type].filter( + (listener) => listener.callback !== callback, + ); + } else if (callback === null || !listeners[type]) { + return; + } + + options = normalizeEventHandlerOptions(options); + + for (let i = 0; i < listeners[type].length; ++i) { + const listener = listeners[type][i]; + if ( + ((typeof listener.options === "boolean" && + listener.options === options.capture) || + (typeof listener.options === "object" && + listener.options.capture === options.capture)) && + listener.callback === callback + ) { + listeners[type].splice(i, 1); + break; + } + } + } + + dispatchEvent(event) { + requiredArguments("EventTarget.dispatchEvent", arguments.length, 1); + const self = this ?? globalThis; + + const listeners = eventTargetData.get(self).listeners; + if (!(event.type in listeners)) { + return true; + } + + if (getDispatched(event)) { + throw new DOMException("Invalid event state.", "InvalidStateError"); + } + + if (event.eventPhase !== Event.NONE) { + throw new DOMException("Invalid event state.", "InvalidStateError"); + } + + return dispatch(self, event); + } + + get [Symbol.toStringTag]() { + return "EventTarget"; + } + + getParent(_event) { + return null; + } + } + + defineEnumerableProps(EventTarget, [ + "addEventListener", + "removeEventListener", + "dispatchEvent", + ]); + + class ErrorEvent extends Event { + #message = ""; + #filename = ""; + #lineno = ""; + #colno = ""; + #error = ""; + + get message() { + return this.#message; + } + get filename() { + return this.#filename; + } + get lineno() { + return this.#lineno; + } + get colno() { + return this.#colno; + } + get error() { + return this.#error; + } + + constructor( + type, + { + bubbles, + cancelable, + composed, + message = "", + filename = "", + lineno = 0, + colno = 0, + error = null, + } = {}, + ) { + super(type, { + bubbles: bubbles, + cancelable: cancelable, + composed: composed, + }); + + this.#message = message; + this.#filename = filename; + this.#lineno = lineno; + this.#colno = colno; + this.#error = error; + } + + get [Symbol.toStringTag]() { + return "ErrorEvent"; + } + } + + defineEnumerableProps(ErrorEvent, [ + "message", + "filename", + "lineno", + "colno", + "error", + ]); + + class CustomEvent extends Event { + #detail = null; + + constructor(type, eventInitDict = {}) { + super(type, eventInitDict); + requiredArguments("CustomEvent", arguments.length, 1); + const { detail } = eventInitDict; + this.#detail = detail; + } + + get detail() { + return this.#detail; + } + + get [Symbol.toStringTag]() { + return "CustomEvent"; + } + } + + Reflect.defineProperty(CustomEvent.prototype, "detail", { + enumerable: true, + }); + + window.Event = Event; + window.EventTarget = EventTarget; + window.ErrorEvent = ErrorEvent; + window.CustomEvent = CustomEvent; + window.__bootstrap.eventTarget = { + setEventTargetData, + }; +})(this); |