From 95b4bfc253c2f393476cf25ba752dc442a94757e Mon Sep 17 00:00:00 2001 From: Fabio Massaioli Date: Tue, 25 Feb 2025 03:30:30 +0100 Subject: [PATCH] Migrate runtime to TypeScript --- .../desktop/@wailsio/runtime/.npmignore | 5 - .../desktop/@wailsio/runtime/package.json | 49 +- .../src/{application.js => application.ts} | 19 +- .../desktop/@wailsio/runtime/src/browser.js | 24 - .../desktop/@wailsio/runtime/src/browser.ts | 24 + .../desktop/@wailsio/runtime/src/callable.ts | 125 +++ .../desktop/@wailsio/runtime/src/calls.js | 221 ----- .../desktop/@wailsio/runtime/src/calls.ts | 233 +++++ .../@wailsio/runtime/src/cancellable.ts | 923 ++++++++++++++++++ .../desktop/@wailsio/runtime/src/clipboard.js | 35 - .../desktop/@wailsio/runtime/src/clipboard.ts | 35 + .../src/{contextmenu.js => contextmenu.ts} | 65 +- .../runtime/src/{create.js => create.ts} | 40 +- .../desktop/@wailsio/runtime/src/dialogs.js | 200 ---- .../desktop/@wailsio/runtime/src/dialogs.ts | 255 +++++ .../desktop/@wailsio/runtime/src/drag.js | 119 --- .../desktop/@wailsio/runtime/src/drag.ts | 209 ++++ .../src/{event_types.js => event_types.ts} | 32 +- .../desktop/@wailsio/runtime/src/events.js | 140 --- .../desktop/@wailsio/runtime/src/events.ts | 135 +++ .../desktop/@wailsio/runtime/src/flags.js | 25 - .../desktop/@wailsio/runtime/src/flags.ts | 23 + .../desktop/@wailsio/runtime/src/global.d.ts | 17 + .../desktop/@wailsio/runtime/src/index.js | 60 -- .../desktop/@wailsio/runtime/src/index.ts | 57 ++ .../desktop/@wailsio/runtime/src/listener.ts | 52 + .../runtime/src/{nanoid.js => nanoid.ts} | 6 +- .../desktop/@wailsio/runtime/src/runtime.js | 92 -- .../desktop/@wailsio/runtime/src/runtime.ts | 67 ++ .../desktop/@wailsio/runtime/src/screens.js | 71 -- .../desktop/@wailsio/runtime/src/screens.ts | 88 ++ .../desktop/@wailsio/runtime/src/system.js | 143 --- .../desktop/@wailsio/runtime/src/system.ts | 156 +++ .../runtime/src/{utils.js => utils.ts} | 22 +- .../desktop/@wailsio/runtime/src/window.js | 638 ------------ .../desktop/@wailsio/runtime/src/window.ts | 520 ++++++++++ .../desktop/@wailsio/runtime/src/wml.js | 250 ----- .../desktop/@wailsio/runtime/src/wml.ts | 209 ++++ .../desktop/@wailsio/runtime/tsconfig.json | 40 +- v3/tasks/events/generate.go | 124 +-- 40 files changed, 3316 insertions(+), 2232 deletions(-) delete mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/.npmignore rename v3/internal/runtime/desktop/@wailsio/runtime/src/{application.js => application.ts} (61%) delete mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/browser.js create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/browser.ts create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/callable.ts delete mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/calls.js create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/calls.ts create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.ts delete mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/clipboard.js create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/clipboard.ts rename v3/internal/runtime/desktop/@wailsio/runtime/src/{contextmenu.js => contextmenu.ts} (55%) rename v3/internal/runtime/desktop/@wailsio/runtime/src/{create.js => create.ts} (69%) delete mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/dialogs.js create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/dialogs.ts delete mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/drag.js create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/drag.ts rename v3/internal/runtime/desktop/@wailsio/runtime/src/{event_types.js => event_types.ts} (96%) delete mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/events.js create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/events.ts delete mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/flags.js create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/flags.ts create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/global.d.ts delete mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/index.js create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/listener.ts rename v3/internal/runtime/desktop/@wailsio/runtime/src/{nanoid.js => nanoid.ts} (96%) delete mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.js create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.ts delete mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/screens.js create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/screens.ts delete mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/system.js create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/system.ts rename v3/internal/runtime/desktop/@wailsio/runtime/src/{utils.js => utils.ts} (85%) delete mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/window.js create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/window.ts delete mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/wml.js create mode 100644 v3/internal/runtime/desktop/@wailsio/runtime/src/wml.ts diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/.npmignore b/v3/internal/runtime/desktop/@wailsio/runtime/.npmignore deleted file mode 100644 index a77a7c831..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/.npmignore +++ /dev/null @@ -1,5 +0,0 @@ -events.test.js -node_modules -types/drag.d.ts -types/contextmenu.d.ts -types/log.d.ts \ No newline at end of file diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/package.json b/v3/internal/runtime/desktop/@wailsio/runtime/package.json index 4c3544130..3f24339c3 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/package.json +++ b/v3/internal/runtime/desktop/@wailsio/runtime/package.json @@ -3,36 +3,47 @@ "type": "module", "version": "3.0.0-alpha.56", "description": "Wails Runtime", - "types": "types/index.d.ts", "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./src/index.js", - "import": "./src/index.js" - } + "types": "./types/index.d.ts", + "import": "./dist/index.js" }, "repository": { "type": "git", - "url": "git+https://github.com/wailsapp/wails.git" - }, - "scripts": { - "prebuild:types": "rimraf ./types", - "build:types": "npx -p typescript tsc src/index.js --declaration --allowJs --emitDeclarationOnly --outDir types", - "postbuild:types": "task generate:events", - "build:docs": "npx typedoc ./src/index.js", - "build:docs:md": "npx typedoc ./src/index.js" + "url": "git+https://github.com/wailsapp/wails.git", + "directory": "v3/internal/runtime/desktop/@wailsio/runtime" }, "author": "The Wails Team", "license": "MIT", + "homepage": "https://v3.wails.io", "bugs": { "url": "https://github.com/wailsapp/wails/issues" }, - "homepage": "https://wails.io", - "private": false, + "files": [ + "./dist", + "./types" + ], + "sideEffects": [ + "./dist/index.js", + "./dist/contextmenu.js", + "./dist/drag.js" + ], + "scripts": { + "clean": "npx rimraf ./dist ./docs ./types ./tsconfig.tsbuildinfo", + "generate:events": "task generate:events", + "generate": "npm run generate:events", + "prebuild": "npm run clean && npm run generate", + "build:code": "npx tsc", + "build:docs": "npx typedoc --plugin typedoc-plugin-mdn-links --plugin typedoc-plugin-missing-exports ./src/index.ts", + "build:docs:md": "npx typedoc --plugin typedoc-plugin-markdown --plugin typedoc-plugin-mdn-links --plugin typedoc-plugin-missing-exports ./src/index.ts", + "build": "npm run build:code & npm run build:docs & wait", + "prepack": "npm run build" + }, "devDependencies": { "rimraf": "^5.0.5", - "typedoc": "^0.25.7", - "typedoc-plugin-markdown": "^3.17.1", - "typescript": "^5.3.3" + "typedoc": "^0.27.7", + "typedoc-plugin-markdown": "^4.4.2", + "typedoc-plugin-mdn-links": "^4.0.13", + "typedoc-plugin-missing-exports": "^3.1.0", + "typescript": "^5.7.3", } } diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/application.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/application.ts similarity index 61% rename from v3/internal/runtime/desktop/@wailsio/runtime/src/application.js rename to v3/internal/runtime/desktop/@wailsio/runtime/src/application.ts index 8f650e287..57a41ac9e 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/application.js +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/application.ts @@ -8,10 +8,8 @@ The electron alternative for Go (c) Lea Anthony 2019-present */ -/* jshint esversion: 9 */ - -import { newRuntimeCallerWithID, objectNames } from "./runtime"; -const call = newRuntimeCallerWithID(objectNames.Application, ''); +import { newRuntimeCaller, objectNames } from "./runtime.js"; +const call = newRuntimeCaller(objectNames.Application); const HideMethod = 0; const ShowMethod = 1; @@ -19,28 +17,21 @@ const QuitMethod = 2; /** * Hides a certain method by calling the HideMethod function. - * - * @return {Promise} - * */ -export function Hide() { +export function Hide(): Promise { return call(HideMethod); } /** * Calls the ShowMethod and returns the result. - * - * @return {Promise} */ -export function Show() { +export function Show(): Promise { return call(ShowMethod); } /** * Calls the QuitMethod to terminate the program. - * - * @return {Promise} */ -export function Quit() { +export function Quit(): Promise { return call(QuitMethod); } diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/browser.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/browser.js deleted file mode 100644 index 64d80c986..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/browser.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ -import {newRuntimeCallerWithID, objectNames} from "./runtime"; - -const call = newRuntimeCallerWithID(objectNames.Browser, ''); -const BrowserOpenURL = 0; - -/** - * Open a browser window to the given URL - * @param {string} url - The URL to open - * @returns {Promise} - */ -export function OpenURL(url) { - return call(BrowserOpenURL, {url}); -} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/browser.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/browser.ts new file mode 100644 index 000000000..465310d3d --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/browser.ts @@ -0,0 +1,24 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import { newRuntimeCaller, objectNames } from "./runtime.js"; + +const call = newRuntimeCaller(objectNames.Browser); + +const BrowserOpenURL = 0; + +/** + * Open a browser window to the given URL. + * + * @param url - The URL to open + */ +export function OpenURL(url: string | URL): Promise { + return call(BrowserOpenURL, {url: url.toString()}); +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/callable.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/callable.ts new file mode 100644 index 000000000..e8e2e4087 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/callable.ts @@ -0,0 +1,125 @@ +// Source: https://github.com/inspect-js/is-callable + +// The MIT License (MIT) +// +// Copyright (c) 2015 Jordan Harband +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +var fnToStr = Function.prototype.toString; +var reflectApply: typeof Reflect.apply | false | null = typeof Reflect === 'object' && Reflect !== null && Reflect.apply; +var badArrayLike: any; +var isCallableMarker: any; +if (typeof reflectApply === 'function' && typeof Object.defineProperty === 'function') { + try { + badArrayLike = Object.defineProperty({}, 'length', { + get: function () { + throw isCallableMarker; + } + }); + isCallableMarker = {}; + // eslint-disable-next-line no-throw-literal + reflectApply(function () { throw 42; }, null, badArrayLike); + } catch (_) { + if (_ !== isCallableMarker) { + reflectApply = null; + } + } +} else { + reflectApply = null; +} + +var constructorRegex = /^\s*class\b/; +var isES6ClassFn = function isES6ClassFunction(value: any): boolean { + try { + var fnStr = fnToStr.call(value); + return constructorRegex.test(fnStr); + } catch (e) { + return false; // not a function + } +}; + +var tryFunctionObject = function tryFunctionToStr(value: any): boolean { + try { + if (isES6ClassFn(value)) { return false; } + fnToStr.call(value); + return true; + } catch (e) { + return false; + } +}; +var toStr = Object.prototype.toString; +var objectClass = '[object Object]'; +var fnClass = '[object Function]'; +var genClass = '[object GeneratorFunction]'; +var ddaClass = '[object HTMLAllCollection]'; // IE 11 +var ddaClass2 = '[object HTML document.all class]'; +var ddaClass3 = '[object HTMLCollection]'; // IE 9-10 +var hasToStringTag = typeof Symbol === 'function' && !!Symbol.toStringTag; // better: use `has-tostringtag` + +var isIE68 = !(0 in [,]); // eslint-disable-line no-sparse-arrays, comma-spacing + +var isDDA: (value: any) => boolean = function isDocumentDotAll() { return false; }; +if (typeof document === 'object') { + // Firefox 3 canonicalizes DDA to undefined when it's not accessed directly + var all = document.all; + if (toStr.call(all) === toStr.call(document.all)) { + isDDA = function isDocumentDotAll(value) { + /* globals document: false */ + // in IE 6-8, typeof document.all is "object" and it's truthy + if ((isIE68 || !value) && (typeof value === 'undefined' || typeof value === 'object')) { + try { + var str = toStr.call(value); + return ( + str === ddaClass + || str === ddaClass2 + || str === ddaClass3 // opera 12.16 + || str === objectClass // IE 6-8 + ) && value('') == null; // eslint-disable-line eqeqeq + } catch (e) { /**/ } + } + return false; + }; + } +} + +function isCallableRefApply(value: T | unknown): value is (...args: any[]) => any { + if (isDDA(value)) { return true; } + if (!value) { return false; } + if (typeof value !== 'function' && typeof value !== 'object') { return false; } + try { + (reflectApply as any)(value, null, badArrayLike); + } catch (e) { + if (e !== isCallableMarker) { return false; } + } + return !isES6ClassFn(value) && tryFunctionObject(value); +} + +function isCallableNoRefApply(value: T | unknown): value is (...args: any[]) => any { + if (isDDA(value)) { return true; } + if (!value) { return false; } + if (typeof value !== 'function' && typeof value !== 'object') { return false; } + if (hasToStringTag) { return tryFunctionObject(value); } + if (isES6ClassFn(value)) { return false; } + var strClass = toStr.call(value); + if (strClass !== fnClass && strClass !== genClass && !(/^\[object HTML/).test(strClass)) { return false; } + return tryFunctionObject(value); +}; + +export default reflectApply ? isCallableRefApply : isCallableNoRefApply; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/calls.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/calls.js deleted file mode 100644 index dad53bb02..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/calls.js +++ /dev/null @@ -1,221 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ -import { newRuntimeCallerWithID, objectNames } from "./runtime"; -import { nanoid } from './nanoid.js'; - -// Setup -window._wails = window._wails || {}; -window._wails.callResultHandler = resultHandler; -window._wails.callErrorHandler = errorHandler; - - -const CallBinding = 0; -const call = newRuntimeCallerWithID(objectNames.Call, ''); -const cancelCall = newRuntimeCallerWithID(objectNames.CancelCall, ''); -let callResponses = new Map(); - -/** - * Generates a unique ID using the nanoid library. - * - * @return {string} - A unique ID that does not exist in the callResponses set. - */ -function generateID() { - let result; - do { - result = nanoid(); - } while (callResponses.has(result)); - return result; -} - -/** - * Handles the result of a call request. - * - * @param {string} id - The id of the request to handle the result for. - * @param {string} data - The result data of the request. - * @param {boolean} isJSON - Indicates whether the data is JSON or not. - * - * @return {undefined} - This method does not return any value. - */ -function resultHandler(id, data, isJSON) { - const promiseHandler = getAndDeleteResponse(id); - if (promiseHandler) { - if (!data) { - promiseHandler.resolve(); - } else if (!isJSON) { - promiseHandler.resolve(data); - } else { - try { - promiseHandler.resolve(JSON.parse(data)); - } catch (err) { - promiseHandler.reject(new TypeError("could not parse result: " + err.message, { cause: err })); - } - } - } -} - -/** - * Handles the error from a call request. - * - * @param {string} id - The id of the promise handler. - * @param {string} data - The error data to reject the promise handler with. - * @param {boolean} isJSON - Indicates whether the data is JSON or not. - * - * @return {void} - */ -function errorHandler(id, data, isJSON) { - const promiseHandler = getAndDeleteResponse(id); - if (promiseHandler) { - if (!isJSON) { - promiseHandler.reject(new Error(data)); - } else { - let error; - try { - error = JSON.parse(data); - } catch (err) { - promiseHandler.reject(new TypeError("could not parse error: " + err.message, { cause: err })); - return; - } - - let options = {}; - if (error.cause) { - options.cause = error.cause; - } - - let exception; - switch (error.kind) { - case "ReferenceError": - exception = new ReferenceError(error.message, options); - break; - case "TypeError": - exception = new TypeError(error.message, options); - break; - case "RuntimeError": - exception = new RuntimeError(error.message, options); - break; - default: - exception = new Error(error.message, options); - break; - } - - promiseHandler.reject(exception); - } - } -} - -/** - * Retrieves and removes the response associated with the given ID from the callResponses map. - * - * @param {any} id - The ID of the response to be retrieved and removed. - * - * @returns {any} The response object associated with the given ID. - */ -function getAndDeleteResponse(id) { - const response = callResponses.get(id); - callResponses.delete(id); - return response; -} - -/** - * Collects all required information for a binding call. - * - * @typedef {Object} CallOptions - * @property {number} [methodID] - The numeric ID of the bound method to call. - * @property {string} [methodName] - The fully qualified name of the bound method to call. - * @property {any[]} args - Arguments to be passed into the bound method. - */ - -/** - * Exception class that will be thrown in case the bound method returns an error. - * The value of the {@link RuntimeError#name} property is "RuntimeError". - */ -export class RuntimeError extends Error { - /** - * Constructs a new RuntimeError instance. - * - * @param {string} message - The error message. - * @param {any[]} args - Optional arguments for the Error constructor. - */ - constructor(message, ...args) { - super(message, ...args); - this.name = "RuntimeError"; - } -} - -/** - * Call a bound method according to the given call options. - * - * In case of failure, the returned promise will reject with an exception - * among ReferenceError (unknown method), TypeError (wrong argument count or type), - * {@link RuntimeError} (method returned an error), or other (network or internal errors). - * The exception might have a "cause" field with the value returned - * by the application- or service-level error marshaling functions. - * - * @param {CallOptions} options - A method call descriptor. - * @returns {Promise} - The result of the call. - */ -export function Call(options) { - const id = generateID(); - const doCancel = () => { return cancelCall(type, {"call-id": id}) }; - let queuedCancel = false, callRunning = false; - let p = new Promise((resolve, reject) => { - options["call-id"] = id; - callResponses.set(id, { resolve, reject }); - call(CallBinding, options).then((_) => { - callRunning = true; - if (queuedCancel) { - return doCancel(); - } - }).catch((error) => { - reject(error); - callResponses.delete(id); - }); - }); - p.cancel = () => { - if (callRunning) { - return doCancel(); - } else { - queuedCancel = true; - } - }; - - return p; -} - -/** - * Calls a bound method by name with the specified arguments. - * See {@link Call} for details. - * - * @param {string} methodName - The name of the method in the format 'package.struct.method'. - * @param {any[]} args - The arguments to pass to the method. - * @returns {Promise} The result of the method call. - */ -export function ByName(methodName, ...args) { - return Call({ - methodName, - args - }); -} - -/** - * Calls a method by its numeric ID with the specified arguments. - * See {@link Call} for details. - * - * @param {number} methodID - The ID of the method to call. - * @param {any[]} args - The arguments to pass to the method. - * @return {Promise} - The result of the method call. - */ -export function ByID(methodID, ...args) { - return Call({ - methodID, - args - }); -} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/calls.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/calls.ts new file mode 100644 index 000000000..fce698bf4 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/calls.ts @@ -0,0 +1,233 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import { CancellablePromise, type CancellablePromiseWithResolvers } from "./cancellable.js"; +import { newRuntimeCaller, objectNames } from "./runtime.js"; +import { nanoid } from "./nanoid.js"; + +// Setup +window._wails = window._wails || {}; +window._wails.callResultHandler = resultHandler; +window._wails.callErrorHandler = errorHandler; + +type PromiseResolvers = Omit, "promise" | "oncancelled"> + +const call = newRuntimeCaller(objectNames.Call); +const cancelCall = newRuntimeCaller(objectNames.CancelCall); +const callResponses = new Map(); + +const CallBinding = 0; +const CancelMethod = 0 + +/** + * Holds all required information for a binding call. + * May provide either a method ID or a method name, but not both. + */ +export type CallOptions = { + /** The numeric ID of the bound method to call. */ + methodID: number; + /** The fully qualified name of the bound method to call. */ + methodName?: never; + /** Arguments to be passed into the bound method. */ + args: any[]; +} | { + /** The numeric ID of the bound method to call. */ + methodID?: never; + /** The fully qualified name of the bound method to call. */ + methodName: string; + /** Arguments to be passed into the bound method. */ + args: any[]; +}; + +/** + * Exception class that will be thrown in case the bound method returns an error. + * The value of the {@link RuntimeError#name} property is "RuntimeError". + */ +export class RuntimeError extends Error { + /** + * Constructs a new RuntimeError instance. + * @param message - The error message. + * @param options - Options to be forwarded to the Error constructor. + */ + constructor(message?: string, options?: ErrorOptions) { + super(message, options); + this.name = "RuntimeError"; + } +} + +/** + * Handles the result of a call request. + * + * @param id - The id of the request to handle the result for. + * @param data - The result data of the request. + * @param isJSON - Indicates whether the data is JSON or not. + */ +function resultHandler(id: string, data: string, isJSON: boolean): void { + const resolvers = getAndDeleteResponse(id); + if (!resolvers) { + return; + } + + if (!data) { + resolvers.resolve(undefined); + } else if (!isJSON) { + resolvers.resolve(data); + } else { + try { + resolvers.resolve(JSON.parse(data)); + } catch (err: any) { + resolvers.reject(new TypeError("could not parse result: " + err.message, { cause: err })); + } + } +} + +/** + * Handles the error from a call request. + * + * @param id - The id of the promise handler. + * @param data - The error data to reject the promise handler with. + * @param isJSON - Indicates whether the data is JSON or not. + */ +function errorHandler(id: string, data: string, isJSON: boolean): void { + const resolvers = getAndDeleteResponse(id); + if (!resolvers) { + return; + } + + if (!isJSON) { + resolvers.reject(new Error(data)); + } else { + let error: any; + try { + error = JSON.parse(data); + } catch (err: any) { + resolvers.reject(new TypeError("could not parse error: " + err.message, { cause: err })); + return; + } + + let options: ErrorOptions = {}; + if (error.cause) { + options.cause = error.cause; + } + + let exception; + switch (error.kind) { + case "ReferenceError": + exception = new ReferenceError(error.message, options); + break; + case "TypeError": + exception = new TypeError(error.message, options); + break; + case "RuntimeError": + exception = new RuntimeError(error.message, options); + break; + default: + exception = new Error(error.message, options); + break; + } + + resolvers.reject(exception); + } +} + +/** + * Retrieves and removes the response associated with the given ID from the callResponses map. + * + * @param id - The ID of the response to be retrieved and removed. + * @returns The response object associated with the given ID, if any. + */ +function getAndDeleteResponse(id: string): PromiseResolvers | undefined { + const response = callResponses.get(id); + callResponses.delete(id); + return response; +} + +/** + * Generates a unique ID using the nanoid library. + * + * @returns A unique ID that does not exist in the callResponses set. + */ +function generateID(): string { + let result; + do { + result = nanoid(); + } while (callResponses.has(result)); + return result; +} + +/** + * Call a bound method according to the given call options. + * + * In case of failure, the returned promise will reject with an exception + * among ReferenceError (unknown method), TypeError (wrong argument count or type), + * {@link RuntimeError} (method returned an error), or other (network or internal errors). + * The exception might have a "cause" field with the value returned + * by the application- or service-level error marshaling functions. + * + * @param options - A method call descriptor. + * @returns The result of the call. + */ +export function Call(options: CallOptions): CancellablePromise { + const id = generateID(); + + const result = CancellablePromise.withResolvers(); + callResponses.set(id, { resolve: result.resolve, reject: result.reject }); + + const request = call(CallBinding, Object.assign({ "call-id": id }, options)); + let running = false; + + request.then(() => { + running = true; + }, (err) => { + callResponses.delete(id); + result.reject(err); + }); + + const cancel = () => { + callResponses.delete(id); + return cancelCall(CancelMethod, {"call-id": id}).catch((err) => { + console.log("Error while requesting binding call cancellation:", err); + }); + }; + + result.oncancelled = () => { + if (running) { + return cancel(); + } else { + return request.then(cancel); + } + }; + + return result.promise; +} + +/** + * Calls a bound method by name with the specified arguments. + * See {@link Call} for details. + * + * @param methodName - The name of the method in the format 'package.struct.method'. + * @param args - The arguments to pass to the method. + * @returns The result of the method call. + */ +export function ByName(methodName: string, ...args: any[]): CancellablePromise { + return Call({ methodName, args }); +} + +/** + * Calls a method by its numeric ID with the specified arguments. + * See {@link Call} for details. + * + * @param methodID - The ID of the method to call. + * @param args - The arguments to pass to the method. + * @return The result of the method call. + */ +export function ByID(methodID: number, ...args: any[]): CancellablePromise { + return Call({ methodID, args }); +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.ts new file mode 100644 index 000000000..8406b78b3 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/cancellable.ts @@ -0,0 +1,923 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import isCallable from "./callable.js"; + +/** + * Exception class that will be used as rejection reason + * in case a {@link CancellablePromise} is cancelled successfully. + * + * The value of the {@link name} property is the string `"CancelError"`. + * The value of the {@link cause} property is the cause passed to the cancel method, if any. + */ +export class CancelError extends Error { + /** + * Constructs a new `CancelError` instance. + * @param message - The error message. + * @param options - Options to be forwarded to the Error constructor. + */ + constructor(message?: string, options?: ErrorOptions) { + super(message, options); + this.name = "CancelError"; + } +} + +/** + * Exception class that will be reported as an unhandled rejection + * in case a {@link CancellablePromise} rejects after being cancelled, + * or when the `oncancelled` callback throws or rejects. + * + * The value of the {@link name} property is the string `"CancelledRejectionError"`. + * The value of the {@link cause} property is the reason the promise rejected with. + * + * Because the original promise was cancelled, + * a wrapper promise will be passed to the unhandled rejection listener instead. + * The {@link promise} property holds a reference to the original promise. + */ +export class CancelledRejectionError extends Error { + /** + * Holds a reference to the promise that was cancelled and then rejected. + */ + promise: CancellablePromise; + + /** + * Constructs a new `CancelledRejectionError` instance. + * @param promise - The promise that caused the error originally. + * @param reason - The rejection reason. + * @param info - An optional informative message specifying the circumstances in which the error was thrown. + * Defaults to the string `"Unhandled rejection in cancelled promise."`. + */ + constructor(promise: CancellablePromise, reason?: any, info?: string) { + super((info ?? "Unhandled rejection in cancelled promise.") + " Reason: " + errorMessage(reason), { cause: reason }); + this.promise = promise; + this.name = "CancelledRejectionError"; + } +} + +type CancellablePromiseResolver = (value: T | PromiseLike | CancellablePromiseLike) => void; +type CancellablePromiseRejector = (reason?: any) => void; +type CancellablePromiseCanceller = (cause?: any) => void | PromiseLike; +type CancellablePromiseExecutor = (resolve: CancellablePromiseResolver, reject: CancellablePromiseRejector) => void; + +export interface CancellablePromiseLike { + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike | CancellablePromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike | CancellablePromiseLike) | undefined | null): CancellablePromiseLike; + cancel(cause?: any): void; +} + +/** + * Wraps a cancellable promise along with its resolution methods. + * The `oncancelled` field will be null initially but may be set to provide a custom cancellation function. + */ +export interface CancellablePromiseWithResolvers { + promise: CancellablePromise; + resolve: CancellablePromiseResolver; + reject: CancellablePromiseRejector; + oncancelled: CancellablePromiseCanceller | null; +} + +interface CancellablePromiseState { + readonly root: CancellablePromiseState; + resolving: boolean; + settled: boolean; + reason?: CancelError; +} + +// Private field names. +const barrierSym = Symbol("barrier"); +const cancelImplSym = Symbol("cancelImpl"); +const species = Symbol.species ?? Symbol("speciesPolyfill"); + +/** + * A promise with an attached method for cancelling long-running operations (see {@link CancellablePromise#cancel}). + * Cancellation can optionally be bound to an {@link AbortSignal} + * for better composability (see {@link CancellablePromise#cancelOn}). + * + * Cancelling a pending promise will result in an immediate rejection + * with an instance of {@link CancelError} as reason, + * but whoever started the promise will be responsible + * for actually aborting the underlying operation. + * To this purpose, the constructor and all chaining methods + * accept optional cancellation callbacks. + * + * If a `CancellablePromise` still resolves after having been cancelled, + * the result will be discarded. If it rejects, the reason + * will be reported as an unhandled rejection, + * wrapped in a {@link CancelledRejectionError} instance. + * To facilitate the handling of cancellation requests, + * cancelled `CancellablePromise`s will _not_ report unhandled `CancelError`s + * whose `cause` field is the same as the one with which the current promise was cancelled. + * + * All usual promise methods are defined and return a `CancellablePromise` + * whose cancel method will cancel the parent operation as well, propagating the cancellation reason + * upwards through promise chains. + * Conversely, cancelling a promise will not automatically cancel dependent promises downstream: + * ```ts + * let root = new CancellablePromise((resolve, reject) => { ... }); + * let child1 = root.then(() => { ... }); + * let child2 = child1.then(() => { ... }); + * let child3 = root.catch(() => { ... }); + * child1.cancel(); // Cancels child1 and root, but not child2 or child3 + * ``` + * Cancelling a promise that has already settled is safe and has no consequence. + * + * The `cancel` method returns a promise that _always fulfills_ + * after the whole chain has processed the cancel request + * and all attached callbacks up to that moment have run. + * + * All ES2024 promise methods (static and instance) are defined on CancellablePromise, + * but actual availability may vary with OS/webview version. + * + * In line with the proposal at https://github.com/tc39/proposal-rm-builtin-subclassing, + * `CancellablePromise` does not support transparent subclassing. + * Extenders should take care to provide their own method implementations. + * This might be reconsidered in case the proposal is retired. + * + * CancellablePromise is a wrapper around the DOM Promise object + * and is compliant with the [Promises/A+ specification](https://promisesaplus.com/) + * (it passes the [compliance suite](https://github.com/promises-aplus/promises-tests)) + * if so is the underlying implementation. + */ +export class CancellablePromise extends Promise implements PromiseLike, CancellablePromiseLike { + // Private fields. + /** @internal */ + private [barrierSym]!: Partial> | null; + /** @internal */ + private readonly [cancelImplSym]!: (reason: CancelError) => void | PromiseLike; + + /** + * Creates a new `CancellablePromise`. + * + * @param executor - A callback used to initialize the promise. This callback is passed two arguments: + * a `resolve` callback used to resolve the promise with a value + * or the result of another promise (possibly cancellable), + * and a `reject` callback used to reject the promise with a provided reason or error. + * If the value provided to the `resolve` callback is a thenable _and_ cancellable object + * (it has a `then` _and_ a `cancel` method), + * cancellation requests will be forwarded to that object and the oncancelled will not be invoked anymore. + * If any one of the two callbacks is called _after_ the promise has been cancelled, + * the provided values will be cancelled and resolved as usual, + * but their results will be discarded. + * However, if the resolution process ultimately ends up in a rejection + * that is not due to cancellation, the rejection reason + * will be wrapped in a {@link CancelledRejectionError} + * and bubbled up as an unhandled rejection. + * @param oncancelled - It is the caller's responsibility to ensure that any operation + * started by the executor is properly halted upon cancellation. + * This optional callback can be used to that purpose. + * It will be called _synchronously_ with a cancellation cause + * when cancellation is requested, _after_ the promise has already rejected + * with a {@link CancelError}, but _before_ + * any {@link then}/{@link catch}/{@link finally} callback runs. + * If the callback returns a thenable, the promise returned from {@link cancel} + * will only fulfill after the former has settled. + * Unhandled exceptions or rejections from the callback will be wrapped + * in a {@link CancelledRejectionError} and bubbled up as unhandled rejections. + * If the `resolve` callback is called before cancellation with a cancellable promise, + * cancellation requests on this promise will be diverted to that promise, + * and the original `oncancelled` callback will be discarded. + */ + constructor(executor: CancellablePromiseExecutor, oncancelled?: CancellablePromiseCanceller) { + let resolve!: (value: T | PromiseLike) => void; + let reject!: (reason?: any) => void; + super((res, rej) => { resolve = res; reject = rej; }); + + if ((this.constructor as any)[species] !== Promise) { + throw new TypeError("CancellablePromise does not support transparent subclassing. Please refrain from overriding the [Symbol.species] static property."); + } + + let promise: CancellablePromiseWithResolvers = { + promise: this, + resolve, + reject, + get oncancelled() { return oncancelled ?? null; }, + set oncancelled(cb) { oncancelled = cb ?? undefined; } + }; + + const state: CancellablePromiseState = { + get root() { return state; }, + resolving: false, + settled: false + }; + + // Setup cancellation system. + void Object.defineProperties(this, { + [barrierSym]: { + configurable: false, + enumerable: false, + writable: true, + value: null + }, + [cancelImplSym]: { + configurable: false, + enumerable: false, + writable: false, + value: cancellerFor(promise, state) + } + }); + + // Run the actual executor. + const rejector = rejectorFor(promise, state); + try { + executor(resolverFor(promise, state), rejector); + } catch (err) { + if (state.resolving) { + console.log("Unhandled exception in CancellablePromise executor.", err); + } else { + rejector(err); + } + } + } + + /** + * Cancels immediately the execution of the operation associated with this promise. + * The promise rejects with a {@link CancelError} instance as reason, + * with the {@link CancelError#cause} property set to the given argument, if any. + * + * Has no effect if called after the promise has already settled; + * repeated calls in particular are safe, but only the first one + * will set the cancellation cause. + * + * The `CancelError` exception _need not_ be handled explicitly _on the promises that are being cancelled:_ + * cancelling a promise with no attached rejection handler does not trigger an unhandled rejection event. + * Therefore, the following idioms are all equally correct: + * ```ts + * new CancellablePromise((resolve, reject) => { ... }).cancel(); + * new CancellablePromise((resolve, reject) => { ... }).then(...).cancel(); + * new CancellablePromise((resolve, reject) => { ... }).then(...).catch(...).cancel(); + * ``` + * Whenever some cancelled promise in a chain rejects with a `CancelError` + * with the same cancellation cause as itself, the error will be discarded silently. + * However, the `CancelError` _will still be delivered_ to all attached rejection handlers + * added by {@link then} and related methods: + * ```ts + * let cancellable = new CancellablePromise((resolve, reject) => { ... }); + * cancellable.then(() => { ... }).catch(console.log); + * cancellable.cancel(); // A CancelError is printed to the console. + * ``` + * If the `CancelError` is not handled downstream by the time it reaches + * a _non-cancelled_ promise, it _will_ trigger an unhandled rejection event, + * just like normal rejections would: + * ```ts + * let cancellable = new CancellablePromise((resolve, reject) => { ... }); + * let chained = cancellable.then(() => { ... }).then(() => { ... }); // No catch... + * cancellable.cancel(); // Unhandled rejection event on chained! + * ``` + * Therefore, it is important to either cancel whole promise chains from their tail, + * as shown in the correct idioms above, or take care of handling errors everywhere. + * + * @returns A cancellable promise that _fulfills_ after the cancel callback (if any) + * and all handlers attached up to the call to cancel have run. + * If the cancel callback returns a thenable, the promise returned by `cancel` + * will also wait for that thenable to settle. + * This enables callers to wait for the cancelled operation to terminate + * without being forced to handle potential errors at the call site. + * ```ts + * cancellable.cancel().then(() => { + * // Cleanup finished, it's safe to do something else. + * }, (err) => { + * // Unreachable: the promise returned from cancel will never reject. + * }); + * ``` + * Note that the returned promise will _not_ handle implicitly any rejection + * that might have occurred already in the cancelled chain. + * It will just track whether registered handlers have been executed or not. + * Therefore, unhandled rejections will never be silently handled by calling cancel. + */ + cancel(cause?: any): CancellablePromise { + return new CancellablePromise((resolve) => { + Promise.allSettled([ + this[cancelImplSym](new CancelError("Promise cancelled.", { cause })), + currentBarrier(this) + ]).then(() => resolve(), () => resolve()); + }); + } + + /** + * Binds promise cancellation to the abort event of the given {@link AbortSignal}. + * If the signal has already aborted, the promise will be cancelled immediately. + * When either condition is verified, the cancellation cause will be set + * to the signal's abort reason (see {@link AbortSignal#reason}). + * + * Has no effect if called (or if the signal aborts) _after_ the promise has already settled. + * Only the first signal to abort will set the cancellation cause. + * + * For more details about the cancellation process, + * see {@link cancel} and the `CancellablePromise` constructor. + * + * This method enables `await`ing cancellable promises without having + * to store them for future cancellation, e.g.: + * ```ts + * await longRunningOperation().cancelOn(signal); + * ``` + * instead of: + * ```ts + * let promiseToBeCancelled = longRunningOperation(); + * await promiseToBeCancelled; + * ``` + * + * @returns This promise, for method chaining. + */ + cancelOn(signal: AbortSignal): CancellablePromise { + if (signal.aborted) { + void this.cancel(signal.reason) + } else { + signal.addEventListener('abort', () => void this.cancel(signal.reason), {capture: true}); + } + + return this; + } + + /** + * Attaches callbacks for the resolution and/or rejection of the `CancellablePromise`. + * + * The optional `oncancelled` argument will be invoked when the returned promise is cancelled, + * with the same semantics as the `oncancelled` argument of the constructor. + * When the parent promise rejects or is cancelled, the `onrejected` callback will run, + * _even after the returned promise has been cancelled:_ + * in that case, should it reject or throw, the reason will be wrapped + * in a {@link CancelledRejectionError} and bubbled up as an unhandled rejection. + * + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A `CancellablePromise` for the completion of whichever callback is executed. + * The returned promise is hooked up to propagate cancellation requests up the chain, but not down: + * + * - if the parent promise is cancelled, the `onrejected` handler will be invoked with a `CancelError` + * and the returned promise _will resolve regularly_ with its result; + * - conversely, if the returned promise is cancelled, _the parent promise is cancelled too;_ + * the `onrejected` handler will still be invoked with the parent's `CancelError`, + * but its result will be discarded + * and the returned promise will reject with a `CancelError` as well. + * + * The promise returned from {@link cancel} will fulfill only after all attached handlers + * up the entire promise chain have been run. + * + * If either callback returns a cancellable promise, + * cancellation requests will be diverted to it, + * and the specified `oncancelled` callback will be discarded. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike | CancellablePromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike | CancellablePromiseLike) | undefined | null, oncancelled?: CancellablePromiseCanceller): CancellablePromise { + if (!(this instanceof CancellablePromise)) { + throw new TypeError("CancellablePromise.prototype.then called on an invalid object."); + } + + // NOTE: TypeScript's built-in type for then is broken, + // as it allows specifying an arbitrary TResult1 != T even when onfulfilled is not a function. + // We cannot fix it if we want to CancellablePromise to implement PromiseLike. + + if (!isCallable(onfulfilled)) { onfulfilled = identity as any; } + if (!isCallable(onrejected)) { onrejected = thrower; } + + if (onfulfilled === identity && onrejected == thrower) { + // Shortcut for trivial arguments. + return new CancellablePromise((resolve) => resolve(this as any)); + } + + const barrier: Partial> = {}; + this[barrierSym] = barrier; + + return new CancellablePromise((resolve, reject) => { + void promiseThen.call(this, + (value) => { + if (this[barrierSym] === barrier) { this[barrierSym] = null; } + barrier.resolve?.(); + + try { + resolve(onfulfilled!(value)); + } catch (err) { + reject(err); + } + }, + (reason?) => { + if (this[barrierSym] === barrier) { this[barrierSym] = null; } + barrier.resolve?.(); + + try { + resolve(onrejected!(reason)); + } catch (err) { + reject(err); + } + } + ); + }, async (cause?) => { + //cancelled = true; + try { + return oncancelled?.(cause); + } finally { + await this.cancel(cause); + } + }); + } + + /** + * Attaches a callback for only the rejection of the Promise. + * + * The optional `oncancelled` argument will be invoked when the returned promise is cancelled, + * with the same semantics as the `oncancelled` argument of the constructor. + * When the parent promise rejects or is cancelled, the `onrejected` callback will run, + * _even after the returned promise has been cancelled:_ + * in that case, should it reject or throw, the reason will be wrapped + * in a {@link CancelledRejectionError} and bubbled up as an unhandled rejection. + * + * It is equivalent to + * ```ts + * cancellablePromise.then(undefined, onrejected, oncancelled); + * ``` + * and the same caveats apply. + * + * @returns A Promise for the completion of the callback. + * Cancellation requests on the returned promise + * will propagate up the chain to the parent promise, + * but not in the other direction. + * + * The promise returned from {@link cancel} will fulfill only after all attached handlers + * up the entire promise chain have been run. + * + * If `onrejected` returns a cancellable promise, + * cancellation requests will be diverted to it, + * and the specified `oncancelled` callback will be discarded. + * See {@link then} for more details. + */ + catch(onrejected?: ((reason: any) => (PromiseLike | TResult)) | undefined | null, oncancelled?: CancellablePromiseCanceller): CancellablePromise { + return this.then(undefined, onrejected, oncancelled); + } + + /** + * Attaches a callback that is invoked when the CancellablePromise is settled (fulfilled or rejected). The + * resolved value cannot be accessed or modified from the callback. + * The returned promise will settle in the same state as the original one + * after the provided callback has completed execution, + * unless the callback throws or returns a rejecting promise, + * in which case the returned promise will reject as well. + * + * The optional `oncancelled` argument will be invoked when the returned promise is cancelled, + * with the same semantics as the `oncancelled` argument of the constructor. + * Once the parent promise settles, the `onfinally` callback will run, + * _even after the returned promise has been cancelled:_ + * in that case, should it reject or throw, the reason will be wrapped + * in a {@link CancelledRejectionError} and bubbled up as an unhandled rejection. + * + * This method is implemented in terms of {@link then} and the same caveats apply. + * It is polyfilled, hence available in every OS/webview version. + * + * @returns A Promise for the completion of the callback. + * Cancellation requests on the returned promise + * will propagate up the chain to the parent promise, + * but not in the other direction. + * + * The promise returned from {@link cancel} will fulfill only after all attached handlers + * up the entire promise chain have been run. + * + * If `onfinally` returns a cancellable promise, + * cancellation requests will be diverted to it, + * and the specified `oncancelled` callback will be discarded. + * See {@link then} for more details. + */ + finally(onfinally?: (() => void) | undefined | null, oncancelled?: CancellablePromiseCanceller): CancellablePromise { + if (!(this instanceof CancellablePromise)) { + throw new TypeError("CancellablePromise.prototype.finally called on an invalid object."); + } + + if (!isCallable(onfinally)) { + return this.then(onfinally, onfinally, oncancelled); + } + + return this.then( + (value) => CancellablePromise.resolve(onfinally()).then(() => value), + (reason?) => CancellablePromise.resolve(onfinally()).then(() => { throw reason; }), + oncancelled, + ); + } + + /** + * We use the `[Symbol.species]` static property, if available, + * to disable the built-in automatic subclassing features from {@link Promise}. + * It is critical for performance reasons that extenders do not override this. + * Once the proposal at https://github.com/tc39/proposal-rm-builtin-subclassing + * is either accepted or retired, this implementation will have to be revised accordingly. + * + * @ignore + * @internal + */ + static get [species]() { + return Promise; + } + + /** + * Creates a CancellablePromise that is resolved with an array of results + * when all of the provided Promises resolve, or rejected when any Promise is rejected. + * + * Every one of the provided objects that is a thenable _and_ cancellable object + * will be cancelled when the returned promise is cancelled, with the same cause. + * + * @group Static Methods + */ + static all(values: Iterable>): CancellablePromise[]>; + static all(values: T): CancellablePromise<{ -readonly [P in keyof T]: Awaited; }>; + static all | ArrayLike>(values: T): CancellablePromise { + let collected = Array.from(values); + return collected.length === 0 + ? CancellablePromise.resolve(collected) + : new CancellablePromise((resolve, reject) => { + void Promise.all(collected).then(resolve, reject); + }, allCanceller(collected)); + } + + /** + * Creates a CancellablePromise that is resolved with an array of results + * when all of the provided Promises resolve or reject. + * + * Every one of the provided objects that is a thenable _and_ cancellable object + * will be cancelled when the returned promise is cancelled, with the same cause. + * + * @group Static Methods + */ + static allSettled(values: Iterable>): CancellablePromise>[]>; + static allSettled(values: T): CancellablePromise<{ -readonly [P in keyof T]: PromiseSettledResult>; }>; + static allSettled | ArrayLike>(values: T): CancellablePromise { + let collected = Array.from(values); + return collected.length === 0 + ? CancellablePromise.resolve(collected) + : new CancellablePromise((resolve, reject) => { + void Promise.allSettled(collected).then(resolve, reject); + }, allCanceller(collected)); + } + + /** + * The any function returns a promise that is fulfilled by the first given promise to be fulfilled, + * or rejected with an AggregateError containing an array of rejection reasons + * if all of the given promises are rejected. + * It resolves all elements of the passed iterable to promises as it runs this algorithm. + * + * Every one of the provided objects that is a thenable _and_ cancellable object + * will be cancelled when the returned promise is cancelled, with the same cause. + * + * @group Static Methods + */ + static any(values: Iterable>): CancellablePromise>; + static any(values: T): CancellablePromise>; + static any | ArrayLike>(values: T): CancellablePromise { + let collected = Array.from(values); + return collected.length === 0 + ? CancellablePromise.resolve(collected) + : new CancellablePromise((resolve, reject) => { + void Promise.any(collected).then(resolve, reject); + }, allCanceller(collected)); + } + + /** + * Creates a Promise that is resolved or rejected when any of the provided Promises are resolved or rejected. + * + * Every one of the provided objects that is a thenable _and_ cancellable object + * will be cancelled when the returned promise is cancelled, with the same cause. + * + * @group Static Methods + */ + static race(values: Iterable>): CancellablePromise>; + static race(values: T): CancellablePromise>; + static race | ArrayLike>(values: T): CancellablePromise { + let collected = Array.from(values); + return new CancellablePromise((resolve, reject) => { + void Promise.race(collected).then(resolve, reject); + }, allCanceller(collected)); + } + + /** + * Creates a new cancelled CancellablePromise for the provided cause. + * + * @group Static Methods + */ + static cancel(cause?: any): CancellablePromise { + const p = new CancellablePromise(() => {}); + p.cancel(cause); + return p; + } + + /** + * Creates a new CancellablePromise that cancels + * after the specified timeout, with the provided cause. + * + * If the {@link AbortSignal.timeout} factory method is available, + * it is used to base the timeout on _active_ time rather than _elapsed_ time. + * Otherwise, `timeout` falls back to {@link setTimeout}. + * + * @group Static Methods + */ + static timeout(milliseconds: number, cause?: any): CancellablePromise { + const promise = new CancellablePromise(() => {}); + if (AbortSignal && typeof AbortSignal === 'function' && AbortSignal.timeout && typeof AbortSignal.timeout === 'function') { + AbortSignal.timeout(milliseconds).addEventListener('abort', () => void promise.cancel(cause)); + } else { + setTimeout(() => void promise.cancel(cause), milliseconds); + } + return promise; + } + + /** + * Creates a new CancellablePromise that resolves after the specified timeout. + * The returned promise can be cancelled without consequences. + * + * @group Static Methods + */ + static sleep(milliseconds: number): CancellablePromise; + /** + * Creates a new CancellablePromise that resolves after + * the specified timeout, with the provided value. + * The returned promise can be cancelled without consequences. + * + * @group Static Methods + */ + static sleep(milliseconds: number, value: T): CancellablePromise; + static sleep(milliseconds: number, value?: T): CancellablePromise { + return new CancellablePromise((resolve) => { + setTimeout(() => resolve(value!), milliseconds); + }); + } + + /** + * Creates a new rejected CancellablePromise for the provided reason. + * + * @group Static Methods + */ + static reject(reason?: any): CancellablePromise { + return new CancellablePromise((_, reject) => reject(reason)); + } + + /** + * Creates a new resolved CancellablePromise. + * + * @group Static Methods + */ + static resolve(): CancellablePromise; + /** + * Creates a new resolved CancellablePromise for the provided value. + * + * @group Static Methods + */ + static resolve(value: T): CancellablePromise>; + /** + * Creates a new resolved CancellablePromise for the provided value. + * + * @group Static Methods + */ + static resolve(value: T | PromiseLike): CancellablePromise>; + static resolve(value?: T | PromiseLike): CancellablePromise> { + if (value instanceof CancellablePromise) { + // Optimise for cancellable promises. + return value; + } + return new CancellablePromise((resolve) => resolve(value)); + } + + /** + * Creates a new CancellablePromise and returns it in an object, along with its resolve and reject functions + * and a getter/setter for the cancellation callback. + * + * This method is polyfilled, hence available in every OS/webview version. + * + * @group Static Methods + */ + static withResolvers(): CancellablePromiseWithResolvers { + let result: CancellablePromiseWithResolvers = { oncancelled: null } as any; + result.promise = new CancellablePromise((resolve, reject) => { + result.resolve = resolve; + result.reject = reject; + }, (cause?: any) => { result.oncancelled?.(cause); }); + return result; + } +} + +/** + * Returns a callback that implements the cancellation algorithm for the given cancellable promise. + * The promise returned from the resulting function does not reject. + */ +function cancellerFor(promise: CancellablePromiseWithResolvers, state: CancellablePromiseState) { + let cancellationPromise: void | PromiseLike = undefined; + + return (reason: CancelError): void | PromiseLike => { + if (!state.settled) { + state.settled = true; + state.reason = reason; + promise.reject(reason); + + // Attach an error handler that ignores this specific rejection reason and nothing else. + // In theory, a sane underlying implementation at this point + // should always reject with our cancellation reason, + // hence the handler will never throw. + void promiseThen.call(promise.promise, undefined, (err) => { + if (err !== reason) { + throw err; + } + }); + } + + // If reason is not set, the promise resolved regularly, hence we must not call oncancelled. + // If oncancelled is unset, no need to go any further. + if (!state.reason || !promise.oncancelled) { return; } + + cancellationPromise = new Promise((resolve) => { + try { + resolve(promise.oncancelled!(state.reason!.cause)); + } catch (err) { + Promise.reject(new CancelledRejectionError(promise.promise, err, "Unhandled exception in oncancelled callback.")); + } + }); + cancellationPromise.then(undefined, (reason?) => { + throw new CancelledRejectionError(promise.promise, reason, "Unhandled rejection in oncancelled callback."); + }); + + // Unset oncancelled to prevent repeated calls. + promise.oncancelled = null; + + return cancellationPromise; + } +} + +/** + * Returns a callback that implements the resolution algorithm for the given cancellable promise. + */ +function resolverFor(promise: CancellablePromiseWithResolvers, state: CancellablePromiseState): CancellablePromiseResolver { + return (value) => { + if (state.resolving) { return; } + state.resolving = true; + + if (value === promise.promise) { + if (state.settled) { return; } + state.settled = true; + promise.reject(new TypeError("A promise cannot be resolved with itself.")); + return; + } + + if (value != null && (typeof value === 'object' || typeof value === 'function')) { + let then: any; + try { + then = (value as any).then; + } catch (err) { + state.settled = true; + promise.reject(err); + return; + } + + if (isCallable(then)) { + try { + let cancel = (value as any).cancel; + if (isCallable(cancel)) { + const oncancelled = (cause?: any) => { + Reflect.apply(cancel, value, [cause]); + }; + if (state.reason) { + // If already cancelled, propagate cancellation. + // The promise returned from the canceller algorithm does not reject + // so it can be discarded safely. + void cancellerFor({ ...promise, oncancelled }, state)(state.reason); + } else { + promise.oncancelled = oncancelled; + } + } + } catch {} + + const newState: CancellablePromiseState = { + root: state.root, + resolving: false, + get settled() { return this.root.settled }, + set settled(value) { this.root.settled = value; }, + get reason() { return this.root.reason } + }; + + const rejector = rejectorFor(promise, newState); + try { + Reflect.apply(then, value, [resolverFor(promise, newState), rejector]); + } catch (err) { + rejector(err); + } + return; // IMPORTANT! + } + } + + if (state.settled) { return; } + state.settled = true; + promise.resolve(value); + }; +} + +/** + * Returns a callback that implements the rejection algorithm for the given cancellable promise. + */ +function rejectorFor(promise: CancellablePromiseWithResolvers, state: CancellablePromiseState): CancellablePromiseRejector { + return (reason?) => { + if (state.resolving) { return; } + state.resolving = true; + + if (state.settled) { + try { + if (reason instanceof CancelError && state.reason instanceof CancelError && Object.is(reason.cause, state.reason.cause)) { + // Swallow late rejections that are CancelErrors whose cancellation cause is the same as ours. + return; + } + } catch {} + + void Promise.reject(new CancelledRejectionError(promise.promise, reason)); + } else { + state.settled = true; + promise.reject(reason); + } + } +} + +/** + * Returns a callback that cancels all values in an iterable that look like cancellable thenables. + */ +function allCanceller(values: Iterable): CancellablePromiseCanceller { + return (cause?) => { + for (const value of values) { + try { + if (isCallable(value.then)) { + let cancel = value.cancel; + if (isCallable(cancel)) { + Reflect.apply(cancel, value, [cause]); + } + } + } catch {} + } + } +} + +/** + * Returns its argument. + */ +function identity(x: T): T { + return x; +} + +/** + * Throws its argument. + */ +function thrower(reason?: any): never { + throw reason; +} + +/** + * Attempts various strategies to convert an error to a string. + */ +function errorMessage(err: any): string { + try { + if (err instanceof Error || typeof err !== 'object' || err.toString !== Object.prototype.toString) { + return "" + err; + } + } catch {} + + try { + return JSON.stringify(err); + } catch {} + + try { + return Object.prototype.toString.call(err); + } catch {} + + return ""; +} + +/** + * Gets the current barrier promise for the given cancellable promise. If necessary, initialises the barrier. + */ +function currentBarrier(promise: CancellablePromise): Promise { + let pwr: Partial> = promise[barrierSym] ?? {}; + if (!('promise' in pwr)) { + Object.assign(pwr, promiseWithResolvers()); + } + if (promise[barrierSym] == null) { + pwr.resolve!(); + promise[barrierSym] = pwr; + } + return pwr.promise!; +} + +// Stop sneaky people from breaking the barrier mechanism. +const promiseThen = Promise.prototype.then; +Promise.prototype.then = function(...args) { + if (this instanceof CancellablePromise) { + return this.then(...args); + } else { + return Reflect.apply(promiseThen, this, args); + } +} + +// Polyfill Promise.withResolvers. +let promiseWithResolvers = Promise.withResolvers; +if (promiseWithResolvers && typeof promiseWithResolvers === 'function') { + promiseWithResolvers = promiseWithResolvers.bind(Promise); +} else { + promiseWithResolvers = function (): PromiseWithResolvers { + let resolve!: (value: T | PromiseLike) => void; + let reject!: (reason?: any) => void; + const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); + return { promise, resolve, reject }; + } +} \ No newline at end of file diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/clipboard.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/clipboard.js deleted file mode 100644 index 15cfc518a..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/clipboard.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ - -import {newRuntimeCallerWithID, objectNames} from "./runtime"; - -const call = newRuntimeCallerWithID(objectNames.Clipboard, ''); -const ClipboardSetText = 0; -const ClipboardText = 1; - -/** - * Sets the text to the Clipboard. - * - * @param {string} text - The text to be set to the Clipboard. - * @return {Promise} - A Promise that resolves when the operation is successful. - */ -export function SetText(text) { - return call(ClipboardSetText, {text}); -} - -/** - * Get the Clipboard text - * @returns {Promise} A promise that resolves with the text from the Clipboard. - */ -export function Text() { - return call(ClipboardText); -} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/clipboard.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/clipboard.ts new file mode 100644 index 000000000..a6f2f1985 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/clipboard.ts @@ -0,0 +1,35 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import {newRuntimeCaller, objectNames} from "./runtime.js"; + +const call = newRuntimeCaller(objectNames.Clipboard); + +const ClipboardSetText = 0; +const ClipboardText = 1; + +/** + * Sets the text to the Clipboard. + * + * @param text - The text to be set to the Clipboard. + * @return A Promise that resolves when the operation is successful. + */ +export function SetText(text: string): Promise { + return call(ClipboardSetText, {text}); +} + +/** + * Get the Clipboard text + * + * @returns A promise that resolves with the text from the Clipboard. + */ +export function Text(): Promise { + return call(ClipboardText); +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/contextmenu.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/contextmenu.ts similarity index 55% rename from v3/internal/runtime/desktop/@wailsio/runtime/src/contextmenu.js rename to v3/internal/runtime/desktop/@wailsio/runtime/src/contextmenu.ts index ab0749901..50ab188f2 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/contextmenu.js +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/contextmenu.ts @@ -8,30 +8,38 @@ The electron alternative for Go (c) Lea Anthony 2019-present */ -/* jshint esversion: 9 */ - -import {newRuntimeCallerWithID, objectNames} from "./runtime"; -import {IsDebug} from "./system"; +import { newRuntimeCaller, objectNames } from "./runtime.js"; +import { IsDebug } from "./system.js"; // setup window.addEventListener('contextmenu', contextMenuHandler); -const call = newRuntimeCallerWithID(objectNames.ContextMenu, ''); +const call = newRuntimeCaller(objectNames.ContextMenu); + const ContextMenuOpen = 0; -function openContextMenu(id, x, y, data) { +function openContextMenu(id: string, x: number, y: number, data: any): void { void call(ContextMenuOpen, {id, x, y, data}); } -function contextMenuHandler(event) { +function contextMenuHandler(event: MouseEvent) { + let target: HTMLElement; + + if (event.target instanceof HTMLElement) { + target = event.target; + } else if (!(event.target instanceof HTMLElement) && event.target instanceof Node) { + target = event.target.parentElement ?? document.body; + } else { + target = document.body; + } + // Check for custom context menu - let element = event.target; - let customContextMenu = window.getComputedStyle(element).getPropertyValue("--custom-contextmenu"); - customContextMenu = customContextMenu ? customContextMenu.trim() : ""; + let customContextMenu = window.getComputedStyle(target).getPropertyValue("--custom-contextmenu").trim(); + if (customContextMenu) { event.preventDefault(); - let customContextMenuData = window.getComputedStyle(element).getPropertyValue("--custom-contextmenu-data"); - openContextMenu(customContextMenu, event.clientX, event.clientY, customContextMenuData); + let data = window.getComputedStyle(target).getPropertyValue("--custom-contextmenu-data"); + openContextMenu(customContextMenu, event.clientX, event.clientY, data); return } @@ -46,47 +54,56 @@ function contextMenuHandler(event) { This rule is inherited like normal CSS rules, so nesting works as expected */ -function processDefaultContextMenu(event) { - +function processDefaultContextMenu(event: MouseEvent) { // Debug builds always show the menu if (IsDebug()) { return; } + let target: HTMLElement; + + if (event.target instanceof HTMLElement) { + target = event.target; + } else if (!(event.target instanceof HTMLElement) && event.target instanceof Node) { + target = event.target.parentElement ?? document.body; + } else { + target = document.body; + } + // Process default context menu - const element = event.target; - const computedStyle = window.getComputedStyle(element); - const defaultContextMenuAction = computedStyle.getPropertyValue("--default-contextmenu").trim(); - switch (defaultContextMenuAction) { + switch (window.getComputedStyle(target).getPropertyValue("--default-contextmenu").trim()) { case "show": return; + case "hide": event.preventDefault(); return; + default: // Check if contentEditable is true - if (element.isContentEditable) { + if (target.isContentEditable) { return; } // Check if text has been selected const selection = window.getSelection(); - const hasSelection = (selection.toString().length > 0) + const hasSelection = selection && selection.toString().length > 0; if (hasSelection) { for (let i = 0; i < selection.rangeCount; i++) { const range = selection.getRangeAt(i); const rects = range.getClientRects(); for (let j = 0; j < rects.length; j++) { const rect = rects[j]; - if (document.elementFromPoint(rect.left, rect.top) === element) { + if (document.elementFromPoint(rect.left, rect.top) === target) { return; } } } } - // Check if tagname is input or textarea - if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") { - if (hasSelection || (!element.readOnly && !element.disabled)) { + + // Check if tag is input or textarea. + if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) { + if (hasSelection || (!target.readOnly && !target.disabled)) { return; } } diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/create.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/create.ts similarity index 69% rename from v3/internal/runtime/desktop/@wailsio/runtime/src/create.js rename to v3/internal/runtime/desktop/@wailsio/runtime/src/create.ts index 6967eef09..dc3db4aad 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/create.js +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/create.ts @@ -8,37 +8,27 @@ The electron alternative for Go (c) Lea Anthony 2019-present */ -/* jshint esversion: 9 */ - /** * Any is a dummy creation function for simple or unknown types. - * @template T - * @param {any} source - * @returns {T} */ -export function Any(source) { - return /** @type {T} */(source); +export function Any(source: any): T { + return source; } /** * ByteSlice is a creation function that replaces * null strings with empty strings. - * @param {any} source - * @returns {string} */ -export function ByteSlice(source) { - return /** @type {any} */((source == null) ? "" : source); +export function ByteSlice(source: any): string { + return ((source == null) ? "" : source); } /** * Array takes a creation function for an arbitrary type * and returns an in-place creation function for an array * whose elements are of that type. - * @template T - * @param {(source: any) => T} element - * @returns {(source: any) => T[]} */ -export function Array(element) { +export function Array(element: (source: any) => T): (source: any) => T[] { if (element === Any) { return (source) => (source === null ? [] : source); } @@ -58,12 +48,8 @@ export function Array(element) { * Map takes creation functions for two arbitrary types * and returns an in-place creation function for an object * whose keys and values are of those types. - * @template K, V - * @param {(source: any) => K} key - * @param {(source: any) => V} value - * @returns {(source: any) => { [_: K]: V }} */ -export function Map(key, value) { +export function Map(key: (source: any) => string, value: (source: any) => V): (source: any) => Record { if (value === Any) { return (source) => (source === null ? {} : source); } @@ -82,11 +68,8 @@ export function Map(key, value) { /** * Nullable takes a creation function for an arbitrary type * and returns a creation function for a nullable value of that type. - * @template T - * @param {(source: any) => T} element - * @returns {(source: any) => (T | null)} */ -export function Nullable(element) { +export function Nullable(element: (source: any) => T): (source: any) => (T | null) { if (element === Any) { return Any; } @@ -97,12 +80,11 @@ export function Nullable(element) { /** * Struct takes an object mapping field names to creation functions * and returns an in-place creation function for a struct. - * @template {{ [_: string]: ((source: any) => any) }} T - * @template {{ [Key in keyof T]?: ReturnType }} U - * @param {T} createField - * @returns {(source: any) => U} */ -export function Struct(createField) { +export function Struct< + T extends { [_: string]: ((source: any) => any) }, + U extends { [Key in keyof T]?: ReturnType } +>(createField: T): (source: any) => U { let allAny = true; for (const name in createField) { if (createField[name] !== Any) { diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/dialogs.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/dialogs.js deleted file mode 100644 index dc7678724..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/dialogs.js +++ /dev/null @@ -1,200 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ - -/** - * @typedef {Object} OpenFileDialogOptions - * @property {boolean} [CanChooseDirectories] - Indicates if directories can be chosen. - * @property {boolean} [CanChooseFiles] - Indicates if files can be chosen. - * @property {boolean} [CanCreateDirectories] - Indicates if directories can be created. - * @property {boolean} [ShowHiddenFiles] - Indicates if hidden files should be shown. - * @property {boolean} [ResolvesAliases] - Indicates if aliases should be resolved. - * @property {boolean} [AllowsMultipleSelection] - Indicates if multiple selection is allowed. - * @property {boolean} [HideExtension] - Indicates if the extension should be hidden. - * @property {boolean} [CanSelectHiddenExtension] - Indicates if hidden extensions can be selected. - * @property {boolean} [TreatsFilePackagesAsDirectories] - Indicates if file packages should be treated as directories. - * @property {boolean} [AllowsOtherFiletypes] - Indicates if other file types are allowed. - * @property {FileFilter[]} [Filters] - Array of file filters. - * @property {string} [Title] - Title of the dialog. - * @property {string} [Message] - Message to show in the dialog. - * @property {string} [ButtonText] - Text to display on the button. - * @property {string} [Directory] - Directory to open in the dialog. - * @property {boolean} [Detached] - Indicates if the dialog should appear detached from the main window. - */ - - -/** - * @typedef {Object} SaveFileDialogOptions - * @property {string} [Filename] - Default filename to use in the dialog. - * @property {boolean} [CanChooseDirectories] - Indicates if directories can be chosen. - * @property {boolean} [CanChooseFiles] - Indicates if files can be chosen. - * @property {boolean} [CanCreateDirectories] - Indicates if directories can be created. - * @property {boolean} [ShowHiddenFiles] - Indicates if hidden files should be shown. - * @property {boolean} [ResolvesAliases] - Indicates if aliases should be resolved. - * @property {boolean} [AllowsMultipleSelection] - Indicates if multiple selection is allowed. - * @property {boolean} [HideExtension] - Indicates if the extension should be hidden. - * @property {boolean} [CanSelectHiddenExtension] - Indicates if hidden extensions can be selected. - * @property {boolean} [TreatsFilePackagesAsDirectories] - Indicates if file packages should be treated as directories. - * @property {boolean} [AllowsOtherFiletypes] - Indicates if other file types are allowed. - * @property {FileFilter[]} [Filters] - Array of file filters. - * @property {string} [Title] - Title of the dialog. - * @property {string} [Message] - Message to show in the dialog. - * @property {string} [ButtonText] - Text to display on the button. - * @property {string} [Directory] - Directory to open in the dialog. - * @property {boolean} [Detached] - Indicates if the dialog should appear detached from the main window. - */ - -/** - * @typedef {Object} MessageDialogOptions - * @property {string} [Title] - The title of the dialog window. - * @property {string} [Message] - The main message to show in the dialog. - * @property {Button[]} [Buttons] - Array of button options to show in the dialog. - * @property {boolean} [Detached] - True if the dialog should appear detached from the main window (if applicable). - */ - -/** - * @typedef {Object} Button - * @property {string} [Label] - Text that appears within the button. - * @property {boolean} [IsCancel] - True if the button should cancel an operation when clicked. - * @property {boolean} [IsDefault] - True if the button should be the default action when the user presses enter. - */ - -/** - * @typedef {Object} FileFilter - * @property {string} [DisplayName] - Display name for the filter, it could be "Text Files", "Images" etc. - * @property {string} [Pattern] - Pattern to match for the filter, e.g. "*.txt;*.md" for text markdown files. - */ - -// setup -window._wails = window._wails || {}; -window._wails.dialogErrorCallback = dialogErrorCallback; -window._wails.dialogResultCallback = dialogResultCallback; - -import {newRuntimeCallerWithID, objectNames} from "./runtime"; - -import { nanoid } from './nanoid.js'; - -// Define constants from the `methods` object in Title Case -const DialogInfo = 0; -const DialogWarning = 1; -const DialogError = 2; -const DialogQuestion = 3; -const DialogOpenFile = 4; -const DialogSaveFile = 5; - -const call = newRuntimeCallerWithID(objectNames.Dialog, ''); -const dialogResponses = new Map(); - -/** - * Generates a unique id that is not present in dialogResponses. - * @returns {string} unique id - */ -function generateID() { - let result; - do { - result = nanoid(); - } while (dialogResponses.has(result)); - return result; -} - -/** - * Shows a dialog of specified type with the given options. - * @param {number} type - type of dialog - * @param {MessageDialogOptions|OpenFileDialogOptions|SaveFileDialogOptions} options - options for the dialog - * @returns {Promise} promise that resolves with result of dialog - */ -function dialog(type, options = {}) { - const id = generateID(); - options["dialog-id"] = id; - return new Promise((resolve, reject) => { - dialogResponses.set(id, {resolve, reject}); - call(type, options).catch((error) => { - reject(error); - dialogResponses.delete(id); - }); - }); -} - -/** - * Handles the callback from a dialog. - * - * @param {string} id - The ID of the dialog response. - * @param {string} data - The data received from the dialog. - * @param {boolean} isJSON - Flag indicating whether the data is in JSON format. - * - * @return {undefined} - */ -function dialogResultCallback(id, data, isJSON) { - let p = dialogResponses.get(id); - if (p) { - dialogResponses.delete(id); - if (isJSON) { - p.resolve(JSON.parse(data)); - } else { - p.resolve(data); - } - } -} - -/** - * Callback function for handling errors in dialog. - * - * @param {string} id - The id of the dialog response. - * @param {string} message - The error message. - * - * @return {void} - */ -function dialogErrorCallback(id, message) { - let p = dialogResponses.get(id); - if (p) { - dialogResponses.delete(id); - p.reject(new Error(message)); - } -} - - -// Replace `methods` with constants in Title Case - -/** - * @param {MessageDialogOptions} options - Dialog options - * @returns {Promise} - The label of the button pressed - */ -export const Info = (options) => dialog(DialogInfo, options); - -/** - * @param {MessageDialogOptions} options - Dialog options - * @returns {Promise} - The label of the button pressed - */ -export const Warning = (options) => dialog(DialogWarning, options); - -/** - * @param {MessageDialogOptions} options - Dialog options - * @returns {Promise} - The label of the button pressed - */ -export const Error = (options) => dialog(DialogError, options); - -/** - * @param {MessageDialogOptions} options - Dialog options - * @returns {Promise} - The label of the button pressed - */ -export const Question = (options) => dialog(DialogQuestion, options); - -/** - * @param {OpenFileDialogOptions} options - Dialog options - * @returns {Promise} Returns selected file or list of files. Returns blank string if no file is selected. - */ -export const OpenFile = (options) => dialog(DialogOpenFile, options); - -/** - * @param {SaveFileDialogOptions} options - Dialog options - * @returns {Promise} Returns the selected file. Returns blank string if no file is selected. - */ -export const SaveFile = (options) => dialog(DialogSaveFile, options); diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/dialogs.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/dialogs.ts new file mode 100644 index 000000000..bb0625b2e --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/dialogs.ts @@ -0,0 +1,255 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import {newRuntimeCaller, objectNames} from "./runtime.js"; +import { nanoid } from './nanoid.js'; + +// setup +window._wails = window._wails || {}; +window._wails.dialogErrorCallback = dialogErrorCallback; +window._wails.dialogResultCallback = dialogResultCallback; + +type PromiseResolvers = Omit, "promise">; + +const call = newRuntimeCaller(objectNames.Dialog); +const dialogResponses = new Map(); + +// Define constants from the `methods` object in Title Case +const DialogInfo = 0; +const DialogWarning = 1; +const DialogError = 2; +const DialogQuestion = 3; +const DialogOpenFile = 4; +const DialogSaveFile = 5; + +export interface OpenFileDialogOptions { + /** Indicates if directories can be chosen. */ + CanChooseDirectories?: boolean; + /** Indicates if files can be chosen. */ + CanChooseFiles?: boolean; + /** Indicates if directories can be created. */ + CanCreateDirectories?: boolean; + /** Indicates if hidden files should be shown. */ + ShowHiddenFiles?: boolean; + /** Indicates if aliases should be resolved. */ + ResolvesAliases?: boolean; + /** Indicates if multiple selection is allowed. */ + AllowsMultipleSelection?: boolean; + /** Indicates if the extension should be hidden. */ + HideExtension?: boolean; + /** Indicates if hidden extensions can be selected. */ + CanSelectHiddenExtension?: boolean; + /** Indicates if file packages should be treated as directories. */ + TreatsFilePackagesAsDirectories?: boolean; + /** Indicates if other file types are allowed. */ + AllowsOtherFiletypes?: boolean; + /** Array of file filters. */ + Filters?: FileFilter[]; + /** Title of the dialog. */ + Title?: string; + /** Message to show in the dialog. */ + Message?: string; + /** Text to display on the button. */ + ButtonText?: string; + /** Directory to open in the dialog. */ + Directory?: string; + /** Indicates if the dialog should appear detached from the main window. */ + Detached?: boolean; +} + +export interface SaveFileDialogOptions { + /** Default filename to use in the dialog. */ + Filename?: string; + /** Indicates if directories can be chosen. */ + CanChooseDirectories?: boolean; + /** Indicates if files can be chosen. */ + CanChooseFiles?: boolean; + /** Indicates if directories can be created. */ + CanCreateDirectories?: boolean; + /** Indicates if hidden files should be shown. */ + ShowHiddenFiles?: boolean; + /** Indicates if aliases should be resolved. */ + ResolvesAliases?: boolean; + /** Indicates if the extension should be hidden. */ + HideExtension?: boolean; + /** Indicates if hidden extensions can be selected. */ + CanSelectHiddenExtension?: boolean; + /** Indicates if file packages should be treated as directories. */ + TreatsFilePackagesAsDirectories?: boolean; + /** Indicates if other file types are allowed. */ + AllowsOtherFiletypes?: boolean; + /** Array of file filters. */ + Filters?: FileFilter[]; + /** Title of the dialog. */ + Title?: string; + /** Message to show in the dialog. */ + Message?: string; + /** Text to display on the button. */ + ButtonText?: string; + /** Directory to open in the dialog. */ + Directory?: string; + /** Indicates if the dialog should appear detached from the main window. */ + Detached?: boolean; +} + +export interface MessageDialogOptions { + /** The title of the dialog window. */ + Title?: string; + /** The main message to show in the dialog. */ + Message?: string; + /** Array of button options to show in the dialog. */ + Buttons?: Button[]; + /** True if the dialog should appear detached from the main window (if applicable). */ + Detached?: boolean; +} + +export interface Button { + /** Text that appears within the button. */ + Label?: string; + /** True if the button should cancel an operation when clicked. */ + IsCancel?: boolean; + /** True if the button should be the default action when the user presses enter. */ + IsDefault?: boolean; +} + +export interface FileFilter { + /** Display name for the filter, it could be "Text Files", "Images" etc. */ + DisplayName?: string; + /** Pattern to match for the filter, e.g. "*.txt;*.md" for text markdown files. */ + Pattern?: string; +} + +/** + * Handles the result of a dialog request. + * + * @param id - The id of the request to handle the result for. + * @param data - The result data of the request. + * @param isJSON - Indicates whether the data is JSON or not. + */ +function dialogResultCallback(id: string, data: string, isJSON: boolean): void { + let resolvers = getAndDeleteResponse(id); + if (!resolvers) { + return; + } + + if (isJSON) { + try { + resolvers.resolve(JSON.parse(data)); + } catch (err: any) { + resolvers.reject(new TypeError("could not parse result: " + err.message, { cause: err })); + } + } else { + resolvers.resolve(data); + } +} + +/** + * Handles the error from a dialog request. + * + * @param id - The id of the promise handler. + * @param message - An error message. + */ +function dialogErrorCallback(id: string, message: string): void { + getAndDeleteResponse(id)?.reject(new window.Error(message)); +} + +/** + * Retrieves and removes the response associated with the given ID from the dialogResponses map. + * + * @param id - The ID of the response to be retrieved and removed. + * @returns The response object associated with the given ID, if any. + */ +function getAndDeleteResponse(id: string): PromiseResolvers | undefined { + const response = dialogResponses.get(id); + dialogResponses.delete(id); + return response; +} + +/** + * Generates a unique ID using the nanoid library. + * + * @returns A unique ID that does not exist in the dialogResponses set. + */ +function generateID(): string { + let result; + do { + result = nanoid(); + } while (dialogResponses.has(result)); + return result; +} + +/** + * Presents a dialog of specified type with the given options. + * + * @param type - Dialog type. + * @param options - Options for the dialog. + * @returns A promise that resolves with result of dialog. + */ +function dialog(type: number, options: MessageDialogOptions | OpenFileDialogOptions | SaveFileDialogOptions = {}): Promise { + const id = generateID(); + return new Promise((resolve, reject) => { + dialogResponses.set(id, { resolve, reject }); + call(type, Object.assign({ "dialog-id": id }, options)).catch((err: any) => { + dialogResponses.delete(id); + reject(err); + }); + }); +} + +/** + * Presents an info dialog. + * + * @param options - Dialog options + * @returns A promise that resolves with the label of the chosen button. + */ +export function Info(options: MessageDialogOptions): Promise { return dialog(DialogInfo, options); } + +/** + * Presents a warning dialog. + * + * @param options - Dialog options. + * @returns A promise that resolves with the label of the chosen button. + */ +export function Warning(options: MessageDialogOptions): Promise { return dialog(DialogWarning, options); } + +/** + * Presents an error dialog. + * + * @param options - Dialog options. + * @returns A promise that resolves with the label of the chosen button. + */ +export function Error(options: MessageDialogOptions): Promise { return dialog(DialogError, options); } + +/** + * Presents a question dialog. + * + * @param options - Dialog options. + * @returns A promise that resolves with the label of the chosen button. + */ +export function Question(options: MessageDialogOptions): Promise { return dialog(DialogQuestion, options); } + +/** + * Presents a file selection dialog to pick one or more files to open. + * + * @param options - Dialog options. + * @returns Selected file or list of files, or a blank string/empty list if no file has been selected. + */ +export function OpenFile(options: OpenFileDialogOptions & { AllowsMultipleSelection: true }): Promise; +export function OpenFile(options: OpenFileDialogOptions & { AllowsMultipleSelection?: false | undefined }): Promise; +export function OpenFile(options: OpenFileDialogOptions): Promise; +export function OpenFile(options: OpenFileDialogOptions): Promise { return dialog(DialogOpenFile, options) ?? []; } + +/** + * Presents a file selection dialog to pick a file to save. + * + * @param options - Dialog options. + * @returns Selected file, or a blank string if no file has been selected. + */ +export function SaveFile(options: SaveFileDialogOptions): Promise { return dialog(DialogSaveFile, options); } diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/drag.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/drag.js deleted file mode 100644 index 9c841796c..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/drag.js +++ /dev/null @@ -1,119 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ -/* jshint esversion: 9 */ -import {invoke, IsWindows} from "./system"; -import {GetFlag} from "./flags"; - -// Setup -let shouldDrag = false; -let resizable = false; -let resizeEdge = null; -let defaultCursor = "auto"; - -window._wails = window._wails || {}; - -window._wails.setResizable = function(value) { - resizable = value; -}; - -window._wails.endDrag = function() { - document.body.style.cursor = 'default'; - shouldDrag = false; -}; - -window.addEventListener('mousedown', onMouseDown); -window.addEventListener('mousemove', onMouseMove); -window.addEventListener('mouseup', onMouseUp); - - -function dragTest(e) { - let val = window.getComputedStyle(e.target).getPropertyValue("--wails-draggable"); - let mousePressed = e.buttons !== undefined ? e.buttons : e.which; - if (!val || val === "" || val.trim() !== "drag" || mousePressed === 0) { - return false; - } - return e.detail === 1; -} - -function onMouseDown(e) { - - // Check for resizing - if (resizeEdge) { - invoke("wails:resize:" + resizeEdge); - e.preventDefault(); - return; - } - - if (dragTest(e)) { - // This checks for clicks on the scroll bar - if (e.offsetX > e.target.clientWidth || e.offsetY > e.target.clientHeight) { - return; - } - shouldDrag = true; - } else { - shouldDrag = false; - } -} - -function onMouseUp() { - shouldDrag = false; -} - -function setResize(cursor) { - document.documentElement.style.cursor = cursor || defaultCursor; - resizeEdge = cursor; -} - -function onMouseMove(e) { - if (shouldDrag) { - shouldDrag = false; - let mousePressed = e.buttons !== undefined ? e.buttons : e.which; - if (mousePressed > 0) { - invoke("wails:drag"); - return; - } - } - if (!resizable || !IsWindows()) { - return; - } - if (defaultCursor == null) { - defaultCursor = document.documentElement.style.cursor; - } - let resizeHandleHeight = GetFlag("system.resizeHandleHeight") || 5; - let resizeHandleWidth = GetFlag("system.resizeHandleWidth") || 5; - - // Extra pixels for the corner areas - let cornerExtra = GetFlag("resizeCornerExtra") || 10; - - let rightBorder = window.outerWidth - e.clientX < resizeHandleWidth; - let leftBorder = e.clientX < resizeHandleWidth; - let topBorder = e.clientY < resizeHandleHeight; - let bottomBorder = window.outerHeight - e.clientY < resizeHandleHeight; - - // Adjust for corners - let rightCorner = window.outerWidth - e.clientX < (resizeHandleWidth + cornerExtra); - let leftCorner = e.clientX < (resizeHandleWidth + cornerExtra); - let topCorner = e.clientY < (resizeHandleHeight + cornerExtra); - let bottomCorner = window.outerHeight - e.clientY < (resizeHandleHeight + cornerExtra); - - // If we aren't on an edge, but were, reset the cursor to default - if (!leftBorder && !rightBorder && !topBorder && !bottomBorder && resizeEdge !== undefined) { - setResize(); - } - // Adjusted for corner areas - else if (rightCorner && bottomCorner) setResize("se-resize"); - else if (leftCorner && bottomCorner) setResize("sw-resize"); - else if (leftCorner && topCorner) setResize("nw-resize"); - else if (topCorner && rightCorner) setResize("ne-resize"); - else if (leftBorder) setResize("w-resize"); - else if (topBorder) setResize("n-resize"); - else if (bottomBorder) setResize("s-resize"); - else if (rightBorder) setResize("e-resize"); -} \ No newline at end of file diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/drag.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/drag.ts new file mode 100644 index 000000000..71fdc0256 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/drag.ts @@ -0,0 +1,209 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import { invoke, IsWindows } from "./system.js"; +import { GetFlag } from "./flags.js"; +import { canTrackButtons } from "./utils.js"; + +// Setup +let canDrag = false; +let dragging = false; + +let resizable = false; +let canResize = false; +let resizing = false; +let resizeEdge: string = ""; +let defaultCursor = "auto"; + +let buttons = 0; +const buttonsTracked = canTrackButtons(); + +window._wails = window._wails || {}; +window._wails.setResizable = (value: boolean): void => { + resizable = value; + if (!resizable) { + // Stop resizing if in progress. + canResize = resizing = false; + setResize(); + } +}; + +window.addEventListener('mousedown', onMouseDown, { capture: true }); +window.addEventListener('mousemove', onMouseMove, { capture: true }); +window.addEventListener('mouseup', onMouseUp, { capture: true }); +for (const ev of ['click', 'contextmenu', 'dblclick', 'pointerdown', 'pointerup']) { + window.addEventListener(ev, suppressEvent, { capture: true }); +} + +function suppressEvent(event: Event) { + if (dragging || resizing) { + // Suppress all button events during dragging & resizing. + event.stopImmediatePropagation(); + event.stopPropagation(); + event.preventDefault(); + } +} + +function onMouseDown(event: MouseEvent): void { + buttons = buttonsTracked ? event.buttons : (buttons | (1 << event.button)); + + if (dragging || resizing) { + // After dragging or resizing has started, only lifting the primary button can stop it. + // Do not let any other events through. + suppressEvent(event); + return; + } + + if ((canDrag || canResize) && (buttons & 1) && event.button !== 0) { + // We were ready before, the primary is pressed and was not released: + // still ready, but let events bubble through the window. + return; + } + + // Reset readiness state. + canDrag = false; + canResize = false; + + // Check for resizing readiness. + if (resizeEdge) { + if (event.button === 0 && event.detail === 1) { + // Ready to resize if the primary button was pressed for the first time. + canResize = true; + invoke("wails:resize:" + resizeEdge); + } + + // Do not start drag operations within resize edges. + return; + } + + let target: HTMLElement; + + if (event.target instanceof HTMLElement) { + target = event.target; + } else if (!(event.target instanceof HTMLElement) && event.target instanceof Node) { + target = event.target.parentElement ?? document.body; + } else { + target = document.body; + } + + const style = window.getComputedStyle(target); + const setting = style.getPropertyValue("--wails-draggable").trim(); + if (setting === "drag" && event.button === 0 && event.detail === 1) { + // Ready to drag if the primary button was pressed for the first time on a draggable element. + // Ignore clicks on the scrollbar. + if ( + event.offsetX - parseFloat(style.paddingLeft) < target.clientWidth + && event.offsetY - parseFloat(style.paddingTop) < target.clientHeight + ) { + canDrag = true; + invoke("wails:drag"); + } + } +} + +function onMouseUp(event: MouseEvent) { + buttons = buttonsTracked ? event.buttons : (buttons & ~(1 << event.button)); + + if (event.button === 0) { + if (resizing) { + // Let mouseup event bubble when a drag ends, but not when a resize ends. + suppressEvent(event); + } + + // Stop dragging and resizing when the primary button is lifted. + canDrag = false; + dragging = false; + canResize = false; + resizing = false; + return; + } + + // After dragging or resizing has started, only lifting the primary button can stop it. + suppressEvent(event); + return; +} + +const cursorForEdge = Object.freeze({ + "se-resize": "nwse-resize", + "sw-resize": "nesw-resize", + "nw-resize": "nwse-resize", + "ne-resize": "nesw-resize", + "w-resize": "ew-resize", + "n-resize": "ns-resize", + "s-resize": "ns-resize", + "e-resize": "ew-resize", +}) + +function setResize(edge?: keyof typeof cursorForEdge): void { + if (edge) { + if (!resizeEdge) { defaultCursor = document.body.style.cursor; } + document.body.style.cursor = cursorForEdge[edge]; + } else if (!edge && resizeEdge) { + document.body.style.cursor = defaultCursor; + } + + resizeEdge = edge || ""; +} + +function onMouseMove(event: MouseEvent): void { + if (canResize && resizeEdge) { + // Start resizing. + resizing = true; + } else if (canDrag) { + // Start dragging. + dragging = true; + } + + if (dragging || resizing) { + // Either drag or resize is ongoing, + // reset readiness and stop processing. + canDrag = canResize = false; + return; + } + + if (!resizable || !IsWindows()) { + if (resizeEdge) { setResize(); } + return; + } + + const resizeHandleHeight = GetFlag("system.resizeHandleHeight") || 5; + const resizeHandleWidth = GetFlag("system.resizeHandleWidth") || 5; + + // Extra pixels for the corner areas. + const cornerExtra = GetFlag("resizeCornerExtra") || 10; + + const rightBorder = (window.outerWidth - event.clientX) < resizeHandleWidth; + const leftBorder = event.clientX < resizeHandleWidth; + const topBorder = event.clientY < resizeHandleHeight; + const bottomBorder = (window.outerHeight - event.clientY) < resizeHandleHeight; + + // Adjust for corner areas. + const rightCorner = (window.outerWidth - event.clientX) < (resizeHandleWidth + cornerExtra); + const leftCorner = event.clientX < (resizeHandleWidth + cornerExtra); + const topCorner = event.clientY < (resizeHandleHeight + cornerExtra); + const bottomCorner = (window.outerHeight - event.clientY) < (resizeHandleHeight + cornerExtra); + + if (!leftCorner && !topCorner && !bottomCorner && !rightCorner) { + // Optimisation: out of all corner areas implies out of borders. + setResize(); + } + // Detect corners. + else if (rightCorner && bottomCorner) setResize("se-resize"); + else if (leftCorner && bottomCorner) setResize("sw-resize"); + else if (leftCorner && topCorner) setResize("nw-resize"); + else if (topCorner && rightCorner) setResize("ne-resize"); + // Detect borders. + else if (leftBorder) setResize("w-resize"); + else if (topBorder) setResize("n-resize"); + else if (bottomBorder) setResize("s-resize"); + else if (rightBorder) setResize("e-resize"); + // Out of border area. + else setResize(); +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.ts similarity index 96% rename from v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.js rename to v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.ts index 9c7feed7c..5c5de0ce2 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.js +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/event_types.ts @@ -1,6 +1,18 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ -export const EventTypes = { - Windows: { +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export const Types = Object.freeze({ + Windows: Object.freeze({ APMPowerSettingChange: "windows:APMPowerSettingChange", APMPowerStatusChange: "windows:APMPowerStatusChange", APMResumeAutomatic: "windows:APMResumeAutomatic", @@ -45,8 +57,8 @@ export const EventTypes = { WindowUnMinimise: "windows:WindowUnMinimise", WindowMaximise: "windows:WindowMaximise", WindowUnMaximise: "windows:WindowUnMaximise", - }, - Mac: { + }), + Mac: Object.freeze({ ApplicationDidBecomeActive: "mac:ApplicationDidBecomeActive", ApplicationDidChangeBackingProperties: "mac:ApplicationDidChangeBackingProperties", ApplicationDidChangeEffectiveAppearance: "mac:ApplicationDidChangeEffectiveAppearance", @@ -179,8 +191,8 @@ export const EventTypes = { WindowZoomIn: "mac:WindowZoomIn", WindowZoomOut: "mac:WindowZoomOut", WindowZoomReset: "mac:WindowZoomReset", - }, - Linux: { + }), + Linux: Object.freeze({ ApplicationStartup: "linux:ApplicationStartup", SystemThemeChanged: "linux:SystemThemeChanged", WindowDeleteEvent: "linux:WindowDeleteEvent", @@ -189,8 +201,8 @@ export const EventTypes = { WindowFocusIn: "linux:WindowFocusIn", WindowFocusOut: "linux:WindowFocusOut", WindowLoadChanged: "linux:WindowLoadChanged", - }, - Common: { + }), + Common: Object.freeze({ ApplicationOpenedWithFile: "common:ApplicationOpenedWithFile", ApplicationStarted: "common:ApplicationStarted", ThemeChanged: "common:ThemeChanged", @@ -215,5 +227,5 @@ export const EventTypes = { WindowZoomIn: "common:WindowZoomIn", WindowZoomOut: "common:WindowZoomOut", WindowZoomReset: "common:WindowZoomReset", - }, -}; + }), +}); diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/events.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/events.js deleted file mode 100644 index afbcbb922..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/events.js +++ /dev/null @@ -1,140 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ - -/** - * @typedef {import("./types").WailsEvent} WailsEvent - */ -import {newRuntimeCallerWithID, objectNames} from "./runtime"; - -import {EventTypes} from "./event_types"; -export const Types = EventTypes; - -// Setup -window._wails = window._wails || {}; -window._wails.dispatchWailsEvent = dispatchWailsEvent; - -const call = newRuntimeCallerWithID(objectNames.Events, ''); -const EmitMethod = 0; -const eventListeners = new Map(); - -class Listener { - constructor(eventName, callback, maxCallbacks) { - this.eventName = eventName; - this.maxCallbacks = maxCallbacks || -1; - this.Callback = (data) => { - callback(data); - if (this.maxCallbacks === -1) return false; - this.maxCallbacks -= 1; - return this.maxCallbacks === 0; - }; - } -} - -export class WailsEvent { - constructor(name, data = null) { - this.name = name; - this.data = data; - } -} - -export function setup() { -} - -function dispatchWailsEvent(event) { - let listeners = eventListeners.get(event.name); - if (listeners) { - let toRemove = listeners.filter(listener => { - let remove = listener.Callback(event); - if (remove) return true; - }); - if (toRemove.length > 0) { - listeners = listeners.filter(l => !toRemove.includes(l)); - if (listeners.length === 0) eventListeners.delete(event.name); - else eventListeners.set(event.name, listeners); - } - } -} - -/** - * Register a callback function to be called multiple times for a specific event. - * - * @param {string} eventName - The name of the event to register the callback for. - * @param {function} callback - The callback function to be called when the event is triggered. - * @param {number} maxCallbacks - The maximum number of times the callback can be called for the event. Once the maximum number is reached, the callback will no longer be called. - * - @return {function} - A function that, when called, will unregister the callback from the event. - */ -export function OnMultiple(eventName, callback, maxCallbacks) { - let listeners = eventListeners.get(eventName) || []; - const thisListener = new Listener(eventName, callback, maxCallbacks); - listeners.push(thisListener); - eventListeners.set(eventName, listeners); - return () => listenerOff(thisListener); -} - -/** - * Registers a callback function to be executed when the specified event occurs. - * - * @param {string} eventName - The name of the event. - * @param {function} callback - The callback function to be executed. It takes no parameters. - * @return {function} - A function that, when called, will unregister the callback from the event. */ -export function On(eventName, callback) { return OnMultiple(eventName, callback, -1); } - -/** - * Registers a callback function to be executed only once for the specified event. - * - * @param {string} eventName - The name of the event. - * @param {function} callback - The function to be executed when the event occurs. - * @return {function} - A function that, when called, will unregister the callback from the event. - */ -export function Once(eventName, callback) { return OnMultiple(eventName, callback, 1); } - -/** - * Removes the specified listener from the event listeners collection. - * If all listeners for the event are removed, the event key is deleted from the collection. - * - * @param {Object} listener - The listener to be removed. - */ -function listenerOff(listener) { - const eventName = listener.eventName; - let listeners = eventListeners.get(eventName).filter(l => l !== listener); - if (listeners.length === 0) eventListeners.delete(eventName); - else eventListeners.set(eventName, listeners); -} - - -/** - * Removes event listeners for the specified event names. - * - * @param {string} eventName - The name of the event to remove listeners for. - * @param {...string} additionalEventNames - Additional event names to remove listeners for. - * @return {undefined} - */ -export function Off(eventName, ...additionalEventNames) { - let eventsToRemove = [eventName, ...additionalEventNames]; - eventsToRemove.forEach(eventName => eventListeners.delete(eventName)); -} -/** - * Removes all event listeners. - * - * @function OffAll - * @returns {void} - */ -export function OffAll() { eventListeners.clear(); } - -/** - * Emits an event using the given event name. - * - * @param {WailsEvent} event - The name of the event to emit. - * @returns {any} - The result of the emitted event. - */ -export function Emit(event) { return call(EmitMethod, event); } diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/events.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/events.ts new file mode 100644 index 000000000..d8c67b65f --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/events.ts @@ -0,0 +1,135 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import { newRuntimeCaller, objectNames } from "./runtime.js"; +import { eventListeners, Listener, listenerOff } from "./listener.js"; + +// Setup +window._wails = window._wails || {}; +window._wails.dispatchWailsEvent = dispatchWailsEvent; + +const call = newRuntimeCaller(objectNames.Events); +const EmitMethod = 0; + +export { Types } from "./event_types.js"; + +/** + * The type of handlers for a given event. + */ +export type Callback = (ev: WailsEvent) => void; + +/** + * Represents a system event or a custom event emitted through wails-provided facilities. + */ +export class WailsEvent { + /** + * The name of the event. + */ + name: string; + + /** + * Optional data associated with the emitted event. + */ + data: any; + + /** + * Name of the originating window. Omitted for application events. + * Will be overridden if set manually. + */ + sender?: string; + + constructor(name: string, data: any = null) { + this.name = name; + this.data = data; + } +} + +function dispatchWailsEvent(event: any) { + let listeners = eventListeners.get(event.name); + if (!listeners) { + return; + } + + let wailsEvent = new WailsEvent(event.name, event.data); + if ('sender' in event) { + wailsEvent.sender = event.sender; + } + + listeners = listeners.filter(listener => !listener.dispatch(wailsEvent)); + if (listeners.length === 0) { + eventListeners.delete(event.name); + } else { + eventListeners.set(event.name, listeners); + } +} + +/** + * Register a callback function to be called multiple times for a specific event. + * + * @param eventName - The name of the event to register the callback for. + * @param callback - The callback function to be called when the event is triggered. + * @param maxCallbacks - The maximum number of times the callback can be called for the event. Once the maximum number is reached, the callback will no longer be called. + * @returns A function that, when called, will unregister the callback from the event. + */ +export function OnMultiple(eventName: string, callback: Callback, maxCallbacks: number) { + let listeners = eventListeners.get(eventName) || []; + const thisListener = new Listener(eventName, callback, maxCallbacks); + listeners.push(thisListener); + eventListeners.set(eventName, listeners); + return () => listenerOff(thisListener); +} + +/** + * Registers a callback function to be executed when the specified event occurs. + * + * @param eventName - The name of the event to register the callback for. + * @param callback - The callback function to be called when the event is triggered. + * @returns A function that, when called, will unregister the callback from the event. + */ +export function On(eventName: string, callback: Callback): () => void { + return OnMultiple(eventName, callback, -1); +} + +/** + * Registers a callback function to be executed only once for the specified event. + * + * @param eventName - The name of the event to register the callback for. + * @param callback - The callback function to be called when the event is triggered. + * @returns A function that, when called, will unregister the callback from the event. + */ +export function Once(eventName: string, callback: Callback): () => void { + return OnMultiple(eventName, callback, 1); +} + +/** + * Removes event listeners for the specified event names. + * + * @param eventNames - The name of the events to remove listeners for. + */ +export function Off(...eventNames: [string, ...string[]]): void { + eventNames.forEach(eventName => eventListeners.delete(eventName)); +} + +/** + * Removes all event listeners. + */ +export function OffAll(): void { + eventListeners.clear(); +} + +/** + * Emits the given event. + * + * @param event - The name of the event to emit. + * @returns A promise that will be fulfilled once the event has been emitted. + */ +export function Emit(event: WailsEvent): Promise { + return call(EmitMethod, event); +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/flags.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/flags.js deleted file mode 100644 index 26be59d76..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/flags.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ - -/** - * Retrieves the value associated with the specified key from the flag map. - * - * @param {string} keyString - The key to retrieve the value for. - * @return {*} - The value associated with the specified key. - */ -export function GetFlag(keyString) { - try { - return window._wails.flags[keyString]; - } catch (e) { - throw new Error("Unable to retrieve flag '" + keyString + "': " + e); - } -} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/flags.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/flags.ts new file mode 100644 index 000000000..9e4ad2427 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/flags.ts @@ -0,0 +1,23 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +/** + * Retrieves the value associated with the specified key from the flag map. + * + * @param key - The key to retrieve the value for. + * @return The value associated with the specified key. + */ +export function GetFlag(key: string): any { + try { + return window._wails.flags[key]; + } catch (e) { + throw new Error("Unable to retrieve flag '" + key + "': " + e, { cause: e }); + } +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/global.d.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/global.d.ts new file mode 100644 index 000000000..231896ce1 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/global.d.ts @@ -0,0 +1,17 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +declare global { + interface Window { + _wails: Record; + } +} + +export {}; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/index.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/index.js deleted file mode 100644 index 071c60092..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/index.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -// Setup -window._wails = window._wails || {}; - -import "./contextmenu"; -import "./drag"; - -// Re-export public API -import * as Application from "./application"; -import * as Browser from "./browser"; -import * as Call from "./calls"; -import * as Clipboard from "./clipboard"; -import * as Create from "./create"; -import * as Dialogs from "./dialogs"; -import * as Events from "./events"; -import * as Flags from "./flags"; -import * as Screens from "./screens"; -import * as System from "./system"; -import Window from "./window"; -import * as WML from "./wml"; - -export { - Application, - Browser, - Call, - Clipboard, - Create, - Dialogs, - Events, - Flags, - Screens, - System, - Window, - WML -}; - -let initialised = false; -export function init() { - window._wails.invoke = System.invoke; - System.invoke("wails:runtime:ready"); - initialised = true; -} - -window.addEventListener("load", () => { - if (!initialised) { - init(); - } -}); - -// Notify backend - diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts new file mode 100644 index 000000000..7905ead84 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts @@ -0,0 +1,57 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +// Setup +window._wails = window._wails || {}; + +import "./contextmenu.js"; +import "./drag.js"; + +// Re-export public API +import * as Application from "./application.js"; +import * as Browser from "./browser.js"; +import * as Call from "./calls.js"; +import * as Clipboard from "./clipboard.js"; +import * as Create from "./create.js"; +import * as Dialogs from "./dialogs.js"; +import * as Events from "./events.js"; +import * as Flags from "./flags.js"; +import * as Screens from "./screens.js"; +import * as System from "./system.js"; +import Window from "./window.js"; +import * as WML from "./wml.js"; + +export { + Application, + Browser, + Call, + Clipboard, + Dialogs, + Events, + Flags, + Screens, + System, + Window, + WML +}; + +/** + * An internal utility consumed by the binding generator. + * + * @private + * @ignore + */ +export { Create }; + +export * from "./cancellable.js"; + +// Notify backend +window._wails.invoke = System.invoke; +System.invoke("wails:runtime:ready"); diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/listener.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/listener.ts new file mode 100644 index 000000000..0d74debca --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/listener.ts @@ -0,0 +1,52 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +// The following utilities have been factored out of ./events.ts +// for testing purposes. + +export const eventListeners = new Map(); + +export class Listener { + eventName: string; + callback: (data: any) => void; + maxCallbacks: number; + + constructor(eventName: string, callback: (data: any) => void, maxCallbacks: number) { + this.eventName = eventName; + this.callback = callback; + this.maxCallbacks = maxCallbacks || -1; + } + + dispatch(data: any): boolean { + try { + this.callback(data); + } catch (err) { + console.error(err); + } + + if (this.maxCallbacks === -1) return false; + this.maxCallbacks -= 1; + return this.maxCallbacks === 0; + } +} + +export function listenerOff(listener: Listener): void { + let listeners = eventListeners.get(listener.eventName); + if (!listeners) { + return; + } + + listeners = listeners.filter(l => l !== listener); + if (listeners.length === 0) { + eventListeners.delete(listener.eventName); + } else { + eventListeners.set(listener.eventName, listeners); + } +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/nanoid.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/nanoid.ts similarity index 96% rename from v3/internal/runtime/desktop/@wailsio/runtime/src/nanoid.js rename to v3/internal/runtime/desktop/@wailsio/runtime/src/nanoid.ts index 37adf3fb0..bfe83048f 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/nanoid.js +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/nanoid.ts @@ -27,10 +27,10 @@ // `'use`, `andom`, and `rict'` // References to the brotli default dictionary: // `-26T`, `1983`, `40px`, `75px`, `bush`, `jack`, `mind`, `very`, and `wolf` -let urlAlphabet = +const urlAlphabet = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict' -export let nanoid = (size = 21) => { +export function nanoid(size: number = 21): string { let id = '' // A compact alternative for `for (var i = 0; i < step; i++)`. let i = size | 0 @@ -39,4 +39,4 @@ export let nanoid = (size = 21) => { id += urlAlphabet[(Math.random() * 64) | 0] } return id -} \ No newline at end of file +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.js deleted file mode 100644 index b1ae204ab..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.js +++ /dev/null @@ -1,92 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ -import { nanoid } from './nanoid.js'; - -const runtimeURL = window.location.origin + "/wails/runtime"; - -// Object Names -export const objectNames = { - Call: 0, - Clipboard: 1, - Application: 2, - Events: 3, - ContextMenu: 4, - Dialog: 5, - Window: 6, - Screens: 7, - System: 8, - Browser: 9, - CancelCall: 10, -} -export let clientId = nanoid(); - -/** - * Creates a runtime caller function that invokes a specified method on a given object within a specified window context. - * - * @param {Object} object - The object on which the method is to be invoked. - * @param {string} windowName - The name of the window context in which the method should be called. - * @returns {Function} A runtime caller function that takes the method name and optionally arguments and invokes the method within the specified window context. - */ -export function newRuntimeCaller(object, windowName) { - return function (method, args=null) { - return runtimeCall(object + "." + method, windowName, args); - }; -} - -/** - * Creates a new runtime caller with specified ID. - * - * @param {number} object - The object to invoke the method on. - * @param {string} windowName - The name of the window. - * @return {Function} - The new runtime caller function. - */ -export function newRuntimeCallerWithID(object, windowName) { - return function (method, args=null) { - return runtimeCallWithID(object, method, windowName, args); - }; -} - - -function runtimeCall(method, windowName, args) { - return runtimeCallWithID(null, method, windowName, args); -} - -async function runtimeCallWithID(objectID, method, windowName, args) { - let url = new URL(runtimeURL); - if (objectID != null) { - url.searchParams.append("object", objectID); - } - if (method != null) { - url.searchParams.append("method", method); - } - let fetchOptions = { - headers: {}, - }; - if (windowName) { - fetchOptions.headers["x-wails-window-name"] = windowName; - } - if (args) { - url.searchParams.append("args", JSON.stringify(args)); - } - fetchOptions.headers["x-wails-client-id"] = clientId; - - let response = await fetch(url, fetchOptions); - if (!response.ok) { - throw new Error(await response.text()); - } - - if (response.headers.get("Content-Type") && response.headers.get("Content-Type").indexOf("application/json") !== -1) { - return response.json(); - } else { - return response.text(); - } -} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.ts new file mode 100644 index 000000000..412427ef5 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/runtime.ts @@ -0,0 +1,67 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import { nanoid } from './nanoid.js'; + +const runtimeURL = window.location.origin + "/wails/runtime"; + +// Object Names +export const objectNames = Object.freeze({ + Call: 0, + Clipboard: 1, + Application: 2, + Events: 3, + ContextMenu: 4, + Dialog: 5, + Window: 6, + Screens: 7, + System: 8, + Browser: 9, + CancelCall: 10, +}); +export let clientId = nanoid(); + +/** + * Creates a new runtime caller with specified ID. + * + * @param object - The object to invoke the method on. + * @param windowName - The name of the window. + * @return The new runtime caller function. + */ +export function newRuntimeCaller(object: number, windowName: string = '') { + return function (method: number, args: any = null) { + return runtimeCallWithID(object, method, windowName, args); + }; +} + +async function runtimeCallWithID(objectID: number, method: number, windowName: string, args: any): Promise { + let url = new URL(runtimeURL); + url.searchParams.append("object", objectID.toString()); + url.searchParams.append("method", method.toString()); + if (args) { url.searchParams.append("args", JSON.stringify(args)); } + + let headers: Record = { + ["x-wails-client-id"]: clientId + } + if (windowName) { + headers["x-wails-window-name"] = windowName; + } + + let response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error(await response.text()); + } + + if ((response.headers.get("Content-Type")?.indexOf("application/json") ?? -1) !== -1) { + return response.json(); + } else { + return response.text(); + } +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/screens.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/screens.js deleted file mode 100644 index 97fc2af02..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/screens.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ - -/** - * @typedef {Object} Size - * @property {number} Width - The width. - * @property {number} Height - The height. - */ - -/** - * @typedef {Object} Rect - * @property {number} X - The X coordinate of the origin. - * @property {number} Y - The Y coordinate of the origin. - * @property {number} Width - The width of the rectangle. - * @property {number} Height - The height of the rectangle. - */ - -/** - * @typedef {Object} Screen - * @property {string} ID - Unique identifier for the screen. - * @property {string} Name - Human readable name of the screen. - * @property {number} ScaleFactor - The scale factor of the screen (DPI/96). 1 = standard DPI, 2 = HiDPI (Retina), etc. - * @property {number} X - The X coordinate of the screen. - * @property {number} Y - The Y coordinate of the screen. - * @property {Size} Size - Contains the width and height of the screen. - * @property {Rect} Bounds - Contains the bounds of the screen in terms of X, Y, Width, and Height. - * @property {Rect} PhysicalBounds - Contains the physical bounds of the screen in terms of X, Y, Width, and Height (before scaling). - * @property {Rect} WorkArea - Contains the area of the screen that is actually usable (excluding taskbar and other system UI). - * @property {Rect} PhysicalWorkArea - Contains the physical WorkArea of the screen (before scaling). - * @property {boolean} IsPrimary - True if this is the primary monitor selected by the user in the operating system. - * @property {number} Rotation - The rotation of the screen. - */ - -import { newRuntimeCallerWithID, objectNames } from "./runtime"; -const call = newRuntimeCallerWithID(objectNames.Screens, ""); - -const getAll = 0; -const getPrimary = 1; -const getCurrent = 2; - -/** - * Gets all screens. - * @returns {Promise} A promise that resolves to an array of Screen objects. - */ -export function GetAll() { - return call(getAll); -} -/** - * Gets the primary screen. - * @returns {Promise} A promise that resolves to the primary screen. - */ -export function GetPrimary() { - return call(getPrimary); -} -/** - * Gets the current active screen. - * - * @returns {Promise} A promise that resolves with the current active screen. - */ -export function GetCurrent() { - return call(getCurrent); -} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/screens.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/screens.ts new file mode 100644 index 000000000..c0ecfd7be --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/screens.ts @@ -0,0 +1,88 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +export interface Size { + /** The width of a rectangular area. */ + Width: number; + /** The height of a rectangular area. */ + Height: number; +} + +export interface Rect { + /** The X coordinate of the origin. */ + X: number; + /** The Y coordinate of the origin. */ + Y: number; + /** The width of the rectangle. */ + Width: number; + /** The height of the rectangle. */ + Height: number; +} + +export interface Screen { + /** Unique identifier for the screen. */ + ID: string; + /** Human-readable name of the screen. */ + Name: string; + /** The scale factor of the screen (DPI/96). 1 = standard DPI, 2 = HiDPI (Retina), etc. */ + ScaleFactor: number; + /** The X coordinate of the screen. */ + X: number; + /** The Y coordinate of the screen. */ + Y: number; + /** Contains the width and height of the screen. */ + Size: Size; + /** Contains the bounds of the screen in terms of X, Y, Width, and Height. */ + Bounds: Rect; + /** Contains the physical bounds of the screen in terms of X, Y, Width, and Height (before scaling). */ + PhysicalBounds: Rect; + /** Contains the area of the screen that is actually usable (excluding taskbar and other system UI). */ + WorkArea: Rect; + /** Contains the physical WorkArea of the screen (before scaling). */ + PhysicalWorkArea: Rect; + /** True if this is the primary monitor selected by the user in the operating system. */ + IsPrimary: boolean; + /** The rotation of the screen. */ + Rotation: number; +} + +import { newRuntimeCaller, objectNames } from "./runtime.js"; +const call = newRuntimeCaller(objectNames.Screens); + +const getAll = 0; +const getPrimary = 1; +const getCurrent = 2; + +/** + * Gets all screens. + * + * @returns A promise that resolves to an array of Screen objects. + */ +export function GetAll(): Promise { + return call(getAll); +} + +/** + * Gets the primary screen. + * + * @returns A promise that resolves to the primary screen. + */ +export function GetPrimary(): Promise { + return call(getPrimary); +} + +/** + * Gets the current active screen. + * + * @returns A promise that resolves with the current active screen. + */ +export function GetCurrent(): Promise { + return call(getCurrent); +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/system.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/system.js deleted file mode 100644 index 18287e656..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/system.js +++ /dev/null @@ -1,143 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ - -import {newRuntimeCallerWithID, objectNames} from "./runtime"; -let call = newRuntimeCallerWithID(objectNames.System, ''); -const systemIsDarkMode = 0; -const environment = 1; - -const _invoke = (() => { - try { - if(window?.chrome?.webview) { - return (msg) => window.chrome.webview.postMessage(msg); - } - if(window?.webkit?.messageHandlers?.external) { - return (msg) => window.webkit.messageHandlers.external.postMessage(msg); - } - } catch(e) { - console.warn('\n%c⚠️ Browser Environment Detected %c\n\n%cOnly UI previews are available in the browser. For full functionality, please run the application in desktop mode.\nMore information at: https://v3.wails.io/learn/build/#using-a-browser-for-development\n', - 'background: #ffffff; color: #000000; font-weight: bold; padding: 4px 8px; border-radius: 4px; border: 2px solid #000000;', - 'background: transparent;', - 'color: #ffffff; font-style: italic; font-weight: bold;'); - } - return null; -})(); - -export function invoke(msg) { - if (!_invoke) return; - return _invoke(msg); -} - -/** - * @function - * Retrieves the system dark mode status. - * @returns {Promise} - A promise that resolves to a boolean value indicating if the system is in dark mode. - */ -export function IsDarkMode() { - return call(systemIsDarkMode); -} - -/** - * Fetches the capabilities of the application from the server. - * - * @async - * @function Capabilities - * @returns {Promise} A promise that resolves to an object containing the capabilities. - */ -export function Capabilities() { - let response = fetch("/wails/capabilities"); - return response.json(); -} - -/** - * @typedef {Object} OSInfo - * @property {string} Branding - The branding of the OS. - * @property {string} ID - The ID of the OS. - * @property {string} Name - The name of the OS. - * @property {string} Version - The version of the OS. - */ - -/** - * @typedef {Object} EnvironmentInfo - * @property {string} Arch - The architecture of the system. - * @property {boolean} Debug - True if the application is running in debug mode, otherwise false. - * @property {string} OS - The operating system in use. - * @property {OSInfo} OSInfo - Details of the operating system. - * @property {Object} PlatformInfo - Additional platform information. - */ - -/** - * @function - * Retrieves environment details. - * @returns {Promise} - A promise that resolves to an object containing OS and system architecture. - */ -export function Environment() { - return call(environment); -} - -/** - * Checks if the current operating system is Windows. - * - * @return {boolean} True if the operating system is Windows, otherwise false. - */ -export function IsWindows() { - return window._wails.environment.OS === "windows"; -} - -/** - * Checks if the current operating system is Linux. - * - * @returns {boolean} Returns true if the current operating system is Linux, false otherwise. - */ -export function IsLinux() { - return window._wails.environment.OS === "linux"; -} - -/** - * Checks if the current environment is a macOS operating system. - * - * @returns {boolean} True if the environment is macOS, false otherwise. - */ -export function IsMac() { - return window._wails.environment.OS === "darwin"; -} - -/** - * Checks if the current environment architecture is AMD64. - * @returns {boolean} True if the current environment architecture is AMD64, false otherwise. - */ -export function IsAMD64() { - return window._wails.environment.Arch === "amd64"; -} - -/** - * Checks if the current architecture is ARM. - * - * @returns {boolean} True if the current architecture is ARM, false otherwise. - */ -export function IsARM() { - return window._wails.environment.Arch === "arm"; -} - -/** - * Checks if the current environment is ARM64 architecture. - * - * @returns {boolean} - Returns true if the environment is ARM64 architecture, otherwise returns false. - */ -export function IsARM64() { - return window._wails.environment.Arch === "arm64"; -} - -export function IsDebug() { - return window._wails.environment.Debug === true; -} - diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/system.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/system.ts new file mode 100644 index 000000000..444c49ef2 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/system.ts @@ -0,0 +1,156 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import { newRuntimeCaller, objectNames } from "./runtime.js"; + +let call = newRuntimeCaller(objectNames.System); + +const SystemIsDarkMode = 0; +const SystemEnvironment = 1; + +const _invoke = (function () { + try { + if ((window as any).chrome?.webview?.postMessage) { + return (window as any).chrome.webview.postMessage.bind((window as any).chrome.webview); + } else if ((window as any).webkit?.messageHandlers?.['external']?.postMessage) { + return (window as any).webkit.messageHandlers['external'].postMessage.bind((window as any).webkit.messageHandlers['external']); + } + } catch(e) {} + + console.warn('\n%c⚠️ Browser Environment Detected %c\n\n%cOnly UI previews are available in the browser. For full functionality, please run the application in desktop mode.\nMore information at: https://v3.wails.io/learn/build/#using-a-browser-for-development\n', + 'background: #ffffff; color: #000000; font-weight: bold; padding: 4px 8px; border-radius: 4px; border: 2px solid #000000;', + 'background: transparent;', + 'color: #ffffff; font-style: italic; font-weight: bold;'); + return null; +})(); + +export function invoke(msg: any): void { + return _invoke?.(msg); +} + +/** + * Retrieves the system dark mode status. + * + * @returns A promise that resolves to a boolean value indicating if the system is in dark mode. + */ +export function IsDarkMode(): Promise { + return call(SystemIsDarkMode); +} + +/** + * Fetches the capabilities of the application from the server. + * + * @returns A promise that resolves to an object containing the capabilities. + */ +export async function Capabilities(): Promise> { + let response = await fetch("/wails/capabilities"); + if (response.ok) { + return response.json(); + } else { + throw new Error("could not fetch capabilities: " + response.statusText); + } +} + +export interface OSInfo { + /** The branding of the OS. */ + Branding: string; + /** The ID of the OS. */ + ID: string; + /** The name of the OS. */ + Name: string; + /** The version of the OS. */ + Version: string; +} + +export interface EnvironmentInfo { + /** The architecture of the system. */ + Arch: string; + /** True if the application is running in debug mode, otherwise false. */ + Debug: boolean; + /** The operating system in use. */ + OS: string; + /** Details of the operating system. */ + OSInfo: OSInfo; + /** Additional platform information. */ + PlatformInfo: Record; +} + +/** + * Retrieves environment details. + * + * @returns A promise that resolves to an object containing OS and system architecture. + */ +export function Environment(): Promise { + return call(SystemEnvironment); +} + +/** + * Checks if the current operating system is Windows. + * + * @return True if the operating system is Windows, otherwise false. + */ +export function IsWindows(): boolean { + return window._wails.environment.OS === "windows"; +} + +/** + * Checks if the current operating system is Linux. + * + * @returns Returns true if the current operating system is Linux, false otherwise. + */ +export function IsLinux(): boolean { + return window._wails.environment.OS === "linux"; +} + +/** + * Checks if the current environment is a macOS operating system. + * + * @returns True if the environment is macOS, false otherwise. + */ +export function IsMac(): boolean { + return window._wails.environment.OS === "darwin"; +} + +/** + * Checks if the current environment architecture is AMD64. + * + * @returns True if the current environment architecture is AMD64, false otherwise. + */ +export function IsAMD64(): boolean { + return window._wails.environment.Arch === "amd64"; +} + +/** + * Checks if the current architecture is ARM. + * + * @returns True if the current architecture is ARM, false otherwise. + */ +export function IsARM(): boolean { + return window._wails.environment.Arch === "arm"; +} + +/** + * Checks if the current environment is ARM64 architecture. + * + * @returns Returns true if the environment is ARM64 architecture, otherwise returns false. + */ +export function IsARM64(): boolean { + return window._wails.environment.Arch === "arm64"; +} + +/** + * Reports whether the app is being run in debug mode. + * + * @returns True if the app is being run in debug mode. + */ +export function IsDebug(): boolean { + return Boolean(window._wails.environment.Debug); +} + diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/utils.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/utils.ts similarity index 85% rename from v3/internal/runtime/desktop/@wailsio/runtime/src/utils.js rename to v3/internal/runtime/desktop/@wailsio/runtime/src/utils.ts index b11035a54..2562d2fb6 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/utils.js +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/utils.ts @@ -10,10 +10,10 @@ The electron alternative for Go /** * Logs a message to the console with custom formatting. - * @param {string} message - The message to be logged. - * @return {void} + * + * @param message - The message to be logged. */ -export function debugLog(message) { +export function debugLog(message: any) { // eslint-disable-next-line console.log( '%c wails3 %c ' + message + ' ', @@ -22,11 +22,17 @@ export function debugLog(message) { ); } +/** + * Checks whether the webview supports the {@link MouseEvent#buttons} property. + * Looking at you macOS High Sierra! + */ +export function canTrackButtons(): boolean { + return (new MouseEvent('mousedown')).buttons === 0; +} + /** * Checks whether the browser supports removing listeners by triggering an AbortSignal - * (see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#signal) - * - * @return {boolean} + * (see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#signal). */ export function canAbortListeners() { if (!EventTarget || !AbortSignal || !AbortController) @@ -75,9 +81,9 @@ export function canAbortListeners() { ***/ let isReady = false; -document.addEventListener('DOMContentLoaded', () => isReady = true); +document.addEventListener('DOMContentLoaded', () => { isReady = true }); -export function whenReady(callback) { +export function whenReady(callback: () => void) { if (isReady || document.readyState === 'complete') { callback(); } else { diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/window.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/window.js deleted file mode 100644 index b02729974..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/window.js +++ /dev/null @@ -1,638 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -/* jshint esversion: 9 */ - -// Import screen jsdoc definition from ./screens.js -/** - * @typedef {import("./screens").Screen} Screen - */ - - -/** - * A record describing the position of a window. - * - * @typedef {Object} Position - * @property {number} x - The horizontal position of the window - * @property {number} y - The vertical position of the window - */ - - -/** - * A record describing the size of a window. - * - * @typedef {Object} Size - * @property {number} width - The width of the window - * @property {number} height - The height of the window - */ - - -import {newRuntimeCallerWithID, objectNames} from "./runtime"; - -const PositionMethod = 0; -const CenterMethod = 1; -const CloseMethod = 2; -const DisableSizeConstraintsMethod = 3; -const EnableSizeConstraintsMethod = 4; -const FocusMethod = 5; -const ForceReloadMethod = 6; -const FullscreenMethod = 7; -const GetScreenMethod = 8; -const GetZoomMethod = 9; -const HeightMethod = 10; -const HideMethod = 11; -const IsFocusedMethod = 12; -const IsFullscreenMethod = 13; -const IsMaximisedMethod = 14; -const IsMinimisedMethod = 15; -const MaximiseMethod = 16; -const MinimiseMethod = 17; -const NameMethod = 18; -const OpenDevToolsMethod = 19; -const RelativePositionMethod = 20; -const ReloadMethod = 21; -const ResizableMethod = 22; -const RestoreMethod = 23; -const SetPositionMethod = 24; -const SetAlwaysOnTopMethod = 25; -const SetBackgroundColourMethod = 26; -const SetFramelessMethod = 27; -const SetFullscreenButtonEnabledMethod = 28; -const SetMaxSizeMethod = 29; -const SetMinSizeMethod = 30; -const SetRelativePositionMethod = 31; -const SetResizableMethod = 32; -const SetSizeMethod = 33; -const SetTitleMethod = 34; -const SetZoomMethod = 35; -const ShowMethod = 36; -const SizeMethod = 37; -const ToggleFullscreenMethod = 38; -const ToggleMaximiseMethod = 39; -const UnFullscreenMethod = 40; -const UnMaximiseMethod = 41; -const UnMinimiseMethod = 42; -const WidthMethod = 43; -const ZoomMethod = 44; -const ZoomInMethod = 45; -const ZoomOutMethod = 46; -const ZoomResetMethod = 47; - -/** - * @type {symbol} - */ -const caller = Symbol(); - -export class Window { - /** - * Initialises a window object with the specified name. - * - * @private - * @param {string} name - The name of the target window. - */ - constructor(name = '') { - /** - * @private - * @name {@link caller} - * @type {(...args: any[]) => any} - */ - this[caller] = newRuntimeCallerWithID(objectNames.Window, name) - - // bind instance method to make them easily usable in event handlers - for (const method of Object.getOwnPropertyNames(Window.prototype)) { - if ( - method !== "constructor" - && typeof this[method] === "function" - ) { - this[method] = this[method].bind(this); - } - } - } - - /** - * Gets the specified window. - * - * @public - * @param {string} name - The name of the window to get. - * @return {Window} - The corresponding window object. - */ - Get(name) { - return new Window(name); - } - - /** - * Returns the absolute position of the window. - * - * @public - * @return {Promise} - The current absolute position of the window. - */ - Position() { - return this[caller](PositionMethod); - } - - /** - * Centers the window on the screen. - * - * @public - * @return {Promise} - */ - Center() { - return this[caller](CenterMethod); - } - - /** - * Closes the window. - * - * @public - * @return {Promise} - */ - Close() { - return this[caller](CloseMethod); - } - - /** - * Disables min/max size constraints. - * - * @public - * @return {Promise} - */ - DisableSizeConstraints() { - return this[caller](DisableSizeConstraintsMethod); - } - - /** - * Enables min/max size constraints. - * - * @public - * @return {Promise} - */ - EnableSizeConstraints() { - return this[caller](EnableSizeConstraintsMethod); - } - - /** - * Focuses the window. - * - * @public - * @return {Promise} - */ - Focus() { - return this[caller](FocusMethod); - } - - /** - * Forces the window to reload the page assets. - * - * @public - * @return {Promise} - */ - ForceReload() { - return this[caller](ForceReloadMethod); - } - - /** - * Doc. - * - * @public - * @return {Promise} - */ - Fullscreen() { - return this[caller](FullscreenMethod); - } - - /** - * Returns the screen that the window is on. - * - * @public - * @return {Promise} - The screen the window is currently on - */ - GetScreen() { - return this[caller](GetScreenMethod); - } - - /** - * Returns the current zoom level of the window. - * - * @public - * @return {Promise} - The current zoom level - */ - GetZoom() { - return this[caller](GetZoomMethod); - } - - /** - * Returns the height of the window. - * - * @public - * @return {Promise} - The current height of the window - */ - Height() { - return this[caller](HeightMethod); - } - - /** - * Hides the window. - * - * @public - * @return {Promise} - */ - Hide() { - return this[caller](HideMethod); - } - - /** - * Returns true if the window is focused. - * - * @public - * @return {Promise} - Whether the window is currently focused - */ - IsFocused() { - return this[caller](IsFocusedMethod); - } - - /** - * Returns true if the window is fullscreen. - * - * @public - * @return {Promise} - Whether the window is currently fullscreen - */ - IsFullscreen() { - return this[caller](IsFullscreenMethod); - } - - /** - * Returns true if the window is maximised. - * - * @public - * @return {Promise} - Whether the window is currently maximised - */ - IsMaximised() { - return this[caller](IsMaximisedMethod); - } - - /** - * Returns true if the window is minimised. - * - * @public - * @return {Promise} - Whether the window is currently minimised - */ - IsMinimised() { - return this[caller](IsMinimisedMethod); - } - - /** - * Maximises the window. - * - * @public - * @return {Promise} - */ - Maximise() { - return this[caller](MaximiseMethod); - } - - /** - * Minimises the window. - * - * @public - * @return {Promise} - */ - Minimise() { - return this[caller](MinimiseMethod); - } - - /** - * Returns the name of the window. - * - * @public - * @return {Promise} - The name of the window - */ - Name() { - return this[caller](NameMethod); - } - - /** - * Opens the development tools pane. - * - * @public - * @return {Promise} - */ - OpenDevTools() { - return this[caller](OpenDevToolsMethod); - } - - /** - * Returns the relative position of the window to the screen. - * - * @public - * @return {Promise} - The current relative position of the window - */ - RelativePosition() { - return this[caller](RelativePositionMethod); - } - - /** - * Reloads the page assets. - * - * @public - * @return {Promise} - */ - Reload() { - return this[caller](ReloadMethod); - } - - /** - * Returns true if the window is resizable. - * - * @public - * @return {Promise} - Whether the window is currently resizable - */ - Resizable() { - return this[caller](ResizableMethod); - } - - /** - * Restores the window to its previous state if it was previously minimised, maximised or fullscreen. - * - * @public - * @return {Promise} - */ - Restore() { - return this[caller](RestoreMethod); - } - - /** - * Sets the absolute position of the window. - * - * @public - * @param {number} x - The desired horizontal absolute position of the window - * @param {number} y - The desired vertical absolute position of the window - * @return {Promise} - */ - SetPosition(x, y) { - return this[caller](SetPositionMethod, { x, y }); - } - - /** - * Sets the window to be always on top. - * - * @public - * @param {boolean} alwaysOnTop - Whether the window should stay on top - * @return {Promise} - */ - SetAlwaysOnTop(alwaysOnTop) { - return this[caller](SetAlwaysOnTopMethod, { alwaysOnTop }); - } - - /** - * Sets the background colour of the window. - * - * @public - * @param {number} r - The desired red component of the window background - * @param {number} g - The desired green component of the window background - * @param {number} b - The desired blue component of the window background - * @param {number} a - The desired alpha component of the window background - * @return {Promise} - */ - SetBackgroundColour(r, g, b, a) { - return this[caller](SetBackgroundColourMethod, { r, g, b, a }); - } - - /** - * Removes the window frame and title bar. - * - * @public - * @param {boolean} frameless - Whether the window should be frameless - * @return {Promise} - */ - SetFrameless(frameless) { - return this[caller](SetFramelessMethod, { frameless }); - } - - /** - * Disables the system fullscreen button. - * - * @public - * @param {boolean} enabled - Whether the fullscreen button should be enabled - * @return {Promise} - */ - SetFullscreenButtonEnabled(enabled) { - return this[caller](SetFullscreenButtonEnabledMethod, { enabled }); - } - - /** - * Sets the maximum size of the window. - * - * @public - * @param {number} width - The desired maximum width of the window - * @param {number} height - The desired maximum height of the window - * @return {Promise} - */ - SetMaxSize(width, height) { - return this[caller](SetMaxSizeMethod, { width, height }); - } - - /** - * Sets the minimum size of the window. - * - * @public - * @param {number} width - The desired minimum width of the window - * @param {number} height - The desired minimum height of the window - * @return {Promise} - */ - SetMinSize(width, height) { - return this[caller](SetMinSizeMethod, { width, height }); - } - - /** - * Sets the relative position of the window to the screen. - * - * @public - * @param {number} x - The desired horizontal relative position of the window - * @param {number} y - The desired vertical relative position of the window - * @return {Promise} - */ - SetRelativePosition(x, y) { - return this[caller](SetRelativePositionMethod, { x, y }); - } - - /** - * Sets whether the window is resizable. - * - * @public - * @param {boolean} resizable - Whether the window should be resizable - * @return {Promise} - */ - SetResizable(resizable) { - return this[caller](SetResizableMethod, { resizable }); - } - - /** - * Sets the size of the window. - * - * @public - * @param {number} width - The desired width of the window - * @param {number} height - The desired height of the window - * @return {Promise} - */ - SetSize(width, height) { - return this[caller](SetSizeMethod, { width, height }); - } - - /** - * Sets the title of the window. - * - * @public - * @param {string} title - The desired title of the window - * @return {Promise} - */ - SetTitle(title) { - return this[caller](SetTitleMethod, { title }); - } - - /** - * Sets the zoom level of the window. - * - * @public - * @param {number} zoom - The desired zoom level - * @return {Promise} - */ - SetZoom(zoom) { - return this[caller](SetZoomMethod, { zoom }); - } - - /** - * Shows the window. - * - * @public - * @return {Promise} - */ - Show() { - return this[caller](ShowMethod); - } - - /** - * Returns the size of the window. - * - * @public - * @return {Promise} - The current size of the window - */ - Size() { - return this[caller](SizeMethod); - } - - /** - * Toggles the window between fullscreen and normal. - * - * @public - * @return {Promise} - */ - ToggleFullscreen() { - return this[caller](ToggleFullscreenMethod); - } - - /** - * Toggles the window between maximised and normal. - * - * @public - * @return {Promise} - */ - ToggleMaximise() { - return this[caller](ToggleMaximiseMethod); - } - - /** - * Un-fullscreens the window. - * - * @public - * @return {Promise} - */ - UnFullscreen() { - return this[caller](UnFullscreenMethod); - } - - /** - * Un-maximises the window. - * - * @public - * @return {Promise} - */ - UnMaximise() { - return this[caller](UnMaximiseMethod); - } - - /** - * Un-minimises the window. - * - * @public - * @return {Promise} - */ - UnMinimise() { - return this[caller](UnMinimiseMethod); - } - - /** - * Returns the width of the window. - * - * @public - * @return {Promise} - The current width of the window - */ - Width() { - return this[caller](WidthMethod); - } - - /** - * Zooms the window. - * - * @public - * @return {Promise} - */ - Zoom() { - return this[caller](ZoomMethod); - } - - /** - * Increases the zoom level of the webview content. - * - * @public - * @return {Promise} - */ - ZoomIn() { - return this[caller](ZoomInMethod); - } - - /** - * Decreases the zoom level of the webview content. - * - * @public - * @return {Promise} - */ - ZoomOut() { - return this[caller](ZoomOutMethod); - } - - /** - * Resets the zoom level of the webview content. - * - * @public - * @return {Promise} - */ - ZoomReset() { - return this[caller](ZoomResetMethod); - } -} - -/** - * The window within which the script is running. - * - * @type {Window} - */ -const thisWindow = new Window(''); - -export default thisWindow; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/window.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/window.ts new file mode 100644 index 000000000..10bef1f08 --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/window.ts @@ -0,0 +1,520 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import {newRuntimeCaller, objectNames} from "./runtime.js"; +import type { Screen } from "./screens.js"; + +const PositionMethod = 0; +const CenterMethod = 1; +const CloseMethod = 2; +const DisableSizeConstraintsMethod = 3; +const EnableSizeConstraintsMethod = 4; +const FocusMethod = 5; +const ForceReloadMethod = 6; +const FullscreenMethod = 7; +const GetScreenMethod = 8; +const GetZoomMethod = 9; +const HeightMethod = 10; +const HideMethod = 11; +const IsFocusedMethod = 12; +const IsFullscreenMethod = 13; +const IsMaximisedMethod = 14; +const IsMinimisedMethod = 15; +const MaximiseMethod = 16; +const MinimiseMethod = 17; +const NameMethod = 18; +const OpenDevToolsMethod = 19; +const RelativePositionMethod = 20; +const ReloadMethod = 21; +const ResizableMethod = 22; +const RestoreMethod = 23; +const SetPositionMethod = 24; +const SetAlwaysOnTopMethod = 25; +const SetBackgroundColourMethod = 26; +const SetFramelessMethod = 27; +const SetFullscreenButtonEnabledMethod = 28; +const SetMaxSizeMethod = 29; +const SetMinSizeMethod = 30; +const SetRelativePositionMethod = 31; +const SetResizableMethod = 32; +const SetSizeMethod = 33; +const SetTitleMethod = 34; +const SetZoomMethod = 35; +const ShowMethod = 36; +const SizeMethod = 37; +const ToggleFullscreenMethod = 38; +const ToggleMaximiseMethod = 39; +const UnFullscreenMethod = 40; +const UnMaximiseMethod = 41; +const UnMinimiseMethod = 42; +const WidthMethod = 43; +const ZoomMethod = 44; +const ZoomInMethod = 45; +const ZoomOutMethod = 46; +const ZoomResetMethod = 47; + +/** + * A record describing the position of a window. + */ +interface Position { + /** The horizontal position of the window. */ + x: number; + /** The vertical position of the window. */ + y: number; +} + +/** + * A record describing the size of a window. + */ +interface Size { + /** The width of the window. */ + width: number; + /** The height of the window. */ + height: number; +} + +// Private field names. +const callerSym = Symbol("caller"); + +class Window { + // Private fields. + private [callerSym]: (message: number, args?: any) => Promise; + + /** + * Initialises a window object with the specified name. + * + * @private + * @param name - The name of the target window. + */ + constructor(name: string = '') { + this[callerSym] = newRuntimeCaller(objectNames.Window, name) + + // bind instance method to make them easily usable in event handlers + for (const method of Object.getOwnPropertyNames(Window.prototype)) { + if ( + method !== "constructor" + && typeof (this as any)[method] === "function" + ) { + (this as any)[method] = (this as any)[method].bind(this); + } + } + } + + /** + * Gets the specified window. + * + * @param name - The name of the window to get. + * @returns The corresponding window object. + */ + Get(name: string): Window { + return new Window(name); + } + + /** + * Returns the absolute position of the window. + * + * @returns The current absolute position of the window. + */ + Position(): Promise { + return this[callerSym](PositionMethod); + } + + /** + * Centers the window on the screen. + */ + Center(): Promise { + return this[callerSym](CenterMethod); + } + + /** + * Closes the window. + */ + Close(): Promise { + return this[callerSym](CloseMethod); + } + + /** + * Disables min/max size constraints. + */ + DisableSizeConstraints(): Promise { + return this[callerSym](DisableSizeConstraintsMethod); + } + + /** + * Enables min/max size constraints. + */ + EnableSizeConstraints(): Promise { + return this[callerSym](EnableSizeConstraintsMethod); + } + + /** + * Focuses the window. + */ + Focus(): Promise { + return this[callerSym](FocusMethod); + } + + /** + * Forces the window to reload the page assets. + */ + ForceReload(): Promise { + return this[callerSym](ForceReloadMethod); + } + + /** + * Switches the window to fullscreen mode. + */ + Fullscreen(): Promise { + return this[callerSym](FullscreenMethod); + } + + /** + * Returns the screen that the window is on. + * + * @returns The screen the window is currently on. + */ + GetScreen(): Promise { + return this[callerSym](GetScreenMethod); + } + + /** + * Returns the current zoom level of the window. + * + * @returns The current zoom level. + */ + GetZoom(): Promise { + return this[callerSym](GetZoomMethod); + } + + /** + * Returns the height of the window. + * + * @returns The current height of the window. + */ + Height(): Promise { + return this[callerSym](HeightMethod); + } + + /** + * Hides the window. + */ + Hide(): Promise { + return this[callerSym](HideMethod); + } + + /** + * Returns true if the window is focused. + * + * @returns Whether the window is currently focused. + */ + IsFocused(): Promise { + return this[callerSym](IsFocusedMethod); + } + + /** + * Returns true if the window is fullscreen. + * + * @returns Whether the window is currently fullscreen. + */ + IsFullscreen(): Promise { + return this[callerSym](IsFullscreenMethod); + } + + /** + * Returns true if the window is maximised. + * + * @returns Whether the window is currently maximised. + */ + IsMaximised(): Promise { + return this[callerSym](IsMaximisedMethod); + } + + /** + * Returns true if the window is minimised. + * + * @returns Whether the window is currently minimised. + */ + IsMinimised(): Promise { + return this[callerSym](IsMinimisedMethod); + } + + /** + * Maximises the window. + */ + Maximise(): Promise { + return this[callerSym](MaximiseMethod); + } + + /** + * Minimises the window. + */ + Minimise(): Promise { + return this[callerSym](MinimiseMethod); + } + + /** + * Returns the name of the window. + * + * @returns The name of the window. + */ + Name(): Promise { + return this[callerSym](NameMethod); + } + + /** + * Opens the development tools pane. + */ + OpenDevTools(): Promise { + return this[callerSym](OpenDevToolsMethod); + } + + /** + * Returns the relative position of the window to the screen. + * + * @returns The current relative position of the window. + */ + RelativePosition(): Promise { + return this[callerSym](RelativePositionMethod); + } + + /** + * Reloads the page assets. + */ + Reload(): Promise { + return this[callerSym](ReloadMethod); + } + + /** + * Returns true if the window is resizable. + * + * @returns Whether the window is currently resizable. + */ + Resizable(): Promise { + return this[callerSym](ResizableMethod); + } + + /** + * Restores the window to its previous state if it was previously minimised, maximised or fullscreen. + */ + Restore(): Promise { + return this[callerSym](RestoreMethod); + } + + /** + * Sets the absolute position of the window. + * + * @param x - The desired horizontal absolute position of the window. + * @param y - The desired vertical absolute position of the window. + */ + SetPosition(x: number, y: number): Promise { + return this[callerSym](SetPositionMethod, { x, y }); + } + + /** + * Sets the window to be always on top. + * + * @param alwaysOnTop - Whether the window should stay on top. + */ + SetAlwaysOnTop(alwaysOnTop: boolean): Promise { + return this[callerSym](SetAlwaysOnTopMethod, { alwaysOnTop }); + } + + /** + * Sets the background colour of the window. + * + * @param r - The desired red component of the window background. + * @param g - The desired green component of the window background. + * @param b - The desired blue component of the window background. + * @param a - The desired alpha component of the window background. + */ + SetBackgroundColour(r: number, g: number, b: number, a: number): Promise { + return this[callerSym](SetBackgroundColourMethod, { r, g, b, a }); + } + + /** + * Removes the window frame and title bar. + * + * @param frameless - Whether the window should be frameless. + */ + SetFrameless(frameless: boolean): Promise { + return this[callerSym](SetFramelessMethod, { frameless }); + } + + /** + * Disables the system fullscreen button. + * + * @param enabled - Whether the fullscreen button should be enabled. + */ + SetFullscreenButtonEnabled(enabled: boolean): Promise { + return this[callerSym](SetFullscreenButtonEnabledMethod, { enabled }); + } + + /** + * Sets the maximum size of the window. + * + * @param width - The desired maximum width of the window. + * @param height - The desired maximum height of the window. + */ + SetMaxSize(width: number, height: number): Promise { + return this[callerSym](SetMaxSizeMethod, { width, height }); + } + + /** + * Sets the minimum size of the window. + * + * @param width - The desired minimum width of the window. + * @param height - The desired minimum height of the window. + */ + SetMinSize(width: number, height: number): Promise { + return this[callerSym](SetMinSizeMethod, { width, height }); + } + + /** + * Sets the relative position of the window to the screen. + * + * @param x - The desired horizontal relative position of the window. + * @param y - The desired vertical relative position of the window. + */ + SetRelativePosition(x: number, y: number): Promise { + return this[callerSym](SetRelativePositionMethod, { x, y }); + } + + /** + * Sets whether the window is resizable. + * + * @param resizable - Whether the window should be resizable. + */ + SetResizable(resizable: boolean): Promise { + return this[callerSym](SetResizableMethod, { resizable }); + } + + /** + * Sets the size of the window. + * + * @param width - The desired width of the window. + * @param height - The desired height of the window. + */ + SetSize(width: number, height: number): Promise { + return this[callerSym](SetSizeMethod, { width, height }); + } + + /** + * Sets the title of the window. + * + * @param title - The desired title of the window. + */ + SetTitle(title: string): Promise { + return this[callerSym](SetTitleMethod, { title }); + } + + /** + * Sets the zoom level of the window. + * + * @param zoom - The desired zoom level. + */ + SetZoom(zoom: number): Promise { + return this[callerSym](SetZoomMethod, { zoom }); + } + + /** + * Shows the window. + */ + Show(): Promise { + return this[callerSym](ShowMethod); + } + + /** + * Returns the size of the window. + * + * @returns The current size of the window. + */ + Size(): Promise { + return this[callerSym](SizeMethod); + } + + /** + * Toggles the window between fullscreen and normal. + */ + ToggleFullscreen(): Promise { + return this[callerSym](ToggleFullscreenMethod); + } + + /** + * Toggles the window between maximised and normal. + */ + ToggleMaximise(): Promise { + return this[callerSym](ToggleMaximiseMethod); + } + + /** + * Un-fullscreens the window. + */ + UnFullscreen(): Promise { + return this[callerSym](UnFullscreenMethod); + } + + /** + * Un-maximises the window. + */ + UnMaximise(): Promise { + return this[callerSym](UnMaximiseMethod); + } + + /** + * Un-minimises the window. + */ + UnMinimise(): Promise { + return this[callerSym](UnMinimiseMethod); + } + + /** + * Returns the width of the window. + * + * @returns The current width of the window. + */ + Width(): Promise { + return this[callerSym](WidthMethod); + } + + /** + * Zooms the window. + */ + Zoom(): Promise { + return this[callerSym](ZoomMethod); + } + + /** + * Increases the zoom level of the webview content. + */ + ZoomIn(): Promise { + return this[callerSym](ZoomInMethod); + } + + /** + * Decreases the zoom level of the webview content. + */ + ZoomOut(): Promise { + return this[callerSym](ZoomOutMethod); + } + + /** + * Resets the zoom level of the webview content. + */ + ZoomReset(): Promise { + return this[callerSym](ZoomResetMethod); + } +} + +/** + * The window within which the script is running. + */ +const thisWindow = new Window(''); + +export default thisWindow; diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/wml.js b/v3/internal/runtime/desktop/@wailsio/runtime/src/wml.js deleted file mode 100644 index 819758514..000000000 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/wml.js +++ /dev/null @@ -1,250 +0,0 @@ -/* - _ __ _ __ -| | / /___ _(_) /____ -| | /| / / __ `/ / / ___/ -| |/ |/ / /_/ / / (__ ) -|__/|__/\__,_/_/_/____/ -The electron alternative for Go -(c) Lea Anthony 2019-present -*/ - -import {OpenURL} from "./browser"; -import {Question} from "./dialogs"; -import {Emit, WailsEvent} from "./events"; -import {canAbortListeners, whenReady} from "./utils"; -import Window from "./window"; - -/** - * Sends an event with the given name and optional data. - * - * @param {string} eventName - The name of the event to send. - * @param {any} [data=null] - Optional data to send along with the event. - * - * @return {void} - */ -function sendEvent(eventName, data=null) { - Emit(new WailsEvent(eventName, data)); -} - -/** - * Calls a method on a specified window. - * @param {string} windowName - The name of the window to call the method on. - * @param {string} methodName - The name of the method to call. - */ -function callWindowMethod(windowName, methodName) { - const targetWindow = Window.Get(windowName); - const method = targetWindow[methodName]; - - if (typeof method !== "function") { - console.error(`Window method '${methodName}' not found`); - return; - } - - try { - method.call(targetWindow); - } catch (e) { - console.error(`Error calling window method '${methodName}': `, e); - } -} - -/** - * Responds to a triggering event by running appropriate WML actions for the current target - * - * @param {Event} ev - * @return {void} - */ -function onWMLTriggered(ev) { - const element = ev.currentTarget; - - function runEffect(choice = "Yes") { - if (choice !== "Yes") - return; - - const eventType = element.getAttribute('data-wml-event'); - const targetWindow = element.getAttribute('data-wml-target-window') || ""; - const windowMethod = element.getAttribute('data-wml-window'); - const url = element.getAttribute('data-wml-openURL'); - - if (eventType !== null) - sendEvent(eventType); - if (windowMethod !== null) - callWindowMethod(targetWindow, windowMethod); - if (url !== null) - void OpenURL(url); - } - - const confirm = element.getAttribute('data-wml-confirm'); - - if (confirm) { - Question({ - Title: "Confirm", - Message: confirm, - Detached: false, - Buttons: [ - { Label: "Yes" }, - { Label: "No", IsDefault: true } - ] - }).then(runEffect); - } else { - runEffect(); - } -} - -/** - * @type {symbol} - */ -const controller = Symbol(); - -/** - * AbortControllerRegistry does not actually remember active event listeners: instead - * it ties them to an AbortSignal and uses an AbortController to remove them all at once. - */ -class AbortControllerRegistry { - constructor() { - /** - * Stores the AbortController that can be used to remove all currently active listeners. - * - * @private - * @name {@link controller} - * @member {AbortController} - */ - this[controller] = new AbortController(); - } - - /** - * Returns an options object for addEventListener that ties the listener - * to the AbortSignal from the current AbortController. - * - * @param {HTMLElement} element An HTML element - * @param {string[]} triggers The list of active WML trigger events for the specified elements - * @returns {AddEventListenerOptions} - */ - set(element, triggers) { - return { signal: this[controller].signal }; - } - - /** - * Removes all registered event listeners. - * - * @returns {void} - */ - reset() { - this[controller].abort(); - this[controller] = new AbortController(); - } -} - -/** - * @type {symbol} - */ -const triggerMap = Symbol(); - -/** - * @type {symbol} - */ -const elementCount = Symbol(); - -/** - * WeakMapRegistry maps active trigger events to each DOM element through a WeakMap. - * This ensures that the mapping remains private to this module, while still allowing garbage - * collection of the involved elements. - */ -class WeakMapRegistry { - constructor() { - /** - * Stores the current element-to-trigger mapping. - * - * @private - * @name {@link triggerMap} - * @member {WeakMap} - */ - this[triggerMap] = new WeakMap(); - - /** - * Counts the number of elements with active WML triggers. - * - * @private - * @name {@link elementCount} - * @member {number} - */ - this[elementCount] = 0; - } - - /** - * Sets the active triggers for the specified element. - * - * @param {HTMLElement} element An HTML element - * @param {string[]} triggers The list of active WML trigger events for the specified element - * @returns {AddEventListenerOptions} - */ - set(element, triggers) { - this[elementCount] += !this[triggerMap].has(element); - this[triggerMap].set(element, triggers); - return {}; - } - - /** - * Removes all registered event listeners. - * - * @returns {void} - */ - reset() { - if (this[elementCount] <= 0) - return; - - for (const element of document.body.querySelectorAll('*')) { - if (this[elementCount] <= 0) - break; - - const triggers = this[triggerMap].get(element); - this[elementCount] -= (typeof triggers !== "undefined"); - - for (const trigger of triggers || []) - element.removeEventListener(trigger, onWMLTriggered); - } - - this[triggerMap] = new WeakMap(); - this[elementCount] = 0; - } -} - -const triggerRegistry = canAbortListeners() ? new AbortControllerRegistry() : new WeakMapRegistry(); - -/** - * Adds event listeners to the specified element. - * - * @param {HTMLElement} element - * @return {void} - */ -function addWMLListeners(element) { - const triggerRegExp = /\S+/g; - const triggerAttr = (element.getAttribute('data-wml-trigger') || "click"); - const triggers = []; - - let match; - while ((match = triggerRegExp.exec(triggerAttr)) !== null) - triggers.push(match[0]); - - const options = triggerRegistry.set(element, triggers); - for (const trigger of triggers) - element.addEventListener(trigger, onWMLTriggered, options); -} - -/** - * Schedules an automatic reload of WML to be performed as soon as the document is fully loaded. - * - * @return {void} - */ -export function Enable() { - whenReady(Reload); -} - -/** - * Reloads the WML page by adding necessary event listeners and browser listeners. - * - * @return {void} - */ -export function Reload() { - triggerRegistry.reset(); - document.body.querySelectorAll('[data-wml-event], [data-wml-window], [data-wml-openURL]').forEach(addWMLListeners); -} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/wml.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/wml.ts new file mode 100644 index 000000000..fad38b6eb --- /dev/null +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/wml.ts @@ -0,0 +1,209 @@ +/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ + +import { OpenURL } from "./browser.js"; +import { Question } from "./dialogs.js"; +import { Emit, WailsEvent } from "./events.js"; +import { canAbortListeners, whenReady } from "./utils.js"; +import Window from "./window.js"; + +/** + * Sends an event with the given name and optional data. + * + * @param eventName - - The name of the event to send. + * @param [data=null] - - Optional data to send along with the event. + */ +function sendEvent(eventName: string, data: any = null): void { + Emit(new WailsEvent(eventName, data)); +} + +/** + * Calls a method on a specified window. + * + * @param windowName - The name of the window to call the method on. + * @param methodName - The name of the method to call. + */ +function callWindowMethod(windowName: string, methodName: string) { + const targetWindow = Window.Get(windowName); + const method = (targetWindow as any)[methodName]; + + if (typeof method !== "function") { + console.error(`Window method '${methodName}' not found`); + return; + } + + try { + method.call(targetWindow); + } catch (e) { + console.error(`Error calling window method '${methodName}': `, e); + } +} + +/** + * Responds to a triggering event by running appropriate WML actions for the current target. + */ +function onWMLTriggered(ev: Event): void { + const element = ev.currentTarget as Element; + + function runEffect(choice = "Yes") { + if (choice !== "Yes") + return; + + const eventType = element.getAttribute('wml-event') || element.getAttribute('data-wml-event'); + const targetWindow = element.getAttribute('wml-target-window') || element.getAttribute('data-wml-target-window') || ""; + const windowMethod = element.getAttribute('wml-window') || element.getAttribute('data-wml-window'); + const url = element.getAttribute('wml-openurl') || element.getAttribute('data-wml-openurl'); + + if (eventType !== null) + sendEvent(eventType); + if (windowMethod !== null) + callWindowMethod(targetWindow, windowMethod); + if (url !== null) + void OpenURL(url); + } + + const confirm = element.getAttribute('wml-confirm') || element.getAttribute('data-wml-confirm'); + + if (confirm) { + Question({ + Title: "Confirm", + Message: confirm, + Detached: false, + Buttons: [ + { Label: "Yes" }, + { Label: "No", IsDefault: true } + ] + }).then(runEffect); + } else { + runEffect(); + } +} + +// Private field names. +const controllerSym = Symbol("controller"); +const triggerMapSym = Symbol("triggerMap"); +const elementCountSym = Symbol("elementCount"); + +/** + * AbortControllerRegistry does not actually remember active event listeners: instead + * it ties them to an AbortSignal and uses an AbortController to remove them all at once. + */ +class AbortControllerRegistry { + // Private fields. + [controllerSym]: AbortController; + + constructor() { + this[controllerSym] = new AbortController(); + } + + /** + * Returns an options object for addEventListener that ties the listener + * to the AbortSignal from the current AbortController. + * + * @param element - An HTML element + * @param triggers - The list of active WML trigger events for the specified elements + */ + set(element: Element, triggers: string[]): AddEventListenerOptions { + return { signal: this[controllerSym].signal }; + } + + /** + * Removes all registered event listeners and resets the registry. + */ + reset(): void { + this[controllerSym].abort(); + this[controllerSym] = new AbortController(); + } +} + +/** + * WeakMapRegistry maps active trigger events to each DOM element through a WeakMap. + * This ensures that the mapping remains private to this module, while still allowing garbage + * collection of the involved elements. + */ +class WeakMapRegistry { + /** Stores the current element-to-trigger mapping. */ + [triggerMapSym]: WeakMap; + /** Counts the number of elements with active WML triggers. */ + [elementCountSym]: number; + + constructor() { + this[triggerMapSym] = new WeakMap(); + this[elementCountSym] = 0; + } + + /** + * Sets active triggers for the specified element. + * + * @param element - An HTML element + * @param triggers - The list of active WML trigger events for the specified element + */ + set(element: Element, triggers: string[]): AddEventListenerOptions { + if (!this[triggerMapSym].has(element)) { this[elementCountSym]++; } + this[triggerMapSym].set(element, triggers); + return {}; + } + + /** + * Removes all registered event listeners. + */ + reset(): void { + if (this[elementCountSym] <= 0) + return; + + for (const element of document.body.querySelectorAll('*')) { + if (this[elementCountSym] <= 0) + break; + + const triggers = this[triggerMapSym].get(element); + if (triggers != null) { this[elementCountSym]--; } + + for (const trigger of triggers || []) + element.removeEventListener(trigger, onWMLTriggered); + } + + this[triggerMapSym] = new WeakMap(); + this[elementCountSym] = 0; + } +} + +const triggerRegistry = canAbortListeners() ? new AbortControllerRegistry() : new WeakMapRegistry(); + +/** + * Adds event listeners to the specified element. + */ +function addWMLListeners(element: Element): void { + const triggerRegExp = /\S+/g; + const triggerAttr = (element.getAttribute('wml-trigger') || element.getAttribute('data-wml-trigger') || "click"); + const triggers: string[] = []; + + let match; + while ((match = triggerRegExp.exec(triggerAttr)) !== null) + triggers.push(match[0]); + + const options = triggerRegistry.set(element, triggers); + for (const trigger of triggers) + element.addEventListener(trigger, onWMLTriggered, options); +} + +/** + * Schedules an automatic reload of WML to be performed as soon as the document is fully loaded. + */ +export function Enable(): void { + whenReady(Reload); +} + +/** + * Reloads the WML page by adding necessary event listeners and browser listeners. + */ +export function Reload(): void { + triggerRegistry.reset(); + document.body.querySelectorAll('[wml-event], [wml-window], [wml-openurl], [data-wml-event], [data-wml-window], [data-wml-openurl]').forEach(addWMLListeners); +} diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/tsconfig.json b/v3/internal/runtime/desktop/@wailsio/runtime/tsconfig.json index 2cbc06eed..63db7a091 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/tsconfig.json +++ b/v3/internal/runtime/desktop/@wailsio/runtime/tsconfig.json @@ -1,20 +1,36 @@ { "include": ["./src/**/*"], + "exclude": ["./src/**/*.test.*"], "compilerOptions": { - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "allowJs": true, + "composite": true, + + "allowJs": false, + + "noEmitOnError": true, "declaration": true, - "declarationMap": true, - "emitDeclarationOnly": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", + "declarationMap": false, + "declarationDir": "types", "outDir": "dist", + "rootDir": "src", + + "target": "ES2015", + "module": "ES2015", + "moduleResolution": "bundler", + "isolatedModules": true, + "verbatimModuleSyntax": true, + "stripInternal": true, + + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "strict": true, - "target": "es6", + "noUnusedLocals": true, + "noUnusedParameters": false, + "noImplicitAny": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true } } \ No newline at end of file diff --git a/v3/tasks/events/generate.go b/v3/tasks/events/generate.go index e643db182..b09b8f028 100644 --- a/v3/tasks/events/generate.go +++ b/v3/tasks/events/generate.go @@ -2,15 +2,12 @@ package main import ( "bytes" - "github.com/Masterminds/semver/v3" - "github.com/tidwall/gjson" - "github.com/tidwall/sjson" "os" "strconv" "strings" ) -var eventsGo = `package events +const eventsGo = `package events type ApplicationEventType uint type WindowEventType uint @@ -64,7 +61,7 @@ $$EVENTTOJS} ` -var darwinEventsH = `//go:build darwin +const darwinEventsH = `//go:build darwin #ifndef _events_h #define _events_h @@ -76,7 +73,7 @@ $$CHEADEREVENTS #endif` -var linuxEventsH = `//go:build linux +const linuxEventsH = `//go:build linux #ifndef _events_h #define _events_h @@ -88,34 +85,32 @@ $$CHEADEREVENTS #endif` -var eventsJS = ` -export const EventTypes = { - Windows: { -$$WINDOWSJSEVENTS }, - Mac: { -$$MACJSEVENTS }, - Linux: { -$$LINUXJSEVENTS }, - Common: { -$$COMMONJSEVENTS }, -}; -` +const eventsTS = `/* + _ __ _ __ +| | / /___ _(_) /____ +| | /| / / __ ` + "`" + `/ / / ___/ +| |/ |/ / /_/ / / (__ ) +|__/|__/\__,_/_/_/____/ +The electron alternative for Go +(c) Lea Anthony 2019-present +*/ -var eventsTS = ` -export declare const EventTypes: { - Windows: { -$$WINDOWSTSEVENTS }, - Mac: { -$$MACTSEVENTS }, - Linux: { -$$LINUXTSEVENTS }, - Common: { -$$COMMONTSEVENTS }, -}; +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export const Types = Object.freeze({ + Windows: Object.freeze({ +$$WINDOWSJSEVENTS }), + Mac: Object.freeze({ +$$MACJSEVENTS }), + Linux: Object.freeze({ +$$LINUXJSEVENTS }), + Common: Object.freeze({ +$$COMMONJSEVENTS }), +}); ` func main() { - eventNames, err := os.ReadFile("../../pkg/events/events.txt") if err != nil { panic(err) @@ -138,11 +133,6 @@ func main() { commonEventsDecl := bytes.NewBufferString("") commonEventsValues := bytes.NewBufferString("") - linuxJSEvents := bytes.NewBufferString("") - macJSEvents := bytes.NewBufferString("") - windowsJSEvents := bytes.NewBufferString("") - commonJSEvents := bytes.NewBufferString("") - linuxTSEvents := bytes.NewBufferString("") macTSEvents := bytes.NewBufferString("") windowsTSEvents := bytes.NewBufferString("") @@ -197,8 +187,7 @@ func main() { } linuxEventsDecl.WriteString("\t" + eventTitle + " " + eventType + "\n") linuxEventsValues.WriteString("\t\t" + event + ": " + strconv.Itoa(id) + ",\n") - linuxJSEvents.WriteString("\t\t" + event + ": \"" + strings.TrimSpace(string(line)) + "\",\n") - linuxTSEvents.WriteString("\t\t" + event + ": string,\n") + linuxTSEvents.WriteString("\t\t" + event + ": \"" + strings.TrimSpace(string(line)) + "\",\n") eventToJS.WriteString("\t" + strconv.Itoa(id) + ": \"" + strings.TrimSpace(string(line)) + "\",\n") maxLinuxEvents = id linuxCHeaderEvents.WriteString("#define Event" + eventTitle + " " + strconv.Itoa(id) + "\n") @@ -212,8 +201,7 @@ func main() { } macEventsDecl.WriteString("\t" + eventTitle + " " + eventType + "\n") macEventsValues.WriteString("\t\t" + event + ": " + strconv.Itoa(id) + ",\n") - macJSEvents.WriteString("\t\t" + event + ": \"" + strings.TrimSpace(string(line)) + "\",\n") - macTSEvents.WriteString("\t\t" + event + ": string,\n") + macTSEvents.WriteString("\t\t" + event + ": \"" + strings.TrimSpace(string(line)) + "\",\n") macCHeaderEvents.WriteString("#define Event" + eventTitle + " " + strconv.Itoa(id) + "\n") eventToJS.WriteString("\t" + strconv.Itoa(id) + ": \"" + strings.TrimSpace(string(line)) + "\",\n") maxMacEvents = id @@ -261,8 +249,7 @@ func main() { } commonEventsDecl.WriteString("\t" + eventTitle + " " + eventType + "\n") commonEventsValues.WriteString("\t\t" + event + ": " + strconv.Itoa(id) + ",\n") - commonJSEvents.WriteString("\t\t" + event + ": \"" + strings.TrimSpace(string(line)) + "\",\n") - commonTSEvents.WriteString("\t\t" + event + ": string,\n") + commonTSEvents.WriteString("\t\t" + event + ": \"" + strings.TrimSpace(string(line)) + "\",\n") eventToJS.WriteString("\t" + strconv.Itoa(id) + ": \"" + strings.TrimSpace(string(line)) + "\",\n") case "windows": eventType := "ApplicationEventType" @@ -274,8 +261,7 @@ func main() { } windowsEventsDecl.WriteString("\t" + eventTitle + " " + eventType + "\n") windowsEventsValues.WriteString("\t\t" + event + ": " + strconv.Itoa(id) + ",\n") - windowsJSEvents.WriteString("\t\t" + event + ": \"" + strings.TrimSpace(string(line)) + "\",\n") - windowsTSEvents.WriteString("\t\t" + event + ": string,\n") + windowsTSEvents.WriteString("\t\t" + event + ": \"" + strings.TrimSpace(string(line)) + "\",\n") eventToJS.WriteString("\t" + strconv.Itoa(id) + ": \"" + strings.TrimSpace(string(line)) + "\",\n") } } @@ -299,22 +285,12 @@ func main() { panic(err) } - // Save the eventsJS template substituting the values and decls - templateToWrite = strings.ReplaceAll(eventsJS, "$$MACJSEVENTS", macJSEvents.String()) - templateToWrite = strings.ReplaceAll(templateToWrite, "$$WINDOWSJSEVENTS", windowsJSEvents.String()) - templateToWrite = strings.ReplaceAll(templateToWrite, "$$LINUXJSEVENTS", linuxJSEvents.String()) - templateToWrite = strings.ReplaceAll(templateToWrite, "$$COMMONJSEVENTS", commonJSEvents.String()) - err = os.WriteFile("../../internal/runtime/desktop/@wailsio/runtime/src/event_types.js", []byte(templateToWrite), 0644) - if err != nil { - panic(err) - } - // Save the eventsTS template substituting the values and decls - templateToWrite = strings.ReplaceAll(eventsTS, "$$MACTSEVENTS", macTSEvents.String()) - templateToWrite = strings.ReplaceAll(templateToWrite, "$$WINDOWSTSEVENTS", windowsTSEvents.String()) - templateToWrite = strings.ReplaceAll(templateToWrite, "$$LINUXTSEVENTS", linuxTSEvents.String()) - templateToWrite = strings.ReplaceAll(templateToWrite, "$$COMMONTSEVENTS", commonTSEvents.String()) - err = os.WriteFile("../../internal/runtime/desktop/@wailsio/runtime/types/event_types.d.ts", []byte(templateToWrite), 0644) + templateToWrite = strings.ReplaceAll(eventsTS, "$$MACJSEVENTS", macTSEvents.String()) + templateToWrite = strings.ReplaceAll(templateToWrite, "$$WINDOWSJSEVENTS", windowsTSEvents.String()) + templateToWrite = strings.ReplaceAll(templateToWrite, "$$LINUXJSEVENTS", linuxTSEvents.String()) + templateToWrite = strings.ReplaceAll(templateToWrite, "$$COMMONJSEVENTS", commonTSEvents.String()) + err = os.WriteFile("../../internal/runtime/desktop/@wailsio/runtime/src/event_types.ts", []byte(templateToWrite), 0644) if err != nil { panic(err) } @@ -402,36 +378,4 @@ func main() { if err != nil { panic(err) } - - // Load the runtime package.json - packageJsonFilename := "../../internal/runtime/desktop/@wailsio/runtime/package.json" - packageJSON, err := os.ReadFile(packageJsonFilename) - if err != nil { - panic(err) - } - version := gjson.Get(string(packageJSON), "version").String() - // Parse and increment version - v := semver.MustParse(version) - prerelease := v.Prerelease() - // Split the prerelease by the "." and increment the last part by 1 - parts := strings.Split(prerelease, ".") - prereleaseDigits, err := strconv.Atoi(parts[len(parts)-1]) - if err != nil { - panic(err) - } - prereleaseNumber := strconv.Itoa(prereleaseDigits + 1) - parts[len(parts)-1] = prereleaseNumber - prerelease = strings.Join(parts, ".") - newVersion, err := v.SetPrerelease(prerelease) - if err != nil { - panic(err) - } - - // Set new version using sjson - newJSON, err := sjson.Set(string(packageJSON), "version", newVersion.String()) - if err != nil { - panic(err) - } - - err = os.WriteFile(packageJsonFilename, []byte(newJSON), 0644) }