Skip to content
This repository was archived by the owner on Nov 27, 2025. It is now read-only.

Commit d3b2a0a

Browse files
authored
Feat: Programatic Plugin Activation (#1611)
1 parent 65d6f73 commit d3b2a0a

File tree

10 files changed

+916
-499
lines changed

10 files changed

+916
-499
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/foundation.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,11 @@ export type {
5858
EditCompletedEvent,
5959
EditCompletedDetail,
6060
} from './foundation/edit-completed-event.js';
61+
62+
/** @returns the cartesian product of `arrays` */
63+
export function crossProduct<T>(...arrays: T[][]): T[][] {
64+
return arrays.reduce<T[][]>(
65+
(a, b) => <T[][]>a.flatMap(d => b.map(e => [d, e].flat())),
66+
[[]]
67+
);
68+
}

packages/core/foundation/scl.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { crossProduct } from '../foundation.js';
2+
3+
function getDataModelChildren(parent: Element): Element[] {
4+
if (['LDevice', 'Server'].includes(parent.tagName))
5+
return Array.from(parent.children).filter(
6+
child =>
7+
child.tagName === 'LDevice' ||
8+
child.tagName === 'LN0' ||
9+
child.tagName === 'LN'
10+
);
11+
12+
const id =
13+
parent.tagName === 'LN' || parent.tagName === 'LN0'
14+
? parent.getAttribute('lnType')
15+
: parent.getAttribute('type');
16+
17+
return Array.from(
18+
parent.ownerDocument.querySelectorAll(
19+
`LNodeType[id="${id}"] > DO, DOType[id="${id}"] > SDO, DOType[id="${id}"] > DA, DAType[id="${id}"] > BDA`
20+
)
21+
);
22+
}
23+
24+
export function existFcdaReference(fcda: Element, ied: Element): boolean {
25+
const [ldInst, prefix, lnClass, lnInst, doName, daName, fc] = [
26+
'ldInst',
27+
'prefix',
28+
'lnClass',
29+
'lnInst',
30+
'doName',
31+
'daName',
32+
'fc',
33+
].map(attr => fcda.getAttribute(attr));
34+
35+
const sinkLdInst = ied.querySelector(`LDevice[inst="${ldInst}"]`);
36+
if (!sinkLdInst) return false;
37+
38+
const prefixSelctors = prefix
39+
? [`[prefix="${prefix}"]`]
40+
: ['[prefix=""]', ':not([prefix])'];
41+
const lnInstSelectors = lnInst
42+
? [`[inst="${lnInst}"]`]
43+
: ['[inst=""]', ':not([inst])'];
44+
45+
const anyLnSelector = crossProduct(
46+
['LN0', 'LN'],
47+
prefixSelctors,
48+
[`[lnClass="${lnClass}"]`],
49+
lnInstSelectors
50+
)
51+
.map(strings => strings.join(''))
52+
.join(',');
53+
54+
const sinkAnyLn = ied.querySelector(anyLnSelector);
55+
if (!sinkAnyLn) return false;
56+
57+
const doNames = doName?.split('.');
58+
if (!doNames) return false;
59+
60+
let parent: Element | undefined = sinkAnyLn;
61+
for (const doNameAttr of doNames) {
62+
parent = getDataModelChildren(parent).find(
63+
child => child.getAttribute('name') === doNameAttr
64+
);
65+
if (!parent) return false;
66+
}
67+
68+
const daNames = daName?.split('.');
69+
const someFcInSink = getDataModelChildren(parent).some(
70+
da => da.getAttribute('fc') === fc
71+
);
72+
if (!daNames && someFcInSink) return true;
73+
if (!daNames) return false;
74+
75+
let sinkFc = '';
76+
for (const daNameAttr of daNames) {
77+
parent = getDataModelChildren(parent).find(
78+
child => child.getAttribute('name') === daNameAttr
79+
);
80+
81+
if (parent?.getAttribute('fc')) sinkFc = parent.getAttribute('fc')!;
82+
83+
if (!parent) return false;
84+
}
85+
86+
if (sinkFc !== fc) return false;
87+
88+
return true;
89+
}

packages/core/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
],
1414
"exports": {
1515
".": "./dist/foundation.js",
16+
"./foundation/scl.js": "./dist/foundation/scl.js",
1617
"./foundation/deprecated/editor.js": "./dist/foundation/deprecated/editor.js",
1718
"./foundation/deprecated/open-event.js": "./dist/foundation/deprecated/open-event.js",
1819
"./foundation/deprecated/settings.js": "./dist/foundation/deprecated/settings.js",
@@ -159,4 +160,4 @@
159160
"prettier --write"
160161
]
161162
}
162-
}
163+
}

