@eclipse-docks/core 0.7.126 → 0.7.130
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.js +4 -4
- package/dist/api/services.d.ts +4 -0
- package/dist/api/services.d.ts.map +1 -1
- package/dist/{api-B3RXoez_.js → api-oPg3Bv1S.js} +422 -293
- package/dist/api-oPg3Bv1S.js.map +1 -0
- package/dist/components/attribution.d.ts +1 -0
- package/dist/components/attribution.d.ts.map +1 -1
- package/dist/components/command.d.ts +4 -5
- package/dist/components/command.d.ts.map +1 -1
- package/dist/components/extensions.d.ts +3 -0
- package/dist/components/extensions.d.ts.map +1 -1
- package/dist/components/filebrowser.d.ts.map +1 -1
- package/dist/components/language-selector.d.ts.map +1 -1
- package/dist/components/layout-switcher.d.ts +1 -1
- package/dist/components/layout-switcher.d.ts.map +1 -1
- package/dist/components/npm-packages.d.ts +1 -0
- package/dist/components/npm-packages.d.ts.map +1 -1
- package/dist/components/release-history.d.ts +9 -2
- package/dist/components/release-history.d.ts.map +1 -1
- package/dist/components/tasks.d.ts.map +1 -1
- package/dist/core/apploader.d.ts +2 -9
- package/dist/core/apploader.d.ts.map +1 -1
- package/dist/core/contribution-names.d.ts +0 -2
- package/dist/core/contribution-names.d.ts.map +1 -1
- package/dist/core/contribution-targets.d.ts +1 -0
- package/dist/core/contribution-targets.d.ts.map +1 -1
- package/dist/core/contributionregistry.d.ts +3 -0
- package/dist/core/contributionregistry.d.ts.map +1 -1
- package/dist/core/dialogservice.d.ts +3 -1
- package/dist/core/dialogservice.d.ts.map +1 -1
- package/dist/core/dropdown-item.d.ts +43 -0
- package/dist/core/dropdown-item.d.ts.map +1 -0
- package/dist/core/dropdown-menu-utils.d.ts +5 -0
- package/dist/core/dropdown-menu-utils.d.ts.map +1 -1
- package/dist/core/layout-panels.d.ts +25 -0
- package/dist/core/layout-panels.d.ts.map +1 -0
- package/dist/dialogs/navigable-info-dialog.d.ts.map +1 -1
- package/dist/dialogs/version-info-dialog.d.ts +18 -2
- package/dist/dialogs/version-info-dialog.d.ts.map +1 -1
- package/dist/externals/lit.js +1 -1
- package/dist/externals/webawesome.js +1 -1
- package/dist/index.js +4 -4
- package/dist/layouts/standard-layout.d.ts +9 -1
- package/dist/layouts/standard-layout.d.ts.map +1 -1
- package/dist/parts/contextmenu.d.ts +5 -2
- package/dist/parts/contextmenu.d.ts.map +1 -1
- package/dist/parts/index.d.ts.map +1 -1
- package/dist/parts/index.js +1 -1
- package/dist/parts/part.d.ts +4 -0
- package/dist/parts/part.d.ts.map +1 -1
- package/dist/parts/resizable-grid.d.ts +10 -3
- package/dist/parts/resizable-grid.d.ts.map +1 -1
- package/dist/parts/tabs.d.ts +6 -0
- package/dist/parts/tabs.d.ts.map +1 -1
- package/dist/{parts-CsoNFVTw.js → parts-CAt8aeU8.js} +1361 -975
- package/dist/parts-CAt8aeU8.js.map +1 -0
- package/dist/{webawesome-Y1_HnKa_.js → webawesome-BGXn04Mw.js} +18 -10
- package/dist/webawesome-BGXn04Mw.js.map +1 -0
- package/dist/{widget-DbLusgAH.js → widget-BqQtioK1.js} +11 -5
- package/dist/widget-BqQtioK1.js.map +1 -0
- package/dist/widgets/index.js +1 -1
- package/dist/{widgets-DnnSZhsi.js → widgets-C9V42Mqq.js} +2 -2
- package/dist/{widgets-DnnSZhsi.js.map → widgets-C9V42Mqq.js.map} +1 -1
- package/package.json +1 -1
- package/src/api/services.ts +20 -0
- package/src/commands/filebrowser-contributions.ts +0 -16
- package/src/components/attribution.ts +2 -0
- package/src/components/command.ts +45 -119
- package/src/components/extensions.ts +20 -3
- package/src/components/filebrowser.ts +15 -12
- package/src/components/language-selector.ts +0 -1
- package/src/components/layout-switcher.ts +39 -26
- package/src/components/npm-packages.ts +2 -0
- package/src/components/release-history.ts +29 -40
- package/src/components/tasks.ts +0 -1
- package/src/contributions/default-layout-contributions.ts +7 -43
- package/src/contributions/default-ui-contributions.ts +4 -4
- package/src/core/apploader.ts +7 -54
- package/src/core/contribution-names.ts +0 -3
- package/src/core/contribution-targets.ts +1 -0
- package/src/core/contributionregistry.ts +19 -3
- package/src/core/dialogservice.ts +76 -19
- package/src/core/dropdown-item.ts +187 -0
- package/src/core/dropdown-menu-utils.ts +59 -0
- package/src/core/layout-panels.ts +121 -0
- package/src/dialogs/navigable-info-dialog.ts +5 -2
- package/src/dialogs/version-info-dialog.ts +163 -21
- package/src/icons/icons.txt +4 -2
- package/src/icons/layout-standard-bottom-panel.svg +4 -4
- package/src/icons/layout-standard-full.svg +4 -4
- package/src/icons/layout-standard-left-aux.svg +8 -0
- package/src/icons/layout-standard-left-sidebar.svg +5 -0
- package/src/icons/layout-standard-right-aux.svg +8 -0
- package/src/icons/layout-standard-right-sidebar.svg +5 -0
- package/src/layouts/standard-layout.ts +81 -5
- package/src/parts/contextmenu.ts +39 -44
- package/src/parts/index.ts +4 -1
- package/src/parts/part.ts +26 -0
- package/src/parts/resizable-grid.ts +125 -57
- package/src/parts/tabs.ts +103 -0
- package/dist/api-B3RXoez_.js.map +0 -1
- package/dist/parts-CsoNFVTw.js.map +0 -1
- package/dist/webawesome-Y1_HnKa_.js.map +0 -1
- package/dist/widget-DbLusgAH.js.map +0 -1
- package/src/icons/layout-standard-bottom-sidebar.svg +0 -6
- package/src/icons/layout-standard.svg +0 -6
|
@@ -44,7 +44,7 @@ contributionRegistry.registerContribution(ABOUT_TABS, {
|
|
|
44
44
|
ranking: 100,
|
|
45
45
|
toolbar: false,
|
|
46
46
|
contextMenu: false,
|
|
47
|
-
component: (id: string) => html`<docks-attribution id="${id}"></docks-attribution>`
|
|
47
|
+
component: (id: string) => html`<docks-attribution id="${id}" embedded></docks-attribution>`
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
contributionRegistry.registerContribution(ABOUT_TABS, {
|
|
@@ -54,7 +54,7 @@ contributionRegistry.registerContribution(ABOUT_TABS, {
|
|
|
54
54
|
ranking: 50,
|
|
55
55
|
toolbar: false,
|
|
56
56
|
contextMenu: false,
|
|
57
|
-
component: (id: string) => html`<docks-release-history id="${id}"></docks-release-history>`
|
|
57
|
+
component: (id: string) => html`<docks-release-history id="${id}" embedded></docks-release-history>`
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
contributionRegistry.registerContribution(ABOUT_TABS, {
|
|
@@ -64,7 +64,7 @@ contributionRegistry.registerContribution(ABOUT_TABS, {
|
|
|
64
64
|
ranking: 0,
|
|
65
65
|
toolbar: false,
|
|
66
66
|
contextMenu: false,
|
|
67
|
-
component: (id: string) => html`<docks-npm-packages id="${id}"></docks-npm-packages>`
|
|
67
|
+
component: (id: string) => html`<docks-npm-packages id="${id}" embedded></docks-npm-packages>`
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
contributionRegistry.registerContribution(SYSTEM_ATTRIBUTIONS, {
|
|
@@ -100,7 +100,7 @@ contributionRegistry.registerContribution(TOOLBAR_BOTTOM_END, {
|
|
|
100
100
|
|
|
101
101
|
contributionRegistry.registerContribution(TOOLBAR_MAIN_RIGHT, {
|
|
102
102
|
name: TOOLBAR_LAYOUT_SWITCHER,
|
|
103
|
-
label: "Layout
|
|
103
|
+
label: "Layout panels",
|
|
104
104
|
component: () => html`<docks-layout-switcher></docks-layout-switcher>`
|
|
105
105
|
} as HTMLContribution);
|
|
106
106
|
|
package/src/core/apploader.ts
CHANGED
|
@@ -26,21 +26,12 @@ import { publish } from "./events";
|
|
|
26
26
|
|
|
27
27
|
const logger = createLogger('AppLoader');
|
|
28
28
|
|
|
29
|
-
/** Layout reference: layout id string, or id + props
|
|
29
|
+
/** Layout reference: layout id string, or id + optional props read by the layout on init. */
|
|
30
30
|
export type LayoutDescriptor = string | { id: string; props?: Record<string, string | boolean> };
|
|
31
31
|
|
|
32
32
|
function getLayoutIdFromApp(app: AppDefinition | undefined): string {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return typeof l === 'object' ? l.id : (l ?? 'standard');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function propsToAttributes(props: Record<string, string | boolean>): Record<string, string> {
|
|
39
|
-
const out: Record<string, string> = {};
|
|
40
|
-
for (const [k, v] of Object.entries(props)) {
|
|
41
|
-
out[k] = typeof v === 'boolean' ? (v ? 'true' : 'false') : v;
|
|
42
|
-
}
|
|
43
|
-
return out;
|
|
33
|
+
const layout = app?.layout ?? 'standard';
|
|
34
|
+
return typeof layout === 'object' ? layout.id : layout;
|
|
44
35
|
}
|
|
45
36
|
|
|
46
37
|
/**
|
|
@@ -177,15 +168,11 @@ export interface AppDefinition {
|
|
|
177
168
|
releaseHistory?: ReleaseHistory | (() => ReleaseHistory | Promise<ReleaseHistory>);
|
|
178
169
|
|
|
179
170
|
/**
|
|
180
|
-
* Layout: id (string) or { id, props }
|
|
181
|
-
* App root is the chosen layout's component. Props are merged as attributes when rendering (e.g. show-bottom-panel).
|
|
171
|
+
* Layout: id (string) or { id, props }. The layout component reads props on init (e.g. panel visibility).
|
|
182
172
|
* Defaults to 'standard' when omitted.
|
|
183
173
|
*/
|
|
184
174
|
layout?: LayoutDescriptor;
|
|
185
175
|
|
|
186
|
-
/** @deprecated Use layout (string or { id, props }) instead. */
|
|
187
|
-
layoutId?: string;
|
|
188
|
-
|
|
189
176
|
/**
|
|
190
177
|
* Optional cleanup function.
|
|
191
178
|
* Called when the app is being unloaded, before extensions are disabled.
|
|
@@ -248,8 +235,6 @@ class AppLoaderService {
|
|
|
248
235
|
private container: HTMLElement = document.body;
|
|
249
236
|
private systemRequiredExtensions: Set<string> = new Set();
|
|
250
237
|
private static readonly PREFERRED_APP_KEY = 'preferredAppName';
|
|
251
|
-
private static readonly PREFERRED_LAYOUT_KEY = 'preferredLayoutId';
|
|
252
|
-
private preferredLayoutId?: string;
|
|
253
238
|
|
|
254
239
|
/**
|
|
255
240
|
* Register an application with the framework.
|
|
@@ -269,6 +254,7 @@ class AppLoaderService {
|
|
|
269
254
|
}
|
|
270
255
|
app.name = app.name ?? 'app';
|
|
271
256
|
app.version = app.version ?? '0.0.0';
|
|
257
|
+
app.layout = app.layout ?? 'standard';
|
|
272
258
|
|
|
273
259
|
if (this.apps.has(app.name)) {
|
|
274
260
|
logger.warn(`App '${app.name}' is already registered. Overwriting.`);
|
|
@@ -443,7 +429,6 @@ class AppLoaderService {
|
|
|
443
429
|
|
|
444
430
|
this.currentApp = app;
|
|
445
431
|
logger.info(`App ${app.name} loaded successfully`);
|
|
446
|
-
this.preferredLayoutId = await this.getPreferredLayoutId();
|
|
447
432
|
this.updateDocumentMetadata(app);
|
|
448
433
|
if (container) {
|
|
449
434
|
this.dispatchLoadProgress('Rendering layout…');
|
|
@@ -485,7 +470,7 @@ class AppLoaderService {
|
|
|
485
470
|
throw new Error('No app loaded. Call loadApp() first.');
|
|
486
471
|
}
|
|
487
472
|
|
|
488
|
-
const layoutId =
|
|
473
|
+
const layoutId = getLayoutIdFromApp(this.currentApp);
|
|
489
474
|
const layouts = contributionRegistry.getContributions<LayoutContribution>(SYSTEM_LAYOUTS);
|
|
490
475
|
let layout = layouts.find((c) => c.id === layoutId);
|
|
491
476
|
if (!layout) {
|
|
@@ -501,10 +486,6 @@ class AppLoaderService {
|
|
|
501
486
|
if (r && typeof r === 'object' && 'tag' in r && r.attributes) {
|
|
502
487
|
effectiveAttributes = { ...r.attributes };
|
|
503
488
|
}
|
|
504
|
-
const appLayout = this.currentApp?.layout;
|
|
505
|
-
if (typeof appLayout === 'object' && appLayout.id === layoutId && appLayout.props) {
|
|
506
|
-
Object.assign(effectiveAttributes, propsToAttributes(appLayout.props));
|
|
507
|
-
}
|
|
508
489
|
|
|
509
490
|
container.innerHTML = '';
|
|
510
491
|
if (typeof r === 'string') {
|
|
@@ -582,35 +563,7 @@ class AppLoaderService {
|
|
|
582
563
|
}
|
|
583
564
|
|
|
584
565
|
getCurrentLayoutId(): string {
|
|
585
|
-
return
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
async getPreferredLayoutId(): Promise<string | undefined> {
|
|
589
|
-
try {
|
|
590
|
-
return await appSettings.get(AppLoaderService.PREFERRED_LAYOUT_KEY);
|
|
591
|
-
} catch (error) {
|
|
592
|
-
logger.debug(`Failed to get preferred layout ID: ${getErrorMessage(error)}`);
|
|
593
|
-
return undefined;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
async setPreferredLayoutId(layoutId: string): Promise<void> {
|
|
598
|
-
const layouts = this.getRegisteredLayouts();
|
|
599
|
-
if (!layouts.some((l) => l.id === layoutId)) {
|
|
600
|
-
throw new Error(`Layout '${layoutId}' not found.`);
|
|
601
|
-
}
|
|
602
|
-
try {
|
|
603
|
-
await appSettings.set(AppLoaderService.PREFERRED_LAYOUT_KEY, layoutId);
|
|
604
|
-
this.preferredLayoutId = layoutId;
|
|
605
|
-
logger.info(`Set preferred layout to: ${layoutId}`);
|
|
606
|
-
if (this.currentApp && this.container) {
|
|
607
|
-
this.renderApp(this.container);
|
|
608
|
-
}
|
|
609
|
-
window.dispatchEvent(new CustomEvent('layout-changed', { detail: { layoutId } }));
|
|
610
|
-
} catch (error) {
|
|
611
|
-
logger.error(`Failed to persist preferred layout: ${getErrorMessage(error)}`);
|
|
612
|
-
throw error;
|
|
613
|
-
}
|
|
566
|
+
return getLayoutIdFromApp(this.currentApp);
|
|
614
567
|
}
|
|
615
568
|
|
|
616
569
|
/**
|
|
@@ -27,8 +27,5 @@ export const ABOUT_TAB_ATTRIBUTIONS = "about.tab.attributions";
|
|
|
27
27
|
|
|
28
28
|
export const ATTRIBUTION_ECLIPSE_DOCKS = "attribution.eclipse-docks";
|
|
29
29
|
|
|
30
|
-
export const TOOLBAR_FILEBROWSER_RENAME = "toolbar.filebrowser.rename";
|
|
31
|
-
export const TOOLBAR_FILEBROWSER_DELETE = "toolbar.filebrowser.delete";
|
|
32
|
-
|
|
33
30
|
export const CTXMENU_FILEBROWSER_RENAME = "contextmenu.filebrowser.rename";
|
|
34
31
|
export const CTXMENU_FILEBROWSER_DELETE = "contextmenu.filebrowser.delete";
|
|
@@ -21,4 +21,5 @@ export const EDITOR_AREA_MAIN = "editor-area-main"
|
|
|
21
21
|
export const SIDEBAR_MAIN = "sidebar-main"
|
|
22
22
|
export const SIDEBAR_MAIN_BOTTOM = "sidebar-main-bottom"
|
|
23
23
|
export const SIDEBAR_AUXILIARY = "sidebar-auxiliary"
|
|
24
|
+
export const SIDEBAR_AUXILIARY_BOTTOM = "sidebar-auxiliary-bottom"
|
|
24
25
|
export const PANEL_BOTTOM = "panel-bottom"
|
|
@@ -39,6 +39,24 @@ export function isCommandContribution(contribution: Contribution): contribution
|
|
|
39
39
|
return "command" in contribution;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export type ReactiveBoolean = boolean | (() => boolean) | Signal.Computed<boolean>;
|
|
43
|
+
|
|
44
|
+
/** Reads a static or reactive boolean; plain functions and {@link Signal.Computed} are evaluated at call time. */
|
|
45
|
+
export function readReactiveBoolean(
|
|
46
|
+
value: ReactiveBoolean | undefined,
|
|
47
|
+
defaultValue: boolean
|
|
48
|
+
): boolean {
|
|
49
|
+
if (value === undefined) return defaultValue;
|
|
50
|
+
if (typeof value === 'boolean') return value;
|
|
51
|
+
if (typeof value === 'object' && value !== null && 'get' in value) {
|
|
52
|
+
return (value as Signal.Computed<boolean>).get() === true;
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === 'function') {
|
|
55
|
+
return (value as () => boolean)() === true;
|
|
56
|
+
}
|
|
57
|
+
return defaultValue;
|
|
58
|
+
}
|
|
59
|
+
|
|
42
60
|
/** Default true when `visible` is omitted. */
|
|
43
61
|
export function getContributionVisible(contribution: Contribution): boolean {
|
|
44
62
|
const visible = contribution.visible as Signal.Computed<boolean> | undefined;
|
|
@@ -48,9 +66,7 @@ export function getContributionVisible(contribution: Contribution): boolean {
|
|
|
48
66
|
|
|
49
67
|
/** Default false when `disabled` is omitted. */
|
|
50
68
|
export function getContributionDisabled(contribution: Contribution): boolean {
|
|
51
|
-
|
|
52
|
-
if (!disabled) return false;
|
|
53
|
-
return disabled.get() === true;
|
|
69
|
+
return readReactiveBoolean(contribution.disabled as ReactiveBoolean | undefined, false);
|
|
54
70
|
}
|
|
55
71
|
|
|
56
72
|
function wrapContributionReactiveFlags(contribution: Contribution): void {
|
|
@@ -60,9 +60,11 @@ export const DELETE_BUTTON: DialogButton = {
|
|
|
60
60
|
|
|
61
61
|
export interface DialogContribution extends Contribution {
|
|
62
62
|
id: string;
|
|
63
|
+
/** When true, `component` renders a complete `wa-dialog`. The service only mounts and manages lifecycle. */
|
|
64
|
+
selfContained?: boolean;
|
|
63
65
|
buttons?: DialogButton[];
|
|
64
66
|
component: (state?: any) => TemplateResult;
|
|
65
|
-
onButton
|
|
67
|
+
onButton?: (id: string, result: any, state?: any) => boolean | Promise<boolean> | void | Promise<void>;
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
let dialogContainer: HTMLElement | null = null;
|
|
@@ -76,6 +78,38 @@ function getDialogContainer(): HTMLElement {
|
|
|
76
78
|
return dialogContainer;
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
type WaDialogElement = HTMLElement & { open?: boolean };
|
|
82
|
+
|
|
83
|
+
function findWaDialog(root: ParentNode): WaDialogElement | null {
|
|
84
|
+
if (root instanceof Element && root.tagName.toLowerCase() === 'wa-dialog') {
|
|
85
|
+
return root as WaDialogElement;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const children = root instanceof Document || root instanceof DocumentFragment || root instanceof Element
|
|
89
|
+
? Array.from(root.children)
|
|
90
|
+
: root instanceof ShadowRoot
|
|
91
|
+
? Array.from(root.children)
|
|
92
|
+
: [];
|
|
93
|
+
|
|
94
|
+
for (const child of children) {
|
|
95
|
+
if (child instanceof Element && child.tagName.toLowerCase() === 'wa-dialog') {
|
|
96
|
+
return child as WaDialogElement;
|
|
97
|
+
}
|
|
98
|
+
if (child instanceof Element && child.shadowRoot) {
|
|
99
|
+
const nested = findWaDialog(child.shadowRoot);
|
|
100
|
+
if (nested) {
|
|
101
|
+
return nested;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const nested = findWaDialog(child);
|
|
105
|
+
if (nested) {
|
|
106
|
+
return nested;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
79
113
|
class DialogService {
|
|
80
114
|
private contributions: Map<string, DialogContribution> = new Map();
|
|
81
115
|
private contributionsChangeScheduled = false;
|
|
@@ -111,7 +145,7 @@ class DialogService {
|
|
|
111
145
|
continue;
|
|
112
146
|
}
|
|
113
147
|
|
|
114
|
-
if (!contribution.onButton) {
|
|
148
|
+
if (!contribution.onButton && !contribution.selfContained) {
|
|
115
149
|
logger.warn(`Dialog contribution "${contribution.id}" has no onButton callback, skipping`);
|
|
116
150
|
continue;
|
|
117
151
|
}
|
|
@@ -130,13 +164,13 @@ class DialogService {
|
|
|
130
164
|
|
|
131
165
|
return new Promise((resolve) => {
|
|
132
166
|
const container = getDialogContainer();
|
|
133
|
-
let
|
|
167
|
+
let closed = false;
|
|
134
168
|
let dialogContentElement: DocksDialogContent | null = null;
|
|
135
169
|
|
|
136
170
|
const cleanup = async () => {
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
171
|
+
if (closed) return;
|
|
172
|
+
closed = true;
|
|
173
|
+
|
|
140
174
|
if (dialogContentElement) {
|
|
141
175
|
try {
|
|
142
176
|
await dialogContentElement.dispose();
|
|
@@ -145,50 +179,72 @@ class DialogService {
|
|
|
145
179
|
logger.error(`Error disposing dialog content for "${dialogId}": ${errorMessage}`);
|
|
146
180
|
}
|
|
147
181
|
}
|
|
148
|
-
|
|
182
|
+
|
|
149
183
|
try {
|
|
150
184
|
const result = dialogContentElement ? dialogContentElement.getResult() : undefined;
|
|
151
|
-
|
|
185
|
+
if (contribution.onButton) {
|
|
186
|
+
await contribution.onButton('close', result, stateWithClose);
|
|
187
|
+
}
|
|
152
188
|
} catch (error) {
|
|
153
189
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
154
190
|
logger.error(`Error executing close callback for dialog "${dialogId}": ${errorMessage}`);
|
|
155
191
|
}
|
|
156
|
-
|
|
192
|
+
|
|
157
193
|
render(html``, container);
|
|
158
194
|
resolve();
|
|
159
195
|
};
|
|
160
196
|
|
|
197
|
+
const closeDialog = () => {
|
|
198
|
+
if (closed) return;
|
|
199
|
+
const dialog = findWaDialog(container);
|
|
200
|
+
if (dialog && dialog.open !== false) {
|
|
201
|
+
dialog.open = false;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
void cleanup();
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const handleAfterHide = () => {
|
|
208
|
+
void cleanup();
|
|
209
|
+
};
|
|
210
|
+
|
|
161
211
|
const handleButtonClick = async (buttonId: string) => {
|
|
212
|
+
if (!contribution.onButton) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
162
215
|
try {
|
|
163
216
|
const result = dialogContentElement ? dialogContentElement.getResult() : undefined;
|
|
164
217
|
const shouldClose = await contribution.onButton(buttonId, result, stateWithClose);
|
|
165
|
-
|
|
218
|
+
|
|
166
219
|
if (shouldClose !== false) {
|
|
167
|
-
|
|
220
|
+
closeDialog();
|
|
168
221
|
}
|
|
169
222
|
} catch (error) {
|
|
170
223
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
171
224
|
logger.error(`Error executing button callback for dialog "${dialogId}": ${errorMessage}`);
|
|
172
|
-
|
|
225
|
+
closeDialog();
|
|
173
226
|
}
|
|
174
227
|
};
|
|
175
228
|
|
|
176
|
-
const buttons = contribution.buttons && contribution.buttons.length > 0
|
|
177
|
-
? contribution.buttons
|
|
229
|
+
const buttons = contribution.buttons && contribution.buttons.length > 0
|
|
230
|
+
? contribution.buttons
|
|
178
231
|
: [OK_BUTTON];
|
|
179
232
|
|
|
233
|
+
const stateWithClose = { ...state, close: closeDialog, onAfterHide: handleAfterHide };
|
|
180
234
|
if (state && typeof state === 'object') {
|
|
181
|
-
(state as
|
|
235
|
+
(state as { close?: () => void }).close = closeDialog;
|
|
236
|
+
(state as { onAfterHide?: () => void }).onAfterHide = handleAfterHide;
|
|
182
237
|
}
|
|
183
|
-
const stateWithClose = { ...state, close: cleanup };
|
|
184
238
|
|
|
185
239
|
const dialogLabel =
|
|
186
240
|
state && typeof state === 'object' && typeof (state as { label?: unknown }).label === 'string'
|
|
187
241
|
? (state as { label: string }).label
|
|
188
242
|
: (contribution.label || dialogId);
|
|
189
243
|
|
|
190
|
-
const template =
|
|
191
|
-
|
|
244
|
+
const template = contribution.selfContained
|
|
245
|
+
? contribution.component(stateWithClose)
|
|
246
|
+
: html`
|
|
247
|
+
<wa-dialog label="${dialogLabel}" open @wa-after-hide=${handleAfterHide}>
|
|
192
248
|
<style>
|
|
193
249
|
.dialog-service-content {
|
|
194
250
|
display: flex;
|
|
@@ -224,7 +280,7 @@ class DialogService {
|
|
|
224
280
|
if (cancelButton) {
|
|
225
281
|
handleButtonClick(cancelButton.id);
|
|
226
282
|
} else {
|
|
227
|
-
|
|
283
|
+
closeDialog();
|
|
228
284
|
}
|
|
229
285
|
}}>
|
|
230
286
|
${contribution.component(state)}
|
|
@@ -232,6 +288,7 @@ class DialogService {
|
|
|
232
288
|
<div class="dialog-service-footer">
|
|
233
289
|
${buttons.map(button => html`
|
|
234
290
|
<wa-button
|
|
291
|
+
data-dialog-button="${button.id}"
|
|
235
292
|
variant="${button.variant || 'default'}"
|
|
236
293
|
?disabled=${button.disabled}
|
|
237
294
|
@click=${() => handleButtonClick(button.id)}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared render helpers for {@link https://webawesome.com/docs/components/dropdown-item | wa-dropdown-item}
|
|
3
|
+
* rows inside any Docks popup list.
|
|
4
|
+
*
|
|
5
|
+
* **Docks surfaces** (distinct UX and contribution targets):
|
|
6
|
+
* - **Toolbar dropdown** — anchored to a toolbar control (`docks-command`, `docks-toolbar`).
|
|
7
|
+
* Part-level registry targets use the `toolbar:` prefix (e.g. `toolbar:view.filebrowser`).
|
|
8
|
+
* - **Context menu** — opened at the pointer (`docks-contextmenu`).
|
|
9
|
+
* Part-level registry targets use the `contextmenu:` prefix (e.g. `contextmenu:view.filebrowser`).
|
|
10
|
+
*
|
|
11
|
+
* A part can expose both surfaces with the same part id but different prefixes, registering
|
|
12
|
+
* different contributions on each. Feature-scoped targets (e.g. `filebrowser.create`) may also
|
|
13
|
+
* be referenced explicitly from either surface when several controls share the same item set.
|
|
14
|
+
*
|
|
15
|
+
* Command-backed items rely on the parent {@link https://webawesome.com/docs/components/dropdown | wa-dropdown}
|
|
16
|
+
* `wa-select` event (see {@link handleDropdownWaSelect}) so the popup closes cleanly before
|
|
17
|
+
* native dialogs such as `showDirectoryPicker` open.
|
|
18
|
+
*/
|
|
19
|
+
import { html, nothing, type TemplateResult } from 'lit';
|
|
20
|
+
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
|
21
|
+
import { commandRegistry } from './commandregistry';
|
|
22
|
+
import { icon } from './icon-utils';
|
|
23
|
+
import { keyBindingManager } from './keybindings';
|
|
24
|
+
import {
|
|
25
|
+
type Contribution,
|
|
26
|
+
type CommandContribution,
|
|
27
|
+
type HTMLContribution,
|
|
28
|
+
type ReactiveBoolean,
|
|
29
|
+
contributionRegistry,
|
|
30
|
+
getContributionVisible,
|
|
31
|
+
readReactiveBoolean,
|
|
32
|
+
} from './contributionregistry';
|
|
33
|
+
|
|
34
|
+
export type { ReactiveBoolean };
|
|
35
|
+
|
|
36
|
+
export interface DropdownItemOptions {
|
|
37
|
+
cmd?: string;
|
|
38
|
+
icon?: string;
|
|
39
|
+
label: string;
|
|
40
|
+
title?: string;
|
|
41
|
+
params?: Record<string, unknown>;
|
|
42
|
+
disabled?: ReactiveBoolean;
|
|
43
|
+
variant?: 'default' | 'danger';
|
|
44
|
+
slot?: string;
|
|
45
|
+
action?: (event: Event) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type WaSelectDetail = {
|
|
49
|
+
item?: HTMLElement & { value?: string; disabled?: boolean };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function parseDropdownItemParams(item: HTMLElement): Record<string, unknown> {
|
|
53
|
+
const raw = item.getAttribute('data-params');
|
|
54
|
+
if (!raw) {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(raw) as Record<string, unknown>;
|
|
59
|
+
} catch {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getDropdownItemCommand(item: HTMLElement): string | undefined {
|
|
65
|
+
return item.getAttribute('data-cmd') || item.getAttribute('value') || undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Handles `wa-select` from a parent `wa-dropdown` and runs the backing command. */
|
|
69
|
+
export function handleDropdownWaSelect(event: CustomEvent<WaSelectDetail>): void {
|
|
70
|
+
const item = event.detail?.item;
|
|
71
|
+
if (!item || item.disabled || item.hasAttribute('disabled')) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const cmd = getDropdownItemCommand(item);
|
|
76
|
+
if (!cmd) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
void commandRegistry.execute(
|
|
81
|
+
cmd,
|
|
82
|
+
commandRegistry.createExecutionContext(parseDropdownItemParams(item)),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function onDropdownActionClick(options: DropdownItemOptions) {
|
|
87
|
+
return (event: Event) => {
|
|
88
|
+
if (readReactiveBoolean(options.disabled, false)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
event.stopPropagation();
|
|
92
|
+
options.action?.(event);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function keybindingDetails(cmd?: string): TemplateResult | typeof nothing {
|
|
97
|
+
if (!cmd) return nothing;
|
|
98
|
+
const bindings = keyBindingManager.getKeyBindingsForCommand(cmd);
|
|
99
|
+
if (bindings.length === 0) return nothing;
|
|
100
|
+
return html`<span slot="details">${bindings[0]}</span>`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Renders a native {@link https://webawesome.com/docs/components/dropdown-item | wa-dropdown-item}. */
|
|
104
|
+
export function renderDropdownItem(options: DropdownItemOptions): TemplateResult {
|
|
105
|
+
const iconSpec = options.icon ?? '';
|
|
106
|
+
const paramsJson =
|
|
107
|
+
options.params && Object.keys(options.params).length > 0
|
|
108
|
+
? JSON.stringify(options.params)
|
|
109
|
+
: undefined;
|
|
110
|
+
|
|
111
|
+
return html`
|
|
112
|
+
<wa-dropdown-item
|
|
113
|
+
slot=${options.slot ?? nothing}
|
|
114
|
+
variant=${options.variant ?? 'default'}
|
|
115
|
+
value=${options.cmd ?? nothing}
|
|
116
|
+
data-cmd=${options.cmd ?? nothing}
|
|
117
|
+
data-params=${paramsJson ?? nothing}
|
|
118
|
+
?disabled=${readReactiveBoolean(options.disabled, false)}
|
|
119
|
+
@click=${options.action ? onDropdownActionClick(options) : nothing}>
|
|
120
|
+
${iconSpec ? icon(iconSpec, { label: options.title ?? options.label, slot: 'icon' }) : nothing}
|
|
121
|
+
${options.label}
|
|
122
|
+
${keybindingDetails(options.cmd)}
|
|
123
|
+
</wa-dropdown-item>
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function renderCommandContribution(
|
|
128
|
+
contribution: CommandContribution,
|
|
129
|
+
options?: { slot?: string }
|
|
130
|
+
): TemplateResult | typeof nothing {
|
|
131
|
+
if (!getContributionVisible(contribution)) return nothing;
|
|
132
|
+
return renderDropdownItem({
|
|
133
|
+
cmd: contribution.command,
|
|
134
|
+
icon: contribution.icon,
|
|
135
|
+
label: contribution.label,
|
|
136
|
+
params: contribution.params,
|
|
137
|
+
disabled: contribution.disabled,
|
|
138
|
+
slot: options?.slot,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function renderDropdownContribution(
|
|
143
|
+
contribution: Contribution,
|
|
144
|
+
options?: { slot?: string }
|
|
145
|
+
): TemplateResult | typeof nothing {
|
|
146
|
+
if ('command' in contribution) {
|
|
147
|
+
return renderCommandContribution(contribution as CommandContribution, options);
|
|
148
|
+
}
|
|
149
|
+
if ('component' in contribution) {
|
|
150
|
+
if (!getContributionVisible(contribution)) return nothing;
|
|
151
|
+
const contents = (contribution as HTMLContribution).component;
|
|
152
|
+
if (contents instanceof Function) {
|
|
153
|
+
return contents();
|
|
154
|
+
}
|
|
155
|
+
return html`${unsafeHTML(contents)}` as TemplateResult;
|
|
156
|
+
}
|
|
157
|
+
return nothing;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Renders all visible contributions registered for a contribution target. */
|
|
161
|
+
export function renderDropdownContributions(
|
|
162
|
+
target: string,
|
|
163
|
+
options?: { slot?: string }
|
|
164
|
+
): TemplateResult {
|
|
165
|
+
const items = contributionRegistry
|
|
166
|
+
.getContributions(target)
|
|
167
|
+
.map((c) => renderDropdownContribution(c, options))
|
|
168
|
+
.filter((item) => item !== nothing);
|
|
169
|
+
return html`${items}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Renders a {@link wa-dropdown-item} with nested items from a contribution target in {@code slot="submenu"}. */
|
|
173
|
+
export function renderDropdownSubmenu(options: {
|
|
174
|
+
icon?: string;
|
|
175
|
+
label: string;
|
|
176
|
+
title?: string;
|
|
177
|
+
contributionTarget: string;
|
|
178
|
+
disabled?: ReactiveBoolean;
|
|
179
|
+
}): TemplateResult {
|
|
180
|
+
return html`
|
|
181
|
+
<wa-dropdown-item ?disabled=${readReactiveBoolean(options.disabled, false)}>
|
|
182
|
+
${options.icon ? icon(options.icon, { label: options.title ?? options.label, slot: 'icon' }) : nothing}
|
|
183
|
+
${options.label}
|
|
184
|
+
${renderDropdownContributions(options.contributionTarget, { slot: 'submenu' })}
|
|
185
|
+
</wa-dropdown-item>
|
|
186
|
+
`;
|
|
187
|
+
}
|
|
@@ -1,4 +1,63 @@
|
|
|
1
1
|
type DropdownItemElement = HTMLElement & { submenuOpen?: boolean };
|
|
2
|
+
type OpenDropdownElement = HTMLElement & { open?: boolean };
|
|
3
|
+
|
|
4
|
+
function isOpenDropdown(element: Element): element is OpenDropdownElement {
|
|
5
|
+
if (element.localName !== "wa-dropdown") return false;
|
|
6
|
+
const dropdown = element as OpenDropdownElement;
|
|
7
|
+
return dropdown.open === true || dropdown.hasAttribute("open");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function elementsFromEventPath(event: Event): Element[] {
|
|
11
|
+
const fromComposedPath = event
|
|
12
|
+
.composedPath()
|
|
13
|
+
.filter((node): node is Element => node instanceof Element);
|
|
14
|
+
if (fromComposedPath.length > 0) return fromComposedPath;
|
|
15
|
+
|
|
16
|
+
if (!(event.target instanceof Element)) return [];
|
|
17
|
+
|
|
18
|
+
const path: Element[] = [];
|
|
19
|
+
let current: Element | null = event.target;
|
|
20
|
+
while (current) {
|
|
21
|
+
path.push(current);
|
|
22
|
+
const root = current.getRootNode();
|
|
23
|
+
if (root instanceof ShadowRoot) {
|
|
24
|
+
current = root.host;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
current = current.parentElement;
|
|
28
|
+
}
|
|
29
|
+
return path;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function elementTargetsOpenDropdown(element: Element): boolean {
|
|
33
|
+
if (isOpenDropdown(element)) return true;
|
|
34
|
+
if (element.localName !== "wa-dropdown-item") return false;
|
|
35
|
+
const dropdown = element.closest("wa-dropdown");
|
|
36
|
+
if (dropdown && isOpenDropdown(dropdown)) return true;
|
|
37
|
+
const root = element.getRootNode();
|
|
38
|
+
return root instanceof ShadowRoot
|
|
39
|
+
&& root.host instanceof Element
|
|
40
|
+
&& isOpenDropdown(root.host);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** True when the event occurred inside a wa-dropdown that is currently open. */
|
|
44
|
+
export function eventTargetsOpenDropdown(event: Event): boolean {
|
|
45
|
+
return elementsFromEventPath(event).some(elementTargetsOpenDropdown);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Prevents the browser context menu on open Docks popup lists (context menus, toolbar dropdowns). */
|
|
49
|
+
export function suppressNativeContextMenuInOpenDropdowns(event: Event): void {
|
|
50
|
+
if (!event.cancelable || !eventTargetsOpenDropdown(event)) return;
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let openDropdownContextMenuGuardInstalled = false;
|
|
55
|
+
|
|
56
|
+
export function installOpenDropdownNativeContextMenuGuard(): void {
|
|
57
|
+
if (openDropdownContextMenuGuardInstalled || typeof document === "undefined") return;
|
|
58
|
+
openDropdownContextMenuGuardInstalled = true;
|
|
59
|
+
document.addEventListener("contextmenu", suppressNativeContextMenuInOpenDropdowns, { capture: true });
|
|
60
|
+
}
|
|
2
61
|
|
|
3
62
|
function visitElementTree(root: Node, visit: (element: Element) => void): void {
|
|
4
63
|
if (root instanceof Element) {
|