@eclipse-docks/core 0.7.125 → 0.7.127

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.
Files changed (51) hide show
  1. package/dist/api/index.js +4 -4
  2. package/dist/api/services.d.ts +3 -0
  3. package/dist/api/services.d.ts.map +1 -1
  4. package/dist/{api-CTU-FxEv.js → api-Da9zQcfd.js} +187 -38
  5. package/dist/api-Da9zQcfd.js.map +1 -0
  6. package/dist/components/command.d.ts +4 -4
  7. package/dist/components/command.d.ts.map +1 -1
  8. package/dist/components/filebrowser.d.ts.map +1 -1
  9. package/dist/components/release-history.d.ts.map +1 -1
  10. package/dist/core/contribution-names.d.ts +0 -2
  11. package/dist/core/contribution-names.d.ts.map +1 -1
  12. package/dist/core/contributionregistry.d.ts +3 -0
  13. package/dist/core/contributionregistry.d.ts.map +1 -1
  14. package/dist/core/dropdown-item.d.ts +35 -0
  15. package/dist/core/dropdown-item.d.ts.map +1 -0
  16. package/dist/core/dropdown-menu-utils.d.ts +5 -0
  17. package/dist/core/dropdown-menu-utils.d.ts.map +1 -1
  18. package/dist/core/markdown-html.d.ts +4 -0
  19. package/dist/core/markdown-html.d.ts.map +1 -0
  20. package/dist/index.js +4 -4
  21. package/dist/parts/contextmenu.d.ts +5 -2
  22. package/dist/parts/contextmenu.d.ts.map +1 -1
  23. package/dist/parts/index.d.ts.map +1 -1
  24. package/dist/parts/index.js +1 -1
  25. package/dist/parts/part.d.ts +2 -0
  26. package/dist/parts/part.d.ts.map +1 -1
  27. package/dist/{parts-BRaXx8PD.js → parts-CRVxaDd-.js} +197 -263
  28. package/dist/parts-CRVxaDd-.js.map +1 -0
  29. package/dist/{widget-DbLusgAH.js → widget-BqQtioK1.js} +11 -5
  30. package/dist/widget-BqQtioK1.js.map +1 -0
  31. package/dist/widgets/index.js +1 -1
  32. package/dist/{widgets-DnnSZhsi.js → widgets-C9V42Mqq.js} +2 -2
  33. package/dist/{widgets-DnnSZhsi.js.map → widgets-C9V42Mqq.js.map} +1 -1
  34. package/package.json +2 -1
  35. package/src/api/services.ts +9 -0
  36. package/src/commands/filebrowser-contributions.ts +0 -16
  37. package/src/components/command.ts +45 -113
  38. package/src/components/filebrowser.ts +15 -12
  39. package/src/components/release-history.ts +2 -4
  40. package/src/core/contribution-names.ts +0 -3
  41. package/src/core/contributionregistry.ts +19 -3
  42. package/src/core/dropdown-item.ts +152 -0
  43. package/src/core/dropdown-menu-utils.ts +59 -0
  44. package/src/core/markdown-html.ts +11 -0
  45. package/src/parts/contextmenu.ts +38 -44
  46. package/src/parts/dialog-content.ts +2 -2
  47. package/src/parts/index.ts +4 -1
  48. package/src/parts/part.ts +2 -0
  49. package/dist/api-CTU-FxEv.js.map +0 -1
  50. package/dist/parts-BRaXx8PD.js.map +0 -1
  51. package/dist/widget-DbLusgAH.js.map +0 -1
