refactor(core): refactor and fix event system following multiwebview support (#8621)

* clippy

* refactor(core): refactor and fix event system following multiwebview support

* update documentation

* update js docs

* lint

* clippy

* update multiwindow example [skip ci]

* enhance event tests

* fix example

* Update .changes/tauri-event-after-multiwebview.md

Co-authored-by: Lucas Nogueira <118899497+lucasfernog-crabnebula@users.noreply.github.com>

* fix tests

* add diagram

* Add `App/AppHandle` even target

* Discard changes to examples/api/src-tauri/tauri-plugin-sample/permissions/schemas/schema.json

* revert accidental changes

* regenerate schemas

* fix doctests

* add helper methods

* update docs

* update api

* update docs [skip ci]

* update docs [skip ci]

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
Co-authored-by: Lucas Nogueira <118899497+lucasfernog-crabnebula@users.noreply.github.com>
This commit is contained in:
Amr Bashir
2024-02-01 13:06:27 +02:00
committed by GitHub
parent 7fcc0bcd34
commit a093682d2d
45 changed files with 2472 additions and 5836 deletions

View File

@@ -11,24 +11,17 @@
import { invoke, transformCallback } from './core'
type EventSource =
| {
kind: 'global'
}
| {
kind: 'window'
label: string
}
| {
kind: 'webview'
label: string
}
type EventTarget =
| { kind: 'Any' }
| { kind: 'AnyLabel'; label: string }
| { kind: 'App' }
| { kind: 'Window'; label: string }
| { kind: 'Webview'; label: string }
| { kind: 'WebviewWindow'; label: string }
interface Event<T> {
/** Event name */
event: EventName
/** The source of the event. Can be a global event, an event from a window or an event from another webview. */
source: EventSource
/** Event identifier used to unlisten */
id: number
/** Event payload */
@@ -43,16 +36,11 @@ type EventName = `${TauriEvent}` | (string & Record<never, never>)
interface Options {
/**
* Window or webview the function targets.
* The event target to listen to, defaults to `{ kind: 'Any' }`, see {@link EventTarget}.
*
* When listening to events and using this value,
* only events triggered by the window with the given label are received.
*
* When emitting events, only the window with the given label will receive it.
* If a string is provided, {@link EventTarget.AnyLabel} is used.
*/
target?:
| { kind: 'window'; label: string }
| { kind: 'webview'; label: string }
target?: string | EventTarget
}
/**
@@ -89,14 +77,13 @@ async function _unlisten(event: string, eventId: number): Promise<void> {
}
/**
* Listen to an event. The event can be either global or window-specific.
* See {@link Event.source} to check the event source.
* Listen to an emitted event to any {@link EventTarget|target}.
*
* @example
* ```typescript
* import { listen } from '@tauri-apps/api/event';
* const unlisten = await listen<string>('error', (event) => {
* console.log(`Got error in window ${event.source}, payload: ${event.payload}`);
* console.log(`Got error, payload: ${event.payload}`);
* });
*
* // you need to call unlisten if your handler goes out of scope e.g. the component is unmounted
@@ -105,6 +92,7 @@ async function _unlisten(event: string, eventId: number): Promise<void> {
*
* @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`.
* @param handler Event handler callback.
* @param options Event listening options.
* @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.
*
@@ -115,9 +103,13 @@ async function listen<T>(
handler: EventCallback<T>,
options?: Options
): Promise<UnlistenFn> {
const target: EventTarget =
typeof options?.target === 'string'
? { kind: 'AnyLabel', label: options.target }
: options?.target ?? { kind: 'Any' }
return invoke<number>('plugin:event|listen', {
event,
target: options?.target,
target,
handler: transformCallback(handler)
}).then((eventId) => {
return async () => _unlisten(event, eventId)
@@ -125,7 +117,7 @@ async function listen<T>(
}
/**
* Listen to an one-off event. See {@link listen} for more information.
* Listens once to an emitted event to any {@link EventTarget|target}.
*
* @example
* ```typescript
@@ -143,6 +135,8 @@ async function listen<T>(
* ```
*
* @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`.
* @param handler Event handler callback.
* @param options Event listening options.
* @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.
*
@@ -164,7 +158,8 @@ async function once<T>(
}
/**
* Emits an event to the backend and all Tauri windows.
* Emits an event to all {@link EventTarget|targets}.
*
* @example
* ```typescript
* import { emit } from '@tauri-apps/api/event';
@@ -172,28 +167,53 @@ async function once<T>(
* ```
*
* @param event Event name. Must include only alphanumeric characters, `-`, `/`, `:` and `_`.
* @param payload Event payload.
*
* @since 1.0.0
*/
async function emit(
event: string,
payload?: unknown,
options?: Options
): Promise<void> {
async function emit(event: string, payload?: unknown): Promise<void> {
await invoke('plugin:event|emit', {
event,
target: options?.target,
payload
})
}
/**
* Emits an event to all {@link EventTarget|targets} matching the given target.
*
* @example
* ```typescript
* import { emit } from '@tauri-apps/api/event';
* await emit('frontend-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.
*
* @since 1.0.0
*/
async function emitTo(
target: EventTarget | string,
event: string,
payload?: unknown
): Promise<void> {
const eventTarget: EventTarget =
typeof target === 'string' ? { kind: 'AnyLabel', label: target } : target
await invoke('plugin:event|emit_to', {
target: eventTarget,
event,
payload
})
}
export type {
EventSource,
Event,
EventTarget,
EventCallback,
UnlistenFn,
EventName,
Options
}
export { listen, once, emit, TauriEvent }
export { listen, once, emit, emitTo, TauriEvent }

View File

@@ -19,7 +19,16 @@
import { PhysicalPosition, PhysicalSize } from './dpi'
import type { LogicalPosition, LogicalSize } from './dpi'
import type { EventName, EventCallback, UnlistenFn } from './event'
import { TauriEvent, emit, listen, once } from './event'
import {
TauriEvent,
// imported for documentation purposes
// eslint-disable-next-line
type EventTarget,
emit,
emitTo,
listen,
once
} from './event'
import { invoke } from './core'
import { Window, getCurrent as getCurrentWindow } from './window'
import type { WindowOptions } from './window'
@@ -190,7 +199,7 @@ class Webview {
}
/**
* Listen to an event emitted by the backend that is tied to the webview.
* Listen to an emitted event on this webview.
*
* @example
* ```typescript
@@ -220,12 +229,12 @@ class Webview {
})
}
return listen(event, handler, {
target: { kind: 'webview', label: this.label }
target: { kind: 'Webview', label: this.label }
})
}
/**
* Listen to an one-off event emitted by the backend that is tied to the webview.
* Listen to an emitted event on this webview only once.
*
* @example
* ```typescript
@@ -252,12 +261,13 @@ class Webview {
})
}
return once(event, handler, {
target: { kind: 'webview', label: this.label }
target: { kind: 'Webview', label: this.label }
})
}
/**
* Emits an event to the backend, tied to the webview.
* Emits an event to all {@link EventTarget|targets}.
*
* @example
* ```typescript
* import { getCurrent } from '@tauri-apps/api/webview';
@@ -274,15 +284,44 @@ class Webview {
handler({
event,
id: -1,
source: { kind: 'webview', label: this.label },
payload
})
}
return Promise.resolve()
}
return emit(event, payload, {
target: { kind: 'webview', label: this.label }
})
return emit(event, payload)
}
/**
* Emits an event to all {@link EventTarget|targets} matching the given target.
*
* @example
* ```typescript
* import { getCurrent } from '@tauri-apps/api/webview';
* await getCurrent().emit('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: string,
event: string,
payload?: unknown
): Promise<void> {
if (localTauriEvents.includes(event)) {
// eslint-disable-next-line
for (const handler of this.listeners[event] || []) {
handler({
event,
id: -1,
payload
})
}
return Promise.resolve()
}
return emitTo(target, event, payload)
}
/** @ignore */
@@ -604,12 +643,79 @@ class WebviewWindow {
// @ts-expect-error `skip` is not defined in the public API but it is handled by the constructor
return getAll().map((w) => new WebviewWindow(w.label, { skip: true }))
}
/**
* Listen to an emitted event on this webivew window.
*
* @example
* ```typescript
* import { WebviewWindow } from '@tauri-apps/api/webview';
* const unlisten = await WebviewWindow.getCurrent().listen<string>('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<T>(
event: EventName,
handler: EventCallback<T>
): Promise<UnlistenFn> {
if (this._handleTauriEvent(event, handler)) {
return Promise.resolve(() => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, security/detect-object-injection
const listeners = this.listeners[event]
listeners.splice(listeners.indexOf(handler), 1)
})
}
return listen(event, handler, {
target: { kind: 'WebviewWindow', label: this.label }
})
}
/**
* Listen to an emitted event on this webivew window only once.
*
* @example
* ```typescript
* import { WebviewWindow } from '@tauri-apps/api/webview';
* const unlisten = await WebviewWindow.getCurrent().once<null>('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<T>(event: string, handler: EventCallback<T>): Promise<UnlistenFn> {
if (this._handleTauriEvent(event, handler)) {
return Promise.resolve(() => {
// 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: 'WebviewWindow', label: this.label }
})
}
}
// order matters, we use window APIs by default
applyMixins(WebviewWindow, [Webview, Window])
// Order matters, we use window APIs by default
applyMixins(WebviewWindow, [Window, Webview])
/** Extends a base class by other specifed classes */
/** Extends a base class by other specifed classes, wihtout overriding existing properties */
function applyMixins(
baseClass: { prototype: unknown },
extendedClasses: unknown
@@ -619,6 +725,12 @@ function applyMixins(
: [extendedClasses]
).forEach((extendedClass: { prototype: unknown }) => {
Object.getOwnPropertyNames(extendedClass.prototype).forEach((name) => {
if (
typeof baseClass.prototype === 'object' &&
baseClass.prototype &&
name in baseClass.prototype
)
return
Object.defineProperty(
baseClass.prototype,
name,

View File

@@ -22,14 +22,17 @@ import {
PhysicalPosition,
PhysicalSize
} from './dpi'
import type {
Event,
EventName,
EventCallback,
UnlistenFn,
EventSource
import type { Event, EventName, EventCallback, UnlistenFn } from './event'
import {
TauriEvent,
// imported for documentation purposes
// eslint-disable-next-line
type EventTarget,
emit,
emitTo,
listen,
once
} from './event'
import { TauriEvent, emit, listen, once } from './event'
import { invoke } from './core'
import { WebviewWindow } from './webview'
@@ -97,15 +100,12 @@ enum UserAttentionType {
class CloseRequestedEvent {
/** Event name */
event: EventName
/** The source of the event. */
source: EventSource
/** Event identifier used to unlisten */
id: number
private _preventDefault = false
constructor(event: Event<null>) {
this.event = event.event
this.source = event.source
this.id = event.id
}
@@ -360,7 +360,7 @@ class Window {
}
/**
* Listen to an event emitted by the backend that is tied to the window.
* Listen to an emitted event on this window.
*
* @example
* ```typescript
@@ -390,12 +390,12 @@ class Window {
})
}
return listen(event, handler, {
target: { kind: 'window', label: this.label }
target: { kind: 'Window', label: this.label }
})
}
/**
* Listen to an one-off event emitted by the backend that is tied to the window.
* Listen to an emitted event on this window only once.
*
* @example
* ```typescript
@@ -422,12 +422,12 @@ class Window {
})
}
return once(event, handler, {
target: { kind: 'window', label: this.label }
target: { kind: 'Window', label: this.label }
})
}
/**
* Emits an event to the backend, tied to the window.
* Emits an event to all {@link EventTarget|targets}.
* @example
* ```typescript
* import { getCurrent } from '@tauri-apps/api/window';
@@ -444,15 +444,43 @@ class Window {
handler({
event,
id: -1,
source: { kind: 'window', label: this.label },
payload
})
}
return Promise.resolve()
}
return emit(event, payload, {
target: { kind: 'window', label: this.label }
})
return emit(event, payload)
}
/**
* Emits an event to all {@link EventTarget|targets} matching the given target.
*
* @example
* ```typescript
* import { getCurrent } from '@tauri-apps/api/window';
* await getCurrent().emit('window-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: string | EventTarget,
event: string,
payload?: unknown
): Promise<void> {
if (localTauriEvents.includes(event)) {
// eslint-disable-next-line
for (const handler of this.listeners[event] || []) {
handler({
event,
id: -1,
payload
})
}
return Promise.resolve()
}
return emitTo(target, event, payload)
}
/** @ignore */