import { PhysicalPosition, PhysicalSize, Size, Position } from './dpi.js'; import { listen, once, emit, emitTo, TauriEvent } from './event.js'; import { invoke } from './core.js'; import { getCurrentWindow, Window } from './window.js'; // Copyright 2019-2024 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT /** * Provides APIs to create webviews, communicate with other webviews and manipulate the current webview. * * #### Webview events * * Events can be listened to using {@link Webview.listen}: * ```typescript * import { getCurrentWebview } from "@tauri-apps/api/webview"; * getCurrentWebview().listen("my-webview-event", ({ event, payload }) => { }); * ``` * * @module */ /** * Get an instance of `Webview` for the current webview. * * @since 2.0.0 */ function getCurrentWebview() { return new Webview(getCurrentWindow(), window.__TAURI_INTERNALS__.metadata.currentWebview.label, { // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor skip: true }); } /** * Gets a list of instances of `Webview` for all available webviews. * * @since 2.0.0 */ async function getAllWebviews() { return invoke('plugin:webview|get_all_webviews').then((webviews) => webviews.map((w) => new Webview(new Window(w.windowLabel, { // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor skip: true }), w.label, { // @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor skip: true }))); } /** @ignore */ // events that are emitted right here instead of by the created webview const localTauriEvents = ['tauri://created', 'tauri://error']; /** * Create new webview or get a handle to an existing one. * * Webviews are identified by a *label* a unique identifier that can be used to reference it later. * It may only contain alphanumeric characters `a-zA-Z` plus the following special characters `-`, `/`, `:` and `_`. * * @example * ```typescript * import { Window } from "@tauri-apps/api/window" * import { Webview } from "@tauri-apps/api/webview" * * const appWindow = new Window('uniqueLabel'); * * // loading embedded asset: * const webview = new Webview(appWindow, 'theUniqueLabel', { * url: 'path/to/page.html' * }); * // alternatively, load a remote URL: * const webview = new Webview(appWindow, 'theUniqueLabel', { * url: 'https://github.com/tauri-apps/tauri' * }); * * webview.once('tauri://created', function () { * // webview successfully created * }); * webview.once('tauri://error', function (e) { * // an error happened creating the webview * }); * * // emit an event to the backend * await webview.emit("some-event", "data"); * // listen to an event from the backend * const unlisten = await webview.listen("event-name", e => {}); * unlisten(); * ``` * * @since 2.0.0 */ class Webview { /** * Creates a new Webview. * @example * ```typescript * import { Window } from '@tauri-apps/api/window' * import { Webview } from '@tauri-apps/api/webview' * const appWindow = new Window('my-label') * const webview = new Webview(appWindow, 'my-label', { * url: 'https://github.com/tauri-apps/tauri' * }); * webview.once('tauri://created', function () { * // webview successfully created * }); * webview.once('tauri://error', function (e) { * // an error happened creating the webview * }); * ``` * * @param window the window to add this webview to. * @param label The unique webview label. Must be alphanumeric: `a-zA-Z-/:_`. * @returns The {@link Webview} instance to communicate with the webview. */ constructor(window, label, options) { this.window = window; this.label = label; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment this.listeners = Object.create(null); // @ts-expect-error `skip` is not a public API so it is not defined in WebviewOptions if (!(options === null || options === void 0 ? void 0 : options.skip)) { invoke('plugin:webview|create_webview', { windowLabel: window.label, label, options }) .then(async () => this.emit('tauri://created')) .catch(async (e) => this.emit('tauri://error', e)); } } /** * Gets the Webview for the webview associated with the given label. * @example * ```typescript * import { Webview } from '@tauri-apps/api/webview'; * const mainWebview = Webview.getByLabel('main'); * ``` * * @param label The webview label. * @returns The Webview instance to communicate with the webview or null if the webview doesn't exist. */ static async getByLabel(label) { var _a; return (_a = (await getAllWebviews()).find((w) => w.label === label)) !== null && _a !== void 0 ? _a : null; } /** * Get an instance of `Webview` for the current webview. */ static getCurrent() { return getCurrentWebview(); } /** * Gets a list of instances of `Webview` for all available webviews. */ static async getAll() { return getAllWebviews(); } /** * Listen to an emitted event on this webview. * * @example * ```typescript * import { getCurrentWebview } from '@tauri-apps/api/webview'; * const unlisten = await getCurrentWebview().listen('state-changed', (event) => { * console.log(`Got error: ${payload}`); * }); * * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted * unlisten(); * ``` * * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. * @param handler Event handler. * @returns A promise resolving to a function to unlisten to the event. * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. */ async listen(event, handler) { if (this._handleTauriEvent(event, handler)) { return () => { // eslint-disable-next-line security/detect-object-injection const listeners = this.listeners[event]; listeners.splice(listeners.indexOf(handler), 1); }; } return listen(event, handler, { target: { kind: 'Webview', label: this.label } }); } /** * Listen to an emitted event on this webview only once. * * @example * ```typescript * import { getCurrentWebview } from '@tauri-apps/api/webview'; * const unlisten = await getCurrent().once('initialized', (event) => { * console.log(`Webview initialized!`); * }); * * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted * unlisten(); * ``` * * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. * @param handler Event handler. * @returns A promise resolving to a function to unlisten to the event. * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. */ async once(event, handler) { if (this._handleTauriEvent(event, handler)) { return () => { // eslint-disable-next-line security/detect-object-injection const listeners = this.listeners[event]; listeners.splice(listeners.indexOf(handler), 1); }; } return once(event, handler, { target: { kind: 'Webview', label: this.label } }); } /** * Emits an event to all {@link EventTarget|targets}. * * @example * ```typescript * import { getCurrentWebview } from '@tauri-apps/api/webview'; * await getCurrentWebview().emit('webview-loaded', { loggedIn: true, token: 'authToken' }); * ``` * * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. * @param payload Event payload. */ async emit(event, payload) { if (localTauriEvents.includes(event)) { // eslint-disable-next-line for (const handler of this.listeners[event] || []) { handler({ event, id: -1, payload }); } return; } return emit(event, payload); } /** * Emits an event to all {@link EventTarget|targets} matching the given target. * * @example * ```typescript * import { getCurrentWebview } from '@tauri-apps/api/webview'; * await getCurrentWebview().emitTo('main', 'webview-loaded', { loggedIn: true, token: 'authToken' }); * ``` * * @param target Label of the target Window/Webview/WebviewWindow or raw {@link EventTarget} object. * @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`. * @param payload Event payload. */ async emitTo(target, event, payload) { if (localTauriEvents.includes(event)) { // eslint-disable-next-line for (const handler of this.listeners[event] || []) { handler({ event, id: -1, payload }); } return; } return emitTo(target, event, payload); } /** @ignore */ _handleTauriEvent(event, handler) { if (localTauriEvents.includes(event)) { if (!(event in this.listeners)) { // eslint-disable-next-line security/detect-object-injection this.listeners[event] = [handler]; } else { // eslint-disable-next-line security/detect-object-injection this.listeners[event].push(handler); } return true; } return false; } // Getters /** * The position of the top-left hand corner of the webview's client area relative to the top-left hand corner of the desktop. * @example * ```typescript * import { getCurrentWebview } from '@tauri-apps/api/webview'; * const position = await getCurrentWebview().position(); * ``` * * @returns The webview's position. */ async position() { return invoke('plugin:webview|webview_position', { label: this.label }).then((p) => new PhysicalPosition(p)); } /** * The physical size of the webview's client area. * The client area is the content of the webview, excluding the title bar and borders. * @example * ```typescript * import { getCurrentWebview } from '@tauri-apps/api/webview'; * const size = await getCurrentWebview().size(); * ``` * * @returns The webview's size. */ async size() { return invoke('plugin:webview|webview_size', { label: this.label }).then((s) => new PhysicalSize(s)); } // Setters /** * Closes the webview. * @example * ```typescript * import { getCurrentWebview } from '@tauri-apps/api/webview'; * await getCurrentWebview().close(); * ``` * * @returns A promise indicating the success or failure of the operation. */ async close() { return invoke('plugin:webview|webview_close', { label: this.label }); } /** * Resizes the webview. * @example * ```typescript * import { getCurrent, LogicalSize } from '@tauri-apps/api/webview'; * await getCurrentWebview().setSize(new LogicalSize(600, 500)); * ``` * * @param size The logical or physical size. * @returns A promise indicating the success or failure of the operation. */ async setSize(size) { return invoke('plugin:webview|set_webview_size', { label: this.label, value: size instanceof Size ? size : new Size(size) }); } /** * Sets the webview position. * @example * ```typescript * import { getCurrent, LogicalPosition } from '@tauri-apps/api/webview'; * await getCurrentWebview().setPosition(new LogicalPosition(600, 500)); * ``` * * @param position The new position, in logical or physical pixels. * @returns A promise indicating the success or failure of the operation. */ async setPosition(position) { return invoke('plugin:webview|set_webview_position', { label: this.label, value: position instanceof Position ? position : new Position(position) }); } /** * Bring the webview to front and focus. * @example * ```typescript * import { getCurrentWebview } from '@tauri-apps/api/webview'; * await getCurrentWebview().setFocus(); * ``` * * @returns A promise indicating the success or failure of the operation. */ async setFocus() { return invoke('plugin:webview|set_webview_focus', { label: this.label }); } /** * Hide the webview. * @example * ```typescript * import { getCurrentWebview } from '@tauri-apps/api/webview'; * await getCurrentWebview().hide(); * ``` * * @returns A promise indicating the success or failure of the operation. */ async hide() { return invoke('plugin:webview|webview_hide', { label: this.label }); } /** * Show the webview. * @example * ```typescript * import { getCurrentWebview } from '@tauri-apps/api/webview'; * await getCurrentWebview().show(); * ``` * * @returns A promise indicating the success or failure of the operation. */ async show() { return invoke('plugin:webview|webview_show', { label: this.label }); } /** * Set webview zoom level. * @example * ```typescript * import { getCurrentWebview } from '@tauri-apps/api/webview'; * await getCurrentWebview().setZoom(1.5); * ``` * * @returns A promise indicating the success or failure of the operation. */ async setZoom(scaleFactor) { return invoke('plugin:webview|set_webview_zoom', { label: this.label, value: scaleFactor }); } /** * Moves this webview to the given label. * @example * ```typescript * import { getCurrentWebview } from '@tauri-apps/api/webview'; * await getCurrentWebview().reparent('other-window'); * ``` * * @returns A promise indicating the success or failure of the operation. */ async reparent(window) { return invoke('plugin:webview|reparent', { label: this.label, window: typeof window === 'string' ? window : window.label }); } /** * Clears all browsing data for this webview. * @example * ```typescript * import { getCurrentWebview } from '@tauri-apps/api/webview'; * await getCurrentWebview().clearAllBrowsingData(); * ``` * * @returns A promise indicating the success or failure of the operation. */ async clearAllBrowsingData() { return invoke('plugin:webview|clear_all_browsing_data'); } /** * Specify the webview background color. * * #### Platfrom-specific: * * - **macOS / iOS**: Not implemented. * - **Windows**: * - On Windows 7, transparency is not supported and the alpha value will be ignored. * - On Windows higher than 7: translucent colors are not supported so any alpha value other than `0` will be replaced by `255` * * @returns A promise indicating the success or failure of the operation. * * @since 2.1.0 */ async setBackgroundColor(color) { return invoke('plugin:webview|set_webview_background_color', { color }); } // Listeners /** * Listen to a file drop event. * The listener is triggered when the user hovers the selected files on the webview, * drops the files or cancels the operation. * * @example * ```typescript * import { getCurrentWebview } from "@tauri-apps/api/webview"; * const unlisten = await getCurrentWebview().onDragDropEvent((event) => { * if (event.payload.type === 'over') { * console.log('User hovering', event.payload.position); * } else if (event.payload.type === 'drop') { * console.log('User dropped', event.payload.paths); * } else { * console.log('File drop cancelled'); * } * }); * * // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted * unlisten(); * ``` * * When the debugger panel is open, the drop position of this event may be inaccurate due to a known limitation. * To retrieve the correct drop position, please detach the debugger. * * @returns A promise resolving to a function to unlisten to the event. * Note that removing the listener is required if your listener goes out of scope e.g. the component is unmounted. */ async onDragDropEvent(handler) { const unlistenDragEnter = await this.listen(TauriEvent.DRAG_ENTER, (event) => { handler({ ...event, payload: { type: 'enter', paths: event.payload.paths, position: new PhysicalPosition(event.payload.position) } }); }); const unlistenDragOver = await this.listen(TauriEvent.DRAG_OVER, (event) => { handler({ ...event, payload: { type: 'over', position: new PhysicalPosition(event.payload.position) } }); }); const unlistenDragDrop = await this.listen(TauriEvent.DRAG_DROP, (event) => { handler({ ...event, payload: { type: 'drop', paths: event.payload.paths, position: new PhysicalPosition(event.payload.position) } }); }); const unlistenDragLeave = await this.listen(TauriEvent.DRAG_LEAVE, (event) => { handler({ ...event, payload: { type: 'leave' } }); }); return () => { unlistenDragEnter(); unlistenDragDrop(); unlistenDragOver(); unlistenDragLeave(); }; } } export { Webview, getAllWebviews, getCurrentWebview };