From 1bedf395d7e0234531fe390ae38a4e71a458826b Mon Sep 17 00:00:00 2001 From: Christopher Lepski Date: Thu, 31 Jul 2025 14:27:44 +0200 Subject: [PATCH] chore: Extend oscd layout --- .../src/addons/CompasLayout.ts | 658 +----------------- packages/openscd/src/addons/Layout.ts | 2 +- 2 files changed, 15 insertions(+), 645 deletions(-) diff --git a/packages/compas-open-scd/src/addons/CompasLayout.ts b/packages/compas-open-scd/src/addons/CompasLayout.ts index 46b4983d23..d1d515a0b9 100644 --- a/packages/compas-open-scd/src/addons/CompasLayout.ts +++ b/packages/compas-open-scd/src/addons/CompasLayout.ts @@ -1,674 +1,44 @@ import { customElement, html, - LitElement, property, - state, TemplateResult, - query, - css, } from 'lit-element'; import { get } from 'lit-translate'; -import { newPendingStateEvent } from '@openscd/core/foundation/deprecated/waiter.js'; -import { newSettingsUIEvent } from '@openscd/core/foundation/deprecated/settings.js'; -import { - MenuItem, - Validator, - MenuPlugin, - pluginIcons, -} from '@openscd/open-scd/src/open-scd.js'; -import { - Plugin, -} from '@openscd/open-scd/src/plugin.js' -import { - HistoryUIKind, - newEmptyIssuesEvent, - newHistoryUIEvent, - newRedoEvent, - newUndoEvent, -} from '@openscd/open-scd/src/addons/History.js'; -import type { Drawer } from '@material/mwc-drawer'; -import type { ActionDetail } from '@material/mwc-list'; -import { List } from '@material/mwc-list'; -import type { ListItem } from '@material/mwc-list/mwc-list-item'; - -import '@material/mwc-drawer'; -import '@material/mwc-list'; -import '@material/mwc-dialog'; -import '@material/mwc-switch'; -import '@material/mwc-select'; -import '@material/mwc-textfield'; -import { pluginTag } from '../plugin-tag.js'; import type { UserInfoEvent } from '../compas/foundation'; -import { HistoryState } from '@openscd/open-scd/src/addons/History.js'; -import { OscdPluginManager } from '@openscd/open-scd/src/addons/plugin-manager/plugin-manager'; -import '@openscd/open-scd/src/addons/plugin-manager/plugin-manager'; -import { OscdCustomPluginDialog } from '@openscd/open-scd/src/addons/plugin-manager/custom-plugin-dialog'; -import '@openscd/open-scd/src/addons/plugin-manager/custom-plugin-dialog'; -import '@openscd/open-scd/src/addons/menu-tabs/menu-tabs.js'; -import { TabActivatedEvent } from '@openscd/open-scd/src/addons/menu-tabs/menu-tabs.js'; - - -@customElement('compas-layout') -export class CompasLayout extends LitElement { - /** The `XMLDocument` to be edited */ - @property({ attribute: false }) doc: XMLDocument | null = null; - /** The name of the current [[`doc`]] */ - @property({ type: String }) docName = ''; - /** Index of the last [[`EditorAction`]] applied. */ - @property({ type: Number }) editCount = -1; - - /** The plugins to render the layout. */ - @property({ type: Array }) plugins: Plugin[] = []; - /** The open-scd host element */ - @property({ type: Object }) host!: HTMLElement; +import { OscdLayout } from '@openscd/open-scd/src/addons/Layout.js'; - @property({ type: Object }) historyState!: HistoryState; +@customElement('compas-layout') +export class CompasLayout extends OscdLayout { @property({ type: String }) username: string | undefined; - @state() validated: Promise = Promise.resolve(); - - @state() shouldValidate = false; - @state() activeEditor: Plugin | undefined = this.calcActiveEditors()[0]; - - @query('#menu') menuUI!: Drawer; - @query('#menuContent') menuContent!: List; - @query('#pluginManager') pluginUI!: OscdPluginManager; - @query('#pluginList') pluginList!: List; - @query('#pluginAdd') pluginDownloadUI!: OscdCustomPluginDialog; - - render(): TemplateResult { - return html` -
this.pluginDownloadUI.show()} - @oscd-activate-editor=${this.handleActivateEditorByEvent} - @oscd-run-menu=${this.handleRunMenuByEvent} - > - - ${this.renderHeader()} ${this.renderAside()} ${this.renderMenuContent()} - ${this.renderContent()} ${this.renderLanding()} ${this.renderPlugging()} -
- `; - } - - private renderPlugging(): TemplateResult { - return html` ${this.renderPluginUI()} ${this.renderDownloadUI()} `; - } - - private getMenuContent(src: string) { - const tag = pluginTag(src); - return this.menuContent.querySelector(tag); - } - - /** Renders the "Add Custom Plug-in" UI*/ - protected renderDownloadUI(): TemplateResult { - return html` - - ` - } - - /** - * Renders the plug-in management UI (turning plug-ins on/off) - */ - protected renderPluginUI(): TemplateResult { - return html` - - ` - } - - // Computed properties - - get validators(): Plugin[] { - return this.plugins.filter( - plugin => plugin.active && plugin.kind === 'validator' - ); - } - get menuEntries(): Plugin[] { - return this.plugins.filter( - plugin => plugin.active && plugin.kind === 'menu' - ); - } - get topMenu(): Plugin[] { - return this.menuEntries.filter(plugin => plugin.position === 'top'); - } - get middleMenu(): Plugin[] { - return this.menuEntries.filter(plugin => plugin.position === 'middle'); - } - get bottomMenu(): Plugin[] { - return this.menuEntries.filter(plugin => plugin.position === 'bottom'); - } - - get menu(): (MenuItem | 'divider')[] { - const topMenu = this.generateMenu(this.topMenu, 'top'); - const middleMenu = this.generateMenu(this.middleMenu, 'middle'); - const bottomMenu = this.generateMenu(this.bottomMenu, 'bottom'); - const validators = this.generateValidatorMenus(this.validators); - - if (middleMenu.length > 0) middleMenu.push('divider'); - if (bottomMenu.length > 0) bottomMenu.push('divider'); - - return [ - 'divider', - ...topMenu, - 'divider', - { - icon: 'undo', - name: 'undo', - actionItem: true, - action: (): void => { - this.dispatchEvent(newUndoEvent()); - }, - disabled: (): boolean => !this.historyState.canUndo, - kind: 'static', - content: () => html``, - }, - { - icon: 'redo', - name: 'redo', - actionItem: true, - action: (): void => { - this.dispatchEvent(newRedoEvent()); - }, - disabled: (): boolean => !this.historyState.canRedo, - kind: 'static', - content: () => html``, - }, - ...validators, - { - icon: 'list', - name: 'menu.viewLog', - actionItem: true, - action: (): void => { - this.dispatchEvent(newHistoryUIEvent(true, HistoryUIKind.log)); - }, - kind: 'static', - content: () => html``, - }, - { - icon: 'history', - name: 'menu.viewHistory', - actionItem: true, - action: (): void => { - this.dispatchEvent(newHistoryUIEvent(true, HistoryUIKind.history)); - }, - kind: 'static', - content: () => html``, - }, - { - icon: 'rule', - name: 'menu.viewDiag', - actionItem: true, - action: (): void => { - this.dispatchEvent(newHistoryUIEvent(true, HistoryUIKind.diagnostic)); - }, - kind: 'static', - content: () => html``, - }, - 'divider', - ...middleMenu, - { - icon: 'settings', - name: 'settings.title', - action: (): void => { - this.dispatchEvent(newSettingsUIEvent(true)); - }, - kind: 'static', - content: () => html``, - }, - ...bottomMenu, - { - icon: 'extension', - name: 'plugins.heading', - action: (): void => this.pluginUI.show(), - kind: 'static', - content: () => html``, - }, - ]; - } - - get editors(): Plugin[] { - return this.plugins.filter( - plugin => plugin.active && plugin.kind === 'editor' - ); - } - - // Keyboard Shortcuts - private handleKeyPress(e: KeyboardEvent): void { - // currently we only handley key shortcuts when users press ctrl - if(!e.ctrlKey){ return } - - const keyFunctionMap: {[key:string]: () => void} = { - 'm': () => this.menuUI.open = !this.menuUI.open, - 'o': () => this.menuUI.querySelector('mwc-list-item[iconid="folder_open"]')?.click(), - 'O': () => this.menuUI.querySelector('mwc-list-item[iconid="create_new_folder"]')?.click(), - 's': () => this.menuUI.querySelector('mwc-list-item[iconid="save"]')?.click(), - 'P': () => this.pluginUI.show(), - } - - const fn = keyFunctionMap[e.key]; - if(!fn){ return; } - - e.preventDefault(); - fn(); - } - connectedCallback(): void { super.connectedCallback(); - this.host.addEventListener('close-drawer', async () => { - this.menuUI.open = false; - }); - this.host.addEventListener('validate', async () => { - this.shouldValidate = true; - await this.validated; - - if (!this.shouldValidate){ return; } - - this.shouldValidate = false; - - this.validated = Promise.allSettled( - this.menuUI - .querySelector('mwc-list')! - .items.filter(item => item.className === 'validator') - .map(item => { - const src = item.dataset.src ?? ''; - - const menuContentElement = this.getMenuContent(src); - - if (!menuContentElement) { - return; - } - - return (menuContentElement as unknown as Validator).validate() - } - ) - ).then(); - this.dispatchEvent(newPendingStateEvent(this.validated)); - }); - this.handleKeyPress = this.handleKeyPress.bind(this); - document.onkeydown = this.handleKeyPress; - - document.addEventListener("open-plugin-download", () => { - this.pluginDownloadUI.show(); - }); this.onUserInfo = this.onUserInfo.bind(this); this.host.addEventListener('userinfo', this.onUserInfo); } - private generateMenu(plugins:Plugin[], kind: 'top' | 'middle' | 'bottom'): (MenuItem | 'divider')[]{ - return plugins.map(plugin => { - return { - icon: plugin.icon || pluginIcons['menu'], - name: plugin.name, - src: plugin.src, - action: ae => { - const menuContentElement = this.getMenuContent(plugin.src); - if (!menuContentElement) { - return; - } - - this.dispatchEvent(newPendingStateEvent((menuContentElement as unknown as MenuPlugin).run())) - }, - disabled: (): boolean => plugin.requireDoc! && this.doc === null, - content: () => { - if(plugin.content){ return plugin.content(); } - return html``; - }, - kind: kind, - } - }) - } - - private generateValidatorMenus(plugins: Plugin[]): (MenuItem | 'divider')[] { - return plugins.map(plugin =>{ - return { - icon: plugin.icon || pluginIcons['validator'], - name: plugin.name, - src: plugin.src, - action: ae => { - this.dispatchEvent(newEmptyIssuesEvent(plugin.src)); - - const menuContentElement = this.getMenuContent(plugin.src); - if (!menuContentElement) { - return; - } - - this.dispatchEvent(newPendingStateEvent((menuContentElement as unknown as Validator).validate())) - }, - disabled: (): boolean => this.doc === null, - content: plugin.content ?? (() => html``), - kind: 'validator', - } - }); - } - private onUserInfo(event: UserInfoEvent) { this.username = event.detail.name; } - private renderMenuItem(me: MenuItem | 'divider'): TemplateResult { - const hasActionItem = me !== 'divider' && me.actionItem; - - if (isDivider(me)) { return html`
  • `; } - if (hasActionItem){ return html``; } - return html` - ${me.icon} - ${get(me.name)} - ${me.hint - ? html`${me.hint}` - : ''} - - `; - } - - private renderActionItem(me: MenuItem | 'divider'): TemplateResult { - if(me === 'divider' || !me.actionItem){ return html`` } - + protected renderActionItems(): TemplateResult { return html` - `; - } - - private renderEditorTab({ name, icon }: Plugin): TemplateResult { - return html` `; - } - - /** Renders top bar which features icon buttons for undo, redo, log, scl history and diagnostics*/ - protected renderHeader(): TemplateResult { - return html` - (this.menuUI.open = true)} - > -
    ${this.docName}
    ${this.username != undefined - ? html`${get('userinfo.loggedInAs', { - name: this.username, - })}` - : ``} - ${this.menu.map(this.renderActionItem)} -
    `; - } - - protected renderMenuContent(): TemplateResult { - return html` - - `; - } - - /** - * Renders a drawer toolbar featuring the scl filename, enabled menu plugins, - * settings, help, scl history and plug-ins management - */ - protected renderAside(): TemplateResult { - return html` - - ${get('menu.title')} - ${renderTitle(this.docName)} - - ${this.menu.map(this.renderMenuItem)} - - - `; - - function renderTitle(docName?: string){ - if(!docName) return html``; - - return html`${docName}`; - } - - function makeListAction(menuItems : (MenuItem|'divider')[]){ - return function listAction(ae: CustomEvent){ - //FIXME: dirty hack to be fixed in open-scd-core - // if clause not necessary when oscd... components in open-scd not list - if (ae.target instanceof List) - (( - menuItems.filter( - item => item !== 'divider' && !item.actionItem - )[ae.detail.index] - ))?.action?.(ae); - } - } - } - - private calcActiveEditors(){ - const hasActiveDoc = Boolean(this.doc); - - return this.editors - .filter(editor => { - // this is necessary because `requireDoc` can be undefined - // and that is not the same as false - const doesNotRequireDoc = editor.requireDoc === false - return doesNotRequireDoc || hasActiveDoc - }) - } - - /** Renders the enabled editor plugins and a tab bar to switch between them*/ - protected renderContent(): TemplateResult { - const activeEditors = this.calcActiveEditors() - .map(this.renderEditorTab) - - const hasActiveEditors = activeEditors.length > 0; - if(!hasActiveEditors){ return html``; } - - return html` - - - ${renderEditorContent(this.doc, this.activeEditor, )} + ? html`${get('userinfo.loggedInAs', { + name: this.username, + })}` + : ``} + ${this.menu.map(this.renderActionItem)} `; - - function renderEditorContent(doc: XMLDocument | null, activeEditor?: Plugin){ - const editor = activeEditor; - const requireDoc = editor?.requireDoc - if(requireDoc && !doc) { return html`` } - - const content = editor?.content; - if(!content) { return html`` } - - return html`${content()}`; - } - } - - private handleEditorTabActivated(e: TabActivatedEvent){ - this.activeEditor = e.detail.editor - } - - private handleActivateEditorByEvent(e: CustomEvent<{name: string, src: string}>): void { - const {name, src} = e.detail; - const editors = this.calcActiveEditors() - const wantedEditor = editors.find(editor => editor.name === name || editor.src === src) - if(!wantedEditor){ return; } // TODO: log error - - this.activeEditor = wantedEditor; } - - private handleRunMenuByEvent(e: CustomEvent<{name: string}>): void { - // TODO: this is a workaround, fix it - this.menuUI.open = true; - const menuEntry = this.menuUI.querySelector(`[data-name="${e.detail.name}"]`) as HTMLElement - - const menuContentElement = this.getMenuContent(menuEntry.dataset.src ?? ''); - if (!menuContentElement) { - return; - } - - (menuContentElement as unknown as MenuPlugin).run(); - } - - /** - * Renders the landing buttons (open project and new project) - * it no document loaded we display the menu item that are in the position - * 'top' and are not disabled - * - * To enable replacement of this part we have to convert it to either an addon - * or a plugin - */ - protected renderLanding(): TemplateResult { - if(this.doc){ return html``; } - - return html` -
    - ${renderMenuItems(this.menu, this.menuUI)} -
    ` - - function renderMenuItems(menuItemsAndDividers: (MenuItem | 'divider')[], menuUI: Drawer){ - - const menuItems = menuItemsAndDividers.filter(mi => mi !== 'divider') as MenuItem[]; - - return menuItems.map((mi: MenuItem, index) => { - if(mi.kind !== 'top' || mi.disabled?.()) { return html``; } - - return html` - -
    ${mi.name}
    -
    - ` - }) - - function clickListItem(index:number) { - const listItem = menuUI.querySelector('mwc-list')!.items[index]; - listItem.click(); - } - - } - } - - static styles = css` - mwc-drawer { - position: absolute; - top: 0; - } - - mwc-top-app-bar-fixed { - --mdc-theme-text-disabled-on-light: rgba(255, 255, 255, 0.38); - } /* hack to fix disabled icon buttons rendering black */ - - mwc-tab { - background-color: var(--primary); - --mdc-theme-primary: var(--mdc-theme-on-primary); - } - - input[type='file'] { - display: none; - } - - mwc-dialog { - --mdc-dialog-max-width: 98vw; - } - - mwc-dialog > form { - display: flex; - flex-direction: column; - } - - mwc-dialog > form > * { - display: block; - margin-top: 16px; - } - - mwc-linear-progress { - position: fixed; - --mdc-linear-progress-buffer-color: var(--primary); - --mdc-theme-primary: var(--secondary); - left: 0px; - top: 0px; - width: 100%; - pointer-events: none; - z-index: 1000; - } - - tt { - font-family: 'Roboto Mono', monospace; - font-weight: 300; - } - - .landing { - position: absolute; - text-align: center; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 100%; - } - - .landing_icon:hover { - box-shadow: 0 12px 17px 2px rgba(0, 0, 0, 0.14), - 0 5px 22px 4px rgba(0, 0, 0, 0.12), 0 7px 8px -4px rgba(0, 0, 0, 0.2); - } - - .landing_icon { - margin: 12px; - border-radius: 16px; - width: 160px; - height: 140px; - text-align: center; - color: var(--mdc-theme-on-secondary); - background: var(--secondary); - --mdc-icon-button-size: 100px; - --mdc-icon-size: 100px; - --mdc-ripple-color: rgba(0, 0, 0, 0); - box-shadow: rgb(0 0 0 / 14%) 0px 6px 10px 0px, - rgb(0 0 0 / 12%) 0px 1px 18px 0px, rgb(0 0 0 / 20%) 0px 3px 5px -1px; - transition: box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); - } - - .landing_label { - width: 160px; - height: 50px; - margin-top: 100px; - margin-left: -30px; - font-family: 'Roboto', sans-serif; - } - - .plugin.menu { - display: flex; - } - - .plugin.validator { - display: flex; - } - `; -} - -function isDivider(item: MenuItem | 'divider'): item is 'divider' { - return item === 'divider'; } diff --git a/packages/openscd/src/addons/Layout.ts b/packages/openscd/src/addons/Layout.ts index d2020649be..f3bf0afbf3 100644 --- a/packages/openscd/src/addons/Layout.ts +++ b/packages/openscd/src/addons/Layout.ts @@ -377,7 +377,7 @@ export class OscdLayout extends LitElement { `; } - private renderActionItem(me: MenuItem | 'divider'): TemplateResult { + protected renderActionItem(me: MenuItem | 'divider'): TemplateResult { if(me === 'divider' || !me.actionItem){ return html`` } return html`