diff --git a/package-lock.json b/package-lock.json index 842e66ab27..13e72a9740 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30433,7 +30433,7 @@ }, "packages/core": { "name": "@openscd/core", - "version": "0.1.1", + "version": "0.1.2", "license": "Apache-2.0", "dependencies": { "@lit/localize": "^0.11.4", diff --git a/packages/openscd/src/addons/Layout.ts b/packages/openscd/src/addons/Layout.ts index 4080a4fb00..c3e6acde2e 100644 --- a/packages/openscd/src/addons/Layout.ts +++ b/packages/openscd/src/addons/Layout.ts @@ -51,6 +51,16 @@ import { EditCompletedEvent } from '@openscd/core'; @customElement('oscd-layout') export class OscdLayout extends LitElement { + + render(): TemplateResult { + return html` + + ${this.renderHeader()} ${this.renderAside()} ${this.renderContent()} + ${this.renderLanding()} ${this.renderPlugging()} + `; + } + + /** The `XMLDocument` to be edited */ @property({ attribute: false }) doc: XMLDocument | null = null; @@ -120,97 +130,13 @@ export class OscdLayout extends LitElement { return this.menuEntries.filter(plugin => plugin.position === 'bottom'); } - get menu(): (MenuItem | 'divider')[] { - const topMenu: (MenuItem | 'divider')[] = []; - const middleMenu: (MenuItem | 'divider')[] = []; - const bottomMenu: (MenuItem | 'divider')[] = []; - const validators: (MenuItem | 'divider')[] = []; - - this.topMenu.forEach(plugin => - topMenu.push({ - icon: plugin.icon || pluginIcons['menu'], - name: plugin.name, - action: ae => { - this.dispatchEvent( - newPendingStateEvent( - (( - (( - (ae.target).items[ae.detail.index].nextElementSibling - )) - )).run() - ) - ); - }, - disabled: (): boolean => plugin.requireDoc! && this.doc === null, - content: plugin.content, - kind: 'top', - }) - ); - - this.middleMenu.forEach(plugin => - middleMenu.push({ - icon: plugin.icon || pluginIcons['menu'], - name: plugin.name, - action: ae => { - this.dispatchEvent( - newPendingStateEvent( - (( - (( - (ae.target).items[ae.detail.index].nextElementSibling - )) - )).run() - ) - ); - }, - disabled: (): boolean => plugin.requireDoc! && this.doc === null, - content: plugin.content, - kind: 'middle', - }) - ); - - this.bottomMenu.forEach(plugin => - bottomMenu.push({ - icon: plugin.icon || pluginIcons['menu'], - name: plugin.name, - action: ae => { - this.dispatchEvent( - newPendingStateEvent( - (( - (( - (ae.target).items[ae.detail.index].nextElementSibling - )) - )).run() - ) - ); - }, - disabled: (): boolean => plugin.requireDoc! && this.doc === null, - content: plugin.content, - kind: 'middle', - }) - ); - this.validators.forEach(plugin => - validators.push({ - icon: plugin.icon || pluginIcons['validator'], - name: plugin.name, - action: ae => { - this.dispatchEvent(newEmptyIssuesEvent(plugin.src)); + get menu(): (MenuItem | 'divider')[] { - this.dispatchEvent( - newPendingStateEvent( - (( - (( - (ae.target).items[ae.detail.index].nextElementSibling - )) - )).validate() - ) - ); - }, - disabled: (): boolean => this.doc === null, - content: plugin.content, - kind: 'validator', - }) - ); + 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'); @@ -295,26 +221,22 @@ export class OscdLayout extends LitElement { // Keyboard Shortcuts private handleKeyPress(e: KeyboardEvent): void { - let handled = false; - const ctrlAnd = (key: string) => - e.key === key && e.ctrlKey && (handled = true); - - if (ctrlAnd('m')) this.menuUI.open = !this.menuUI.open; - if (ctrlAnd('o')) - this.menuUI - .querySelector('mwc-list-item[iconid="folder_open"]') - ?.click(); - if (ctrlAnd('O')) - this.menuUI - .querySelector('mwc-list-item[iconid="create_new_folder"]') - ?.click(); - if (ctrlAnd('s')) - this.menuUI - .querySelector('mwc-list-item[iconid="save"]') - ?.click(); - if (ctrlAnd('P')) this.pluginUI.show(); - - if (handled) e.preventDefault(); + // 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(); } private handleAddPlugin() { @@ -370,7 +292,7 @@ export class OscdLayout extends LitElement { this.shouldValidate = true; await this.validated; - if (!this.shouldValidate) return; + if (!this.shouldValidate){ return; } this.shouldValidate = false; @@ -403,10 +325,59 @@ export class OscdLayout extends LitElement { ); } + + private generateMenu(plugins:Plugin[], kind: 'top' | 'middle' | 'bottom'): (MenuItem | 'divider')[]{ + return plugins.map(plugin => { + return { + icon: plugin.icon || pluginIcons['menu'], + name: plugin.name, + action: ae => { + this.dispatchEvent( + newPendingStateEvent( + (( + (( + (ae.target).items[ae.detail.index].nextElementSibling + )) + )).run() + ) + ); + }, + disabled: (): boolean => plugin.requireDoc! && this.doc === null, + content: plugin.content, + kind: kind, + } + }) + } + + private generateValidatorMenus(plugins: Plugin[]): (MenuItem | 'divider')[] { + return plugins.map(plugin =>{ + return { + icon: plugin.icon || pluginIcons['validator'], + name: plugin.name, + action: ae => { + this.dispatchEvent(newEmptyIssuesEvent(plugin.src)); + + this.dispatchEvent( + newPendingStateEvent( + (( + (( + (ae.target).items[ae.detail.index].nextElementSibling + )) + )).validate() + ) + ); + }, + disabled: (): boolean => this.doc === null, + content: plugin.content, + kind: 'validator', + } + }); + } + private renderMenuItem(me: MenuItem | 'divider'): TemplateResult { - if (me === 'divider') - return html`
  • `; - if (me.actionItem) return html``; + if (me === 'divider') { return html`
  • `; } + if (me.actionItem){ return html``; } + return html` `; - else return html``; + if(me === 'divider' || !me.actionItem){ return html`` } + + return html` + `; } private renderEditorTab({ name, icon }: Plugin): TemplateResult { @@ -453,76 +425,111 @@ export class OscdLayout extends LitElement { `; } - /** Renders a drawer toolbar featuring the scl filename, enabled menu plugins, settings, help, scl history and plug-ins management */ + /** + * 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')} - ${this.docName - ? html`${this.docName}` - : ''} + ${renderTitle(this.docName)} ) => { - //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) - (( - this.menu.filter( - item => item !== 'divider' && !item.actionItem - )[ae.detail.index] - ))?.action?.(ae); - }} + @action=${makeListAction(this.menu)} > ${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); + } + } + + } /** Renders the enabled editor plugins and a tab bar to switch between them*/ protected renderContent(): TemplateResult { + + if(!this.doc) return html``; + return html` - ${this.doc - ? html` - (this.activeTab = e.detail.index)} - > - ${this.editors.map(this.renderEditorTab)} - - ${this.editors[this.activeTab]?.content - ? this.editors[this.activeTab].content - : ``}` - : ``} + (this.activeTab = e.detail.index)}> + ${this.editors.map(this.renderEditorTab)} + + ${renderEditorContent(this.editors, this.activeTab)} `; + + function renderEditorContent(editors: Plugin[], activeTab: number){ + const content = editors[activeTab]?.content; + if(!content) { return html`` } + + return html`${content}`; + } } - /** Renders the landing buttons (open project and new project)*/ + /** + * 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 { - return html` ${!this.doc - ? html`
    - ${(this.menu.filter(mi => mi !== 'divider')).map( - (mi: MenuItem, index) => - mi.kind === 'top' && !mi.disabled?.() - ? html` - -
    ${mi.name}
    -
    - ` - : html`` - )} -
    ` - : ``}`; - } + 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(); + } + + } + } /** Renders the "Add Custom Plug-in" UI*/ + // TODO: this should be its own isolated element protected renderDownloadUI(): TemplateResult { return html` @@ -615,30 +622,34 @@ export class OscdLayout extends LitElement { `; } + // Note: why is the type here if note used? private renderPluginKind( type: PluginKind | MenuPosition, plugins: Plugin[] ): TemplateResult { return html` - ${plugins.map( - plugin => - html` html` + - ${plugin.icon || pluginIcons[plugin.kind]} + + ${plugin.icon || pluginIcons[plugin.kind]} + ${plugin.name} - ` + + ` )} `; } - /** Renders the plug-in management UI (turning plug-ins on/off)*/ + /** + * Renders the plug-in management UI (turning plug-ins on/off) + * TODO: this is big enough to be its own isolated element + */ protected renderPluginUI(): TemplateResult { return html` - ${this.renderHeader()} ${this.renderAside()} ${this.renderContent()} - ${this.renderLanding()} ${this.renderPlugging()} - `; - } + static styles = css` mwc-drawer { diff --git a/packages/openscd/src/open-scd.ts b/packages/openscd/src/open-scd.ts index f6e8673205..f12a63aad2 100644 --- a/packages/openscd/src/open-scd.ts +++ b/packages/openscd/src/open-scd.ts @@ -38,7 +38,7 @@ import './addons/Layout.js'; import { ActionDetail } from '@material/mwc-list'; -import { officialPlugins } from './plugins.js'; +import { officialPlugins as builtinPlugins } from './plugins.js'; import { initializeNsdoc, Nsdoc } from './foundation/nsdoc.js'; import type { PluginSet, @@ -343,7 +343,7 @@ export class OpenSCD extends LitElement { } private resetPlugins(): void { this.storePlugins( - (officialPlugins as Plugin[]).concat(this.parsedPlugins).map(plugin => { + (builtinPlugins as Plugin[]).concat(this.parsedPlugins).map(plugin => { return { src: plugin.src, installed: plugin.default ?? false, @@ -360,48 +360,66 @@ export class OpenSCD extends LitElement { plugins: PluginSet = { menu: [], editor: [] }; get parsedPlugins(): Plugin[] { - return this.plugins.menu - .map((p: CorePlugin) => ({ - ...p, - position: - typeof p.position !== 'number' - ? (p.position as MenuPosition) - : undefined, - kind: 'menu' as PluginKind, - installed: p.active ?? false, - })) - .concat( - this.plugins.editor.map((p: CorePlugin) => ({ - ...p, - position: undefined, - kind: 'editor' as PluginKind, - installed: p.active ?? false, - })) - ); + + const menuPlugins = this.plugins.menu.map((plugin: CorePlugin) => { + let newPosition: MenuPosition | undefined = plugin.position as MenuPosition; + if(typeof plugin.position === 'number') { + newPosition = undefined + } + + return { + ...plugin, + position: newPosition, + kind: 'menu' as PluginKind, + installed: plugin.active ?? false, + } + }) + + const editorPlugins = this.plugins.editor.map((plugin: CorePlugin) => ({ + ...plugin, + position: undefined, + kind: 'editor' as PluginKind, + installed: plugin.active ?? false, + })) + + const allPlugnis = [...menuPlugins, ...editorPlugins] + return allPlugnis } private get sortedStoredPlugins(): Plugin[] { - return this.storedPlugins - .map(plugin => { - if (!plugin.official) return plugin; - const officialPlugin = (officialPlugins as Plugin[]) - .concat(this.parsedPlugins) - .find(needle => needle.src === plugin.src); + + const mergedPlugins = this.storedPlugins.map(plugin => { + if (!plugin.official){ return plugin }; + + const officialPlugin = (builtinPlugins as Plugin[]) + .concat(this.parsedPlugins) + .find(needle => needle.src === plugin.src); + return { ...officialPlugin, ...plugin, }; - }) + }) + + + return mergedPlugins .sort(compareNeedsDoc) .sort(menuCompare); } private get storedPlugins(): Plugin[] { - return ( - JSON.parse(localStorage.getItem('plugins') ?? '[]', (key, value) => - value.src && value.installed ? this.addContent(value) : value - ) - ); + const pluginsConfigStr = localStorage.getItem('plugins') ?? '[]' + const storedPlugins = JSON.parse(pluginsConfigStr) as Plugin[] + + const plugins = storedPlugins.map(plugin => { + const isInstalled = plugin.src && plugin.installed + if(!isInstalled) { plugin } + + return this.addContent(plugin) + }) + + return plugins + } protected get locale(): string { @@ -420,16 +438,20 @@ export class OpenSCD extends LitElement { private setPlugins(indices: Set) { const newPlugins = this.sortedStoredPlugins.map((plugin, index) => { - return { ...plugin, installed: indices.has(index) }; + return { + ...plugin, + installed: indices.has(index) + }; }); this.storePlugins(newPlugins); } private updatePlugins() { + const stored: Plugin[] = this.storedPlugins; const officialStored = stored.filter(p => p.official); const newOfficial: Array = ( - officialPlugins as Plugin[] + builtinPlugins as Plugin[] ) .concat(this.parsedPlugins) .filter(p => !officialStored.find(o => o.src === p.src)) @@ -440,9 +462,10 @@ export class OpenSCD extends LitElement { official: true as const, }; }); + const oldOfficial = officialStored.filter( p => - !(officialPlugins as Plugin[]) + !(builtinPlugins as Plugin[]) .concat(this.parsedPlugins) .find(o => p.src === o.src) );