Migrate runtime to TypeScript

This commit is contained in:
Fabio Massaioli 2025-02-25 03:30:30 +01:00
commit 95b4bfc253
40 changed files with 3317 additions and 2233 deletions

View file

@ -1,5 +0,0 @@
events.test.js
node_modules
types/drag.d.ts
types/contextmenu.d.ts
types/log.d.ts

View file

@ -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",
}
}

View file

@ -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<void>}
*
*/
export function Hide() {
export function Hide(): Promise<void> {
return call(HideMethod);
}
/**
* Calls the ShowMethod and returns the result.
*
* @return {Promise<void>}
*/
export function Show() {
export function Show(): Promise<void> {
return call(ShowMethod);
}
/**
* Calls the QuitMethod to terminate the program.
*
* @return {Promise<void>}
*/
export function Quit() {
export function Quit(): Promise<void> {
return call(QuitMethod);
}

View file

@ -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<string>}
*/
export function OpenURL(url) {
return call(BrowserOpenURL, {url});
}

View file

@ -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<void> {
return call(BrowserOpenURL, {url: url.toString()});
}

View file

@ -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<T>(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<T>(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;

View file

@ -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<any>} - 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<any>} 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<any>} - The result of the method call.
*/
export function ByID(methodID, ...args) {
return Call({
methodID,
args
});
}

View file

@ -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<CancellablePromiseWithResolvers<any>, "promise" | "oncancelled">
const call = newRuntimeCaller(objectNames.Call);
const cancelCall = newRuntimeCaller(objectNames.CancelCall);
const callResponses = new Map<string, PromiseResolvers>();
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<any> {
const id = generateID();
const result = CancellablePromise.withResolvers<any>();
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<any> {
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<any> {
return Call({ methodID, args });
}

View file

@ -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<unknown>;
/**
* 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<unknown>, reason?: any, info?: string) {
super((info ?? "Unhandled rejection in cancelled promise.") + " Reason: " + errorMessage(reason), { cause: reason });
this.promise = promise;
this.name = "CancelledRejectionError";
}
}
type CancellablePromiseResolver<T> = (value: T | PromiseLike<T> | CancellablePromiseLike<T>) => void;
type CancellablePromiseRejector = (reason?: any) => void;
type CancellablePromiseCanceller = (cause?: any) => void | PromiseLike<void>;
type CancellablePromiseExecutor<T> = (resolve: CancellablePromiseResolver<T>, reject: CancellablePromiseRejector) => void;
export interface CancellablePromiseLike<T> {
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1> | CancellablePromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2> | CancellablePromiseLike<TResult2>) | undefined | null): CancellablePromiseLike<TResult1 | TResult2>;
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<T> {
promise: CancellablePromise<T>;
resolve: CancellablePromiseResolver<T>;
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<T> extends Promise<T> implements PromiseLike<T>, CancellablePromiseLike<T> {
// Private fields.
/** @internal */
private [barrierSym]!: Partial<PromiseWithResolvers<void>> | null;
/** @internal */
private readonly [cancelImplSym]!: (reason: CancelError) => void | PromiseLike<void>;
/**
* 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<T>, oncancelled?: CancellablePromiseCanceller) {
let resolve!: (value: T | PromiseLike<T>) => 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<T> = {
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<void> {
return new CancellablePromise<void>((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<T> {
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<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1> | CancellablePromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2> | CancellablePromiseLike<TResult2>) | undefined | null, oncancelled?: CancellablePromiseCanceller): CancellablePromise<TResult1 | TResult2> {
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<T>.
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<PromiseWithResolvers<void>> = {};
this[barrierSym] = barrier;
return new CancellablePromise<TResult1 | TResult2>((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<TResult = never>(onrejected?: ((reason: any) => (PromiseLike<TResult> | TResult)) | undefined | null, oncancelled?: CancellablePromiseCanceller): CancellablePromise<T | TResult> {
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<T> {
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<T>(values: Iterable<T | PromiseLike<T>>): CancellablePromise<Awaited<T>[]>;
static all<T extends readonly unknown[] | []>(values: T): CancellablePromise<{ -readonly [P in keyof T]: Awaited<T[P]>; }>;
static all<T extends Iterable<unknown> | ArrayLike<unknown>>(values: T): CancellablePromise<unknown> {
let collected = Array.from(values);
return collected.length === 0
? CancellablePromise.resolve(collected)
: new CancellablePromise<unknown>((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<T>(values: Iterable<T | PromiseLike<T>>): CancellablePromise<PromiseSettledResult<Awaited<T>>[]>;
static allSettled<T extends readonly unknown[] | []>(values: T): CancellablePromise<{ -readonly [P in keyof T]: PromiseSettledResult<Awaited<T[P]>>; }>;
static allSettled<T extends Iterable<unknown> | ArrayLike<unknown>>(values: T): CancellablePromise<unknown> {
let collected = Array.from(values);
return collected.length === 0
? CancellablePromise.resolve(collected)
: new CancellablePromise<unknown>((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<T>(values: Iterable<T | PromiseLike<T>>): CancellablePromise<Awaited<T>>;
static any<T extends readonly unknown[] | []>(values: T): CancellablePromise<Awaited<T[number]>>;
static any<T extends Iterable<unknown> | ArrayLike<unknown>>(values: T): CancellablePromise<unknown> {
let collected = Array.from(values);
return collected.length === 0
? CancellablePromise.resolve(collected)
: new CancellablePromise<unknown>((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<T>(values: Iterable<T | PromiseLike<T>>): CancellablePromise<Awaited<T>>;
static race<T extends readonly unknown[] | []>(values: T): CancellablePromise<Awaited<T[number]>>;
static race<T extends Iterable<unknown> | ArrayLike<unknown>>(values: T): CancellablePromise<unknown> {
let collected = Array.from(values);
return new CancellablePromise<unknown>((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<T = never>(cause?: any): CancellablePromise<T> {
const p = new CancellablePromise<T>(() => {});
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<T = never>(milliseconds: number, cause?: any): CancellablePromise<T> {
const promise = new CancellablePromise<T>(() => {});
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<void>;
/**
* 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<T>(milliseconds: number, value: T): CancellablePromise<T>;
static sleep<T = void>(milliseconds: number, value?: T): CancellablePromise<T> {
return new CancellablePromise<T>((resolve) => {
setTimeout(() => resolve(value!), milliseconds);
});
}
/**
* Creates a new rejected CancellablePromise for the provided reason.
*
* @group Static Methods
*/
static reject<T = never>(reason?: any): CancellablePromise<T> {
return new CancellablePromise<T>((_, reject) => reject(reason));
}
/**
* Creates a new resolved CancellablePromise.
*
* @group Static Methods
*/
static resolve(): CancellablePromise<void>;
/**
* Creates a new resolved CancellablePromise for the provided value.
*
* @group Static Methods
*/
static resolve<T>(value: T): CancellablePromise<Awaited<T>>;
/**
* Creates a new resolved CancellablePromise for the provided value.
*
* @group Static Methods
*/
static resolve<T>(value: T | PromiseLike<T>): CancellablePromise<Awaited<T>>;
static resolve<T = void>(value?: T | PromiseLike<T>): CancellablePromise<Awaited<T>> {
if (value instanceof CancellablePromise) {
// Optimise for cancellable promises.
return value;
}
return new CancellablePromise<any>((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<T>(): CancellablePromiseWithResolvers<T> {
let result: CancellablePromiseWithResolvers<T> = { oncancelled: null } as any;
result.promise = new CancellablePromise<T>((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<T>(promise: CancellablePromiseWithResolvers<T>, state: CancellablePromiseState) {
let cancellationPromise: void | PromiseLike<void> = undefined;
return (reason: CancelError): void | PromiseLike<void> => {
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<T>(promise: CancellablePromiseWithResolvers<T>, state: CancellablePromiseState): CancellablePromiseResolver<T> {
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<T>(promise: CancellablePromiseWithResolvers<T>, 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<any>): 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<T>(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 "<could not convert error to string>";
}
/**
* Gets the current barrier promise for the given cancellable promise. If necessary, initialises the barrier.
*/
function currentBarrier<T>(promise: CancellablePromise<T>): Promise<void> {
let pwr: Partial<PromiseWithResolvers<void>> = promise[barrierSym] ?? {};
if (!('promise' in pwr)) {
Object.assign(pwr, promiseWithResolvers<void>());
}
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 <T>(): PromiseWithResolvers<T> {
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: any) => void;
const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej; });
return { promise, resolve, reject };
}
}

View file

@ -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<string>} A promise that resolves with the text from the Clipboard.
*/
export function Text() {
return call(ClipboardText);
}

View file

@ -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<void> {
return call(ClipboardSetText, {text});
}
/**
* Get the Clipboard text
*
* @returns A promise that resolves with the text from the Clipboard.
*/
export function Text(): Promise<string> {
return call(ClipboardText);
}

View file

@ -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;
}
}

View file

@ -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<T>(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<T>(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<V>(key: (source: any) => string, value: (source: any) => V): (source: any) => Record<string, V> {
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<T>(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<T[Key]> }} 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<T[Key]> }
>(createField: T): (source: any) => U {
let allAny = true;
for (const name in createField) {
if (createField[name] !== Any) {

View file

@ -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<string>} - The label of the button pressed
*/
export const Info = (options) => dialog(DialogInfo, options);
/**
* @param {MessageDialogOptions} options - Dialog options
* @returns {Promise<string>} - The label of the button pressed
*/
export const Warning = (options) => dialog(DialogWarning, options);
/**
* @param {MessageDialogOptions} options - Dialog options
* @returns {Promise<string>} - The label of the button pressed
*/
export const Error = (options) => dialog(DialogError, options);
/**
* @param {MessageDialogOptions} options - Dialog options
* @returns {Promise<string>} - The label of the button pressed
*/
export const Question = (options) => dialog(DialogQuestion, options);
/**
* @param {OpenFileDialogOptions} options - Dialog options
* @returns {Promise<string[]|string>} 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<string>} Returns the selected file. Returns blank string if no file is selected.
*/
export const SaveFile = (options) => dialog(DialogSaveFile, options);

View file

@ -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<PromiseWithResolvers<any>, "promise">;
const call = newRuntimeCaller(objectNames.Dialog);
const dialogResponses = new Map<string, PromiseResolvers>();
// 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<any> {
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<string> { 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<string> { 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<string> { 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<string> { 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<string[]>;
export function OpenFile(options: OpenFileDialogOptions & { AllowsMultipleSelection?: false | undefined }): Promise<string>;
export function OpenFile(options: OpenFileDialogOptions): Promise<string | string[]>;
export function OpenFile(options: OpenFileDialogOptions): Promise<string | string[]> { 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<string> { return dialog(DialogSaveFile, options); }

View file

@ -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");
}

View file

@ -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();
}

View file

@ -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",
},
};
}),
});

View file

@ -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); }

View file

@ -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<void> {
return call(EmitMethod, event);
}

View file

@ -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);
}
}

View file

@ -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 });
}
}

