refactor(editor): separate lit and slot in global (#10666)

This commit is contained in:
Saul-Mirone
2025-03-06 10:24:59 +00:00
parent 56b842f2e1
commit 84e2dda3f8
322 changed files with 366 additions and 353 deletions
@@ -1,100 +0,0 @@
type DisposeCallback = () => void;
export interface Disposable {
dispose: DisposeCallback;
}
export interface DisposableManager extends Disposable {
add(d: Disposable | DisposeCallback): void;
}
export class DisposableGroup implements DisposableManager {
private _disposables: Disposable[] = [];
private _disposed = false;
get disposed() {
return this._disposed;
}
/**
* Add to group to be disposed with others.
* This will be immediately disposed if this group has already been disposed.
*/
add(d: Disposable | DisposeCallback) {
if (typeof d === 'function') {
if (this._disposed) d();
else this._disposables.push({ dispose: d });
} else {
if (this._disposed) d.dispose();
else this._disposables.push(d);
}
}
addFromEvent<N extends keyof WindowEventMap>(
element: Window,
eventName: N,
handler: (e: WindowEventMap[N]) => void,
options?: boolean | AddEventListenerOptions
): void;
addFromEvent<N extends keyof DocumentEventMap>(
element: Document,
eventName: N,
handler: (e: DocumentEventMap[N]) => void,
eventOptions?: boolean | AddEventListenerOptions
): void;
addFromEvent<N extends keyof HTMLElementEventMap>(
element: HTMLElement,
eventName: N,
handler: (e: HTMLElementEventMap[N]) => void,
eventOptions?: boolean | AddEventListenerOptions
): void;
addFromEvent<N extends keyof VisualViewportEventMap>(
element: VisualViewport,
eventName: N,
handler: (e: VisualViewportEventMap[N]) => void,
eventOptions?: boolean | AddEventListenerOptions
): void;
addFromEvent<N extends keyof VirtualKeyboardEventMap>(
element: VirtualKeyboard,
eventName: N,
handler: (e: VirtualKeyboardEventMap[N]) => void,
eventOptions?: boolean | AddEventListenerOptions
): void;
addFromEvent(
target: HTMLElement | Window | Document | VisualViewport | VirtualKeyboard,
type: string,
handler: (e: Event) => void,
eventOptions?: boolean | AddEventListenerOptions
) {
this.add({
dispose: () => {
target.removeEventListener(type, handler as () => void, eventOptions);
},
});
target.addEventListener(type, handler as () => void, eventOptions);
}
dispose() {
disposeAll(this._disposables);
this._disposables = [];
this._disposed = true;
}
}
export function flattenDisposables(disposables: Disposable[]): Disposable {
return {
dispose: () => disposeAll(disposables),
};
}
function disposeAll(disposables: Disposable[]) {
for (const disposable of disposables) {
try {
disposable.dispose();
} catch (err) {
console.error(err);
}
}
}
@@ -1,8 +1,4 @@
export * from './crypto.js';
export * from './disposable.js';
export * from './function.js';
export * from './logger.js';
export * from './signal-watcher.js';
export * from './slot.js';
export * from './types.js';
export * from './with-disposable.js';
@@ -1,73 +0,0 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
import { effect } from '@preact/signals-core';
import type { ReactiveElement } from 'lit';
type ReactiveElementConstructor = abstract new (
...args: any[]
) => ReactiveElement;
/**
* Adds the ability for a LitElement or other ReactiveElement class to
* watch for access to Preact signals during the update lifecycle and
* trigger a new update when signals values change.
*/
export function SignalWatcher<T extends ReactiveElementConstructor>(
Base: T
): T {
abstract class SignalWatcher extends Base {
private __dispose?: () => void;
override connectedCallback(): void {
super.connectedCallback();
// In order to listen for signals again after re-connection, we must
// re-render to capture all the current signal accesses.
this.requestUpdate();
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this.__dispose?.();
}
override performUpdate() {
// ReactiveElement.performUpdate() also does this check, so we want to
// also bail early so we don't erroneously appear to not depend on any
// signals.
if (this.isUpdatePending === false || this.isConnected === false) {
return;
}
// If we have a previous effect, dispose it
this.__dispose?.();
// Tracks whether the effect callback is triggered by this performUpdate
// call directly, or by a signal change.
let updateFromLit = true;
// We create a new effect to capture all signal access within the
// performUpdate phase (update, render, updated, etc) of the element.
// Q: Do we need to create a new effect each render?
// TODO: test various combinations of render triggers:
// - from requestUpdate()
// - from signals
// - from both (do we get one or two re-renders)
// and see if we really need a new effect here.
this.__dispose = effect(() => {
if (updateFromLit) {
updateFromLit = false;
super.performUpdate();
} else {
// This branch is an effect run from Preact signals.
// This will cause another call into performUpdate, which will
// then create a new effect watching that update pass.
this.requestUpdate();
}
});
}
}
return SignalWatcher;
}
@@ -1,86 +0,0 @@
import { type Disposable, flattenDisposables } from './disposable.js';
export class Slot<T = void> implements Disposable {
private _callbacks: ((v: T) => unknown)[] = [];
private _disposables: Disposable[] = [];
private _emitting = false;
dispose() {
flattenDisposables(this._disposables).dispose();
this._callbacks = [];
this._disposables = [];
}
emit(v: T) {
// Prevent recursive emit calls
if (this._emitting) {
return;
}
const prevEmitting = this._emitting;
this._emitting = true;
try {
this._callbacks.forEach(f => {
try {
f(v);
} catch (err) {
console.error(err);
}
});
} finally {
this._emitting = prevEmitting;
}
}
on(callback: (v: T) => unknown): Disposable {
if (this._emitting) {
const newCallback = [...this._callbacks, callback];
this._callbacks = newCallback;
} else {
this._callbacks.push(callback);
}
return {
dispose: () => {
if (this._emitting) {
this._callbacks = this._callbacks.filter(v => v !== callback);
} else {
const index = this._callbacks.indexOf(callback);
if (index > -1) {
this._callbacks.splice(index, 1); // remove one item only
}
}
},
};
}
once(callback: (v: T) => unknown): Disposable {
let dispose: Disposable['dispose'] | undefined = undefined;
const handler = (v: T) => {
callback(v);
if (dispose) {
dispose();
}
};
const disposable = this.on(handler);
dispose = disposable.dispose;
return disposable;
}
filter(testFun: (v: T) => boolean): Slot<T> {
const result = new Slot<T>();
// if the original slot is disposed, dispose the filtered one
this._disposables.push({
dispose: () => result.dispose(),
});
this.on((v: T) => {
if (testFun(v)) {
result.emit(v);
}
});
return result;
}
}
@@ -1,53 +0,0 @@
import type { LitElement } from 'lit';
import { DisposableGroup } from './disposable.js';
import type { Constructor } from './types.js';
// See https://lit.dev/docs/composition/mixins/#mixins-in-typescript
// This definition should be exported, see https://github.com/microsoft/TypeScript/issues/30355#issuecomment-839834550
export declare class DisposableClass {
protected _disposables: DisposableGroup;
readonly disposables: DisposableGroup;
}
/**
* Mixin that adds a `_disposables: DisposableGroup` property to the class.
*
* The `_disposables` property is initialized in `connectedCallback` and disposed in `disconnectedCallback`.
*
* see https://lit.dev/docs/composition/mixins/
*
* @example
* ```ts
* class MyElement extends WithDisposable(ShadowlessElement) {
* onClick() {
* this._disposables.add(...);
* }
* }
* ```
*/
export function WithDisposable<T extends Constructor<LitElement>>(
SuperClass: T
) {
class DerivedClass extends SuperClass {
protected _disposables = new DisposableGroup();
get disposables() {
return this._disposables;
}
override connectedCallback() {
super.connectedCallback();
if (this._disposables.disposed) {
this._disposables = new DisposableGroup();
}
}
override disconnectedCallback() {
super.disconnectedCallback();
this._disposables.dispose();
}
}
return DerivedClass as unknown as T & Constructor<DisposableClass>;
}