@@ -0,0 +1,152 @@
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
+ import { html, nothing, type TemplateResult } from 'lit';
16
+ import { unsafeHTML } from 'lit/directives/unsafe-html.js';
17
+ import { commandRegistry } from './commandregistry';
18
+ import { icon } from './icon-utils';
19
+ import { keyBindingManager } from './keybindings';
20
+ import {
21
+ type Contribution,
22
+ type CommandContribution,
23
+ type HTMLContribution,
24
+ type ReactiveBoolean,
25
+ contributionRegistry,
26
+ getContributionVisible,
27
+ readReactiveBoolean,
28
+ } from './contributionregistry';
29
+
30
+ export type { ReactiveBoolean };
31
+
32
+ export interface DropdownItemOptions {
33
+ cmd?: string;
34
+ icon?: string;
35
+ label: string;
36
+ title?: string;
37
+ params?: Record<string, unknown>;
38
+ disabled?: ReactiveBoolean;
39
+ variant?: 'default' | 'danger';
40
+ slot?: string;
41
+ action?: (event: Event) => void;
42
+ }
43
+
44
+ function closeParentDropdown(event: Event): void {
45
+ const dropdown = (event.target as Element | null)?.closest('wa-dropdown') as { open?: boolean } | null;
46
+ if (dropdown && dropdown.open !== undefined) {
47
+ dropdown.open = false;
48
+ }
49
+ }
50
+
51
+ function onDropdownItemClick(options: DropdownItemOptions) {
52
+ return (event: Event) => {
53
+ if (readReactiveBoolean(options.disabled, false)) return;
54
+ event.stopPropagation();
55
+ closeParentDropdown(event);
56
+ if (options.action) {
57
+ options.action(event);
58
+ return;
59
+ }
60
+ if (options.cmd) {
61
+ void commandRegistry.execute(
62
+ options.cmd,
63
+ commandRegistry.createExecutionContext(options.params ?? {})
64
+ );
65
+ }
66
+ };
67
+ }
68
+
69
+ function keybindingDetails(cmd?: string): TemplateResult | typeof nothing {
70
+ if (!cmd) return nothing;
71
+ const bindings = keyBindingManager.getKeyBindingsForCommand(cmd);
72
+ if (bindings.length === 0) return nothing;
73
+ return html`<span slot="details">${bindings[0]}</span>`;
74
+ }
75
+
76
+ /** Renders a native {@link https://webawesome.com/docs/components/dropdown-item | wa-dropdown-item}. */
77
+ export function renderDropdownItem(options: DropdownItemOptions): TemplateResult {
78
+ const iconSpec = options.icon ?? '';
79
+ return html`
80
+ <wa-dropdown-item
81
+ slot=${options.slot ?? nothing}
82
+ variant=${options.variant ?? 'default'}
83
+ ?disabled=${readReactiveBoolean(options.disabled, false)}
84
+ @click=${onDropdownItemClick(options)}>
85
+ ${iconSpec ? icon(iconSpec, { label: options.title ?? options.label, slot: 'icon' }) : nothing}
86
+ ${options.label}
87
+ ${keybindingDetails(options.cmd)}
88
+ </wa-dropdown-item>
89
+ `;
90
+ }
91
+
92
+ export function renderCommandContribution(
93
+ contribution: CommandContribution,
94
+ options?: { slot?: string }
95
+ ): TemplateResult | typeof nothing {
96
+ if (!getContributionVisible(contribution)) return nothing;
97
+ return renderDropdownItem({
98
+ cmd: contribution.command,
99
+ icon: contribution.icon,
100
+ label: contribution.label,
101
+ params: contribution.params,
102
+ disabled: contribution.disabled,
103
+ slot: options?.slot,
104
+ });
105
+ }
106
+
107
+ export function renderDropdownContribution(
108
+ contribution: Contribution,
109
+ options?: { slot?: string }
110
+ ): TemplateResult | typeof nothing {
111
+ if ('command' in contribution) {
112
+ return renderCommandContribution(contribution as CommandContribution, options);
113
+ }
114
+ if ('component' in contribution) {
115
+ if (!getContributionVisible(contribution)) return nothing;
116
+ const contents = (contribution as HTMLContribution).component;
117
+ if (contents instanceof Function) {
118
+ return contents();
119
+ }
120
+ return html`${unsafeHTML(contents)}` as TemplateResult;
121
+ }
122
+ return nothing;
123
+ }
124
+
125
+ /** Renders all visible contributions registered for a contribution target. */
126
+ export function renderDropdownContributions(
127
+ target: string,
128
+ options?: { slot?: string }
129
+ ): TemplateResult {
130
+ const items = contributionRegistry
131
+ .getContributions(target)
132
+ .map((c) => renderDropdownContribution(c, options))
133
+ .filter((item) => item !== nothing);
134
+ return html`${items}`;
135
+ }
136
+
137
+ /** Renders a {@link wa-dropdown-item} with nested items from a contribution target in {@code slot="submenu"}. */
138
+ export function renderDropdownSubmenu(options: {
139
+ icon?: string;
140
+ label: string;
141
+ title?: string;
142
+ contributionTarget: string;
143
+ disabled?: ReactiveBoolean;
144
+ }): TemplateResult {
145
+ return html`
146
+ <wa-dropdown-item ?disabled=${readReactiveBoolean(options.disabled, false)}>
147
+ ${options.icon ? icon(options.icon, { label: options.title ?? options.label, slot: 'icon' }) : nothing}
148
+ ${options.label}
149
+ ${renderDropdownContributions(options.contributionTarget, { slot: 'submenu' })}
150
+ </wa-dropdown-item>
151
+ `;
152
+ }
@@ -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) {
@@ -0,0 +1,11 @@
1
+ import DOMPurify from 'dompurify';
2
+ import { marked, type MarkedOptions } from 'marked';
3
+
4
+ export function sanitizeHtml(html: string): string {
5
+ return DOMPurify.sanitize(html);
6
+ }
7
+
8
+ export function parseMarkdownHtml(markdown: string, options?: MarkedOptions): string {
9
+ const html = marked.parse(markdown, { async: false, ...options }) as string;
10
+ return sanitizeHtml(html);
11
+ }
@@ -2,20 +2,24 @@ import {css, html, nothing} from 'lit'
2
2
  import {customElement, property, state} from 'lit/decorators.js'
3
3
  import {DocksElement} from "./element";
4
4
  import {
5
- CommandContribution,
6
5
  Contribution,
7
6
  ContributionChangeEvent,
8
7
  contributionRegistry,
9
- getContributionDisabled,
10
- HTMLContribution,
11
- getContributionVisible,
12
8
  TOPIC_CONTRIBUTEIONS_CHANGED
13
9
  } from "../core/contributionregistry";
14
- import {unsafeHTML} from "lit/directives/unsafe-html.js";
15
10
  import {subscribe} from "../core/events";
16
11
  import {closeSiblingSubmenus} from "../core/dropdown-menu-utils";
17
12
  import {createRef, ref} from "lit/directives/ref.js";
18
- import '../components/command';
13
+ import {renderDropdownContribution} from "../core/dropdown-item";
14
+
15
+ interface VirtualPopupAnchor {
16
+ getBoundingClientRect(): DOMRect;
17
+ }
18
+
19
+ interface WaPopupElement extends HTMLElement {
20
+ anchor?: Element | string | VirtualPopupAnchor;
21
+ reposition(): void;
22
+ }
19
23
 
20
24
  @customElement('docks-contextmenu')
21
25
  export class DocksContextMenu extends DocksElement {
@@ -31,10 +35,15 @@ export class DocksContextMenu extends DocksElement {
31
35
  @state()
32
36
  private isOpen: boolean = false;
33
37
 
34
- @state()
35
- private position: { x: number, y: number } = { x: 0, y: 0 };
38
+ private menuPosition: { x: number; y: number } = { x: 0, y: 0 };
39
+
40
+ private readonly virtualAnchor: VirtualPopupAnchor = {
41
+ getBoundingClientRect: () => {
42
+ const { x, y } = this.menuPosition;
43
+ return new DOMRect(x, y, 0, 0);
44
+ },
45
+ };
36
46
 
37
- private anchorRef = createRef<HTMLElement>();
38
47
  private dropdownRef = createRef<HTMLElement>();
39
48
 
40
49
  private boundHandleDocumentPointerDown = this.handleDocumentPointerDown.bind(this);
@@ -225,43 +234,38 @@ export class DocksContextMenu extends DocksElement {
225
234
  await this.updateComplete;
226
235
  }
227
236
 
228
- this.position = position;
237
+ this.menuPosition = position;
229
238
  this.isOpen = true;
230
239
  await this.updateComplete;
240
+ this.syncVirtualPopupAnchor();
231
241
  document.addEventListener('pointerdown', this.boundHandleDocumentPointerDown, { capture: true });
232
242
  return true;
233
243
  }
234
244
 
245
+ /** Points wa-dropdown's internal wa-popup at the pointer via a VirtualElement anchor. */
246
+ private syncVirtualPopupAnchor(): void {
247
+ const dropdown = this.dropdownRef.value;
248
+ const popup = dropdown?.shadowRoot?.querySelector('wa-popup') as WaPopupElement | null;
249
+ if (!popup) return;
250
+ popup.anchor = this.virtualAnchor;
251
+ popup.reposition();
252
+ }
253
+
235
254
  private onClose() {
236
255
  if (!this.isOpen) return;
237
256
  this.isOpen = false;
238
257
  document.removeEventListener('pointerdown', this.boundHandleDocumentPointerDown, { capture: true });
239
258
  }
240
259
 
241
- private renderContribution(contribution: Contribution) {
242
- if ("command" in contribution) {
243
- const commandContribution = contribution as CommandContribution;
244
- if (!getContributionVisible(commandContribution)) {
245
- return nothing;
246
- }
247
- const disabled = getContributionDisabled(commandContribution);
248
- return html`
249
- <docks-command
250
- cmd="${commandContribution.command}"
251
- icon="${commandContribution.icon ?? ''}"
252
- .params=${commandContribution.params ?? {}}
253
- ?disabled="${disabled}">
254
- ${commandContribution.label}
255
- </docks-command>
256
- `;
257
- } else if ("component" in contribution) {
258
- const contents = (contribution as HTMLContribution).component;
259
- if (contents instanceof Function) {
260
- return contents();
261
- }
262
- return unsafeHTML(contents);
260
+ protected updated(changedProperties: Map<string, unknown>): void {
261
+ super.updated(changedProperties);
262
+ if (changedProperties.has('isOpen') && this.isOpen) {
263
+ this.syncVirtualPopupAnchor();
263
264
  }
264
- return nothing;
265
+ }
266
+
267
+ private renderContribution(contribution: Contribution) {
268
+ return renderDropdownContribution(contribution);
265
269
  }
266
270
 
267
271
  render() {
@@ -273,17 +277,7 @@ export class DocksContextMenu extends DocksElement {
273
277
  ?open=${this.isOpen}
274
278
  @submenu-opening=${this.handleSubmenuOpening}
275
279
  @wa-after-hide=${this.onClose}>
276
-
277
- <div
278
- slot="trigger"
279
- ${ref(this.anchorRef)}
280
- style="position: fixed;
281
- left: ${this.position.x}px;
282
- top: ${this.position.y}px;
283
- width: 1px;
284
- height: 1px;
285
- pointer-events: none;">
286
- </div>
280
+ <span slot="trigger" hidden aria-hidden="true"></span>
287
281
 
288
282
  ${partContent}
289
283
  ${this.contributions.map(c => this.renderContribution(c))}
@@ -1,6 +1,6 @@
1
1
  import { html, TemplateResult, css } from "lit";
2
2
  import { unsafeHTML } from "lit/directives/unsafe-html.js";
3
- import { marked } from "marked";
3
+ import { parseMarkdownHtml } from "../core/markdown-html";
4
4
  import { DocksElement } from "./element";
5
5
 
6
6
  export abstract class DocksDialogContent extends DocksElement {
@@ -22,7 +22,7 @@ export abstract class DocksDialogContent extends DocksElement {
22
22
 
23
23
  protected renderMessage(message: string, markdown: boolean = false): TemplateResult {
24
24
  if (markdown) {
25
- const htmlContent = marked.parse(message, { async: false }) as string;
25
+ const htmlContent = parseMarkdownHtml(message);
26
26
  return html`<div class="dialog-message" style="white-space: normal;">${unsafeHTML(htmlContent)}</div>`;
27
27
  }
28
28
  return html`<div class="dialog-message" style="white-space: pre-line;">${message}</div>`;
@@ -1,5 +1,8 @@
1
1
  import "../externals/webawesome"
2
+ import { installOpenDropdownNativeContextMenuGuard } from "../core/dropdown-menu-utils";
2
3
  import "./toolbar"
3
4
  import "./contextmenu"
4
5
  import "./tabs"
5
- import "./resizable-grid"
6
+ import "./resizable-grid"
7
+
8
+ installOpenDropdownNativeContextMenuGuard();
package/src/parts/part.ts CHANGED
@@ -135,11 +135,13 @@ export abstract class DocksPart extends DocksContainer {
135
135
  return nothing;
136
136
  }
137
137
 
138
+ /** Part-level toolbar contributions (`toolbar:${partId}`). */
138
139
  private getToolbarTarget(): string | undefined {
139
140
  const contributionKey = this.tabContribution?.editorId ?? this.id ?? this.tabContribution?.name;
140
141
  return contributionKey ? `toolbar:${contributionKey}` : undefined;
141
142
  }
142
143
 
144
+ /** Part-level context-menu contributions (`contextmenu:${partId}`). */
143
145
  private getContextMenuTarget(): string | undefined {
144
146
  const contributionKey = this.tabContribution?.editorId ?? this.id ?? this.tabContribution?.name;
145
147
  return contributionKey ? `contextmenu:${contributionKey}` : undefined;