diff --git a/.eslintrc.json b/.eslintrc.json index 43405e21eb..c6b145d786 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,6 +14,7 @@ "@typescript-eslint/array-type": ["error", "array-simple"], "@typescript-eslint/explicit-member-accessibility": ["off"], "@typescript-eslint/no-non-null-assertion": ["off"], + "@typescript-eslint/no-use-before-define": ["off"], "@typescript-eslint/no-parameter-properties": ["off"], "@typescript-eslint/no-unused-vars": [ "error", diff --git a/cli/main.rs b/cli/main.rs index 50d65c06dd..ec6c466165 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -314,6 +314,7 @@ fn run_script(flags: DenoFlags, argv: Vec) { worker .execute_mod_async(&main_module, false) .and_then(move |()| { + js_check(worker.execute("window.dispatchEvent(new Event('load'))")); worker.then(|result| { js_check(result); Ok(()) diff --git a/js/dom_types.ts b/js/dom_types.ts index 07f7d78984..a7c36df321 100644 --- a/js/dom_types.ts +++ b/js/dom_types.ts @@ -71,11 +71,16 @@ export enum NodeType { DOCUMENT_FRAGMENT_NODE = 11 } +export const eventTargetHost: unique symbol = Symbol(); +export const eventTargetListeners: unique symbol = Symbol(); +export const eventTargetMode: unique symbol = Symbol(); +export const eventTargetNodeType: unique symbol = Symbol(); + export interface EventTarget { - host: EventTarget | null; - listeners: { [type in string]: EventListener[] }; - mode: string; - nodeType: NodeType; + [eventTargetHost]: EventTarget | null; + [eventTargetListeners]: { [type in string]: EventListener[] }; + [eventTargetMode]: string; + [eventTargetNodeType]: NodeType; addEventListener( type: string, callback: (event: Event) => void | null, diff --git a/js/dom_util.ts b/js/dom_util.ts index 2f22a4b515..7a5dc80b94 100644 --- a/js/dom_util.ts +++ b/js/dom_util.ts @@ -9,16 +9,18 @@ export function isNode(nodeImpl: domTypes.EventTarget | null): boolean { export function isShadowRoot(nodeImpl: domTypes.EventTarget | null): boolean { return Boolean( nodeImpl && - nodeImpl.nodeType === domTypes.NodeType.DOCUMENT_FRAGMENT_NODE && - "host" in nodeImpl + nodeImpl[domTypes.eventTargetNodeType] === + domTypes.NodeType.DOCUMENT_FRAGMENT_NODE && + nodeImpl[domTypes.eventTargetHost] != null ); } export function isSlotable(nodeImpl: domTypes.EventTarget | null): boolean { return Boolean( nodeImpl && - (nodeImpl.nodeType === domTypes.NodeType.ELEMENT_NODE || - nodeImpl.nodeType === domTypes.NodeType.TEXT_NODE) + (nodeImpl[domTypes.eventTargetNodeType] === + domTypes.NodeType.ELEMENT_NODE || + nodeImpl[domTypes.eventTargetNodeType] === domTypes.NodeType.TEXT_NODE) ); } @@ -36,7 +38,7 @@ export function isShadowInclusiveAncestor( } if (isShadowRoot(node)) { - node = node && node.host; + node = node && node[domTypes.eventTargetHost]; } else { node = null; // domSymbolTree.parent(node); } @@ -77,7 +79,7 @@ export function retarget( return a; } - a = aRoot.host; + a = aRoot[domTypes.eventTargetHost]; } } } diff --git a/js/event_target.ts b/js/event_target.ts index 0e06dc34ff..e376e78a1c 100644 --- a/js/event_target.ts +++ b/js/event_target.ts @@ -98,13 +98,19 @@ export class EventListener implements domTypes.EventListener { } } +export const eventTargetAssignedSlot: unique symbol = Symbol(); +export const eventTargetHasActivationBehavior: unique symbol = Symbol(); + export class EventTarget implements domTypes.EventTarget { - public host: domTypes.EventTarget | null = null; - public listeners: { [type in string]: domTypes.EventListener[] } = {}; - public mode = ""; - public nodeType: domTypes.NodeType = domTypes.NodeType.DOCUMENT_FRAGMENT_NODE; - private _assignedSlot = false; - private _hasActivationBehavior = false; + 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, @@ -112,7 +118,7 @@ export class EventTarget implements domTypes.EventTarget { options?: domTypes.AddEventListenerOptions | boolean ): void { requiredArguments("EventTarget.addEventListener", arguments.length, 2); - const normalizedOptions: domTypes.AddEventListenerOptions = this._normalizeAddEventHandlerOptions( + const normalizedOptions: domTypes.AddEventListenerOptions = eventTargetHelpers.normalizeAddEventHandlerOptions( options ); @@ -120,12 +126,14 @@ export class EventTarget implements domTypes.EventTarget { return; } - if (!hasOwnProperty(this.listeners, type)) { - this.listeners[type] = []; + const listeners = this[domTypes.eventTargetListeners]; + + if (!hasOwnProperty(listeners, type)) { + listeners[type] = []; } - for (let i = 0; i < this.listeners[type].length; ++i) { - const listener = this.listeners[type][i]; + for (let i = 0; i < listeners[type].length; ++i) { + const listener = listeners[type][i]; if ( ((typeof listener.options === "boolean" && listener.options === normalizedOptions.capture) || @@ -137,7 +145,7 @@ export class EventTarget implements domTypes.EventTarget { } } - this.listeners[type].push(new EventListener(callback, normalizedOptions)); + listeners[type].push(new EventListener(callback, normalizedOptions)); } public removeEventListener( @@ -146,13 +154,14 @@ export class EventTarget implements domTypes.EventTarget { options?: domTypes.EventListenerOptions | boolean ): void { requiredArguments("EventTarget.removeEventListener", arguments.length, 2); - if (hasOwnProperty(this.listeners, type) && callback !== null) { - this.listeners[type] = this.listeners[type].filter( + 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 = this._normalizeEventHandlerOptions( + const normalizedOptions: domTypes.EventListenerOptions = eventTargetHelpers.normalizeEventHandlerOptions( options ); @@ -161,12 +170,12 @@ export class EventTarget implements domTypes.EventTarget { return; } - if (!this.listeners[type]) { + if (!listeners[type]) { return; } - for (let i = 0; i < this.listeners[type].length; ++i) { - const listener = this.listeners[type][i]; + for (let i = 0; i < listeners[type].length; ++i) { + const listener = listeners[type][i]; if ( ((typeof listener.options === "boolean" && @@ -175,7 +184,7 @@ export class EventTarget implements domTypes.EventTarget { listener.options.capture === normalizedOptions.capture)) && listener.callback === callback ) { - this.listeners[type].splice(i, 1); + listeners[type].splice(i, 1); break; } } @@ -183,7 +192,8 @@ export class EventTarget implements domTypes.EventTarget { public dispatchEvent(event: domTypes.Event): boolean { requiredArguments("EventTarget.dispatchEvent", arguments.length, 1); - if (!hasOwnProperty(this.listeners, event.type)) { + const listeners = this[domTypes.eventTargetListeners]; + if (!hasOwnProperty(listeners, event.type)) { return true; } @@ -201,15 +211,21 @@ export class EventTarget implements domTypes.EventTarget { ); } - return this._dispatch(event); + return eventTargetHelpers.dispatch(this, event); } + get [Symbol.toStringTag](): string { + return "EventTarget"; + } +} + +const eventTargetHelpers = { // https://dom.spec.whatwg.org/#concept-event-dispatch - _dispatch( + dispatch( + targetImpl: EventTarget, eventImpl: domTypes.Event, targetOverride?: domTypes.EventTarget ): boolean { - let targetImpl = this; let clearTargets = false; let activationTarget = null; @@ -224,7 +240,7 @@ export class EventTarget implements domTypes.EventTarget { ) { const touchTargets: domTypes.EventTarget[] = []; - this._appendToEventPath( + eventTargetHelpers.appendToEventPath( eventImpl, targetImpl, targetOverride, @@ -235,13 +251,15 @@ export class EventTarget implements domTypes.EventTarget { const isActivationEvent = eventImpl.type === "click"; - if (isActivationEvent && targetImpl._hasActivationBehavior) { + if (isActivationEvent && targetImpl[eventTargetHasActivationBehavior]) { activationTarget = targetImpl; } let slotInClosedTree = false; let slotable = - isSlotable(targetImpl) && targetImpl._assignedSlot ? targetImpl : null; + isSlotable(targetImpl) && targetImpl[eventTargetAssignedSlot] + ? targetImpl + : null; let parent = getEventTargetParent(targetImpl, eventImpl); // Populate event path @@ -254,7 +272,7 @@ export class EventTarget implements domTypes.EventTarget { if ( isShadowRoot(parentRoot) && parentRoot && - parentRoot.mode === "closed" + parentRoot[domTypes.eventTargetMode] === "closed" ) { slotInClosedTree = true; } @@ -266,7 +284,7 @@ export class EventTarget implements domTypes.EventTarget { isNode(parent) && isShadowInclusiveAncestor(getRoot(targetImpl), parent) ) { - this._appendToEventPath( + eventTargetHelpers.appendToEventPath( eventImpl, parent, null, @@ -282,12 +300,12 @@ export class EventTarget implements domTypes.EventTarget { if ( isActivationEvent && activationTarget === null && - targetImpl._hasActivationBehavior + targetImpl[eventTargetHasActivationBehavior] ) { activationTarget = targetImpl; } - this._appendToEventPath( + eventTargetHelpers.appendToEventPath( eventImpl, parent, targetImpl, @@ -328,7 +346,7 @@ export class EventTarget implements domTypes.EventTarget { const tuple = eventImpl.path[i]; if (tuple.target === null) { - this._invokeEventListeners(tuple, eventImpl); + eventTargetHelpers.invokeEventListeners(targetImpl, tuple, eventImpl); } } @@ -346,7 +364,7 @@ export class EventTarget implements domTypes.EventTarget { eventImpl.bubbles) || eventImpl.eventPhase === domTypes.EventPhase.AT_TARGET ) { - this._invokeEventListeners(tuple, eventImpl); + eventTargetHelpers.invokeEventListeners(targetImpl, tuple, eventImpl); } } } @@ -372,10 +390,11 @@ export class EventTarget implements domTypes.EventTarget { // } return !eventImpl.defaultPrevented; - } + }, // https://dom.spec.whatwg.org/#concept-event-listener-invoke - _invokeEventListeners( + invokeEventListeners( + targetImpl: EventTarget, tuple: domTypes.EventPath, eventImpl: domTypes.Event ): void { @@ -396,11 +415,16 @@ export class EventTarget implements domTypes.EventTarget { eventImpl.currentTarget = tuple.item; - this._innerInvokeEventListeners(eventImpl, tuple.item.listeners); - } + eventTargetHelpers.innerInvokeEventListeners( + targetImpl, + eventImpl, + tuple.item[domTypes.eventTargetListeners] + ); + }, // https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke - _innerInvokeEventListeners( + innerInvokeEventListeners( + targetImpl: EventTarget, eventImpl: domTypes.Event, targetListeners: { [type in string]: domTypes.EventListener[] } ): boolean { @@ -471,9 +495,9 @@ export class EventTarget implements domTypes.EventTarget { } return found; - } + }, - _normalizeAddEventHandlerOptions( + normalizeAddEventHandlerOptions( options: boolean | domTypes.AddEventListenerOptions | undefined ): domTypes.AddEventListenerOptions { if (typeof options === "boolean" || typeof options === "undefined") { @@ -487,9 +511,9 @@ export class EventTarget implements domTypes.EventTarget { } else { return options; } - } + }, - _normalizeEventHandlerOptions( + normalizeEventHandlerOptions( options: boolean | domTypes.EventListenerOptions | undefined ): domTypes.EventListenerOptions { if (typeof options === "boolean" || typeof options === "undefined") { @@ -501,10 +525,10 @@ export class EventTarget implements domTypes.EventTarget { } else { return options; } - } + }, // https://dom.spec.whatwg.org/#concept-event-path-append - _appendToEventPath( + appendToEventPath( eventImpl: domTypes.Event, target: domTypes.EventTarget, targetOverride: domTypes.EventTarget | null, @@ -513,7 +537,8 @@ export class EventTarget implements domTypes.EventTarget { slotInClosedTree: boolean ): void { const itemInShadowTree = isNode(target) && isShadowRoot(getRoot(target)); - const rootOfClosedTree = isShadowRoot(target) && target.mode === "closed"; + const rootOfClosedTree = + isShadowRoot(target) && target[domTypes.eventTargetMode] === "closed"; eventImpl.path.push({ item: target, @@ -525,31 +550,11 @@ export class EventTarget implements domTypes.EventTarget { slotInClosedTree }); } - - get [Symbol.toStringTag](): string { - return "EventTarget"; - } -} +}; /** Built-in objects providing `get` methods for our * interceptable JavaScript operations. */ -Reflect.defineProperty(EventTarget.prototype, "host", { - enumerable: true, - writable: true -}); -Reflect.defineProperty(EventTarget.prototype, "listeners", { - enumerable: true, - writable: true -}); -Reflect.defineProperty(EventTarget.prototype, "mode", { - enumerable: true, - writable: true -}); -Reflect.defineProperty(EventTarget.prototype, "nodeType", { - enumerable: true, - writable: true -}); Reflect.defineProperty(EventTarget.prototype, "addEventListener", { enumerable: true }); diff --git a/js/globals.ts b/js/globals.ts index 1d33f65233..3be55e9794 100644 --- a/js/globals.ts +++ b/js/globals.ts @@ -69,6 +69,7 @@ window.console = console; window.setTimeout = timers.setTimeout; window.setInterval = timers.setInterval; window.location = (undefined as unknown) as domTypes.Location; +window.onload = undefined as undefined | Function; // The following Crypto interface implementation is not up to par with the // standard https://www.w3.org/TR/WebCryptoAPI/#crypto-interface as it does not // yet incorporate the SubtleCrypto interface as its "subtle" property. @@ -135,6 +136,28 @@ window.postMessage = workers.postMessage; window.Worker = workers.WorkerImpl; export type Worker = workers.Worker; +window[domTypes.eventTargetHost] = null; +window[domTypes.eventTargetListeners] = {}; +window[domTypes.eventTargetMode] = ""; +window[domTypes.eventTargetNodeType] = 0; +window[eventTarget.eventTargetAssignedSlot] = false; +window[eventTarget.eventTargetHasActivationBehavior] = false; +window.addEventListener = eventTarget.EventTarget.prototype.addEventListener; +window.dispatchEvent = eventTarget.EventTarget.prototype.dispatchEvent; +window.removeEventListener = + eventTarget.EventTarget.prototype.removeEventListener; + +// Registers the handler for window.onload function. +window.addEventListener( + "load", + (e: domTypes.Event): void => { + const onload = window.onload; + if (typeof onload === "function") { + onload(e); + } + } +); + // below are interfaces that are available in TypeScript but // have different signatures export interface ImportMeta { diff --git a/js/window.ts b/js/window.ts index 6773ea5b24..3d3d6601f7 100644 --- a/js/window.ts +++ b/js/window.ts @@ -1,5 +1,4 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. - // (0, eval) is indirect eval. // See the links below for details: // - https://stackoverflow.com/a/14120023 diff --git a/tests/034_onload.out b/tests/034_onload.out new file mode 100644 index 0000000000..0939be8cd1 --- /dev/null +++ b/tests/034_onload.out @@ -0,0 +1,7 @@ +log from nest_imported script +log from imported script +log from main +got load event in onload function +got load event in event handler (nest_imported) +got load event in event handler (imported) +got load event in event handler (main) diff --git a/tests/034_onload.test b/tests/034_onload.test new file mode 100644 index 0000000000..d3105656f6 --- /dev/null +++ b/tests/034_onload.test @@ -0,0 +1,2 @@ +args: run --reload tests/034_onload/main.ts +output: tests/034_onload.out diff --git a/tests/034_onload/imported.ts b/tests/034_onload/imported.ts new file mode 100644 index 0000000000..5cf2d7b4cc --- /dev/null +++ b/tests/034_onload/imported.ts @@ -0,0 +1,8 @@ +import "./nest_imported.ts"; +window.addEventListener( + "load", + (e: Event): void => { + console.log(`got ${e.type} event in event handler (imported)`); + } +); +console.log("log from imported script"); diff --git a/tests/034_onload/main.ts b/tests/034_onload/main.ts new file mode 100644 index 0000000000..68851950a1 --- /dev/null +++ b/tests/034_onload/main.ts @@ -0,0 +1,14 @@ +import "./imported.ts"; + +window.addEventListener( + "load", + (e: Event): void => { + console.log(`got ${e.type} event in event handler (main)`); + } +); + +window.onload = (e: Event): void => { + console.log(`got ${e.type} event in onload function`); +}; + +console.log("log from main"); diff --git a/tests/034_onload/nest_imported.ts b/tests/034_onload/nest_imported.ts new file mode 100644 index 0000000000..2e2bee1d5d --- /dev/null +++ b/tests/034_onload/nest_imported.ts @@ -0,0 +1,7 @@ +window.addEventListener( + "load", + (e: Event): void => { + console.log(`got ${e.type} event in event handler (nest_imported)`); + } +); +console.log("log from nest_imported script"); diff --git a/tests/034_onload_imported.ts b/tests/034_onload_imported.ts new file mode 100644 index 0000000000..d97aabeca7 --- /dev/null +++ b/tests/034_onload_imported.ts @@ -0,0 +1 @@ +console.log("from imported script"); diff --git a/website/manual.md b/website/manual.md index 4025e5acca..0424a2d258 100644 --- a/website/manual.md +++ b/website/manual.md @@ -425,7 +425,7 @@ $ deno run --allow-net=deno.land allow-net-whitelist-example.ts Example: ```ts -async function main() { +window.onload = async function() { // create subprocess const p = Deno.run({ args: ["echo", "hello"] @@ -433,9 +433,7 @@ async function main() { // await its completion await p.status(); -} - -main(); +}; ``` Run it: @@ -445,12 +443,17 @@ $ deno run --allow-run ./subprocess_simple.ts hello ``` +Here a function is assigned to `window.onload`. This function is called after +the main script is loaded. This is the same as +[onload](https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onload) +of the browsers, and it can be used as the main entrypoint. + By default when you use `Deno.run()` subprocess inherits `stdin`, `stdout` and `stderr` of parent process. If you want to communicate with started subprocess you can use `"piped"` option. ```ts -async function main() { +window.onload = async function() { const decoder = new TextDecoder(); const fileNames = Deno.args.slice(1); @@ -479,9 +482,7 @@ async function main() { } Deno.exit(code); -} - -main(); +}; ``` When you run it: @@ -833,14 +834,12 @@ Example: import { serve } from "http/server.ts"; -async function main() { +window.onload = async function() { const body = new TextEncoder().encode("Hello World\n"); for await (const req of serve(":8000")) { req.respond({ body }); } -} - -main(); +}; ``` ```shell