Single-canvas juggling (#15)

This PR adds a `CanvasProviderElement` to `@storyteller/studio-web` to manage
the complexity of maintaining a single concrete `HTMLCanvasElement` for
`@storyteller/studio`, while still being able to treat components like the
`ViewerElement` as self-contained "views" that behave as expected with regards
to CSS styling, DOM placement, etc.

Consuming applications should wrap the new `<sts-canvas-provider />` element
around some common ancestor of all views into the `@storyteller/studio` app,
similar to a React context provider.

The `react-frontend` application has been updated to take advantage of the new
`CanvasProviderElement` by demonstrating a more realistic use case for the
`ViewerElement`. The new demo presents a grid of "thumbnails," one for each
object in our hard-coded "object library." Clicking on one of these thumbnails
will render a `ViewerElement` for that object in a modal dialog.
This commit is contained in:
Danny McGee
2023-12-19 16:11:25 -05:00
committed by GitHub
parent cba70a62ae
commit 0a0acacfdb
24 changed files with 774 additions and 102 deletions
@@ -0,0 +1,94 @@
import { Token, UniqueToken, provide } from "@storyteller/framework";
import { LitElement, html, unsafeCSS } from "lit";
import { customElement } from "lit/decorators.js";
import styles from "./canvas-provider.element.scss?inline";
export interface CanvasProvider {
readonly host: HTMLElement;
readonly canvasId: string;
readonly canvas: HTMLCanvasElement | undefined;
/**
* Remove the canvas element from the provider host and return it. This can
* be used to temporarily move the canvas element to a different location in
* the DOM tree (e.g., for displaying in a modal dialog).
*
* `returnCanvas` must be called to restore the canvas element to its
* original location before the canvas's new parent element is removed from
* the document. If the canvas element is entirely absent from the document
* at any point after initialization (i.e., if
* `document.getElementById(canvasId)` returns `null`), the
* `@storyteller/studio` app will panic.
*/
takeCanvas(): HTMLCanvasElement | undefined;
/**
* Return the canvas element to the provider after removing it via
* `takeCanvas`.
*
* When `takeCanvas` is used to move the canvas element to a new location,
* this method must be called before the canvas's new parent is removed from
* the DOM tree, to ensure that the canvas element is always accessible to
* `@storyteller/studio`.
*/
returnCanvas(canvas: HTMLCanvasElement): void;
}
export const CANVAS_PROVIDER: Token<CanvasProvider> = new UniqueToken("CanvasProvider");
@customElement("sts-canvas-provider")
@provide(CANVAS_PROVIDER)
export class CanvasProviderElement
extends LitElement
implements CanvasProvider
{
static override styles = unsafeCSS(styles);
get host() { return this; }
get canvasId() { return this.#canvasId; }
get canvas(): HTMLCanvasElement | undefined { return this.#canvas; }
#canvasId = "studio_canvas";
#canvas?: HTMLCanvasElement;
takeCanvas(): HTMLCanvasElement | undefined {
if (!this.#canvas) return;
const canvas = this.removeChild(this.#canvas);
this.#canvas = undefined;
return canvas;
}
returnCanvas(canvas: HTMLCanvasElement) {
this.appendChild(canvas);
this.#canvas = canvas;
}
override connectedCallback(): void {
this.#canvas ??= this.#appendLightDomCanvas();
super.connectedCallback();
}
protected override render = () => html`
<slot></slot>
<slot name="canvas"></slot>
`;
#appendLightDomCanvas(): HTMLCanvasElement {
const canvas = document.createElement("canvas");
canvas.id = this.#canvasId;
canvas.slot = "canvas";
this.appendChild(canvas);
return canvas;
}
}
declare global {
export interface HTMLElementTagNameMap {
"sts-canvas-provider": CanvasProviderElement;
}
}