diff --git a/examples/blocks/src/superstore/index.html b/examples/blocks/src/superstore/index.html index 32b1127c8b..28c0bcbe37 100644 --- a/examples/blocks/src/superstore/index.html +++ b/examples/blocks/src/superstore/index.html @@ -6,17 +6,12 @@ diff --git a/packages/perspective-viewer-datagrid/src/less/row-hover.less b/packages/perspective-viewer-datagrid/src/less/row-hover.less index 616130f6a3..8d60e95475 100644 --- a/packages/perspective-viewer-datagrid/src/less/row-hover.less +++ b/packages/perspective-viewer-datagrid/src/less/row-hover.less @@ -20,15 +20,12 @@ regular-table { tr:hover:after { border-color: var(--rt-hover--border-color, #c5c9d080) !important; background-color: transparent; - box-shadow: 0px 1px 0px var(--rt-hover--border-color, #c5c9d080), - 0px 3px 0px rgba(0, 0, 0, 0.05), 0px 5px 0px rgba(0, 0, 0, 0.01); + box-shadow: 0px 1px 0px var(--rt-hover--border-color, #c5c9d080); &.psp-menu-open { box-shadow: inset -2px 0px 0px var(--icon--color), inset 2px 0px 0px var(--icon--color), - 0px 1px 0px var(--rt-hover--border-color, #c5c9d080), - 0px 3px 0px rgba(0, 0, 0, 0.05), - 0px 5px 0px rgba(0, 0, 0, 0.01); + 0px 1px 0px var(--rt-hover--border-color, #c5c9d080); } } @@ -37,8 +34,7 @@ regular-table { box-shadow: inset -2px 0px 0px var(--icon--color), inset 2px 0px 0px var(--icon--color), inset 0px -2px 0px var(--icon--color), - 0px 1px 0px var(--rt-hover--border-color, #c5c9d080), - 0px 3px 0px rgba(0, 0, 0, 0.05), 0px 5px 0px rgba(0, 0, 0, 0.01); + 0px 1px 0px var(--rt-hover--border-color, #c5c9d080); } tr:hover diff --git a/packages/perspective-workspace/build.js b/packages/perspective-workspace/build.js index ad7d4375a5..f39bce6223 100644 --- a/packages/perspective-workspace/build.js +++ b/packages/perspective-workspace/build.js @@ -143,6 +143,8 @@ async function build_all() { pro_dark.compile().get("output.css") ); + await Promise.all(BUILD.map(build)).catch(() => process.exit(1)); + try { await $`npx tsc --project ./tsconfig.json`.stdio( "inherit", @@ -152,8 +154,6 @@ async function build_all() { } catch (e) { process.exit(1); } - - await Promise.all(BUILD.map(build)).catch(() => process.exit(1)); } build_all(); diff --git a/packages/perspective-workspace/src/less/menu.less b/packages/perspective-workspace/src/less/menu.less index f184081323..7c5660e298 100644 --- a/packages/perspective-workspace/src/less/menu.less +++ b/packages/perspective-workspace/src/less/menu.less @@ -12,6 +12,10 @@ @import "@lumino/widgets/style/menu.css"; +:host { + position: absolute; +} + .lm-Menu { font-size: 12px; padding: 8px; diff --git a/packages/perspective-workspace/src/less/viewer.less b/packages/perspective-workspace/src/less/viewer.less index 7aa9897dac..ae4fdf8113 100644 --- a/packages/perspective-workspace/src/less/viewer.less +++ b/packages/perspective-workspace/src/less/viewer.less @@ -64,7 +64,7 @@ } } -perspective-viewer { +::slotted(perspective-viewer) { flex: 1; position: relative; display: block; @@ -76,10 +76,11 @@ perspective-viewer { overflow: visible !important; } -.lm-mod-override-cursor { +:host-context(.lm-mod-override-cursor) { cursor: grabbing !important; } -.lm-mod-override-cursor perspective-viewer > * { - pointer-events: none; +:host-context(.lm-mod-override-cursor) ::slotted(perspective-viewer), +.context-menu ::slotted(perspective-viewer) { + --override-content-pointer-events: none; } diff --git a/packages/perspective-workspace/src/less/workspace.less b/packages/perspective-workspace/src/less/workspace.less index 6cf4f4d217..7b8c283ce9 100644 --- a/packages/perspective-workspace/src/less/workspace.less +++ b/packages/perspective-workspace/src/less/workspace.less @@ -21,6 +21,8 @@ @import "@lumino/widgets/style/tabbar.css"; @import "@lumino/widgets/style/tabpanel.css"; +@import "./injected.less"; + :host { @import "./tabbar.less"; @import "./dockpanel.less"; @@ -28,8 +30,8 @@ background-color: hsl(210deg 18% 90%); - width: 100%; - height: 100%; + // width: 100%; + // height: 100%; .workspace { width: 100%; diff --git a/packages/perspective-workspace/src/themes/pro-dark.less b/packages/perspective-workspace/src/themes/pro-dark.less index ce2b459b64..bf03489331 100644 --- a/packages/perspective-workspace/src/themes/pro-dark.less +++ b/packages/perspective-workspace/src/themes/pro-dark.less @@ -22,7 +22,7 @@ perspective-indicator[theme="Pro Dark"] { --theme-name: "Pro Dark"; } -.lm-Menu { +perspective-workspace-menu { @include perspective-viewer-pro-dark--colors; } @@ -62,7 +62,7 @@ perspective-viewer[theme="Pro Dark"].workspace-master-widget { --plugin--background: @grey800; } -.lm-Menu { +perspective-workspace-menu { font-family: "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas", "Liberation Mono", monospace; font-weight: 300; diff --git a/packages/perspective-workspace/src/themes/pro.less b/packages/perspective-workspace/src/themes/pro.less index 6c8296ac47..cf8d3b0882 100644 --- a/packages/perspective-workspace/src/themes/pro.less +++ b/packages/perspective-workspace/src/themes/pro.less @@ -24,7 +24,7 @@ perspective-workspace { background-color: #dadada; } -.lm-Menu { +perspective-workspace-menu { @include perspective-viewer-pro--colors; background-color: #ffffff; } @@ -84,7 +84,7 @@ perspective-viewer[theme="Pro Light"].workspace-master-widget { --workspace-tabbar-tab--border-width: 1px 1px 0px 1px; } -.lm-Menu { +perspective-workspace-menu { font-family: "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas", "Liberation Mono", monospace; font-weight: 300; diff --git a/packages/perspective-workspace/src/ts/perspective-workspace.ts b/packages/perspective-workspace/src/ts/perspective-workspace.ts index 6753e69407..0254226b4b 100644 --- a/packages/perspective-workspace/src/ts/perspective-workspace.ts +++ b/packages/perspective-workspace/src/ts/perspective-workspace.ts @@ -27,7 +27,6 @@ import { import { bindTemplate, CustomElementProto } from "./utils/custom_elements"; import style from "../../build/css/workspace.css"; import template from "../html/workspace.html"; -import injectedStyles from "../../build/css/injected.css"; /** * A Custom Element for coordinating a set of `` light DOM @@ -52,13 +51,10 @@ import injectedStyles from "../../build/css/injected.css"; * name="View One" * table="superstore"> * - * * * - * * * ``` * @@ -295,11 +291,6 @@ export class HTMLPerspectiveWorkspaceElement extends HTMLElement { } } -const _injectStyle = document.createElement("style"); -_injectStyle.toggleAttribute("injected", true); -_injectStyle.innerHTML = injectedStyles; -document.head.appendChild(_injectStyle); - bindTemplate( template, style diff --git a/packages/perspective-workspace/src/ts/utils/custom_elements.ts b/packages/perspective-workspace/src/ts/utils/custom_elements.ts index 13b631daa3..a3264fb0c7 100644 --- a/packages/perspective-workspace/src/ts/utils/custom_elements.ts +++ b/packages/perspective-workspace/src/ts/utils/custom_elements.ts @@ -51,14 +51,9 @@ export function registerElement( `` + template.innerHTML; } - template.innerHTML = - `` + template.innerHTML; - const _perspective_element = class extends proto { - _initialized: boolean; - _initializing: boolean; + private _initialized: boolean; + private _initializing: boolean; constructor() { super(); diff --git a/packages/perspective-workspace/src/ts/workspace/commands.ts b/packages/perspective-workspace/src/ts/workspace/commands.ts index 09cc6ea761..afef923352 100644 --- a/packages/perspective-workspace/src/ts/workspace/commands.ts +++ b/packages/perspective-workspace/src/ts/workspace/commands.ts @@ -20,6 +20,7 @@ import type { } from "@finos/perspective-viewer"; import type { PerspectiveWorkspace } from "./workspace"; +import { WorkspaceMenu } from "./menu"; export const createCommands = ( workspace: PerspectiveWorkspace, @@ -43,7 +44,10 @@ export const createCommands = ( workspace.get_context_menu()?.init_overlay?.(); menu.addEventListener("blur", () => { const context_menu = workspace.get_context_menu()!; - const signal = context_menu.aboutToClose as Signal; + const signal = context_menu.aboutToClose as Signal< + WorkspaceMenu, + any + >; signal.emit({}); }); }, @@ -83,7 +87,7 @@ export const createCommands = ( menu.addEventListener("blur", () => { ( workspace.get_context_menu()?.aboutToClose as - | Signal + | Signal | undefined )?.emit({}); }); diff --git a/packages/perspective-workspace/src/ts/workspace/dockpanel.ts b/packages/perspective-workspace/src/ts/workspace/dockpanel.ts index f34994b97a..25e25faa98 100644 --- a/packages/perspective-workspace/src/ts/workspace/dockpanel.ts +++ b/packages/perspective-workspace/src/ts/workspace/dockpanel.ts @@ -11,7 +11,6 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import { DockLayout, DockPanel, TabBar, Widget } from "@lumino/widgets"; -import { toArray } from "@lumino/algorithm"; import { PerspectiveTabBar } from "./tabbar"; import { PerspectiveTabBarRenderer } from "./tabbarrenderer"; import { PerspectiveWorkspace } from "./workspace"; @@ -55,7 +54,7 @@ export class PerspectiveDockPanel extends DockPanel { const widget = sender.titles[args.index].owner; const layout = this.layout as DockLayout; const old = layout.saveLayout(); - if (toArray(layout.widgets()).length > 1) { + if (Array.from(layout.widgets()).length > 1) { layout.removeWidget(widget); } @@ -65,6 +64,8 @@ export class PerspectiveDockPanel extends DockPanel { // @ts-ignore: accessing a private member `_drag` const drag = this._drag; if (drag) { + drag.dragImage?.parentElement.removeChild(drag.dragImage); + drag.dragImage = null; drag._promise.then(() => { if (!widget.node.isConnected) { layout.restoreLayout(old); diff --git a/packages/perspective-workspace/src/ts/workspace/menu.ts b/packages/perspective-workspace/src/ts/workspace/menu.ts index b7d1b27c4a..6e942130fe 100644 --- a/packages/perspective-workspace/src/ts/workspace/menu.ts +++ b/packages/perspective-workspace/src/ts/workspace/menu.ts @@ -10,10 +10,61 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +import { ElementExt } from "@lumino/domutils"; +import { MessageLoop } from "@lumino/messaging"; import { h } from "@lumino/virtualdom"; -import { Menu } from "@lumino/widgets"; +import { Menu, Widget } from "@lumino/widgets"; -export class MenuRenderer extends Menu.Renderer { +export class WorkspaceMenu extends Menu { + private _host: ShadowRoot; + init_overlay?: () => void; + + constructor(host: ShadowRoot, options: Menu.IOptions) { + options.renderer = new MenuRenderer(); + super(options); + this._host = host; + (this as any)._openChildMenu = this._overrideOpenChildMenu.bind(this); + } + + open(x: number, y: number, options?: Menu.IOpenOptions) { + options ||= {}; + options.host = this._host as any as HTMLElement; + super.open(x, y, options); + } + + // Override this lumino private method because it will otherwise always + // attach to `document.body`. + private _overrideOpenChildMenu(activateFirst = false) { + const self = this as any; + let item = this.activeItem; + if (!item || item.type !== "submenu" || !item.submenu) { + self._closeChildMenu(); + return; + } + + let submenu = item.submenu; + if (submenu === self._childMenu) { + return; + } + + Menu.saveWindowData(); + self._closeChildMenu(); + self._childMenu = submenu; + self._childIndex = self._activeIndex; + (submenu as any)._parentMenu = this; + MessageLoop.sendMessage(this, Widget.Msg.UpdateRequest); + let itemNode = this.contentNode.children[self._activeIndex]; + openSubmenu(submenu, itemNode as HTMLElement, self._host); + if (activateFirst) { + submenu.activeIndex = -1; + submenu.activateNextItem(); + } + + submenu.activate(); + } +} + +class MenuRenderer extends Menu.Renderer { formatLabel(data: Menu.IRenderData) { let { label, mnemonic } = data.item; if (mnemonic < 0 || mnemonic >= label.length) { @@ -67,3 +118,72 @@ export class MenuRenderer extends Menu.Renderer { ); } } + +// Prevent submenus from attaching outside the Shadow DOM. +// Forked from [Lumino](https://github.com/jupyterlab/lumino/blob/main/packages/widgets/src/menu.ts). +// [License](https://github.com/jupyterlab/lumino/blob/main/LICENSE) +export function openSubmenu( + submenu: Menu, + itemNode: HTMLElement, + host: HTMLElement +): void { + const windowData = getWindowData(); + let px = windowData.pageXOffset; + let py = windowData.pageYOffset; + let cw = windowData.clientWidth; + let ch = windowData.clientHeight; + const hostData = (host as any).host.getBoundingClientRect(); + let hx = hostData.x; + let hy = hostData.y; + MessageLoop.sendMessage(submenu, Widget.Msg.UpdateRequest); + let maxHeight = ch; + let node = submenu.node; + let style = node.style; + style.opacity = "0"; + style.maxHeight = `${maxHeight}px`; + Widget.attach(submenu, host); + let { width, height } = node.getBoundingClientRect(); + let box = ElementExt.boxSizing(submenu.node); + let itemRect = itemNode.getBoundingClientRect(); + let x = itemRect.right - SUBMENU_OVERLAP - hx; + if (x + width > px + cw + hx) { + x = itemRect.left + SUBMENU_OVERLAP - width; + } + + let y = itemRect.top - box.borderTop - box.paddingTop - hy; + if (y + height > py + ch + hy) { + y = itemRect.bottom + box.borderBottom + box.paddingBottom - height; + } + + style.transform = `translate(${Math.max(0, x)}px, ${Math.max(0, y)}px`; + style.opacity = "1"; +} + +export const SUBMENU_OVERLAP = 3; + +let transientWindowDataCache: IWindowData | null = null; +let transientCacheCounter: number = 0; + +function getWindowData(): IWindowData { + if (transientCacheCounter > 0) { + transientCacheCounter--; + return transientWindowDataCache!; + } + return _getWindowData(); +} + +function _getWindowData(): IWindowData { + return { + pageXOffset: window.pageXOffset, + pageYOffset: window.pageYOffset, + clientWidth: document.documentElement.clientWidth, + clientHeight: document.documentElement.clientHeight, + }; +} + +interface IWindowData { + pageXOffset: number; + pageYOffset: number; + clientWidth: number; + clientHeight: number; +} diff --git a/packages/perspective-workspace/src/ts/workspace/tabbar.ts b/packages/perspective-workspace/src/ts/workspace/tabbar.ts index 363b78c962..f446aef8a0 100644 --- a/packages/perspective-workspace/src/ts/workspace/tabbar.ts +++ b/packages/perspective-workspace/src/ts/workspace/tabbar.ts @@ -13,20 +13,16 @@ import { ArrayExt } from "@lumino/algorithm"; import { ElementExt } from "@lumino/domutils"; import { TabBar } from "@lumino/widgets"; -import { - TabBarItems, - DEFAULT_TITLE, - PerspectiveTabBarRenderer, -} from "./tabbarrenderer"; -import { VirtualDOM, VirtualElement } from "@lumino/virtualdom"; +import { TabBarItems, PerspectiveTabBarRenderer } from "./tabbarrenderer"; +import { VirtualDOM } from "@lumino/virtualdom"; import { CommandRegistry } from "@lumino/commands"; -import { MenuRenderer } from "./menu"; import { Menu } from "@lumino/widgets"; import { PerspectiveWorkspace } from "./workspace"; import { Message } from "@lumino/messaging"; import { Title } from "@lumino/widgets"; import { Signal } from "@lumino/signaling"; import { ReadonlyJSONObject, ReadonlyJSONValue } from "@lumino/coreutils"; +import { WorkspaceMenu } from "./menu"; export class PerspectiveTabBar extends TabBar { _workspace: PerspectiveWorkspace; @@ -97,8 +93,10 @@ export class PerspectiveTabBar extends TabBar { onClick(otherTitles: Title[], index: number, event: MouseEvent) { const commands = new CommandRegistry(); - const renderer = new MenuRenderer(); - this._menu = new Menu({ commands, renderer }); + this._menu = new WorkspaceMenu(this._workspace.menu_elem.shadowRoot!, { + commands, + }); + this._menu.addClass("perspective-workspace-menu"); this._menu.dataset.minwidth = this.__titles[index]; for (const title of otherTitles) { @@ -132,6 +130,7 @@ export class PerspectiveTabBar extends TabBar { const box = (event.target as HTMLElement).getBoundingClientRect(); this._menu.open(box.x, box.y + box.height); + this._menu.aboutToClose.connect(() => { this._menu = undefined; }); diff --git a/packages/perspective-workspace/src/ts/workspace/widget.ts b/packages/perspective-workspace/src/ts/workspace/widget.ts index 0f9e12e3ae..60a389d964 100644 --- a/packages/perspective-workspace/src/ts/workspace/widget.ts +++ b/packages/perspective-workspace/src/ts/workspace/widget.ts @@ -88,14 +88,8 @@ export class PerspectiveViewerWidget extends Widget { table: this.viewer.getAttribute("table"), }; - if (this.viewer.hasAttribute("selectable")) { - config.selectable = this.viewer.getAttribute("selectable"); - } - - if (this.viewer.hasAttribute("editable")) { - config.editable = this.viewer.getAttribute("editable"); - } - + delete config["theme"]; + delete config["settings"]; return config; } diff --git a/packages/perspective-workspace/src/ts/workspace/workspace.ts b/packages/perspective-workspace/src/ts/workspace/workspace.ts index e2d256d01d..c430242103 100644 --- a/packages/perspective-workspace/src/ts/workspace/workspace.ts +++ b/packages/perspective-workspace/src/ts/workspace/workspace.ts @@ -12,30 +12,21 @@ import { find, toArray } from "@lumino/algorithm"; import { CommandRegistry } from "@lumino/commands"; -import { - SplitPanel, - Panel, - Menu, - DockPanel, - Title, - Widget, -} from "@lumino/widgets"; -import { Slot } from "@lumino/signaling"; +import { SplitPanel, Panel, DockPanel } from "@lumino/widgets"; import uniqBy from "lodash/uniqBy"; -import { DebouncedFunc } from "lodash"; +import { DebouncedFunc, isEqual } from "lodash"; import debounce from "lodash/debounce"; import type { HTMLPerspectiveViewerElement, ViewerConfigUpdate, } from "@finos/perspective-viewer"; import type * as psp from "@finos/perspective"; - +import injectedStyles from "../../../build/css/injected.css"; import { PerspectiveDockPanel } from "./dockpanel"; -import { MenuRenderer } from "./menu"; +import { WorkspaceMenu } from "./menu"; import { createCommands } from "./commands"; import { PerspectiveViewerWidget } from "./widget"; import { ObservableMap } from "../utils/observable_map"; -import { ReadonlyJSONObject } from "@lumino/coreutils"; const DEFAULT_WORKSPACE_SIZE = [1, 3]; @@ -63,18 +54,19 @@ export class PerspectiveWorkspace extends SplitPanel { private detailPanel: Panel; private masterPanel: SplitPanel; private element: HTMLElement; + menu_elem: HTMLElement; private _tables: ObservableMap>; private listeners: WeakMap void>; private indicator: HTMLElement; private commands: CommandRegistry; - private menuRenderer: MenuRenderer; + private _menu?: WorkspaceMenu; private _minimizedLayoutSlots?: DockPanel.ILayoutConfig; private _minimizedLayout?: DockPanel.ILayoutConfig; private _maximizedWidget?: PerspectiveViewerWidget; - private _save?: DebouncedFunc<() => false | Promise>; - private _context_menu?: Menu & { init_overlay?: () => void }; + private _last_updated_state?: PerspectiveWorkspaceConfig; + // private _context_menu?: Menu & { init_overlay?: () => void }; - constructor(element: HTMLElement, options = {}) { + constructor(element: HTMLElement) { super({ orientation: "horizontal" }); this.addClass("perspective-workspace"); this.dockpanel = new PerspectiveDockPanel(this); @@ -84,7 +76,10 @@ export class PerspectiveWorkspace extends SplitPanel { this.detailPanel.addWidget(this.dockpanel); this.masterPanel = new SplitPanel({ orientation: "vertical" }); this.masterPanel.addClass("master-panel"); - this.dockpanel.layoutModified.connect(() => this.workspaceUpdated()); + this.dockpanel.layoutModified.connect(() => { + this.workspaceUpdated(); + }); + this.addWidget(this.detailPanel); this.spacing = 6; this.element = element; @@ -94,14 +89,20 @@ export class PerspectiveWorkspace extends SplitPanel { this._tables.addDeleteListener(this._delete_listener.bind(this)); this.indicator = this.init_indicator(); this.commands = createCommands(this, this.indicator); - this.menuRenderer = new MenuRenderer(); + this.menu_elem = document.createElement("perspective-workspace-menu"); + this.menu_elem.attachShadow({ mode: "open" }); + this.element.shadowRoot!.insertBefore( + this.menu_elem, + this.element.shadowRoot!.lastElementChild! + ); + element.addEventListener("contextmenu", (event) => this.showContextMenu(null, event) ); } - get_context_menu(): (Menu & { init_overlay?: () => void }) | undefined { - return this._context_menu; + get_context_menu(): WorkspaceMenu | undefined { + return this._menu; } get_dock_panel(): PerspectiveDockPanel { @@ -638,19 +639,16 @@ export class PerspectiveWorkspace extends SplitPanel { */ createContextMenu(widget: PerspectiveViewerWidget | null) { - const contextMenu: Menu & { init_overlay?: () => void } = new Menu({ + this._menu = new WorkspaceMenu(this.menu_elem.shadowRoot!, { commands: this.commands, - renderer: this.menuRenderer, }); - this._context_menu = contextMenu; - const tabbar = find( this.dockpanel.tabBars(), (bar) => bar.currentTitle?.owner === widget ); - const init_overlay = () => { + this._menu.init_overlay = () => { if (widget) { widget.addClass("context-focus"); widget.viewer.classList.add("context-focus"); @@ -660,23 +658,24 @@ export class PerspectiveWorkspace extends SplitPanel { if ( widget.viewer.classList.contains("workspace-master-widget") ) { - contextMenu.node.classList.add("workspace-master-menu"); + this._menu!.node.classList.add("workspace-master-menu"); } else { - contextMenu.node.classList.remove("workspace-master-menu"); + this._menu!.node.classList.remove("workspace-master-menu"); } } }; - contextMenu.init_overlay = init_overlay; if (widget?.parent === this.dockpanel || widget === null) { - contextMenu.addItem({ + this._menu.addItem({ type: "submenu", command: "workspace:newmenu", submenu: (() => { - const submenu = new Menu({ - commands: this.commands, - renderer: this.menuRenderer, - }); + const submenu = new WorkspaceMenu( + this.menu_elem.shadowRoot!, + { + commands: this.commands, + } + ); for (const table of this.tables.keys()) { let args; @@ -725,7 +724,6 @@ export class PerspectiveWorkspace extends SplitPanel { } submenu.title.label = "New Table"; - return submenu; })(), }); @@ -733,51 +731,51 @@ export class PerspectiveWorkspace extends SplitPanel { if (widget) { if (widget?.parent === this.dockpanel) { - contextMenu.addItem({ type: "separator" }); + this._menu.addItem({ type: "separator" }); } - contextMenu.addItem({ + this._menu.addItem({ command: "workspace:duplicate", args: { widget_name: widget.name }, }); - contextMenu.addItem({ + this._menu.addItem({ command: "workspace:master", args: { widget_name: widget.name }, }); - contextMenu.addItem({ type: "separator" }); + this._menu.addItem({ type: "separator" }); - contextMenu.addItem({ + this._menu.addItem({ command: "workspace:settings", args: { widget_name: widget.name }, }); - contextMenu.addItem({ + this._menu.addItem({ command: "workspace:reset", args: { widget_name: widget.name }, }); - contextMenu.addItem({ + this._menu.addItem({ command: "workspace:export", args: { widget_name: widget.name }, }); - contextMenu.addItem({ + this._menu.addItem({ command: "workspace:copy", args: { widget_name: widget.name }, }); - contextMenu.addItem({ type: "separator" }); + this._menu.addItem({ type: "separator" }); - contextMenu.addItem({ + this._menu.addItem({ command: "workspace:close", args: { widget_name: widget.name }, }); - contextMenu.addItem({ + this._menu.addItem({ command: "workspace:help", }); } - contextMenu.aboutToClose.connect(() => { + this._menu.aboutToClose.connect(() => { if (widget) { this.element.classList.remove("context-menu"); this.removeClass("context-menu"); @@ -786,14 +784,21 @@ export class PerspectiveWorkspace extends SplitPanel { } }); - return contextMenu; + return this._menu; } showContextMenu(widget: PerspectiveViewerWidget | null, event: MouseEvent) { if (!event.shiftKey) { + this.menu_elem.shadowRoot!.innerHTML = ``; + const menu = this.createContextMenu(widget); menu.init_overlay?.(); - menu.open(event.clientX, event.clientY); + const rect = this.element.getBoundingClientRect(); + menu.open(event.clientX - rect.x, event.clientY - rect.y, { + host: this.menu_elem.shadowRoot as unknown as HTMLElement, + }); + + // this.menu_elem = menu_elem; event.preventDefault(); event.stopPropagation(); } @@ -968,10 +973,6 @@ export class PerspectiveWorkspace extends SplitPanel { // @ts-ignore widget.viewer.addEventListener("perspective-config-update", updated); - widget.viewer.addEventListener( - "perspective-plugin-update", - this.workspaceUpdated.bind(this) - ); this.listeners.set(widget, () => { widget.node.removeEventListener("contextmenu", contextMenu); @@ -985,11 +986,6 @@ export class PerspectiveWorkspace extends SplitPanel { "perspective-config-update", updated ); - - widget.viewer.removeEventListener( - "perspective-plugin-update", - this.workspaceUpdated.bind(this) - ); }); } @@ -1010,9 +1006,18 @@ export class PerspectiveWorkspace extends SplitPanel { * */ - async _fireUpdateEvent() { + async workspaceUpdated() { const layout = await this.save(); if (layout) { + if (this._last_updated_state) { + if (isEqual(this._last_updated_state, layout)) { + return; + } + } + + this._last_updated_state = + layout as any as PerspectiveWorkspaceConfig; + const tables: Record> = {}; this.tables.forEach((value, key) => { tables[key] = value; @@ -1025,17 +1030,4 @@ export class PerspectiveWorkspace extends SplitPanel { ); } } - - async workspaceUpdated() { - if (!this._save) { - this._save = debounce( - () => - this.dockpanel.mode !== "single-document" && - this._fireUpdateEvent(), - 500 - ); - } - - this._save(); - } } diff --git a/packages/perspective-workspace/test/js/restore.spec.js b/packages/perspective-workspace/test/js/restore.spec.js index 125abe83eb..a5d92caf7d 100644 --- a/packages/perspective-workspace/test/js/restore.spec.js +++ b/packages/perspective-workspace/test/js/restore.spec.js @@ -84,6 +84,7 @@ function tests(context, compare) { }, config); await page.evaluate(async () => { + const workspace = document.getElementById("workspace"); await workspace.flush(); }); diff --git a/rust/bundle/main.rs b/rust/bundle/main.rs index c57749678a..770338d01f 100644 --- a/rust/bundle/main.rs +++ b/rust/bundle/main.rs @@ -67,6 +67,7 @@ fn bindgen(outdir: &Path, artifact: &str, is_release: bool) { Bindgen::new() .web(true) .unwrap() + .keep_debug(!is_release) .input_path(input) .typescript(true) .out_name(&format!("{}.wasm", artifact.replace('_', "-"))) diff --git a/rust/perspective-viewer/Cargo.toml b/rust/perspective-viewer/Cargo.toml index 01ff8c9233..f7da584c04 100644 --- a/rust/perspective-viewer/Cargo.toml +++ b/rust/perspective-viewer/Cargo.toml @@ -83,7 +83,7 @@ js-sys = "0.3.64" nom = "7.1.1" # MessagePack serialization -rmp-serde = "1.1.1" +rmp-serde = "1.3.0" # Serialization for tokens and JS APIs serde = { version = "1.0", features = ["derive"] } diff --git a/rust/perspective-viewer/src/less/viewer.less b/rust/perspective-viewer/src/less/viewer.less index 75db84eec1..80a61259ce 100644 --- a/rust/perspective-viewer/src/less/viewer.less +++ b/rust/perspective-viewer/src/less/viewer.less @@ -14,6 +14,10 @@ --settings-panel-z-index: 10; } +::slotted(*) { + pointer-events: var(--override-content-pointer-events); +} + :host .sidebar_close_button { position: absolute; top: 0; diff --git a/rust/perspective-viewer/src/rust/config/viewer_config.rs b/rust/perspective-viewer/src/rust/config/viewer_config.rs index 4991f4ca48..66b7b73402 100644 --- a/rust/perspective-viewer/src/rust/config/viewer_config.rs +++ b/rust/perspective-viewer/src/rust/config/viewer_config.rs @@ -120,14 +120,14 @@ impl ViewerConfig { match format { Some(ViewerConfigEncoding::String) => { let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); - let bytes = rmp_serde::to_vec(&self.token())?; + let bytes = rmp_serde::to_vec_named(&self.token())?; encoder.write_all(&bytes)?; let encoded = encoder.finish()?; Ok(JsValue::from(base64::encode(encoded))) }, Some(ViewerConfigEncoding::ArrayBuffer) => { let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); - let bytes = rmp_serde::to_vec(&self.token())?; + let bytes = rmp_serde::to_vec_named(&self.token())?; encoder.write_all(&bytes)?; let encoded = encoder.finish()?; let array = js_sys::Uint8Array::from(&encoded[..]); diff --git a/rust/perspective-viewer/test/js/save_restore.spec.js b/rust/perspective-viewer/test/js/save_restore.spec.js index c51ceb44dd..5ea57413b7 100644 --- a/rust/perspective-viewer/test/js/save_restore.spec.js +++ b/rust/perspective-viewer/test/js/save_restore.spec.js @@ -142,4 +142,69 @@ test.describe("Save/Restore", async () => { "restore-restores-config-from-save.txt", ]); }); + + test("save/restore works in string format", async ({ page }) => { + const config = await page.evaluate(async () => { + const viewer = document.querySelector("perspective-viewer"); + await viewer.getTable(); + await viewer.restore({ + settings: true, + group_by: ["State"], + columns: ["Profit", "Sales"], + }); + return await viewer.save("string"); + }); + + const config3 = await page.evaluate(async (config) => { + const viewer = document.querySelector("perspective-viewer"); + await viewer.reset(); + await viewer.restore(config); + return await viewer.save(); + }, config); + + expect(config3).toEqual({ + ...DEFAULT_CONFIG, + columns: ["Profit", "Sales"], + plugin: "Debug", + group_by: ["State"], + settings: true, + theme: "Pro Light", + }); + + const contents = await get_contents(page); + await compareContentsToSnapshot(contents, [ + "save-restore-works-in-string-format.txt", + ]); + }); + + test("save/restore works in arraybuffer format", async ({ page }) => { + const config3 = await page.evaluate(async () => { + const viewer = document.querySelector("perspective-viewer"); + await viewer.getTable(); + await viewer.restore({ + settings: true, + group_by: ["State"], + columns: ["Profit", "Sales"], + }); + + const config = await viewer.save("arraybuffer"); + await viewer.reset(); + await viewer.restore(config); + return await viewer.save(); + }); + + expect(config3).toEqual({ + ...DEFAULT_CONFIG, + columns: ["Profit", "Sales"], + plugin: "Debug", + group_by: ["State"], + settings: true, + theme: "Pro Light", + }); + + const contents = await get_contents(page); + await compareContentsToSnapshot(contents, [ + "save-restore-works-in-arraybuffer-format.txt", + ]); + }); }); diff --git a/tools/perspective-test/results.tar.gz b/tools/perspective-test/results.tar.gz index 2f686a40d1..671ec179c3 100644 Binary files a/tools/perspective-test/results.tar.gz and b/tools/perspective-test/results.tar.gz differ diff --git a/tools/perspective-test/src/html/workspace-test.html b/tools/perspective-test/src/html/workspace-test.html index 3dda82c296..eabcbd27f4 100644 --- a/tools/perspective-test/src/html/workspace-test.html +++ b/tools/perspective-test/src/html/workspace-test.html @@ -23,11 +23,17 @@ padding: 0; overflow: hidden; } - #container { width: 100%; height: 100%; } + perspective-workspace { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + } perspective-workspace, perspective-viewer { font-family: "Roboto Mono" !important;