View file

@ -0,0 +1,17 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The electron alternative for Go
(c) Lea Anthony 2019-present
*/
declare global {
interface Window {
_wails: Record<string, any>;
}
}
export {};

View file

@ -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

View file

@ -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");

View file

@ -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<string, Listener[]>();
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);
}
}

View file

@ -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
}
}

View file

@ -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();
}
}

View file

@ -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<any> {
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<string, string> = {
["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();
}
}

View file

@ -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<Screen[]>} A promise that resolves to an array of Screen objects.
*/
export function GetAll() {
return call(getAll);
}
/**
* Gets the primary screen.
* @returns {Promise<Screen>} A promise that resolves to the primary screen.
*/
export function GetPrimary() {
return call(getPrimary);
}
/**
* Gets the current active screen.
*
* @returns {Promise<Screen>} A promise that resolves with the current active screen.
*/
export function GetCurrent() {
return call(getCurrent);
}

View file

@ -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<Screen[]> {
return call(getAll);
}
/**
* Gets the primary screen.
*
* @returns A promise that resolves to the primary screen.
*/
export function GetPrimary(): Promise<Screen> {
return call(getPrimary);
}
/**
* Gets the current active screen.
*
* @returns A promise that resolves with the current active screen.
*/
export function GetCurrent(): Promise<Screen> {
return call(getCurrent);
}

