Files
finos--perspective/packages/workspace/src/ts/perspective-workspace.ts
T
2026-07-13 12:23:53 +08:00

267 lines
10 KiB
TypeScript

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { MessageLoop } from "@lumino/messaging";
import { Widget } from "@lumino/widgets";
import type { HTMLPerspectiveViewerElement } from "@perspective-dev/viewer";
import type * as psp from "@perspective-dev/client";
import type * as psp_viewer from "@perspective-dev/viewer";
import * as msg from "@lumino/messaging";
export { PerspectiveWorkspace, addViewer, genId } from "./workspace";
export { PerspectiveViewerWidget } from "./workspace/widget";
export * from "./extensions";
import { PerspectiveWorkspace, PerspectiveWorkspaceConfig } from "./workspace";
import { bindTemplate, CustomElementProto } from "./utils/custom_elements";
import style from "../../dist/css/workspace.css";
import template from "../html/workspace.html";
export { PerspectiveWorkspaceConfig };
/**
* A Custom Element for coordinating a set of `<perspective-viewer>` light DOM
* children. `<perspective-workspace>` is built on Lumino.js to allow a more
* app-like experience than `<perspective-viewer>`, providing
* these features additionally:
*
* - Docking, arranging, tabbing and max/min-ing of `<perspective-viewer>`s.
* - Trivial `<perspective-viewer>` duplication.
* - Global Filter sidebar, for using selection state of a pivot table to filter
* siblings.
* - Total persistence/serializable state and child state.
* - Easy sharing/ownership of `Table()` among different `<perspective-viewer>`.
* - A cool DOM-reactive API.
*
* There are a few ways to use this Custom Element. In plain HTML, you can
* express your initial view simply:
*
* ```html
* <perspective-workspace></perspective-workspace>
* ```
*
* You can also use the DOM API in Javascript:
*
* ```javascript
* const workspace = document.createElement("perspective-workspace");
* document.body.appendChild(workspace);
* ```
*
* This will yield a `<perspective-workspace> with the default layout. To load
* a `Table()`, add it to `tables` via the `Map()` API where it will be
* auto-wired into all matching `<perspective-viewer>`s immediately:
*
* ```javascript
* workspace.tables.set("superstore", await worker.table(my_data));
* ```
*
*
*/
export class HTMLPerspectiveWorkspaceElement extends HTMLElement {
private workspace?: PerspectiveWorkspace;
private _resize_observer?: ResizeObserver;
constructor() {
super();
this.setAutoSize(true);
}
/***************************************************************************
*
* Public
*
*/
/**
* Persists this `<perspective-workspace>` to a token `Object`. This object
* is JSON serializable and describes the state of the Workspace and it's
* child `<perspective-viewer>` elements. Some important keys:
*
* - `viewers`: The serialized state of `<perspective-viewer>` children,
* named by `slot`.
* - `detail`: The main layout.
* - `master`: The contents of the Global Filter sidebar.
*
* While the `table` attribute is persisted for each `perspective-viewer`,
* `Table`s themselves must be added to the `tables` property `Map()`
* separately.
*
* @return {Object} A configuration token, compatible with
* `restore(config)`.
* @example
* // Save this layout to local storage
* const workspace = document.querySelector("perspective-workspace");
* localStorage.set("CONFIG", JSON.stringify(workspace.save()));
*/
save(): Promise<PerspectiveWorkspaceConfig> {
return this.workspace!.save();
}
/**
* Restore this `<perspective-workspace>` to a previous state captured by
* `save()`. Calling this method will completely rewrite this element's
* `innerHTML`, but may reuse `<perspective-viewer>` children depending
* on the `slot` attribute names. However, it should always be possible
* to recreate any given state from within the UI itself, as the attributes
* on `<perspective-viewer>` itself create immutable views.
*
* While the `table` attribute is set for each `perspective-viewer`,
* `Table`s themselves must be added to the `tables` property `Map()`
* separately.
* @param {Object} config A configuration token, as returned by `save()`.
* @example
* // Restore this layout from local storage
* const workspace = document.querySelector("perspective-workspace");
* workspace.restore(JSON.parse(localStorage.get("CONFIG"));
*
* // Add `Table` separately.
* workspace.tables.set("superstore", await worker.table(data));
*/
async restore(layout: PerspectiveWorkspaceConfig) {
await this.workspace!.restore(layout);
}
async clear() {
await this.restore({
sizes: [],
master: undefined,
detail: { main: null },
viewers: {},
});
}
/**
* Await all asynchronous tasks for all viewers in this workspace. This is
* useful to make sure asynchonous side effects of synchronous methods calls
* are applied.
*/
async flush() {
if (!this.workspace) {
return;
}
msg.MessageLoop.flush();
await new Promise((x) => requestAnimationFrame(x));
await this.workspace._mutex.lock(async () => {
await Promise.all(
Array.from(this.querySelectorAll("perspective-viewer")).map(
(x) => {
const psp_widget = x as HTMLPerspectiveViewerElement;
return psp_widget
.flush()
.then(() => psp_widget.flush());
},
),
);
});
}
/**
* Add a new viewer to the workspace for a given `ViewerConfigUpdateExt`.
* @param config
*/
async addViewer(config: psp_viewer.ViewerConfigUpdate) {
await this.workspace!.addViewer(config);
await this.flush();
}
async load(client: psp.Client | Promise<psp.Client>) {
if (this.workspace) {
this.workspace.client.push(await client);
} else {
throw new Error("Workspace not mounted");
}
}
/**
* Invalidate this component's dimensions and recalculate.
*/
async resize() {
this.workspace!.update();
await this.flush();
}
/**
* Set whether this workspace element should auto-size itself via a
* `ResizeObserver`.
*/
setAutoSize(is_auto_size: boolean) {
this._resize_observer?.unobserve(this);
this._resize_observer = undefined;
if (is_auto_size) {
this._resize_observer = new ResizeObserver((...args) =>
this.workspace?.update(),
);
this._resize_observer.observe(this);
}
}
connectedCallback() {
if (!this.workspace) {
const container = this.shadowRoot!.querySelector("#container")!;
this.workspace = new PerspectiveWorkspace(this);
this._register_light_dom_listener();
MessageLoop.sendMessage(this.workspace, Widget.Msg.BeforeAttach);
container.insertBefore(this.workspace.node, null);
MessageLoop.sendMessage(this.workspace, Widget.Msg.AfterAttach);
}
}
/***************************************************************************
*
* Private
*
*/
private _light_dom_changed() {
if (!this.workspace) {
return;
}
this.workspace._mutex.lock(async () => {
const viewers = Array.from(
this.childNodes,
) as HTMLPerspectiveViewerElement[];
for (const viewer of viewers) {
if (viewer.nodeType !== Node.ELEMENT_NODE) {
continue;
}
if (viewer.tagName !== "PERSPECTIVE-VIEWER") {
console.warn("Not a <perspective-viewer>");
continue;
}
let _task = this.workspace!.update_widget_for_viewer(
viewer as HTMLPerspectiveViewerElement,
);
}
this.workspace!.remove_unslotted_widgets(viewers);
this.workspace!.update_details_panel(viewers);
});
}
private _register_light_dom_listener() {
let observer = new MutationObserver(this._light_dom_changed.bind(this));
let config = { attributes: false, childList: true, subtree: false };
observer.observe(this, config);
this._light_dom_changed();
}
}
bindTemplate(
template,
style,
)(HTMLPerspectiveWorkspaceElement as unknown as CustomElementProto);