Skip to content

Commit

Permalink
Feat: Programatic Plugin Activation (#1611)
Browse files Browse the repository at this point in the history
  • Loading branch information
trusz authored Feb 4, 2025
1 parent 65d6f73 commit d3b2a0a
Show file tree
Hide file tree
Showing 10 changed files with 916 additions and 499 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions packages/core/foundation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,11 @@ export type {
EditCompletedEvent,
EditCompletedDetail,
} from './foundation/edit-completed-event.js';

/** @returns the cartesian product of `arrays` */
export function crossProduct<T>(...arrays: T[][]): T[][] {
return arrays.reduce<T[][]>(
(a, b) => <T[][]>a.flatMap(d => b.map(e => [d, e].flat())),
[[]]
);
}
89 changes: 89 additions & 0 deletions packages/core/foundation/scl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { crossProduct } from '../foundation.js';

function getDataModelChildren(parent: Element): Element[] {
if (['LDevice', 'Server'].includes(parent.tagName))
return Array.from(parent.children).filter(
child =>
child.tagName === 'LDevice' ||
child.tagName === 'LN0' ||
child.tagName === 'LN'
);

const id =
parent.tagName === 'LN' || parent.tagName === 'LN0'
? parent.getAttribute('lnType')
: parent.getAttribute('type');

return Array.from(
parent.ownerDocument.querySelectorAll(
`LNodeType[id="${id}"] > DO, DOType[id="${id}"] > SDO, DOType[id="${id}"] > DA, DAType[id="${id}"] > BDA`
)
);
}

export function existFcdaReference(fcda: Element, ied: Element): boolean {
const [ldInst, prefix, lnClass, lnInst, doName, daName, fc] = [
'ldInst',
'prefix',
'lnClass',
'lnInst',
'doName',
'daName',
'fc',
].map(attr => fcda.getAttribute(attr));

const sinkLdInst = ied.querySelector(`LDevice[inst="${ldInst}"]`);
if (!sinkLdInst) return false;

const prefixSelctors = prefix
? [`[prefix="${prefix}"]`]
: ['[prefix=""]', ':not([prefix])'];
const lnInstSelectors = lnInst
? [`[inst="${lnInst}"]`]
: ['[inst=""]', ':not([inst])'];

const anyLnSelector = crossProduct(
['LN0', 'LN'],
prefixSelctors,
[`[lnClass="${lnClass}"]`],
lnInstSelectors
)
.map(strings => strings.join(''))
.join(',');

const sinkAnyLn = ied.querySelector(anyLnSelector);
if (!sinkAnyLn) return false;

const doNames = doName?.split('.');
if (!doNames) return false;

let parent: Element | undefined = sinkAnyLn;
for (const doNameAttr of doNames) {
parent = getDataModelChildren(parent).find(
child => child.getAttribute('name') === doNameAttr
);
if (!parent) return false;
}

const daNames = daName?.split('.');
const someFcInSink = getDataModelChildren(parent).some(
da => da.getAttribute('fc') === fc
);
if (!daNames && someFcInSink) return true;
if (!daNames) return false;

let sinkFc = '';
for (const daNameAttr of daNames) {
parent = getDataModelChildren(parent).find(
child => child.getAttribute('name') === daNameAttr
);

if (parent?.getAttribute('fc')) sinkFc = parent.getAttribute('fc')!;

if (!parent) return false;
}

if (sinkFc !== fc) return false;

return true;
}
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
],
"exports": {
".": "./dist/foundation.js",
"./foundation/scl.js": "./dist/foundation/scl.js",
"./foundation/deprecated/editor.js": "./dist/foundation/deprecated/editor.js",
"./foundation/deprecated/open-event.js": "./dist/foundation/deprecated/open-event.js",
"./foundation/deprecated/settings.js": "./dist/foundation/deprecated/settings.js",
Expand Down Expand Up @@ -159,4 +160,4 @@
"prettier --write"
]
}
}
}
1 change: 0 additions & 1 deletion packages/distribution/snowpack.config.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export default {
plugins: ['@snowpack/plugin-typescript'],
packageOptions: {
external: [
'@web/dev-server-core',
Expand Down
87 changes: 70 additions & 17 deletions packages/openscd/src/addons/Layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ import '@material/mwc-dialog';
import '@material/mwc-switch';
import '@material/mwc-select';
import '@material/mwc-textfield';
import { EditCompletedEvent } from '@openscd/core';
import { nothing } from 'lit';


@customElement('oscd-layout')
export class OscdLayout extends LitElement {
Expand All @@ -61,6 +62,8 @@ export class OscdLayout extends LitElement {
return html`
<div
@open-plugin-download=${() => this.pluginDownloadUI.show()}
@oscd-activate-editor=${this.handleActivateEditorByEvent}
@oscd-run-menu=${this.handleRunMenuByEvent}
>
<slot></slot>
${this.renderHeader()} ${this.renderAside()} ${this.renderContent()}
Expand Down Expand Up @@ -155,6 +158,7 @@ export class OscdLayout extends LitElement {
},
disabled: (): boolean => !this.historyState.canUndo,
kind: 'static',
content: () => html``,
},
{
icon: 'redo',
Expand All @@ -165,6 +169,7 @@ export class OscdLayout extends LitElement {
},
disabled: (): boolean => !this.historyState.canRedo,
kind: 'static',
content: () => html``,
},
...validators,
{
Expand All @@ -175,6 +180,7 @@ export class OscdLayout extends LitElement {
this.dispatchEvent(newHistoryUIEvent(true, HistoryUIKind.log));
},
kind: 'static',
content: () => html``,
},
{
icon: 'history',
Expand All @@ -184,6 +190,7 @@ export class OscdLayout extends LitElement {
this.dispatchEvent(newHistoryUIEvent(true, HistoryUIKind.history));
},
kind: 'static',
content: () => html``,
},
{
icon: 'rule',
Expand All @@ -193,6 +200,7 @@ export class OscdLayout extends LitElement {
this.dispatchEvent(newHistoryUIEvent(true, HistoryUIKind.diagnostic));
},
kind: 'static',
content: () => html``,
},
'divider',
...middleMenu,
Expand All @@ -203,13 +211,15 @@ export class OscdLayout extends LitElement {
this.dispatchEvent(newSettingsUIEvent(true));
},
kind: 'static',
content: () => html``,
},
...bottomMenu,
{
icon: 'extension',
name: 'plugins.heading',
action: (): void => this.pluginUI.show(),
kind: 'static',
content: () => html``,
},
];
}
Expand Down Expand Up @@ -333,7 +343,10 @@ export class OscdLayout extends LitElement {
);
},
disabled: (): boolean => plugin.requireDoc! && this.doc === null,
content: plugin.content,
content: () => {
if(plugin.content){ return plugin.content(); }
return html``;
},
kind: kind,
}
})
Expand All @@ -358,29 +371,32 @@ export class OscdLayout extends LitElement {
);
},
disabled: (): boolean => this.doc === null,
content: plugin.content,
content: plugin.content ?? (() => html``),
kind: 'validator',
}
});
}

private renderMenuItem(me: MenuItem | 'divider'): TemplateResult {
if (me === 'divider') { return html`<li divider padded role="separator"></li>`; }
if (me.actionItem){ return html``; }
const isDivider = me === 'divider';
const hasActionItem = me !== 'divider' && me.actionItem;

if (isDivider) { return html`<li divider padded role="separator"></li>`; }
if (hasActionItem){ return html``; }
return html`
<mwc-list-item
class="${me.kind}"
iconid="${me.icon}"
graphic="icon"
data-name="${me.name}"
.disabled=${me.disabled?.() || !me.action}
><mwc-icon slot="graphic">${me.icon}</mwc-icon>
<span>${get(me.name)}</span>
${me.hint
? html`<span slot="secondary"><tt>${me.hint}</tt></span>`
: ''}
</mwc-list-item>
${me.content ?? ''}
${me.content ? me.content() : nothing}
`;
}

Expand Down Expand Up @@ -456,24 +472,32 @@ export class OscdLayout extends LitElement {

}

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 hasActiveDoc = Boolean(this.doc);

const activeEditors = 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
})
.map(this.renderEditorTab)
const activeEditors = this.calcActiveEditors()
.map(this.renderEditorTab)

const hasActiveEditors = activeEditors.length > 0;
if(!hasActiveEditors){ return html``; }

return html`
<mwc-tab-bar @MDCTabBar:activated=${(e: CustomEvent) => (this.activeTab = e.detail.index)}>
<mwc-tab-bar
@MDCTabBar:activated=${this.handleActivatedEditorTabByUser}
activeIndex=${this.activeTab}
>
${activeEditors}
</mwc-tab-bar>
${renderEditorContent(this.editors, this.activeTab, this.doc)}
Expand All @@ -487,10 +511,39 @@ export class OscdLayout extends LitElement {
const content = editor?.content;
if(!content) { return html`` }

return html`${content}`;
return html`${content()}`;
}
}

private handleActivatedEditorTabByUser(e: CustomEvent): void {
const tabIndex = e.detail.index;
this.activateTab(tabIndex);
}

private handleActivateEditorByEvent(e: CustomEvent<{name: string, src: string}>): void {
const {name, src} = e.detail;
const editors = this.calcActiveEditors()
const wantedEditorIndex = editors.findIndex(editor => editor.name === name || editor.src === src)
if(wantedEditorIndex < 0){ return; } // TODO: log error

this.activateTab(wantedEditorIndex);
}

private activateTab(index: number){
this.activeTab = index;
}

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 menuElement = menuEntry.nextElementSibling
if(!menuElement){ return; } // TODO: log error

(menuElement 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
Expand Down
Loading

0 comments on commit d3b2a0a

Please sign in to comment.