View file

@ -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<boolean>} - 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<Object>} 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<EnvironmentInfo>} - 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;
}

View file

@ -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<boolean> {
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<Record<string, any>> {
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<string, any>;
}
/**
* Retrieves environment details.
*
* @returns A promise that resolves to an object containing OS and system architecture.
*/
export function Environment(): Promise<EnvironmentInfo> {
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);
}

View file

@ -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 {

View file

@ -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<Position>} - The current absolute position of the window.
*/
Position() {
return this[caller](PositionMethod);
}
/**
* Centers the window on the screen.
*
* @public
* @return {Promise<void>}
*/
Center() {
return this[caller](CenterMethod);
}
/**
* Closes the window.
*
* @public
* @return {Promise<void>}
*/
Close() {
return this[caller](CloseMethod);
}
/**
* Disables min/max size constraints.
*
* @public
* @return {Promise<void>}
*/
DisableSizeConstraints() {
return this[caller](DisableSizeConstraintsMethod);
}
/**
* Enables min/max size constraints.
*
* @public
* @return {Promise<void>}
*/
EnableSizeConstraints() {
return this[caller](EnableSizeConstraintsMethod);
}
/**
* Focuses the window.
*
* @public
* @return {Promise<void>}
*/
Focus() {
return this[caller](FocusMethod);
}
/**
* Forces the window to reload the page assets.
*
* @public
* @return {Promise<void>}
*/
ForceReload() {
return this[caller](ForceReloadMethod);
}
/**
* Doc.
*
* @public
* @return {Promise<void>}
*/
Fullscreen() {
return this[caller](FullscreenMethod);
}
/**
* Returns the screen that the window is on.
*
* @public
* @return {Promise<Screen>} - The screen the window is currently on
*/
GetScreen() {
return this[caller](GetScreenMethod);
}
/**
* Returns the current zoom level of the window.
*
* @public
* @return {Promise<number>} - The current zoom level
*/
GetZoom() {
return this[caller](GetZoomMethod);
}
/**
* Returns the height of the window.
*
* @public
* @return {Promise<number>} - The current height of the window
*/
Height() {
return this[caller](HeightMethod);
}
/**
* Hides the window.
*
* @public
* @return {Promise<void>}
*/
Hide() {
return this[caller](HideMethod);
}
/**
* Returns true if the window is focused.
*
* @public
* @return {Promise<boolean>} - Whether the window is currently focused
*/
IsFocused() {
return this[caller](IsFocusedMethod);
}
/**
* Returns true if the window is fullscreen.
*
* @public
* @return {Promise<boolean>} - Whether the window is currently fullscreen
*/
IsFullscreen() {
return this[caller](IsFullscreenMethod);
}
/**
* Returns true if the window is maximised.
*
* @public
* @return {Promise<boolean>} - Whether the window is currently maximised
*/
IsMaximised() {
return this[caller](IsMaximisedMethod);
}
/**
* Returns true if the window is minimised.
*
* @public
* @return {Promise<boolean>} - Whether the window is currently minimised
*/
IsMinimised() {
return this[caller](IsMinimisedMethod);
}
/**
* Maximises the window.
*
* @public
* @return {Promise<void>}
*/
Maximise() {
return this[caller](MaximiseMethod);
}
/**
* Minimises the window.
*
* @public
* @return {Promise<void>}
*/
Minimise() {
return this[caller](MinimiseMethod);
}
/**
* Returns the name of the window.
*
* @public
* @return {Promise<string>} - The name of the window
*/
Name() {
return this[caller](NameMethod);
}
/**
* Opens the development tools pane.
*
* @public
* @return {Promise<void>}
*/
OpenDevTools() {
return this[caller](OpenDevToolsMethod);
}
/**
* Returns the relative position of the window to the screen.
*
* @public
* @return {Promise<Position>} - The current relative position of the window
*/
RelativePosition() {
return this[caller](RelativePositionMethod);
}
/**
* Reloads the page assets.
*
* @public
* @return {Promise<void>}
*/
Reload() {
return this[caller](ReloadMethod);
}
/**
* Returns true if the window is resizable.
*
* @public
* @return {Promise<boolean>} - 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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
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<void>}
*/
SetTitle(title) {
return this[caller](SetTitleMethod, { title });
}
/**
* Sets the zoom level of the window.
*
* @public
* @param {number} zoom - The desired zoom level
* @return {Promise<void>}
*/
SetZoom(zoom) {
return this[caller](SetZoomMethod, { zoom });
}
/**
* Shows the window.
*
* @public
* @return {Promise<void>}
*/
Show() {
return this[caller](ShowMethod);
}
/**
* Returns the size of the window.
*
* @public
* @return {Promise<Size>} - The current size of the window
*/
Size() {
return this[caller](SizeMethod);
}
/**
* Toggles the window between fullscreen and normal.
*
* @public
* @return {Promise<void>}
*/
ToggleFullscreen() {
return this[caller](ToggleFullscreenMethod);
}
/**
* Toggles the window between maximised and normal.
*
* @public
* @return {Promise<void>}
*/
ToggleMaximise() {
return this[caller](ToggleMaximiseMethod);
}
/**
* Un-fullscreens the window.
*
* @public
* @return {Promise<void>}
*/
UnFullscreen() {
return this[caller](UnFullscreenMethod);
}
/**
* Un-maximises the window.
*
* @public
* @return {Promise<void>}
*/
UnMaximise() {
return this[caller](UnMaximiseMethod);
}
/**
* Un-minimises the window.
*
* @public
* @return {Promise<void>}
*/
UnMinimise() {
return this[caller](UnMinimiseMethod);
}
/**
* Returns the width of the window.
*
* @public
* @return {Promise<number>} - The current width of the window
*/
Width() {
return this[caller](WidthMethod);
}
/**
* Zooms the window.
*
* @public
* @return {Promise<void>}
*/
Zoom() {
return this[caller](ZoomMethod);
}
/**
* Increases the zoom level of the webview content.
*
* @public
* @return {Promise<void>}
*/
ZoomIn() {
return this[caller](ZoomInMethod);
}
/**
* Decreases the zoom level of the webview content.
*
* @public
* @return {Promise<void>}
*/
ZoomOut() {
return this[caller](ZoomOutMethod);
}
/**
* Resets the zoom level of the webview content.
*
* @public
* @return {Promise<void>}
*/
ZoomReset() {
return this[caller](ZoomResetMethod);
}
}
/**
* The window within which the script is running.
*
* @type {Window}
*/
const thisWindow = new Window('');
export default thisWindow;