packages/distribution/snowpack.config.mjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export default {
2-
plugins: ['@snowpack/plugin-typescript'],
32
packageOptions: {
43
external: [
54
'@web/dev-server-core',

packages/openscd/src/addons/Layout.ts

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ import '@material/mwc-dialog';
5252
import '@material/mwc-switch';
5353
import '@material/mwc-select';
5454
import '@material/mwc-textfield';
55-
import { EditCompletedEvent } from '@openscd/core';
55+
import { nothing } from 'lit';
56+
5657

5758
@customElement('oscd-layout')
5859
export class OscdLayout extends LitElement {
@@ -61,6 +62,8 @@ export class OscdLayout extends LitElement {
6162
return html`
6263
<div
6364
@open-plugin-download=${() => this.pluginDownloadUI.show()}
65+
@oscd-activate-editor=${this.handleActivateEditorByEvent}
66+
@oscd-run-menu=${this.handleRunMenuByEvent}
6467
>
6568
<slot></slot>
6669
${this.renderHeader()} ${this.renderAside()} ${this.renderContent()}
@@ -155,6 +158,7 @@ export class OscdLayout extends LitElement {
155158
},
156159
disabled: (): boolean => !this.historyState.canUndo,
157160
kind: 'static',
161+
content: () => html``,
158162
},
159163
{
160164
icon: 'redo',
@@ -165,6 +169,7 @@ export class OscdLayout extends LitElement {
165169
},
166170
disabled: (): boolean => !this.historyState.canRedo,
167171
kind: 'static',
172+
content: () => html``,
168173
},
169174
...validators,
170175
{
@@ -175,6 +180,7 @@ export class OscdLayout extends LitElement {
175180
this.dispatchEvent(newHistoryUIEvent(true, HistoryUIKind.log));
176181
},
177182
kind: 'static',
183+
content: () => html``,
178184
},
179185
{
180186
icon: 'history',
@@ -184,6 +190,7 @@ export class OscdLayout extends LitElement {
184190
this.dispatchEvent(newHistoryUIEvent(true, HistoryUIKind.history));
185191
},
186192
kind: 'static',
193+
content: () => html``,
187194
},
188195
{
189196
icon: 'rule',
@@ -193,6 +200,7 @@ export class OscdLayout extends LitElement {
193200
this.dispatchEvent(newHistoryUIEvent(true, HistoryUIKind.diagnostic));
194201
},
195202
kind: 'static',
203+
content: () => html``,
196204
},
197205
'divider',
198206
...middleMenu,
@@ -203,13 +211,15 @@ export class OscdLayout extends LitElement {
203211
this.dispatchEvent(newSettingsUIEvent(true));
204212
},
205213
kind: 'static',
214+
content: () => html``,
206215
},
207216
...bottomMenu,
208217
{
209218
icon: 'extension',
210219
name: 'plugins.heading',
211220
action: (): void => this.pluginUI.show(),
212221
kind: 'static',
222+
content: () => html``,
213223
},
214224
];
215225
}
@@ -333,7 +343,10 @@ export class OscdLayout extends LitElement {
333343
);
334344
},
335345
disabled: (): boolean => plugin.requireDoc! && this.doc === null,
336-
content: plugin.content,
346+
content: () => {
347+
if(plugin.content){ return plugin.content(); }
348+
return html``;
349+
},
337350
kind: kind,
338351
}
339352
})
@@ -358,29 +371,32 @@ export class OscdLayout extends LitElement {
358371
);
359372
},
360373
disabled: (): boolean => this.doc === null,
361-
content: plugin.content,
374+
content: plugin.content ?? (() => html``),
362375
kind: 'validator',
363376
}
364377
});
365378
}
366379

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

384+
if (isDivider) { return html`<li divider padded role="separator"></li>`; }
385+
if (hasActionItem){ return html``; }
371386
return html`
372387
<mwc-list-item
373388
class="${me.kind}"
374389
iconid="${me.icon}"
375390
graphic="icon"
391+
data-name="${me.name}"
376392
.disabled=${me.disabled?.() || !me.action}
377393
><mwc-icon slot="graphic">${me.icon}</mwc-icon>
378394
<span>${get(me.name)}</span>
379395
${me.hint
380396
? html`<span slot="secondary"><tt>${me.hint}</tt></span>`
381397
: ''}
382398
</mwc-list-item>
383-
${me.content ?? ''}
399+
${me.content ? me.content() : nothing}
384400
`;
385401
}
386402

@@ -456,24 +472,32 @@ export class OscdLayout extends LitElement {
456472

457473
}
458474

475+
private calcActiveEditors(){
476+
const hasActiveDoc = Boolean(this.doc);
477+
478+
return this.editors
479+
.filter(editor => {
480+
// this is necessary because `requireDoc` can be undefined
481+
// and that is not the same as false
482+
const doesNotRequireDoc = editor.requireDoc === false
483+
return doesNotRequireDoc || hasActiveDoc
484+
})
485+
}
486+
459487
/** Renders the enabled editor plugins and a tab bar to switch between them*/
460488
protected renderContent(): TemplateResult {
461-
const hasActiveDoc = Boolean(this.doc);
462489

463-
const activeEditors = this.editors
464-
.filter(editor => {
465-
// this is necessary because `requireDoc` can be undefined
466-
// and that is not the same as false
467-
const doesNotRequireDoc = editor.requireDoc === false
468-
return doesNotRequireDoc || hasActiveDoc
469-
})
470-
.map(this.renderEditorTab)
490+
const activeEditors = this.calcActiveEditors()
491+
.map(this.renderEditorTab)
471492

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

475496
return html`
476-
<mwc-tab-bar @MDCTabBar:activated=${(e: CustomEvent) => (this.activeTab = e.detail.index)}>
497+
<mwc-tab-bar
498+
@MDCTabBar:activated=${this.handleActivatedEditorTabByUser}
499+
activeIndex=${this.activeTab}
500+
>
477501
${activeEditors}
478502
</mwc-tab-bar>
479503
${renderEditorContent(this.editors, this.activeTab, this.doc)}
@@ -487,10 +511,39 @@ export class OscdLayout extends LitElement {
487511
const content = editor?.content;
488512
if(!content) { return html`` }
489513

490-
return html`${content}`;
514+
return html`${content()}`;
491515
}
492516
}
493517

518+
private handleActivatedEditorTabByUser(e: CustomEvent): void {
519+
const tabIndex = e.detail.index;
520+
this.activateTab(tabIndex);
521+
}
522+
523+
private handleActivateEditorByEvent(e: CustomEvent<{name: string, src: string}>): void {
524+
const {name, src} = e.detail;
525+
const editors = this.calcActiveEditors()
526+
const wantedEditorIndex = editors.findIndex(editor => editor.name === name || editor.src === src)
527+
if(wantedEditorIndex < 0){ return; } // TODO: log error
528+
529+
this.activateTab(wantedEditorIndex);
530+
}
531+
532+
private activateTab(index: number){
533+
this.activeTab = index;
534+
}
535+
536+
private handleRunMenuByEvent(e: CustomEvent<{name: string}>): void {
537+
538+
// TODO: this is a workaround, fix it
539+
this.menuUI.open = true;
540+
const menuEntry = this.menuUI.querySelector(`[data-name="${e.detail.name}"]`) as HTMLElement
541+
const menuElement = menuEntry.nextElementSibling
542+
if(!menuElement){ return; } // TODO: log error
543+
544+
(menuElement as unknown as MenuPlugin).run()
545+
}
546+
494547
/**
495548
* Renders the landing buttons (open project and new project)
496549
* it no document loaded we display the menu item that are in the position

0 commit comments

Comments
 (0)