View file

@ -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<any>;
/**
* 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<Position> {
return this[callerSym](PositionMethod);
}
/**
* Centers the window on the screen.
*/
Center(): Promise<void> {
return this[callerSym](CenterMethod);
}
/**
* Closes the window.
*/
Close(): Promise<void> {
return this[callerSym](CloseMethod);
}
/**
* Disables min/max size constraints.
*/
DisableSizeConstraints(): Promise<void> {
return this[callerSym](DisableSizeConstraintsMethod);
}
/**
* Enables min/max size constraints.
*/
EnableSizeConstraints(): Promise<void> {
return this[callerSym](EnableSizeConstraintsMethod);
}
/**
* Focuses the window.
*/
Focus(): Promise<void> {
return this[callerSym](FocusMethod);
}
/**
* Forces the window to reload the page assets.
*/
ForceReload(): Promise<void> {
return this[callerSym](ForceReloadMethod);
}
/**
* Switches the window to fullscreen mode.
*/
Fullscreen(): Promise<void> {
return this[callerSym](FullscreenMethod);
}
/**
* Returns the screen that the window is on.
*
* @returns The screen the window is currently on.
*/
GetScreen(): Promise<Screen> {
return this[callerSym](GetScreenMethod);
}
/**
* Returns the current zoom level of the window.
*
* @returns The current zoom level.
*/
GetZoom(): Promise<number> {
return this[callerSym](GetZoomMethod);
}
/**
* Returns the height of the window.
*
* @returns The current height of the window.
*/
Height(): Promise<number> {
return this[callerSym](HeightMethod);
}
/**
* Hides the window.
*/
Hide(): Promise<void> {
return this[callerSym](HideMethod);
}
/**
* Returns true if the window is focused.
*
* @returns Whether the window is currently focused.
*/
IsFocused(): Promise<boolean> {
return this[callerSym](IsFocusedMethod);
}
/**
* Returns true if the window is fullscreen.
*
* @returns Whether the window is currently fullscreen.
*/
IsFullscreen(): Promise<boolean> {
return this[callerSym](IsFullscreenMethod);
}
/**
* Returns true if the window is maximised.
*
* @returns Whether the window is currently maximised.
*/
IsMaximised(): Promise<boolean> {
return this[callerSym](IsMaximisedMethod);
}
/**
* Returns true if the window is minimised.
*
* @returns Whether the window is currently minimised.
*/
IsMinimised(): Promise<boolean> {
return this[callerSym](IsMinimisedMethod);
}
/**
* Maximises the window.
*/
Maximise(): Promise<void> {
return this[callerSym](MaximiseMethod);
}
/**
* Minimises the window.
*/
Minimise(): Promise<void> {
return this[callerSym](MinimiseMethod);
}
/**
* Returns the name of the window.
*
* @returns The name of the window.
*/
Name(): Promise<string> {
return this[callerSym](NameMethod);
}
/**
* Opens the development tools pane.
*/
OpenDevTools(): Promise<void> {
return this[callerSym](OpenDevToolsMethod);
}
/**
* Returns the relative position of the window to the screen.
*
* @returns The current relative position of the window.
*/
RelativePosition(): Promise<Position> {
return this[callerSym](RelativePositionMethod);
}
/**
* Reloads the page assets.
*/
Reload(): Promise<void> {
return this[callerSym](ReloadMethod);
}
/**
* Returns true if the window is resizable.
*
* @returns Whether the window is currently resizable.
*/
Resizable(): Promise<boolean> {
return this[callerSym](ResizableMethod);
}
/**
* Restores the window to its previous state if it was previously minimised, maximised or fullscreen.
*/
Restore(): Promise<void> {
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<void> {
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<void> {
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<void> {
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<void> {
return this[callerSym](SetFramelessMethod, { frameless });
}
/**
* Disables the system fullscreen button.
*
* @param enabled - Whether the fullscreen button should be enabled.
*/
SetFullscreenButtonEnabled(enabled: boolean): Promise<void> {
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<void> {
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<void> {
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<void> {
return this[callerSym](SetRelativePositionMethod, { x, y });
}
/**
* Sets whether the window is resizable.
*
* @param resizable - Whether the window should be resizable.
*/
SetResizable(resizable: boolean): Promise<void> {
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<void> {
return this[callerSym](SetSizeMethod, { width, height });
}
/**
* Sets the title of the window.
*
* @param title - The desired title of the window.
*/
SetTitle(title: string): Promise<void> {
return this[callerSym](SetTitleMethod, { title });
}
/**
* Sets the zoom level of the window.
*
* @param zoom - The desired zoom level.
*/
SetZoom(zoom: number): Promise<void> {
return this[callerSym](SetZoomMethod, { zoom });
}
/**
* Shows the window.
*/
Show(): Promise<void> {
return this[callerSym](ShowMethod);
}
/**
* Returns the size of the window.
*
* @returns The current size of the window.
*/
Size(): Promise<Size> {
return this[callerSym](SizeMethod);
}
/**
* Toggles the window between fullscreen and normal.
*/
ToggleFullscreen(): Promise<void> {
return this[callerSym](ToggleFullscreenMethod);
}
/**
* Toggles the window between maximised and normal.
*/
ToggleMaximise(): Promise<void> {
return this[callerSym](ToggleMaximiseMethod);
}
/**
* Un-fullscreens the window.
*/
UnFullscreen(): Promise<void> {
return this[callerSym](UnFullscreenMethod);
}
/**
* Un-maximises the window.
*/
UnMaximise(): Promise<void> {
return this[callerSym](UnMaximiseMethod);
}
/**
* Un-minimises the window.
*/
UnMinimise(): Promise<void> {
return this[callerSym](UnMinimiseMethod);
}
/**
* Returns the width of the window.
*
* @returns The current width of the window.
*/
Width(): Promise<number> {
return this[callerSym](WidthMethod);
}
/**
* Zooms the window.
*/
Zoom(): Promise<void> {
return this[callerSym](ZoomMethod);
}
/**
* Increases the zoom level of the webview content.
*/
ZoomIn(): Promise<void> {
return this[callerSym](ZoomInMethod);
}
/**
* Decreases the zoom level of the webview content.
*/
ZoomOut(): Promise<void> {
return this[callerSym](ZoomOutMethod);
}
/**
* Resets the zoom level of the webview content.
*/
ZoomReset(): Promise<void> {
return this[callerSym](ZoomResetMethod);
}
}
/**
* The window within which the script is running.
*/
const thisWindow = new Window('');
export default thisWindow;

View file

@ -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<HTMLElement, string[]>}
*/
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);
}

View file

@ -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<Element, string[]>;
/** 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);
}

View file

@ -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
}
}

View file

@ -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)
}