From 0307ab80d8988d9db6a764743d216a03b04aaecc Mon Sep 17 00:00:00 2001 From: Tobias Date: Mon, 30 Oct 2023 23:14:30 +0100 Subject: [PATCH 01/21] added smart connector UIExtension and Actions for data request --- packages/client/css/smart-connector.css | 39 ++++ packages/client/src/default-modules.ts | 4 +- .../smart-connector/smart-connector-module.ts | 15 ++ .../smart-connector/smart-connector.ts | 177 ++++++++++++++++++ .../tools/edge-creation/edge-creation-tool.ts | 6 + packages/client/src/index.ts | 2 + .../protocol/src/action-protocol/index.ts | 1 + .../src/action-protocol/smart-connector.ts | 56 ++++++ 8 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 packages/client/css/smart-connector.css create mode 100644 packages/client/src/features/smart-connector/smart-connector-module.ts create mode 100644 packages/client/src/features/smart-connector/smart-connector.ts create mode 100644 packages/protocol/src/action-protocol/smart-connector.ts diff --git a/packages/client/css/smart-connector.css b/packages/client/css/smart-connector.css new file mode 100644 index 00000000..6a6e637e --- /dev/null +++ b/packages/client/css/smart-connector.css @@ -0,0 +1,39 @@ + +.smart-connector { + display: flex; + min-width: 200px; + max-width: 800px; + flex-wrap: nowrap; + justify-content: space-between; + align-items: center; + position: absolute; + pointer-events: none; +} + +.smart-connector-button-container { + border-left: 1px solid black; + border-right: 1px solid black; + background-color:rgba(1, 1, 1, 0.6); + overflow-y: scroll; + max-height: 150px; + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +.smart-connector-button-container::-webkit-scrollbar { + display: none; +} + +.smart-connector-button { + border-top: 1px solid black; + margin: 0 5px; + padding: 8px 0; + z-index: 9999; + pointer-events: auto; + cursor: pointer; +} + +.smart-connector-button-container div:first-child { + border-top: 0; +} + diff --git a/packages/client/src/default-modules.ts b/packages/client/src/default-modules.ts index e3ea8738..45587920 100644 --- a/packages/client/src/default-modules.ts +++ b/packages/client/src/default-modules.ts @@ -60,6 +60,7 @@ import { nodeCreationToolModule } from './features/tools/node-creation/node-crea import { toolFocusLossModule } from './features/tools/tool-focus-loss-module'; import { markerNavigatorModule, validationModule } from './features/validation/validation-modules'; import { viewportModule } from './features/viewport/viewport-modules'; +import { smartConnectorModule } from './features/smart-connector/smart-connector-module'; export const DEFAULT_MODULES = [ defaultModule, @@ -97,7 +98,8 @@ export const DEFAULT_MODULES = [ validationModule, zorderModule, svgMetadataModule, - statusModule + statusModule, + smartConnectorModule, ] as const; /** diff --git a/packages/client/src/features/smart-connector/smart-connector-module.ts b/packages/client/src/features/smart-connector/smart-connector-module.ts new file mode 100644 index 00000000..59c9f4ef --- /dev/null +++ b/packages/client/src/features/smart-connector/smart-connector-module.ts @@ -0,0 +1,15 @@ +import { FeatureModule, TYPES, bindAsService, configureActionHandler, OpenSmartConnectorAction, CloseSmartConnectorAction, MoveAction, SetBoundsAction, SetViewportAction, DeleteElementOperation } from '~glsp-sprotty'; +import '../../../css/smart-connector.css' +import { SmartConnector } from './smart-connector'; + +export const smartConnectorModule = new FeatureModule((bind, unbind, isBound, rebind) => { + const context = { bind, unbind, isBound, rebind }; + bindAsService(context, TYPES.IUIExtension, SmartConnector); + bind(TYPES.IDiagramStartup).toService(SmartConnector); + configureActionHandler(context, OpenSmartConnectorAction.KIND, SmartConnector); + configureActionHandler(context, CloseSmartConnectorAction.KIND, SmartConnector); + configureActionHandler(context, MoveAction.KIND, SmartConnector); + configureActionHandler(context, SetBoundsAction.KIND, SmartConnector); + configureActionHandler(context, SetViewportAction.KIND, SmartConnector); + configureActionHandler(context, DeleteElementOperation.KIND, SmartConnector); +}); \ No newline at end of file diff --git a/packages/client/src/features/smart-connector/smart-connector.ts b/packages/client/src/features/smart-connector/smart-connector.ts new file mode 100644 index 00000000..c27b5887 --- /dev/null +++ b/packages/client/src/features/smart-connector/smart-connector.ts @@ -0,0 +1,177 @@ +import { inject, injectable } from 'inversify'; +import { GLSPActionDispatcher } from '../../base/action-dispatcher'; +import { EditorContextService } from '../../base/editor-context-service'; +import { FocusTracker } from '../../base/focus/focus-tracker'; +import { + AbstractUIExtension, + Action, + DeleteElementOperation, + IActionHandler, + ICommand, + KeyListener, + SModelElement, + SModelRoot, + isDeletable, + isSelectable, + matchesKeystroke, + OpenSmartConnectorAction, + SetUIExtensionVisibilityAction, + ViewportResult, + RequestContextActions, + SetContextActions, + PaletteItem, + TriggerEdgeCreationAction, + TriggerNodeCreationAction, + Args, +} from '~glsp-sprotty' +import { GetViewportAction } from 'sprotty-protocol/lib/actions' +import { IDiagramStartup } from '../../base/model/diagram-loader'; +import { createIcon } from '../tool-palette/tool-palette'; + + +@injectable() +export class SmartConnector extends AbstractUIExtension implements IActionHandler, IDiagramStartup { + + static readonly ID = 'smart-connector'; + static readonly CONTAINER_PADDING = 16; + + private selectedElementId: string; + private paletteItems: PaletteItem[]; + private edgeContainer: HTMLElement; + private nodeContainer: HTMLElement; + + @inject(GLSPActionDispatcher) + protected actionDispatcher: GLSPActionDispatcher; + + @inject(EditorContextService) + protected editorContext: EditorContextService; + + @inject(FocusTracker) + protected focusTracker: FocusTracker; + + override id(): string { + return SmartConnector.ID; + } + override containerClass(): string { + return SmartConnector.ID; + } + + protected override async onBeforeShow(containerElement: HTMLElement, root: Readonly, ...contextElementIds: string[]): Promise { + var response: ViewportResult = await this.actionDispatcher.request(GetViewportAction.create()) + var zoom = response.viewport.zoom; + // TODO: get element from server instead of DOM + var elementFromDom = document.getElementById(`${this.options.baseDiv}_${this.selectedElementId}`) as any as SVGGElement; + + if (elementFromDom) { + var elementSvgWidth = elementFromDom.getBBox().width; + var elementBoundsFromDom = elementFromDom.getBoundingClientRect(); + containerElement.style.width = `${elementSvgWidth + this.edgeContainer.offsetWidth + this.nodeContainer.offsetWidth + SmartConnector.CONTAINER_PADDING/zoom}px` + containerElement.style.left = `${elementBoundsFromDom.x - containerElement.offsetWidth/2 + elementBoundsFromDom.width/2 - response.canvasBounds.x}px`; + containerElement.style.top = `${elementBoundsFromDom.y - containerElement.offsetHeight/2 + elementBoundsFromDom.height/2 - response.canvasBounds.y}px`; + } + containerElement.style.transform = `scale(${zoom})`; + } + + protected initializeContents(containerElement: HTMLElement): void { + containerElement.setAttribute('aria-label', 'Smart-Connector'); + var container; + for (const item of this.paletteItems) { + if (item.children) { + container = this.createContainer(item); + containerElement.appendChild(container); + if (item.id == 'node-group') this.nodeContainer = container; + if (item.id == 'edge-group') this.edgeContainer = container; + } + + } + } + + private createContainer(item: PaletteItem): HTMLElement { + const container = document.createElement('div'); + container.className = 'smart-connector-button-container'; + for (const child of item.children!) { + container.appendChild(this.createToolButton(child)); + } + return container; + } + + protected createToolButton(item: PaletteItem): HTMLElement { + const button = document.createElement('div'); + button.tabIndex = 0; + button.classList.add('smart-connector-button'); + if (item.icon) { + button.appendChild(createIcon(item.icon)); + return button; + } + button.insertAdjacentText('beforeend', item.label); + button.onclick = this.onClickCreateToolButton(button, item); + //TODO: keyboard support + //button.onkeydown = ev => this.clearToolOnEscape(ev); + return button; + } + + protected onClickCreateToolButton(button: HTMLElement, item: PaletteItem) { + return (_ev: MouseEvent) => { + if (!this.editorContext.isReadonly) { + item.actions.forEach(e => { + var args: Args; + if (TriggerEdgeCreationAction.is(e)) { + args = { source: this.selectedElementId }; + (e as TriggerEdgeCreationAction).args = args; + } + if (TriggerNodeCreationAction.is(e)) { + args = { createEdge: true, source: this.selectedElementId }; + (e as TriggerNodeCreationAction).args = args; + } + }); + this.actionDispatcher.dispatchAll(item.actions.concat([SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: false })])); + button.focus(); + } + }; + } + + handle(action: Action): ICommand | Action | void { + if (OpenSmartConnectorAction.is(action)) { + this.selectedElementId = action.selectedElementID; + this.actionDispatcher.dispatch( + SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: true }) + ); + } + else + this.actionDispatcher.dispatch( + SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: false }) + ); + } + + async preRequestModel(): Promise { + const requestAction = RequestContextActions.create({ + contextId: SmartConnector.ID, + editorContext: { + selectedElementIds: [] + } + }); + const response = await this.actionDispatcher.request(requestAction); + this.paletteItems = response.actions.map(e => e as PaletteItem); + } +} + +@injectable() +export class SmartConnectorKeyListener extends KeyListener { + override keyDown(element: SModelElement, event: KeyboardEvent): Action[] { + if (matchesKeystroke(event, 'Delete', 'ctrl')) { + const deleteElementIds = Array.from( + element.root.index + .all() + .filter(e => isDeletable(e) && isSelectable(e) && e.selected) + .filter(e => e.id !== e.root.id) + .map(e => e.id) + ); + if (deleteElementIds.length > 0) { + return [DeleteElementOperation.create(deleteElementIds)]; + } + } + return []; + } +} + + diff --git a/packages/client/src/features/tools/edge-creation/edge-creation-tool.ts b/packages/client/src/features/tools/edge-creation/edge-creation-tool.ts index 4aca5b6b..16a8a414 100644 --- a/packages/client/src/features/tools/edge-creation/edge-creation-tool.ts +++ b/packages/client/src/features/tools/edge-creation/edge-creation-tool.ts @@ -74,6 +74,12 @@ export class EdgeCreationToolMouseListener extends DragAwareMouseListener { super(); this.proxyEdge = new SEdge(); this.proxyEdge.type = triggerAction.elementTypeId; + if (triggerAction.args?.source) { + this.source = triggerAction.args?.source as string; + this.tool.registerFeedback([ + DrawFeedbackEdgeAction.create({ elementTypeId: this.triggerAction.elementTypeId, sourceId: this.source }) + ]); + } } protected reinitialize(): void { diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 768b68bd..ec34ede3 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -104,6 +104,7 @@ export * from './features/tools/marquee-selection/marquee-tool-feedback'; export * from './features/tools/marquee-selection/model'; export * from './features/tools/marquee-selection/view'; export * from './features/tools/node-creation/node-creation-tool'; +export * from './features/smart-connector/smart-connector'; export * from './features/undo-redo/undo-redo-key-listener'; export * from './features/validation/issue-marker'; export * from './features/validation/marker-navigator'; @@ -157,6 +158,7 @@ export * from './features/tools/edge-edit/edge-edit-module'; export * from './features/tools/marquee-selection/marquee-selection-module'; export * from './features/tools/node-creation/node-creation-module'; export * from './features/tools/tool-focus-loss-module'; +export * from './features/smart-connector/smart-connector-module'; export * from './features/undo-redo/undo-redo-module'; export * from './features/validation/validation-modules'; export * from './features/viewport/viewport-modules'; diff --git a/packages/protocol/src/action-protocol/index.ts b/packages/protocol/src/action-protocol/index.ts index ad23bda3..a02fc15d 100644 --- a/packages/protocol/src/action-protocol/index.ts +++ b/packages/protocol/src/action-protocol/index.ts @@ -31,6 +31,7 @@ export * from './model-layout'; export * from './model-saving'; export * from './model-structure'; export * from './node-modification'; +export * from './smart-connector'; export * from './tool-palette'; export * from './types'; export * from './undo-redo'; diff --git a/packages/protocol/src/action-protocol/smart-connector.ts b/packages/protocol/src/action-protocol/smart-connector.ts new file mode 100644 index 00000000..57bebeea --- /dev/null +++ b/packages/protocol/src/action-protocol/smart-connector.ts @@ -0,0 +1,56 @@ +import { hasStringProp } from '../utils/type-util'; +import { Bounds } from 'sprotty-protocol'; +import { Action } from './base-protocol'; + +/** + * TODO + */ +export interface OpenSmartConnectorAction extends Action { + kind: typeof OpenSmartConnectorAction.KIND; + + /** + * The identifier of the element where the smart connector is to be opened. + */ + selectedElementID: string; + /** + * Bounds of the node where the smart connector is to be opened. + */ + bounds: Bounds; +} + +export namespace OpenSmartConnectorAction { + export const KIND = 'openSmartConnector'; + + export function is(object: any): object is OpenSmartConnectorAction { + return Action.hasKind(object, KIND) && hasStringProp(object, 'selectedElementID'); + } + + export function create(selectedElementID: string, bounds: Bounds): OpenSmartConnectorAction { + return { + kind: KIND, + selectedElementID, + bounds + }; + } +} + +/** + * TODO + */ +export interface CloseSmartConnectorAction extends Action { + kind: typeof CloseSmartConnectorAction.KIND; +} + +export namespace CloseSmartConnectorAction { + export const KIND = 'closeSmartConnector'; + + export function is(object: any): object is CloseSmartConnectorAction { + return Action.hasKind(object, KIND); + } + + export function create(): CloseSmartConnectorAction { + return { + kind: KIND + }; + } +} \ No newline at end of file From 7efe329d46a6ade4912fc88750728d47a73696e1 Mon Sep 17 00:00:00 2001 From: Tobias Date: Tue, 14 Nov 2023 15:07:23 +0100 Subject: [PATCH 02/21] added possibility to set position for smart connector containers (left, right, up, down) --- packages/client/css/smart-connector.css | 29 ++- .../smart-connector/smart-connector.ts | 218 +++++++++++++++--- .../src/action-protocol/smart-connector.ts | 8 +- .../protocol/src/action-protocol/types.ts | 35 ++- 4 files changed, 245 insertions(+), 45 deletions(-) diff --git a/packages/client/css/smart-connector.css b/packages/client/css/smart-connector.css index 6a6e637e..9e32ec67 100644 --- a/packages/client/css/smart-connector.css +++ b/packages/client/css/smart-connector.css @@ -1,13 +1,11 @@ .smart-connector { - display: flex; - min-width: 200px; - max-width: 800px; - flex-wrap: nowrap; - justify-content: space-between; - align-items: center; position: absolute; - pointer-events: none; + z-index: 1; +} + +.smart-connector-container { + position: absolute; } .smart-connector-button-container { @@ -16,8 +14,10 @@ background-color:rgba(1, 1, 1, 0.6); overflow-y: scroll; max-height: 150px; + max-width: 400px; -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */ + /* display: flex; */ } .smart-connector-button-container::-webkit-scrollbar { @@ -26,7 +26,7 @@ .smart-connector-button { border-top: 1px solid black; - margin: 0 5px; + margin: 0 2px; padding: 8px 0; z-index: 9999; pointer-events: auto; @@ -34,6 +34,19 @@ } .smart-connector-button-container div:first-child { + background-color:rgba(1, 1, 1, 0.6); border-top: 0; } +.smart-connector-expand-button { + height: 32px; + width: 32px; + border-radius: 50%; + border: 1px solid black; + z-index: 9999; + display: flex; /* or inline-flex */ + align-items: center; + justify-content: center; + position: absolute; + background: #cccccc; +} diff --git a/packages/client/src/features/smart-connector/smart-connector.ts b/packages/client/src/features/smart-connector/smart-connector.ts index c27b5887..ad9b8f72 100644 --- a/packages/client/src/features/smart-connector/smart-connector.ts +++ b/packages/client/src/features/smart-connector/smart-connector.ts @@ -23,10 +23,14 @@ import { TriggerEdgeCreationAction, TriggerNodeCreationAction, Args, + SmartConnectorGroupItem, + SmartConnectorPosition, } from '~glsp-sprotty' import { GetViewportAction } from 'sprotty-protocol/lib/actions' -import { IDiagramStartup } from '../../base/model/diagram-loader'; import { createIcon } from '../tool-palette/tool-palette'; +import { IDiagramStartup } from 'src'; + + @injectable() @@ -34,11 +38,15 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle static readonly ID = 'smart-connector'; static readonly CONTAINER_PADDING = 16; + private selectedElementId: string; - private paletteItems: PaletteItem[]; - private edgeContainer: HTMLElement; - private nodeContainer: HTMLElement; + private smartConnectorItems: SmartConnectorGroupItem[]; + protected smartConnectorItemsCopy: SmartConnectorGroupItem[] = []; + private smartConnectorContainer: HTMLElement; + private expandButton: HTMLElement; + + protected searchField: HTMLInputElement; @inject(GLSPActionDispatcher) protected actionDispatcher: GLSPActionDispatcher; @@ -57,42 +65,140 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle } protected override async onBeforeShow(containerElement: HTMLElement, root: Readonly, ...contextElementIds: string[]): Promise { - var response: ViewportResult = await this.actionDispatcher.request(GetViewportAction.create()) - var zoom = response.viewport.zoom; + this.hideSmartConnector(); + // TODO temporary for testing, to be replaced by settings + var position = SmartConnectorPosition.Top; + var viewportResult: ViewportResult = await this.actionDispatcher.request(GetViewportAction.create()) + this.setMainPosition(viewportResult) + this.setPosition(viewportResult, this.expandButton, position) + await this.requestAvailableOptions(contextElementIds) + var sameSide = this.smartConnectorItems.every((e) => e.position === this.smartConnectorItems[0].position) + if (sameSide) + this.setPosition(viewportResult, this.smartConnectorContainer, this.smartConnectorItems[0].position) + else { + for (var i = 0; i < this.smartConnectorContainer.childElementCount; i++) { + this.setPosition(viewportResult, this.smartConnectorContainer.children[i] as HTMLElement, this.smartConnectorItems[i].position, true) + } + } + } + + protected async requestAvailableOptions(contextElementIds: string[]) { + const requestAction = RequestContextActions.create({ + contextId: SmartConnector.ID, + editorContext: { + selectedElementIds: contextElementIds + } + }); + const response = await this.actionDispatcher.request(requestAction); + this.smartConnectorItems = response.actions.map(e => e as SmartConnectorGroupItem); + } + + protected setMainPosition(viewport: ViewportResult) { // TODO: get element from server instead of DOM - var elementFromDom = document.getElementById(`${this.options.baseDiv}_${this.selectedElementId}`) as any as SVGGElement; - - if (elementFromDom) { - var elementSvgWidth = elementFromDom.getBBox().width; - var elementBoundsFromDom = elementFromDom.getBoundingClientRect(); - containerElement.style.width = `${elementSvgWidth + this.edgeContainer.offsetWidth + this.nodeContainer.offsetWidth + SmartConnector.CONTAINER_PADDING/zoom}px` - containerElement.style.left = `${elementBoundsFromDom.x - containerElement.offsetWidth/2 + elementBoundsFromDom.width/2 - response.canvasBounds.x}px`; - containerElement.style.top = `${elementBoundsFromDom.y - containerElement.offsetHeight/2 + elementBoundsFromDom.height/2 - response.canvasBounds.y}px`; + var nodeFromDom = document.getElementById(`${this.options.baseDiv}_${this.selectedElementId}`) as any as SVGGElement; + if (nodeFromDom) { + var nodeBoundsFromDom = nodeFromDom.getBoundingClientRect(); + var xCenter = nodeBoundsFromDom.x + nodeBoundsFromDom.width/2 - viewport.canvasBounds.x + var yCenter = nodeBoundsFromDom.y + nodeBoundsFromDom.height/2 - viewport.canvasBounds.y + this.containerElement.style.left = `${xCenter}px`; + this.containerElement.style.top = `${yCenter}px`; + } + } + + protected setPosition(viewport: ViewportResult, element: HTMLElement, position: SmartConnectorPosition, setAbsolute?: boolean) { + var zoom = viewport.viewport.zoom; + // TODO: get element from server instead of DOM + var nodeFromDom = document.getElementById(`${this.options.baseDiv}_${this.selectedElementId}`) as any as SVGGElement; + if (nodeFromDom) { + var nodeHeight = nodeFromDom.getBoundingClientRect().height; + var nodeWidth = nodeFromDom.getBoundingClientRect().width; + var xDiff = -element.offsetWidth/2; + var yDiff = -element.offsetHeight/2; + if (position == SmartConnectorPosition.Right) { + xDiff = nodeWidth/2; + //element.style.flexDirection = 'column'; + } + if (position == SmartConnectorPosition.Left) { + xDiff = -(nodeWidth/2 + element.offsetWidth); + //element.style.flexDirection = 'column'; + } + if (position == SmartConnectorPosition.Top) { + yDiff = -(nodeHeight/2 + element.offsetHeight); + //element.style.flexDirection = 'row'; + } + if (position == SmartConnectorPosition.Bottom) { + yDiff = nodeHeight/2; + //element.style.flexDirection = 'row'; + } + if (setAbsolute) element.style.position = 'absolute'; + element.style.left = `${xDiff}px`; + element.style.top = `${yDiff}px`; + console.log('width', nodeWidth, 'height', nodeHeight) + element.style.transform = `scale(${zoom})`; } - containerElement.style.transform = `scale(${zoom})`; } protected initializeContents(containerElement: HTMLElement): void { + this.createBody(); + this.containerElement.appendChild(this.smartConnectorContainer) + this.createExpandButton(); + this.containerElement.appendChild(this.expandButton); containerElement.setAttribute('aria-label', 'Smart-Connector'); - var container; - for (const item of this.paletteItems) { + } + + protected createBody() { + this.smartConnectorContainer = document.createElement('div'); + this.smartConnectorContainer.classList.add('smart-connector-container') + for (const item of this.smartConnectorItems) { if (item.children) { - container = this.createContainer(item); - containerElement.appendChild(container); - if (item.id == 'node-group') this.nodeContainer = container; - if (item.id == 'edge-group') this.edgeContainer = container; + var group = this.createGroup(item); + this.smartConnectorContainer.appendChild(group); } - } } - private createContainer(item: PaletteItem): HTMLElement { - const container = document.createElement('div'); - container.className = 'smart-connector-button-container'; + protected createExpandButton() { + this.expandButton = document.createElement('div'); + this.expandButton.className = 'smart-connector-expand-button'; + this.expandButton.innerHTML = '+' + this.expandButton.onclick = this.showSmartConnector(); + } + + protected showSmartConnector() { + return (_ev: MouseEvent) => { + if (!this.editorContext.isReadonly) { + this.smartConnectorContainer.style.visibility = 'visible'; + this.expandButton.style.visibility = 'hidden'; + } + }; + } + + // default state + protected hideSmartConnector() { + if (this.smartConnectorContainer && this.expandButton) { + this.smartConnectorContainer.style.visibility = 'hidden'; + this.expandButton.style.visibility = 'visible'; + } + } + + private createGroup(item: SmartConnectorGroupItem): HTMLElement { + const group = document.createElement('div'); + group.classList.add('smart-connector-button-container'); + group.id = item.id; + if (item.showTitle) { + const header = document.createElement('div'); + header.classList.add('group-header'); + if (item.icon) { + header.appendChild(createIcon(item.icon)); + } + header.insertAdjacentText('beforeend', item.label); + group.appendChild(header); + } + for (const child of item.children!) { - container.appendChild(this.createToolButton(child)); + group.appendChild(this.createToolButton(child)); } - return container; + return group; } protected createToolButton(item: PaletteItem): HTMLElement { @@ -110,6 +216,57 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle return button; } + protected createSearchField(): HTMLInputElement { + const searchField = document.createElement('input'); + searchField.classList.add('search-input'); + searchField.id = this.containerElement.id + '_search_field'; + searchField.type = 'text'; + searchField.placeholder = ' Search...'; + searchField.style.display = 'none'; + searchField.onkeyup = () => this.requestFilterUpdate(this.searchField.value); + searchField.onkeydown = ev => this.clearOnEscape(ev); + return searchField; + } + + protected requestFilterUpdate(filter: string): void { + // Initialize the copy if it's empty + if (this.smartConnectorItemsCopy.length === 0) { + // Creating deep copy + this.smartConnectorItemsCopy = JSON.parse(JSON.stringify(this.smartConnectorItems)); + } + + // Reset the paletteItems before searching + this.smartConnectorItems = JSON.parse(JSON.stringify(this.smartConnectorItemsCopy)); + // Filter the entries + const filteredPaletteItems: PaletteItem[] = []; + + for (var itemGroup of this.smartConnectorItems) { + if (itemGroup.children) { + // Fetch the labels according to the filter + const matchingChildren = itemGroup.children.filter(child => child.label.toLowerCase().includes(filter.toLowerCase())); + + // Add the itemgroup containing the correct entries + if (matchingChildren.length > 0) { + // Clear existing children + itemGroup.children.splice(0, itemGroup.children.length); + // Push the matching children + matchingChildren.forEach(child => itemGroup.children!.push(child)); + filteredPaletteItems.push(itemGroup); + } + } + itemGroup.children?.push(...filteredPaletteItems); + } + + this.createBody(); + } + + protected clearOnEscape(event: KeyboardEvent): void { + if (matchesKeystroke(event, 'Escape')) { + this.searchField.value = ''; + this.requestFilterUpdate(''); + } + } + protected onClickCreateToolButton(button: HTMLElement, item: PaletteItem) { return (_ev: MouseEvent) => { if (!this.editorContext.isReadonly) { @@ -125,6 +282,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle } }); this.actionDispatcher.dispatchAll(item.actions.concat([SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: false })])); + this.hideSmartConnector(); button.focus(); } }; @@ -137,10 +295,12 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: true }) ); } - else + else { this.actionDispatcher.dispatch( SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: false }) ); + this.hideSmartConnector(); + } } async preRequestModel(): Promise { @@ -151,7 +311,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle } }); const response = await this.actionDispatcher.request(requestAction); - this.paletteItems = response.actions.map(e => e as PaletteItem); + this.smartConnectorItems = response.actions.map(e => e as SmartConnectorGroupItem); } } diff --git a/packages/protocol/src/action-protocol/smart-connector.ts b/packages/protocol/src/action-protocol/smart-connector.ts index 57bebeea..c7b3121e 100644 --- a/packages/protocol/src/action-protocol/smart-connector.ts +++ b/packages/protocol/src/action-protocol/smart-connector.ts @@ -1,5 +1,4 @@ import { hasStringProp } from '../utils/type-util'; -import { Bounds } from 'sprotty-protocol'; import { Action } from './base-protocol'; /** @@ -12,10 +11,6 @@ export interface OpenSmartConnectorAction extends Action { * The identifier of the element where the smart connector is to be opened. */ selectedElementID: string; - /** - * Bounds of the node where the smart connector is to be opened. - */ - bounds: Bounds; } export namespace OpenSmartConnectorAction { @@ -25,11 +20,10 @@ export namespace OpenSmartConnectorAction { return Action.hasKind(object, KIND) && hasStringProp(object, 'selectedElementID'); } - export function create(selectedElementID: string, bounds: Bounds): OpenSmartConnectorAction { + export function create(selectedElementID: string): OpenSmartConnectorAction { return { kind: KIND, selectedElementID, - bounds }; } } diff --git a/packages/protocol/src/action-protocol/types.ts b/packages/protocol/src/action-protocol/types.ts index d4c78470..aec70d7e 100644 --- a/packages/protocol/src/action-protocol/types.ts +++ b/packages/protocol/src/action-protocol/types.ts @@ -15,7 +15,7 @@ ********************************************************************************/ import * as sprotty from 'sprotty-protocol'; import { Dimension, Point } from 'sprotty-protocol'; -import { AnyObject, hasArrayProp, hasStringProp } from '../utils/type-util'; +import { AnyObject, hasArrayProp, hasBooleanProp, hasObjectProp, hasStringProp } from '../utils/type-util'; import { Action } from './base-protocol'; import { TriggerEdgeCreationAction, TriggerNodeCreationAction } from './tool-palette'; // A collection of convenience and utility types that are used in the GLSP action protocol. @@ -173,6 +173,39 @@ export namespace PaletteItem { } } +export enum SmartConnectorGroupUIType { + Icons, + Labels +} + +export enum SmartConnectorPosition { + Left, + Right, + Top, + Bottom +} + +/** + * TODO + */ +export interface SmartConnectorGroupItem extends PaletteItem { + /** Show the title of a group */ + readonly showTitle: boolean; + /** Show a group as a collapsed submenu if true, open if false */ + readonly submenu: boolean; + /** Position of the group */ + readonly position: SmartConnectorPosition; + /** Show either only icons or labels. Show both when not given*/ + readonly showOnlyForChildren?: SmartConnectorGroupUIType; + +} + +export namespace SmartConnectorGroupItem { + export function is(object: any): object is SmartConnectorGroupItem { + return PaletteItem.is(object) && hasBooleanProp(object, 'showTitle') && hasBooleanProp(object, 'submenu') && hasObjectProp(object, 'position'); + } +} + /** * A special {@link LabeledAction} that is used to denote items in a menu. */ From b41978d11ef5d0883c9c7d69dc3ec2ec40b5688c Mon Sep 17 00:00:00 2001 From: yentelmanero Date: Wed, 29 Nov 2023 23:37:56 +0100 Subject: [PATCH 03/21] added search, keyboard navigation and customization options to smart connector --- packages/client/css/smart-connector.css | 110 +++- .../smart-connector/smart-connector-module.ts | 5 +- .../smart-connector/smart-connector.ts | 512 ++++++++++++------ .../src/action-protocol/smart-connector.ts | 4 +- .../protocol/src/action-protocol/types.ts | 23 +- 5 files changed, 454 insertions(+), 200 deletions(-) diff --git a/packages/client/css/smart-connector.css b/packages/client/css/smart-connector.css index 9e32ec67..62d77bef 100644 --- a/packages/client/css/smart-connector.css +++ b/packages/client/css/smart-connector.css @@ -1,52 +1,106 @@ - .smart-connector { position: absolute; z-index: 1; } -.smart-connector-container { +/* combined container */ + +#smart-connector-container { position: absolute; + visibility: hidden; +} + +/* expand button */ + +#smart-connector-expand-button { + height: 32px; + width: 32px; + border-radius: 50%; + border: 1px solid black; + z-index: 9999; + display: flex; /* or inline-flex */ + align-items: center; + justify-content: center; + position: absolute; + background: #cccccc; + visibility: hidden; } -.smart-connector-button-container { +/* search bar */ + +.smart-connector-search { + background: #dfdfdf; + color: black; + border: #bddaef; + padding-left: 3px; + width: 100%; + box-sizing: border-box; +} + +.smart-connector-submenu-search-container { + max-height: 0; + overflow: hidden; +} + +.smart-connector-search-container { + max-height: 50px; +} + +/* header */ + +.header-title { + display: flex; + align-items: center; + padding-right: 8px; +} + +.smart-connector-group-header { + justify-content: space-between; +} + +/* seperated containers */ + +.smart-connector-group-container { border-left: 1px solid black; border-right: 1px solid black; background-color:rgba(1, 1, 1, 0.6); + max-width: 200px; + /* display: flex; */ +} + +.smart-connector-group-container:hover, .smart-connector-group-container:focus-within { + z-index: 10000; +} + +.smart-connector-group-container div:first-child { + border-top: 0; +} + +/* container items */ + +.smart-connector-group { overflow-y: scroll; - max-height: 150px; - max-width: 400px; -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */ - /* display: flex; */ } -.smart-connector-button-container::-webkit-scrollbar { +/* scrollbar */ +.smart-connector-group::-webkit-scrollbar { display: none; } +.collapsable-group { + max-height: 0; + /* transition: max-height 0.2s ease-out; */ +} + +/* single item */ + .smart-connector-button { border-top: 1px solid black; - margin: 0 2px; + margin: 0 4px; padding: 8px 0; - z-index: 9999; + z-index: 9998; pointer-events: auto; cursor: pointer; -} - -.smart-connector-button-container div:first-child { - background-color:rgba(1, 1, 1, 0.6); - border-top: 0; -} - -.smart-connector-expand-button { - height: 32px; - width: 32px; - border-radius: 50%; - border: 1px solid black; - z-index: 9999; - display: flex; /* or inline-flex */ - align-items: center; - justify-content: center; - position: absolute; - background: #cccccc; -} +} \ No newline at end of file diff --git a/packages/client/src/features/smart-connector/smart-connector-module.ts b/packages/client/src/features/smart-connector/smart-connector-module.ts index 59c9f4ef..1bd8a21c 100644 --- a/packages/client/src/features/smart-connector/smart-connector-module.ts +++ b/packages/client/src/features/smart-connector/smart-connector-module.ts @@ -1,6 +1,6 @@ -import { FeatureModule, TYPES, bindAsService, configureActionHandler, OpenSmartConnectorAction, CloseSmartConnectorAction, MoveAction, SetBoundsAction, SetViewportAction, DeleteElementOperation } from '~glsp-sprotty'; +import { FeatureModule, TYPES, bindAsService, configureActionHandler, OpenSmartConnectorAction, CloseSmartConnectorAction, MoveAction, SetBoundsAction, SetViewportAction, DeleteElementOperation } from '@eclipse-glsp/sprotty'; import '../../../css/smart-connector.css' -import { SmartConnector } from './smart-connector'; +import { SmartConnector, SmartConnectorKeyListener } from './smart-connector'; export const smartConnectorModule = new FeatureModule((bind, unbind, isBound, rebind) => { const context = { bind, unbind, isBound, rebind }; @@ -12,4 +12,5 @@ export const smartConnectorModule = new FeatureModule((bind, unbind, isBound, re configureActionHandler(context, SetBoundsAction.KIND, SmartConnector); configureActionHandler(context, SetViewportAction.KIND, SmartConnector); configureActionHandler(context, DeleteElementOperation.KIND, SmartConnector); + bindAsService(bind, TYPES.KeyListener, SmartConnectorKeyListener); }); \ No newline at end of file diff --git a/packages/client/src/features/smart-connector/smart-connector.ts b/packages/client/src/features/smart-connector/smart-connector.ts index ad9b8f72..eac967d0 100644 --- a/packages/client/src/features/smart-connector/smart-connector.ts +++ b/packages/client/src/features/smart-connector/smart-connector.ts @@ -5,14 +5,11 @@ import { FocusTracker } from '../../base/focus/focus-tracker'; import { AbstractUIExtension, Action, - DeleteElementOperation, IActionHandler, ICommand, KeyListener, - SModelElement, - SModelRoot, - isDeletable, - isSelectable, + GModelElement, + GModelRoot, matchesKeystroke, OpenSmartConnectorAction, SetUIExtensionVisibilityAction, @@ -24,29 +21,47 @@ import { TriggerNodeCreationAction, Args, SmartConnectorGroupItem, + SmartConnectorNodeItem, SmartConnectorPosition, -} from '~glsp-sprotty' + KeyCode, +} from '@eclipse-glsp/sprotty' import { GetViewportAction } from 'sprotty-protocol/lib/actions' -import { createIcon } from '../tool-palette/tool-palette'; -import { IDiagramStartup } from 'src'; - - +import { changeCodiconClass, createIcon } from '../tool-palette/tool-palette'; +import { IDiagramStartup } from '../../base/model/diagram-loader'; +import { EnableDefaultToolsAction } from '../../base/tool-manager/tool'; +const CONTAINER_PADDING_PX = 20; +const MAX_HEIGHT_GROUP = '150px'; +const SEARCH_FIELD_SUFFIX = '_search_field'; +const SMART_CONNECTOR_CONTAINER_ID = 'smart-connector-container'; +const EXPAND_BUTTON_ID = 'smart-connector-expand-button'; +const GROUP_CONTAINER_CLASS = 'smart-connector-group-container'; +const HEADER_CLASS = 'smart-connector-group-header'; +const GROUP_CLASS = 'smart-connector-group'; +const TOOL_BUTTON_CLASS = 'smart-connector-button'; +const COLLAPSABLE_CLASS = 'collapsable-group'; @injectable() export class SmartConnector extends AbstractUIExtension implements IActionHandler, IDiagramStartup { static readonly ID = 'smart-connector'; - static readonly CONTAINER_PADDING = 16; + protected selectedElementId: string; + protected smartConnectorItems: SmartConnectorGroupItem[]; + protected smartConnectorContainer: HTMLElement; + protected expandButton: HTMLElement; + protected currentZoom: number; + + protected smartConnectorGroups: Record = {}; + protected groupIsCollapsed: Record = {}; + protected groupIsTop: Record = {}; + protected searchFields: Record = {}; - private selectedElementId: string; - private smartConnectorItems: SmartConnectorGroupItem[]; - protected smartConnectorItemsCopy: SmartConnectorGroupItem[] = []; - private smartConnectorContainer: HTMLElement; - private expandButton: HTMLElement; + protected previousElementKeyCode: KeyCode = 'ArrowUp'; + protected nextElementKeyCode: KeyCode = 'ArrowDown'; - protected searchField: HTMLInputElement; + // Sets the position of the expand button + protected expandButtonPosition = SmartConnectorPosition.Top; @inject(GLSPActionDispatcher) protected actionDispatcher: GLSPActionDispatcher; @@ -64,113 +79,119 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle return SmartConnector.ID; } - protected override async onBeforeShow(containerElement: HTMLElement, root: Readonly, ...contextElementIds: string[]): Promise { - this.hideSmartConnector(); - // TODO temporary for testing, to be replaced by settings - var position = SmartConnectorPosition.Top; - var viewportResult: ViewportResult = await this.actionDispatcher.request(GetViewportAction.create()) - this.setMainPosition(viewportResult) - this.setPosition(viewportResult, this.expandButton, position) - await this.requestAvailableOptions(contextElementIds) + protected override async onBeforeShow(_containerElement: HTMLElement, root: Readonly): Promise { + var viewportResult: ViewportResult = await this.actionDispatcher.request(GetViewportAction.create()); + await this.requestAvailableOptions(root.index.getById(this.selectedElementId)); + this.currentZoom = viewportResult.viewport.zoom; + var nodeFromDom = document.getElementById(`${this.options.baseDiv}_${this.selectedElementId}`) as any as SVGGElement; + var nodeBoundsFromDom = nodeFromDom.getBoundingClientRect(); + this.setMainPosition(viewportResult, nodeBoundsFromDom); + // set position of expand button + this.setPosition(this.expandButton, this.expandButtonPosition, nodeBoundsFromDom); + // set position of container(s) var sameSide = this.smartConnectorItems.every((e) => e.position === this.smartConnectorItems[0].position) if (sameSide) - this.setPosition(viewportResult, this.smartConnectorContainer, this.smartConnectorItems[0].position) + this.setPosition(this.smartConnectorContainer, this.smartConnectorItems[0].position, nodeBoundsFromDom) else { for (var i = 0; i < this.smartConnectorContainer.childElementCount; i++) { - this.setPosition(viewportResult, this.smartConnectorContainer.children[i] as HTMLElement, this.smartConnectorItems[i].position, true) + this.setPosition(this.smartConnectorContainer.children[i] as HTMLElement, this.smartConnectorItems[i].position, nodeBoundsFromDom, true) } } + this.hideSmartConnector(); } - protected async requestAvailableOptions(contextElementIds: string[]) { + protected async requestAvailableOptions(contextElement?: GModelElement) { const requestAction = RequestContextActions.create({ contextId: SmartConnector.ID, editorContext: { - selectedElementIds: contextElementIds + selectedElementIds: [this.selectedElementId], + args: { nodeType: contextElement!.type } } }); const response = await this.actionDispatcher.request(requestAction); this.smartConnectorItems = response.actions.map(e => e as SmartConnectorGroupItem); + this.createBody(); } - protected setMainPosition(viewport: ViewportResult) { - // TODO: get element from server instead of DOM - var nodeFromDom = document.getElementById(`${this.options.baseDiv}_${this.selectedElementId}`) as any as SVGGElement; - if (nodeFromDom) { - var nodeBoundsFromDom = nodeFromDom.getBoundingClientRect(); - var xCenter = nodeBoundsFromDom.x + nodeBoundsFromDom.width/2 - viewport.canvasBounds.x - var yCenter = nodeBoundsFromDom.y + nodeBoundsFromDom.height/2 - viewport.canvasBounds.y - this.containerElement.style.left = `${xCenter}px`; - this.containerElement.style.top = `${yCenter}px`; - } + protected setMainPosition(viewport: ViewportResult, nodeBounds: DOMRect) { + var xCenter = nodeBounds.x + nodeBounds.width/2 - viewport.canvasBounds.x + var yCenter = nodeBounds.y + nodeBounds.height/2 - viewport.canvasBounds.y + this.containerElement.style.left = `${xCenter}px`; + this.containerElement.style.top = `${yCenter}px`; + } - protected setPosition(viewport: ViewportResult, element: HTMLElement, position: SmartConnectorPosition, setAbsolute?: boolean) { - var zoom = viewport.viewport.zoom; - // TODO: get element from server instead of DOM - var nodeFromDom = document.getElementById(`${this.options.baseDiv}_${this.selectedElementId}`) as any as SVGGElement; - if (nodeFromDom) { - var nodeHeight = nodeFromDom.getBoundingClientRect().height; - var nodeWidth = nodeFromDom.getBoundingClientRect().width; - var xDiff = -element.offsetWidth/2; - var yDiff = -element.offsetHeight/2; - if (position == SmartConnectorPosition.Right) { - xDiff = nodeWidth/2; - //element.style.flexDirection = 'column'; - } - if (position == SmartConnectorPosition.Left) { - xDiff = -(nodeWidth/2 + element.offsetWidth); - //element.style.flexDirection = 'column'; - } - if (position == SmartConnectorPosition.Top) { - yDiff = -(nodeHeight/2 + element.offsetHeight); - //element.style.flexDirection = 'row'; - } - if (position == SmartConnectorPosition.Bottom) { - yDiff = nodeHeight/2; - //element.style.flexDirection = 'row'; - } - if (setAbsolute) element.style.position = 'absolute'; + protected setPosition(element: HTMLElement, position: SmartConnectorPosition, nodeBounds: DOMRect, singleContainer?: boolean) { + var zoom = this.currentZoom; + element.style.transform = `scale(${zoom})`; + var nodeHeight = nodeBounds.height; + var nodeWidth = nodeBounds.width; + var xDiff = -element.offsetWidth/2; + var yDiff = -element.offsetHeight/2*zoom; + if (position == SmartConnectorPosition.Right || position == SmartConnectorPosition.Left) { + xDiff = nodeWidth/2 + CONTAINER_PADDING_PX*zoom; + element.style.top = `${yDiff}px`; + } + if (position == SmartConnectorPosition.Top || position == SmartConnectorPosition.Bottom) { + yDiff = nodeHeight/2 + CONTAINER_PADDING_PX*zoom; + element.style.left = `${xDiff}px`; + } + if (position == SmartConnectorPosition.Right) { + element.style.transformOrigin = 'top left' element.style.left = `${xDiff}px`; + } + if (position == SmartConnectorPosition.Left) { + element.style.transformOrigin = 'top right' + element.style.right = `${xDiff}px`; + } + if (position == SmartConnectorPosition.Top) { + element.style.transformOrigin = 'bottom' + element.style.bottom = `${yDiff}px`; + } + if (position == SmartConnectorPosition.Bottom) { + element.style.transformOrigin = 'top' element.style.top = `${yDiff}px`; - console.log('width', nodeWidth, 'height', nodeHeight) - element.style.transform = `scale(${zoom})`; - } + } + if (singleContainer) element.style.position = 'absolute'; } protected initializeContents(containerElement: HTMLElement): void { this.createBody(); - this.containerElement.appendChild(this.smartConnectorContainer) this.createExpandButton(); this.containerElement.appendChild(this.expandButton); containerElement.setAttribute('aria-label', 'Smart-Connector'); } protected createBody() { - this.smartConnectorContainer = document.createElement('div'); - this.smartConnectorContainer.classList.add('smart-connector-container') + var smartConnectorContainer = document.createElement('div'); + smartConnectorContainer.id = SMART_CONNECTOR_CONTAINER_ID for (const item of this.smartConnectorItems) { if (item.children) { var group = this.createGroup(item); - this.smartConnectorContainer.appendChild(group); + this.smartConnectorGroups[group.id] = group + smartConnectorContainer.appendChild(group); } } + if (this.smartConnectorContainer) { + this.containerElement.removeChild(this.smartConnectorContainer) + } + this.containerElement.appendChild(smartConnectorContainer) + this.smartConnectorContainer = smartConnectorContainer; } protected createExpandButton() { this.expandButton = document.createElement('div'); - this.expandButton.className = 'smart-connector-expand-button'; + this.expandButton.id = EXPAND_BUTTON_ID; this.expandButton.innerHTML = '+' - this.expandButton.onclick = this.showSmartConnector(); - } - - protected showSmartConnector() { - return (_ev: MouseEvent) => { - if (!this.editorContext.isReadonly) { - this.smartConnectorContainer.style.visibility = 'visible'; - this.expandButton.style.visibility = 'hidden'; - } - }; + this.expandButton.onclick = _ev => { + if(!this.editorContext.isReadonly) + showSmartConnector(this.smartConnectorContainer, this.expandButton) + }; + this.expandButton.onkeydown = ev => { + if(matchesKeystroke(ev, 'Space') && !this.editorContext.isReadonly) + showSmartConnector(this.smartConnectorContainer, this.expandButton) + }; + } // default state @@ -181,113 +202,242 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle } } - private createGroup(item: SmartConnectorGroupItem): HTMLElement { + protected createGroup(item: SmartConnectorGroupItem): HTMLElement { + const searchField = this.createSearchField(item); const group = document.createElement('div'); - group.classList.add('smart-connector-button-container'); + if (item.children!.length == 0) return group; + const groupItems = document.createElement('div'); + group.classList.add(GROUP_CONTAINER_CLASS); + groupItems.classList.add(GROUP_CLASS); group.id = item.id; + for (const child of item.children!) { + groupItems.appendChild(this.createToolButton(child)); + } if (item.showTitle) { - const header = document.createElement('div'); - header.classList.add('group-header'); - if (item.icon) { - header.appendChild(createIcon(item.icon)); + const header = this.createGroupHeader(item, groupItems, searchField) + if (item.position == SmartConnectorPosition.Top) { + // the header is at the bottom on top + group.appendChild(groupItems); + if (item.children!.length > 1) group.appendChild(searchField); + group.appendChild(header); + this.groupIsTop[group.id] = true + return group; } - header.insertAdjacentText('beforeend', item.label); - group.appendChild(header); + group.appendChild(header); } - - for (const child of item.children!) { - group.appendChild(this.createToolButton(child)); - } + if (item.children!.length > 1) group.appendChild(searchField); + group.appendChild(groupItems); + this.groupIsTop[group.id] = false return group; } + protected createGroupHeader(group: SmartConnectorGroupItem, groupItems: HTMLElement, searchField: HTMLElement): HTMLElement{ + const header = document.createElement('div'); + const headerTitle = document.createElement('div'); + header.classList.add(HEADER_CLASS); + // for same css as palette header + header.classList.add('group-header'); + if (group.icon) { + headerTitle.appendChild(createIcon(group.icon)); + } + headerTitle.insertAdjacentText('beforeend', group.label); + headerTitle.classList.add('header-title'); + header.appendChild(headerTitle); + header.tabIndex = 0; + if (group.submenu) { + const submenuIcon = group.position == SmartConnectorPosition.Top ? createIcon('chevron-up') : createIcon('chevron-down'); + header.appendChild(submenuIcon); + groupItems.classList.add(COLLAPSABLE_CLASS); + header.onclick = _ev => { + this.toggleSubmenu(submenuIcon, group, groupItems, searchField); + }; + header.onkeydown = ev => { + if (matchesKeystroke(ev, 'Enter')) { + this.toggleSubmenu(submenuIcon, group, groupItems, searchField); + } + this.headerNavigation(ev, groupItems, header) + } + this.groupIsCollapsed[group.id] = true + } + return header; + } + + protected toggleSubmenu(icon: HTMLElement, group: SmartConnectorGroupItem, groupItems: HTMLElement, searchField: HTMLElement) { + changeCodiconClass(icon, 'chevron-up'); + changeCodiconClass(icon, 'chevron-down'); + this.groupIsCollapsed[group.id] = !this.groupIsCollapsed[group.id]; + if (groupItems.style.maxHeight) { + groupItems.style.maxHeight = ''; + searchField.style.maxHeight = ''; + } else { + groupItems.style.maxHeight = MAX_HEIGHT_GROUP; + searchField.style.maxHeight = '50px'; + } + } + protected createToolButton(item: PaletteItem): HTMLElement { const button = document.createElement('div'); button.tabIndex = 0; - button.classList.add('smart-connector-button'); + button.classList.add(TOOL_BUTTON_CLASS); if (item.icon) { button.appendChild(createIcon(item.icon)); return button; } button.insertAdjacentText('beforeend', item.label); button.onclick = this.onClickCreateToolButton(button, item); - //TODO: keyboard support - //button.onkeydown = ev => this.clearToolOnEscape(ev); + button.onkeydown = ev => this.toolButtonKeyHandler(ev, item); + button.id = item.id; return button; } - protected createSearchField(): HTMLInputElement { + protected createSearchField(itemGroup: SmartConnectorGroupItem): HTMLElement { const searchField = document.createElement('input'); - searchField.classList.add('search-input'); - searchField.id = this.containerElement.id + '_search_field'; + searchField.classList.add('smart-connector-search'); + searchField.id = itemGroup.id + SEARCH_FIELD_SUFFIX; searchField.type = 'text'; searchField.placeholder = ' Search...'; - searchField.style.display = 'none'; - searchField.onkeyup = () => this.requestFilterUpdate(this.searchField.value); - searchField.onkeydown = ev => this.clearOnEscape(ev); - return searchField; + searchField.onkeyup = () => this.requestFilterUpdate(this.searchFields[itemGroup.id].value, itemGroup); + searchField.onkeydown = ev => this.searchFieldKeyHandler(ev, itemGroup); + this.searchFields[itemGroup.id] = searchField; + const searchContainer = document.createElement('div'); + var containerClass = itemGroup.submenu ? 'smart-connector-submenu-search-container' : 'smart-connector-search-container'; + searchContainer.classList.add(containerClass); + searchContainer.appendChild(searchField) + return searchContainer; } - protected requestFilterUpdate(filter: string): void { - // Initialize the copy if it's empty - if (this.smartConnectorItemsCopy.length === 0) { - // Creating deep copy - this.smartConnectorItemsCopy = JSON.parse(JSON.stringify(this.smartConnectorItems)); - } + //#region event handlers - // Reset the paletteItems before searching - this.smartConnectorItems = JSON.parse(JSON.stringify(this.smartConnectorItemsCopy)); - // Filter the entries - const filteredPaletteItems: PaletteItem[] = []; - - for (var itemGroup of this.smartConnectorItems) { - if (itemGroup.children) { - // Fetch the labels according to the filter - const matchingChildren = itemGroup.children.filter(child => child.label.toLowerCase().includes(filter.toLowerCase())); - - // Add the itemgroup containing the correct entries - if (matchingChildren.length > 0) { - // Clear existing children - itemGroup.children.splice(0, itemGroup.children.length); - // Push the matching children - matchingChildren.forEach(child => itemGroup.children!.push(child)); - filteredPaletteItems.push(itemGroup); + protected requestFilterUpdate(filter: string, itemGroup: SmartConnectorGroupItem): void { + if (itemGroup.children) { + const matchingChildren = itemGroup.children.filter(child => child.label.toLowerCase().includes(filter.toLowerCase())); + if (matchingChildren.length > 0) { + var items = document.getElementById(itemGroup.id)?.getElementsByClassName(TOOL_BUTTON_CLASS); + if (items) { + Array.from(items).forEach(item => { + if (matchingChildren.find(child => child.id == item.id)) (item as HTMLElement).style.display = 'block'; + else (item as HTMLElement).style.display = 'none'; + }) } } - itemGroup.children?.push(...filteredPaletteItems); } - - this.createBody(); } - protected clearOnEscape(event: KeyboardEvent): void { + protected toolButtonKeyHandler(event: KeyboardEvent, item: PaletteItem): void { if (matchesKeystroke(event, 'Escape')) { - this.searchField.value = ''; - this.requestFilterUpdate(''); + this.actionDispatcher.dispatch(EnableDefaultToolsAction.create()); } + if (matchesKeystroke(event, 'Enter')) { + this.createTool(item) + } + if (event.ctrlKey) { + // matchesKeystroke with Ctrl and Ctrl+F does not seem to work on Windows 11/Chrome + //if (matchesKeystroke(event, 'ControlLeft')) { + //if (matchesKeystroke(event, 'KeyF', 'ctrlCmd')) { + var parentId = this.smartConnectorItems.find(e => e.children?.includes(item))!.id; + var searchFieldId = parentId + SEARCH_FIELD_SUFFIX; + var searchField = document.getElementById(searchFieldId); + if (searchField) (searchField as HTMLElement).focus(); + } + this.toolButtonNavigation(event, item) } - protected onClickCreateToolButton(button: HTMLElement, item: PaletteItem) { - return (_ev: MouseEvent) => { - if (!this.editorContext.isReadonly) { - item.actions.forEach(e => { - var args: Args; - if (TriggerEdgeCreationAction.is(e)) { - args = { source: this.selectedElementId }; - (e as TriggerEdgeCreationAction).args = args; - } - if (TriggerNodeCreationAction.is(e)) { - args = { createEdge: true, source: this.selectedElementId }; - (e as TriggerNodeCreationAction).args = args; - } - }); - this.actionDispatcher.dispatchAll(item.actions.concat([SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: false })])); - this.hideSmartConnector(); - button.focus(); + protected searchFieldKeyHandler(event: KeyboardEvent, itemGroup: SmartConnectorGroupItem): void { + if (matchesKeystroke(event, 'Escape')) { + this.searchFields[itemGroup.id].value = ''; + this.requestFilterUpdate('', itemGroup); + } + this.searchFieldNavigation(event, itemGroup) + } + + // #region navigation handlers + + protected searchFieldNavigation(event: KeyboardEvent, itemGroup: SmartConnectorGroupItem) { + if (matchesKeystroke(event, this.previousElementKeyCode)) { + (document.getElementById(itemGroup.children![itemGroup.children!.length-1].id) as HTMLElement).focus() + } + if (matchesKeystroke(event, this.nextElementKeyCode)) { + (document.getElementById(itemGroup.children![0].id) as HTMLElement).focus() + } + } + + protected headerNavigation(event: KeyboardEvent, groupItems: HTMLElement, header: HTMLElement) { + var parent = header.parentElement!; + if (matchesKeystroke(event, this.previousElementKeyCode)) { + if ((this.groupIsCollapsed[parent.id] || !header.previous()) && parent.previous()) { + var collapsableHeader = getHeaderIfGroupContainsCollapsable(parent.previous()) + if (collapsableHeader && (this.groupIsTop[parent.previous().id] || this.groupIsCollapsed[parent.previous().id])) collapsableHeader.focus(); + else this.getPreviousGroupLastItem(parent).focus() + } + else if (!this.groupIsCollapsed[parent.id] && header.previous()) (groupItems.last()).focus() + } + if (matchesKeystroke(event, this.nextElementKeyCode)) { + if ((this.groupIsCollapsed[parent.id] || !header.next()) && parent.next()) { + var collapsableHeader = getHeaderIfGroupContainsCollapsable(parent.next()) + if (collapsableHeader && (!this.groupIsTop[parent.next().id] || this.groupIsCollapsed[parent.next().id])) collapsableHeader.focus(); + else this.getNextGroupFirstItem(parent).focus() } + else if (!this.groupIsCollapsed[parent.id] && header.next()) (groupItems.first()).focus() + } + } + + protected toolButtonNavigation(event: KeyboardEvent, item: PaletteItem) { + var parentId = this.smartConnectorItems.find(e => e.children?.includes(item))!.id; + var parent = document.getElementById(parentId)!; + var toolButton = document.getElementById(item.id)!; + var collapsableHeader = getHeaderIfGroupContainsCollapsable(parent) + if (matchesKeystroke(event, this.previousElementKeyCode)) { + var previousGroupCollapsableHeader = getHeaderIfGroupContainsCollapsable(parent.previous()) + if (toolButton.previous()) toolButton.previous().focus() + else if (collapsableHeader && !this.groupIsTop[parent.id]) collapsableHeader.focus(); + else if (previousGroupCollapsableHeader && this.groupIsTop[parent.previous().id]) previousGroupCollapsableHeader.focus(); + else if (parent.previous()) this.getPreviousGroupLastItem(parent).focus() + } + if (matchesKeystroke(event, this.nextElementKeyCode)) { + var nextGroupCollapsableHeader = getHeaderIfGroupContainsCollapsable(parent.next()) + if (toolButton.next()) toolButton.next().focus() + else if (collapsableHeader && this.groupIsTop[parent.id]) collapsableHeader.focus(); + else if (nextGroupCollapsableHeader && !this.groupIsTop[parent.previous().id]) nextGroupCollapsableHeader.focus(); + else if (parent.next()) this.getNextGroupFirstItem(parent).focus() + } + } + + protected getNextGroupFirstItem(parent: HTMLElement): HTMLElement { + return parent.nextElementSibling!.getElementsByClassName(GROUP_CLASS)[0].firstElementChild as HTMLElement; + } + + protected getPreviousGroupLastItem(parent: HTMLElement): HTMLElement { + return parent.previousElementSibling!.getElementsByClassName(GROUP_CLASS)[0].lastElementChild as HTMLElement; + } + + // #endregion + + protected onClickCreateToolButton(_button: HTMLElement, item: PaletteItem) { + return (_ev: MouseEvent) => { + this.createTool(item) }; } + protected createTool(item: PaletteItem) { + if (!this.editorContext.isReadonly) { + item.actions.forEach(e => { + var args: Args; + if (TriggerEdgeCreationAction.is(e)) { + args = { source: this.selectedElementId }; + (e as TriggerEdgeCreationAction).args = args; + } + if (TriggerNodeCreationAction.is(e)) { + args = { createEdge: true, source: this.selectedElementId, edgeType: (item as SmartConnectorNodeItem).edgeType }; + (e as TriggerNodeCreationAction).args = args; + } + }); + this.actionDispatcher.dispatchAll(item.actions.concat([SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: false })])); + this.hideSmartConnector(); + } + } + + //#endregion + handle(action: Action): ICommand | Action | void { if (OpenSmartConnectorAction.is(action)) { this.selectedElementId = action.selectedElementID; @@ -315,20 +465,58 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle } } +// HTMLElement extensions for readability and convenience (reduce casting) +declare global { + interface HTMLElement { + next(): HTMLElement + previous(): HTMLElement + first(): HTMLElement + last(): HTMLElement + } +} + +HTMLElement.prototype.next = function() { + return this.nextElementSibling as HTMLElement +} + +HTMLElement.prototype.previous = function() { + return this.previousElementSibling as HTMLElement +} + +HTMLElement.prototype.first = function() { + return this.firstElementChild as HTMLElement +} + +HTMLElement.prototype.last = function() { + return this.lastElementChild as HTMLElement +} + + +export function showSmartConnector(container: HTMLElement, expandButton: HTMLElement) { + container.style.visibility = 'visible'; + expandButton.style.visibility = 'hidden'; +} + +function getHeaderIfGroupContainsCollapsable(group: HTMLElement) { + if (group && group.getElementsByClassName(COLLAPSABLE_CLASS).length != 0) { + return group.getElementsByClassName(HEADER_CLASS)[0] as HTMLElement; + } + return null; +} + @injectable() export class SmartConnectorKeyListener extends KeyListener { - override keyDown(element: SModelElement, event: KeyboardEvent): Action[] { - if (matchesKeystroke(event, 'Delete', 'ctrl')) { - const deleteElementIds = Array.from( - element.root.index - .all() - .filter(e => isDeletable(e) && isSelectable(e) && e.selected) - .filter(e => e.id !== e.root.id) - .map(e => e.id) - ); - if (deleteElementIds.length > 0) { - return [DeleteElementOperation.create(deleteElementIds)]; + override keyDown(_element: GModelElement, event: KeyboardEvent) { + var container = document.getElementById(SMART_CONNECTOR_CONTAINER_ID)! + var expandButton = document.getElementById(EXPAND_BUTTON_ID) + var smartConnector = expandButton?.parentElement; + if (matchesKeystroke(event, 'Space') && smartConnector?.style.visibility == 'visible' && expandButton?.style.visibility == 'visible') { + showSmartConnector(container, expandButton); + var collapsableHeader = getHeaderIfGroupContainsCollapsable(container.firstElementChild as HTMLElement); + if (collapsableHeader) { + collapsableHeader.focus() } + else (container.firstElementChild!.getElementsByClassName(GROUP_CLASS)[0].firstElementChild as HTMLElement).focus() } return []; } diff --git a/packages/protocol/src/action-protocol/smart-connector.ts b/packages/protocol/src/action-protocol/smart-connector.ts index c7b3121e..2a8fb6a0 100644 --- a/packages/protocol/src/action-protocol/smart-connector.ts +++ b/packages/protocol/src/action-protocol/smart-connector.ts @@ -2,7 +2,7 @@ import { hasStringProp } from '../utils/type-util'; import { Action } from './base-protocol'; /** - * TODO + * Action that opens the smart connector at the position of the element */ export interface OpenSmartConnectorAction extends Action { kind: typeof OpenSmartConnectorAction.KIND; @@ -29,7 +29,7 @@ export namespace OpenSmartConnectorAction { } /** - * TODO + * Action that closes the smart connector */ export interface CloseSmartConnectorAction extends Action { kind: typeof CloseSmartConnectorAction.KIND; diff --git a/packages/protocol/src/action-protocol/types.ts b/packages/protocol/src/action-protocol/types.ts index 204e7f41..316d257c 100644 --- a/packages/protocol/src/action-protocol/types.ts +++ b/packages/protocol/src/action-protocol/types.ts @@ -186,15 +186,15 @@ export enum SmartConnectorPosition { } /** - * TODO + * Represents a group of the smart connector, which can be positioned around the clicked node */ export interface SmartConnectorGroupItem extends PaletteItem { - /** Show the title of a group */ - readonly showTitle: boolean; - /** Show a group as a collapsed submenu if true, open if false */ - readonly submenu: boolean; /** Position of the group */ readonly position: SmartConnectorPosition; + /** Shows the title of a group */ + readonly showTitle: boolean; + /** Shows a group as a collapsed submenu if true, open if false */ + readonly submenu?: boolean; /** Show either only icons or labels. Show both when not given*/ readonly showOnlyForChildren?: SmartConnectorGroupUIType; @@ -202,7 +202,18 @@ export interface SmartConnectorGroupItem extends PaletteItem { export namespace SmartConnectorGroupItem { export function is(object: any): object is SmartConnectorGroupItem { - return PaletteItem.is(object) && hasBooleanProp(object, 'showTitle') && hasBooleanProp(object, 'submenu') && hasObjectProp(object, 'position'); + return PaletteItem.is(object) && hasObjectProp(object, 'position') && hasBooleanProp(object, 'showTitle'); + } +} + +export interface SmartConnectorNodeItem extends PaletteItem { + /** default edge when creating an outgoing edge */ + readonly edgeType: string; +} + +export namespace SmartConnectorNodeItem { + export function is(object: any): object is SmartConnectorNodeItem { + return PaletteItem.is(object) && hasStringProp(object, 'edgeType'); } } From 7a0e67a678dc067dbcfa6615a2d4c011b2907b22 Mon Sep 17 00:00:00 2001 From: yentelmanero Date: Thu, 30 Nov 2023 01:26:05 +0100 Subject: [PATCH 04/21] fixed keyboard, width and click event when invisible bugs in smart connector --- packages/client/css/smart-connector.css | 3 ++- .../smart-connector/smart-connector.ts | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/client/css/smart-connector.css b/packages/client/css/smart-connector.css index 62d77bef..b9f3b34f 100644 --- a/packages/client/css/smart-connector.css +++ b/packages/client/css/smart-connector.css @@ -56,6 +56,7 @@ .smart-connector-group-header { justify-content: space-between; + width: 100px; } /* seperated containers */ @@ -82,6 +83,7 @@ overflow-y: scroll; -ms-overflow-style: none; /* IE and Edge */ scrollbar-width: none; /* Firefox */ + max-height: 150px; } /* scrollbar */ @@ -101,6 +103,5 @@ margin: 0 4px; padding: 8px 0; z-index: 9998; - pointer-events: auto; cursor: pointer; } \ No newline at end of file diff --git a/packages/client/src/features/smart-connector/smart-connector.ts b/packages/client/src/features/smart-connector/smart-connector.ts index eac967d0..a287c944 100644 --- a/packages/client/src/features/smart-connector/smart-connector.ts +++ b/packages/client/src/features/smart-connector/smart-connector.ts @@ -202,6 +202,14 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle } } + // to avoid onclicks on nested visible + protected hideAll() { + if (this.smartConnectorContainer && this.expandButton) { + this.smartConnectorContainer.style.visibility = 'hidden'; + this.expandButton.style.visibility = 'hidden'; + } + } + protected createGroup(item: SmartConnectorGroupItem): HTMLElement { const searchField = this.createSearchField(item); const group = document.createElement('div'); @@ -364,7 +372,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle protected headerNavigation(event: KeyboardEvent, groupItems: HTMLElement, header: HTMLElement) { var parent = header.parentElement!; if (matchesKeystroke(event, this.previousElementKeyCode)) { - if ((this.groupIsCollapsed[parent.id] || !header.previous()) && parent.previous()) { + if ((this.groupIsCollapsed[parent.id] || !this.groupIsTop[parent.id]) && parent.previous()) { var collapsableHeader = getHeaderIfGroupContainsCollapsable(parent.previous()) if (collapsableHeader && (this.groupIsTop[parent.previous().id] || this.groupIsCollapsed[parent.previous().id])) collapsableHeader.focus(); else this.getPreviousGroupLastItem(parent).focus() @@ -372,7 +380,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle else if (!this.groupIsCollapsed[parent.id] && header.previous()) (groupItems.last()).focus() } if (matchesKeystroke(event, this.nextElementKeyCode)) { - if ((this.groupIsCollapsed[parent.id] || !header.next()) && parent.next()) { + if ((this.groupIsCollapsed[parent.id] || this.groupIsTop[parent.id]) && parent.next()) { var collapsableHeader = getHeaderIfGroupContainsCollapsable(parent.next()) if (collapsableHeader && (!this.groupIsTop[parent.next().id] || this.groupIsCollapsed[parent.next().id])) collapsableHeader.focus(); else this.getNextGroupFirstItem(parent).focus() @@ -397,7 +405,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle var nextGroupCollapsableHeader = getHeaderIfGroupContainsCollapsable(parent.next()) if (toolButton.next()) toolButton.next().focus() else if (collapsableHeader && this.groupIsTop[parent.id]) collapsableHeader.focus(); - else if (nextGroupCollapsableHeader && !this.groupIsTop[parent.previous().id]) nextGroupCollapsableHeader.focus(); + else if (nextGroupCollapsableHeader && !this.groupIsTop[parent.next().id]) nextGroupCollapsableHeader.focus(); else if (parent.next()) this.getNextGroupFirstItem(parent).focus() } } @@ -432,7 +440,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle } }); this.actionDispatcher.dispatchAll(item.actions.concat([SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: false })])); - this.hideSmartConnector(); + this.hideAll(); } } @@ -449,7 +457,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle this.actionDispatcher.dispatch( SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: false }) ); - this.hideSmartConnector(); + this.hideAll(); } } From 99fc2d4df9ec5bc1be148bd91bdd64bc54231a95 Mon Sep 17 00:00:00 2001 From: yentelmanero Date: Thu, 30 Nov 2023 13:09:04 +0100 Subject: [PATCH 05/21] addressed lint issues --- packages/client/src/default-modules.ts | 2 +- .../smart-connector/smart-connector-module.ts | 23 +- .../smart-connector/smart-connector.ts | 319 +++++++++--------- .../src/action-protocol/smart-connector.ts | 19 +- .../protocol/src/action-protocol/types.ts | 1 - 5 files changed, 206 insertions(+), 158 deletions(-) diff --git a/packages/client/src/default-modules.ts b/packages/client/src/default-modules.ts index f3ddd93c..ba62c708 100644 --- a/packages/client/src/default-modules.ts +++ b/packages/client/src/default-modules.ts @@ -101,7 +101,7 @@ export const DEFAULT_MODULES = [ zorderModule, svgMetadataModule, statusModule, - smartConnectorModule, + smartConnectorModule ] as const; /** diff --git a/packages/client/src/features/smart-connector/smart-connector-module.ts b/packages/client/src/features/smart-connector/smart-connector-module.ts index 1bd8a21c..65a81570 100644 --- a/packages/client/src/features/smart-connector/smart-connector-module.ts +++ b/packages/client/src/features/smart-connector/smart-connector-module.ts @@ -1,5 +1,22 @@ -import { FeatureModule, TYPES, bindAsService, configureActionHandler, OpenSmartConnectorAction, CloseSmartConnectorAction, MoveAction, SetBoundsAction, SetViewportAction, DeleteElementOperation } from '@eclipse-glsp/sprotty'; -import '../../../css/smart-connector.css' +/******************************************************************************** + * Copyright (c) 2023 EclipseSource and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { FeatureModule, TYPES, bindAsService, configureActionHandler, OpenSmartConnectorAction, + CloseSmartConnectorAction, MoveAction, SetBoundsAction, SetViewportAction, + DeleteElementOperation } from '@eclipse-glsp/sprotty'; +import '../../../css/smart-connector.css'; import { SmartConnector, SmartConnectorKeyListener } from './smart-connector'; export const smartConnectorModule = new FeatureModule((bind, unbind, isBound, rebind) => { @@ -13,4 +30,4 @@ export const smartConnectorModule = new FeatureModule((bind, unbind, isBound, re configureActionHandler(context, SetViewportAction.KIND, SmartConnector); configureActionHandler(context, DeleteElementOperation.KIND, SmartConnector); bindAsService(bind, TYPES.KeyListener, SmartConnectorKeyListener); -}); \ No newline at end of file +}); diff --git a/packages/client/src/features/smart-connector/smart-connector.ts b/packages/client/src/features/smart-connector/smart-connector.ts index a287c944..c5d09f93 100644 --- a/packages/client/src/features/smart-connector/smart-connector.ts +++ b/packages/client/src/features/smart-connector/smart-connector.ts @@ -1,3 +1,18 @@ +/******************************************************************************** + * Copyright (c) 2021-2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ import { inject, injectable } from 'inversify'; import { GLSPActionDispatcher } from '../../base/action-dispatcher'; import { EditorContextService } from '../../base/editor-context-service'; @@ -23,9 +38,9 @@ import { SmartConnectorGroupItem, SmartConnectorNodeItem, SmartConnectorPosition, - KeyCode, -} from '@eclipse-glsp/sprotty' -import { GetViewportAction } from 'sprotty-protocol/lib/actions' + KeyCode +} from '@eclipse-glsp/sprotty'; +import { GetViewportAction } from 'sprotty-protocol/lib/actions'; import { changeCodiconClass, createIcon } from '../tool-palette/tool-palette'; import { IDiagramStartup } from '../../base/model/diagram-loader'; import { EnableDefaultToolsAction } from '../../base/tool-manager/tool'; @@ -43,10 +58,10 @@ const COLLAPSABLE_CLASS = 'collapsable-group'; @injectable() export class SmartConnector extends AbstractUIExtension implements IActionHandler, IDiagramStartup { - + static readonly ID = 'smart-connector'; - - protected selectedElementId: string; + + protected selectedElementId: string; protected smartConnectorItems: SmartConnectorGroupItem[]; protected smartConnectorContainer: HTMLElement; protected expandButton: HTMLElement; @@ -62,7 +77,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle // Sets the position of the expand button protected expandButtonPosition = SmartConnectorPosition.Top; - + @inject(GLSPActionDispatcher) protected actionDispatcher: GLSPActionDispatcher; @@ -78,29 +93,30 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle override containerClass(): string { return SmartConnector.ID; } - + protected override async onBeforeShow(_containerElement: HTMLElement, root: Readonly): Promise { - var viewportResult: ViewportResult = await this.actionDispatcher.request(GetViewportAction.create()); - await this.requestAvailableOptions(root.index.getById(this.selectedElementId)); + const viewportResult: ViewportResult = await this.actionDispatcher.request(GetViewportAction.create()); + await this.requestAvailableOptions(root.index.getById(this.selectedElementId)); this.currentZoom = viewportResult.viewport.zoom; - var nodeFromDom = document.getElementById(`${this.options.baseDiv}_${this.selectedElementId}`) as any as SVGGElement; - var nodeBoundsFromDom = nodeFromDom.getBoundingClientRect(); + const nodeFromDom = document.getElementById(`${this.options.baseDiv}_${this.selectedElementId}`) as any as SVGGElement; + const nodeBoundsFromDom = nodeFromDom.getBoundingClientRect(); this.setMainPosition(viewportResult, nodeBoundsFromDom); // set position of expand button this.setPosition(this.expandButton, this.expandButtonPosition, nodeBoundsFromDom); // set position of container(s) - var sameSide = this.smartConnectorItems.every((e) => e.position === this.smartConnectorItems[0].position) + const sameSide = this.smartConnectorItems.every(e => e.position === this.smartConnectorItems[0].position); if (sameSide) - this.setPosition(this.smartConnectorContainer, this.smartConnectorItems[0].position, nodeBoundsFromDom) + {this.setPosition(this.smartConnectorContainer, this.smartConnectorItems[0].position, nodeBoundsFromDom);} else { - for (var i = 0; i < this.smartConnectorContainer.childElementCount; i++) { - this.setPosition(this.smartConnectorContainer.children[i] as HTMLElement, this.smartConnectorItems[i].position, nodeBoundsFromDom, true) + for (let i = 0; i < this.smartConnectorContainer.childElementCount; i++) { + this.setPosition(this.smartConnectorContainer.children[i] as HTMLElement, this.smartConnectorItems[i].position, + nodeBoundsFromDom, true); } } this.hideSmartConnector(); } - protected async requestAvailableOptions(contextElement?: GModelElement) { + protected async requestAvailableOptions(contextElement?: GModelElement): Promise { const requestAction = RequestContextActions.create({ contextId: SmartConnector.ID, editorContext: { @@ -113,46 +129,45 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle this.createBody(); } - protected setMainPosition(viewport: ViewportResult, nodeBounds: DOMRect) { - var xCenter = nodeBounds.x + nodeBounds.width/2 - viewport.canvasBounds.x - var yCenter = nodeBounds.y + nodeBounds.height/2 - viewport.canvasBounds.y + protected setMainPosition(viewport: ViewportResult, nodeBounds: DOMRect): void { + const xCenter = nodeBounds.x + nodeBounds.width/2 - viewport.canvasBounds.x; + const yCenter = nodeBounds.y + nodeBounds.height/2 - viewport.canvasBounds.y; this.containerElement.style.left = `${xCenter}px`; this.containerElement.style.top = `${yCenter}px`; - } - protected setPosition(element: HTMLElement, position: SmartConnectorPosition, nodeBounds: DOMRect, singleContainer?: boolean) { - var zoom = this.currentZoom; + protected setPosition(element: HTMLElement, position: SmartConnectorPosition, nodeBounds: DOMRect, single?: boolean): void { + const zoom = this.currentZoom; element.style.transform = `scale(${zoom})`; - var nodeHeight = nodeBounds.height; - var nodeWidth = nodeBounds.width; - var xDiff = -element.offsetWidth/2; - var yDiff = -element.offsetHeight/2*zoom; - if (position == SmartConnectorPosition.Right || position == SmartConnectorPosition.Left) { + const nodeHeight = nodeBounds.height; + const nodeWidth = nodeBounds.width; + let xDiff = -element.offsetWidth/2; + let yDiff = -element.offsetHeight/2*zoom; + if (position === SmartConnectorPosition.Right || position === SmartConnectorPosition.Left) { xDiff = nodeWidth/2 + CONTAINER_PADDING_PX*zoom; element.style.top = `${yDiff}px`; } - if (position == SmartConnectorPosition.Top || position == SmartConnectorPosition.Bottom) { + if (position === SmartConnectorPosition.Top || position === SmartConnectorPosition.Bottom) { yDiff = nodeHeight/2 + CONTAINER_PADDING_PX*zoom; element.style.left = `${xDiff}px`; } - if (position == SmartConnectorPosition.Right) { - element.style.transformOrigin = 'top left' + if (position === SmartConnectorPosition.Right) { + element.style.transformOrigin = 'top left'; element.style.left = `${xDiff}px`; - } - if (position == SmartConnectorPosition.Left) { - element.style.transformOrigin = 'top right' + } + if (position === SmartConnectorPosition.Left) { + element.style.transformOrigin = 'top right'; element.style.right = `${xDiff}px`; } - if (position == SmartConnectorPosition.Top) { - element.style.transformOrigin = 'bottom' + if (position === SmartConnectorPosition.Top) { + element.style.transformOrigin = 'bottom'; element.style.bottom = `${yDiff}px`; } - if (position == SmartConnectorPosition.Bottom) { - element.style.transformOrigin = 'top' + if (position === SmartConnectorPosition.Bottom) { + element.style.transformOrigin = 'top'; element.style.top = `${yDiff}px`; } - if (singleContainer) element.style.position = 'absolute'; + if (single) {element.style.position = 'absolute';} } protected initializeContents(containerElement: HTMLElement): void { @@ -162,48 +177,47 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle containerElement.setAttribute('aria-label', 'Smart-Connector'); } - protected createBody() { - var smartConnectorContainer = document.createElement('div'); - smartConnectorContainer.id = SMART_CONNECTOR_CONTAINER_ID + protected createBody(): void { + const smartConnectorContainer = document.createElement('div'); + smartConnectorContainer.id = SMART_CONNECTOR_CONTAINER_ID; for (const item of this.smartConnectorItems) { if (item.children) { - var group = this.createGroup(item); - this.smartConnectorGroups[group.id] = group - smartConnectorContainer.appendChild(group); + const group = this.createGroup(item); + this.smartConnectorGroups[group.id] = group; + smartConnectorContainer.appendChild(group); } } if (this.smartConnectorContainer) { - this.containerElement.removeChild(this.smartConnectorContainer) + this.containerElement.removeChild(this.smartConnectorContainer); } - this.containerElement.appendChild(smartConnectorContainer) + this.containerElement.appendChild(smartConnectorContainer); this.smartConnectorContainer = smartConnectorContainer; } - protected createExpandButton() { + protected createExpandButton(): void { this.expandButton = document.createElement('div'); this.expandButton.id = EXPAND_BUTTON_ID; - this.expandButton.innerHTML = '+' - this.expandButton.onclick = _ev => { - if(!this.editorContext.isReadonly) - showSmartConnector(this.smartConnectorContainer, this.expandButton) + this.expandButton.innerHTML = '+'; + this.expandButton.onclick = _ev => { + if(!this.editorContext.isReadonly) + {showSmartConnector(this.smartConnectorContainer, this.expandButton);} }; - this.expandButton.onkeydown = ev => { - if(matchesKeystroke(ev, 'Space') && !this.editorContext.isReadonly) - showSmartConnector(this.smartConnectorContainer, this.expandButton) + this.expandButton.onkeydown = ev => { + if(matchesKeystroke(ev, 'Space') && !this.editorContext.isReadonly) + {showSmartConnector(this.smartConnectorContainer, this.expandButton);} }; - } // default state - protected hideSmartConnector() { + protected hideSmartConnector(): void { if (this.smartConnectorContainer && this.expandButton) { this.smartConnectorContainer.style.visibility = 'hidden'; this.expandButton.style.visibility = 'visible'; } } - // to avoid onclicks on nested visible - protected hideAll() { + // to avoid onclicks on nested hidden > visible + protected hideAll(): void { if (this.smartConnectorContainer && this.expandButton) { this.smartConnectorContainer.style.visibility = 'hidden'; this.expandButton.style.visibility = 'hidden'; @@ -213,29 +227,29 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle protected createGroup(item: SmartConnectorGroupItem): HTMLElement { const searchField = this.createSearchField(item); const group = document.createElement('div'); - if (item.children!.length == 0) return group; + if (item.children!.length === 0) {return group;} const groupItems = document.createElement('div'); group.classList.add(GROUP_CONTAINER_CLASS); groupItems.classList.add(GROUP_CLASS); group.id = item.id; for (const child of item.children!) { groupItems.appendChild(this.createToolButton(child)); - } + } if (item.showTitle) { - const header = this.createGroupHeader(item, groupItems, searchField) - if (item.position == SmartConnectorPosition.Top) { + const header = this.createGroupHeader(item, groupItems, searchField); + if (item.position === SmartConnectorPosition.Top) { // the header is at the bottom on top group.appendChild(groupItems); - if (item.children!.length > 1) group.appendChild(searchField); + if (item.children!.length > 1) {group.appendChild(searchField);} group.appendChild(header); - this.groupIsTop[group.id] = true + this.groupIsTop[group.id] = true; return group; } group.appendChild(header); } - if (item.children!.length > 1) group.appendChild(searchField); + if (item.children!.length > 1) {group.appendChild(searchField);} group.appendChild(groupItems); - this.groupIsTop[group.id] = false + this.groupIsTop[group.id] = false; return group; } @@ -253,7 +267,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle header.appendChild(headerTitle); header.tabIndex = 0; if (group.submenu) { - const submenuIcon = group.position == SmartConnectorPosition.Top ? createIcon('chevron-up') : createIcon('chevron-down'); + const submenuIcon = group.position === SmartConnectorPosition.Top ? createIcon('chevron-up') : createIcon('chevron-down'); header.appendChild(submenuIcon); groupItems.classList.add(COLLAPSABLE_CLASS); header.onclick = _ev => { @@ -263,14 +277,14 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle if (matchesKeystroke(ev, 'Enter')) { this.toggleSubmenu(submenuIcon, group, groupItems, searchField); } - this.headerNavigation(ev, groupItems, header) - } - this.groupIsCollapsed[group.id] = true + this.headerNavigation(ev, groupItems, header); + }; + this.groupIsCollapsed[group.id] = true; } return header; } - protected toggleSubmenu(icon: HTMLElement, group: SmartConnectorGroupItem, groupItems: HTMLElement, searchField: HTMLElement) { + protected toggleSubmenu(icon: HTMLElement, group: SmartConnectorGroupItem, groupItems: HTMLElement, searchField: HTMLElement): void { changeCodiconClass(icon, 'chevron-up'); changeCodiconClass(icon, 'chevron-down'); this.groupIsCollapsed[group.id] = !this.groupIsCollapsed[group.id]; @@ -308,24 +322,24 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle searchField.onkeydown = ev => this.searchFieldKeyHandler(ev, itemGroup); this.searchFields[itemGroup.id] = searchField; const searchContainer = document.createElement('div'); - var containerClass = itemGroup.submenu ? 'smart-connector-submenu-search-container' : 'smart-connector-search-container'; + const containerClass = itemGroup.submenu ? 'smart-connector-submenu-search-container' : 'smart-connector-search-container'; searchContainer.classList.add(containerClass); - searchContainer.appendChild(searchField) + searchContainer.appendChild(searchField); return searchContainer; } - //#region event handlers + // #region event handlers protected requestFilterUpdate(filter: string, itemGroup: SmartConnectorGroupItem): void { if (itemGroup.children) { - const matchingChildren = itemGroup.children.filter(child => child.label.toLowerCase().includes(filter.toLowerCase())); + const matchingChildren = itemGroup.children.filter(child => child.label.toLowerCase().includes(filter.toLowerCase())); if (matchingChildren.length > 0) { - var items = document.getElementById(itemGroup.id)?.getElementsByClassName(TOOL_BUTTON_CLASS); + const items = document.getElementById(itemGroup.id)?.getElementsByClassName(TOOL_BUTTON_CLASS); if (items) { Array.from(items).forEach(item => { - if (matchingChildren.find(child => child.id == item.id)) (item as HTMLElement).style.display = 'block'; - else (item as HTMLElement).style.display = 'none'; - }) + if (matchingChildren.find(child => child.id === item.id)) {(item as HTMLElement).style.display = 'block';} + else {(item as HTMLElement).style.display = 'none';} + }); } } } @@ -336,18 +350,18 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle this.actionDispatcher.dispatch(EnableDefaultToolsAction.create()); } if (matchesKeystroke(event, 'Enter')) { - this.createTool(item) + this.createTool(item); } if (event.ctrlKey) { // matchesKeystroke with Ctrl and Ctrl+F does not seem to work on Windows 11/Chrome - //if (matchesKeystroke(event, 'ControlLeft')) { - //if (matchesKeystroke(event, 'KeyF', 'ctrlCmd')) { - var parentId = this.smartConnectorItems.find(e => e.children?.includes(item))!.id; - var searchFieldId = parentId + SEARCH_FIELD_SUFFIX; - var searchField = document.getElementById(searchFieldId); - if (searchField) (searchField as HTMLElement).focus(); - } - this.toolButtonNavigation(event, item) + // if (matchesKeystroke(event, 'ControlLeft')) { + // if (matchesKeystroke(event, 'KeyF', 'ctrlCmd')) { + const parentId = this.smartConnectorItems.find(e => e.children?.includes(item))!.id; + const searchFieldId = parentId + SEARCH_FIELD_SUFFIX; + const searchField = document.getElementById(searchFieldId); + if (searchField) {(searchField as HTMLElement).focus();} + } + this.toolButtonNavigation(event, item); } protected searchFieldKeyHandler(event: KeyboardEvent, itemGroup: SmartConnectorGroupItem): void { @@ -355,58 +369,60 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle this.searchFields[itemGroup.id].value = ''; this.requestFilterUpdate('', itemGroup); } - this.searchFieldNavigation(event, itemGroup) + this.searchFieldNavigation(event, itemGroup); } // #region navigation handlers - protected searchFieldNavigation(event: KeyboardEvent, itemGroup: SmartConnectorGroupItem) { + protected searchFieldNavigation(event: KeyboardEvent, itemGroup: SmartConnectorGroupItem): void { if (matchesKeystroke(event, this.previousElementKeyCode)) { - (document.getElementById(itemGroup.children![itemGroup.children!.length-1].id) as HTMLElement).focus() + (document.getElementById(itemGroup.children![itemGroup.children!.length-1].id) as HTMLElement).focus(); } if (matchesKeystroke(event, this.nextElementKeyCode)) { - (document.getElementById(itemGroup.children![0].id) as HTMLElement).focus() + (document.getElementById(itemGroup.children![0].id) as HTMLElement).focus(); } } - protected headerNavigation(event: KeyboardEvent, groupItems: HTMLElement, header: HTMLElement) { - var parent = header.parentElement!; + protected headerNavigation(event: KeyboardEvent, groupItems: HTMLElement, header: HTMLElement): void { + const parent = header.parentElement!; if (matchesKeystroke(event, this.previousElementKeyCode)) { if ((this.groupIsCollapsed[parent.id] || !this.groupIsTop[parent.id]) && parent.previous()) { - var collapsableHeader = getHeaderIfGroupContainsCollapsable(parent.previous()) - if (collapsableHeader && (this.groupIsTop[parent.previous().id] || this.groupIsCollapsed[parent.previous().id])) collapsableHeader.focus(); - else this.getPreviousGroupLastItem(parent).focus() + const collapsableHeader = getHeaderIfGroupContainsCollapsable(parent.previous()); + if (collapsableHeader && + (this.groupIsTop[parent.previous().id] || this.groupIsCollapsed[parent.previous().id])) {collapsableHeader.focus();} + else {this.getPreviousGroupLastItem(parent).focus();} } - else if (!this.groupIsCollapsed[parent.id] && header.previous()) (groupItems.last()).focus() + else if (!this.groupIsCollapsed[parent.id] && header.previous()) {(groupItems.last()).focus();} } if (matchesKeystroke(event, this.nextElementKeyCode)) { if ((this.groupIsCollapsed[parent.id] || this.groupIsTop[parent.id]) && parent.next()) { - var collapsableHeader = getHeaderIfGroupContainsCollapsable(parent.next()) - if (collapsableHeader && (!this.groupIsTop[parent.next().id] || this.groupIsCollapsed[parent.next().id])) collapsableHeader.focus(); - else this.getNextGroupFirstItem(parent).focus() + const collapsableHeader = getHeaderIfGroupContainsCollapsable(parent.next()); + if (collapsableHeader && + (!this.groupIsTop[parent.next().id] || this.groupIsCollapsed[parent.next().id])) {collapsableHeader.focus();} + else {this.getNextGroupFirstItem(parent).focus();} } - else if (!this.groupIsCollapsed[parent.id] && header.next()) (groupItems.first()).focus() + else if (!this.groupIsCollapsed[parent.id] && header.next()) {(groupItems.first()).focus();} } } - protected toolButtonNavigation(event: KeyboardEvent, item: PaletteItem) { - var parentId = this.smartConnectorItems.find(e => e.children?.includes(item))!.id; - var parent = document.getElementById(parentId)!; - var toolButton = document.getElementById(item.id)!; - var collapsableHeader = getHeaderIfGroupContainsCollapsable(parent) + protected toolButtonNavigation(event: KeyboardEvent, item: PaletteItem): void { + const parentId = this.smartConnectorItems.find(e => e.children?.includes(item))!.id; + const parent = document.getElementById(parentId)!; + const toolButton = document.getElementById(item.id)!; + const collapsableHeader = getHeaderIfGroupContainsCollapsable(parent); if (matchesKeystroke(event, this.previousElementKeyCode)) { - var previousGroupCollapsableHeader = getHeaderIfGroupContainsCollapsable(parent.previous()) - if (toolButton.previous()) toolButton.previous().focus() - else if (collapsableHeader && !this.groupIsTop[parent.id]) collapsableHeader.focus(); - else if (previousGroupCollapsableHeader && this.groupIsTop[parent.previous().id]) previousGroupCollapsableHeader.focus(); - else if (parent.previous()) this.getPreviousGroupLastItem(parent).focus() + const previousGroupCollapsableHeader = getHeaderIfGroupContainsCollapsable(parent.previous()); + if (toolButton.previous()) {toolButton.previous().focus();} + else if (collapsableHeader && !this.groupIsTop[parent.id]) {collapsableHeader.focus();} + else if (previousGroupCollapsableHeader && this.groupIsTop[parent.previous().id]) {previousGroupCollapsableHeader.focus();} + else if (parent.previous()) {this.getPreviousGroupLastItem(parent).focus();} } if (matchesKeystroke(event, this.nextElementKeyCode)) { - var nextGroupCollapsableHeader = getHeaderIfGroupContainsCollapsable(parent.next()) - if (toolButton.next()) toolButton.next().focus() - else if (collapsableHeader && this.groupIsTop[parent.id]) collapsableHeader.focus(); - else if (nextGroupCollapsableHeader && !this.groupIsTop[parent.next().id]) nextGroupCollapsableHeader.focus(); - else if (parent.next()) this.getNextGroupFirstItem(parent).focus() + const nextGroupCollapsableHeader = getHeaderIfGroupContainsCollapsable(parent.next()); + if (toolButton.next()) {toolButton.next().focus();} + else if (collapsableHeader && this.groupIsTop[parent.id]) {collapsableHeader.focus();} + else if (nextGroupCollapsableHeader && !this.groupIsTop[parent.next().id]) {nextGroupCollapsableHeader.focus();} + else if (parent.next()) {this.getNextGroupFirstItem(parent).focus();} } } @@ -417,19 +433,19 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle protected getPreviousGroupLastItem(parent: HTMLElement): HTMLElement { return parent.previousElementSibling!.getElementsByClassName(GROUP_CLASS)[0].lastElementChild as HTMLElement; } - + // #endregion - protected onClickCreateToolButton(_button: HTMLElement, item: PaletteItem) { + protected onClickCreateToolButton(_button: HTMLElement, item: PaletteItem) { return (_ev: MouseEvent) => { - this.createTool(item) + this.createTool(item); }; } - protected createTool(item: PaletteItem) { + protected createTool(item: PaletteItem): void { if (!this.editorContext.isReadonly) { item.actions.forEach(e => { - var args: Args; + let args: Args; if (TriggerEdgeCreationAction.is(e)) { args = { source: this.selectedElementId }; (e as TriggerEdgeCreationAction).args = args; @@ -439,12 +455,14 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle (e as TriggerNodeCreationAction).args = args; } }); - this.actionDispatcher.dispatchAll(item.actions.concat([SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: false })])); + this.actionDispatcher.dispatchAll(item.actions.concat([SetUIExtensionVisibilityAction.create( + { extensionId: SmartConnector.ID, visible: false }) + ])); this.hideAll(); } } - //#endregion + // #endregion handle(action: Action): ICommand | Action | void { if (OpenSmartConnectorAction.is(action)) { @@ -483,51 +501,50 @@ declare global { } } -HTMLElement.prototype.next = function() { - return this.nextElementSibling as HTMLElement -} - -HTMLElement.prototype.previous = function() { - return this.previousElementSibling as HTMLElement -} +HTMLElement.prototype.next = function () { + return this.nextElementSibling as HTMLElement; +}; -HTMLElement.prototype.first = function() { - return this.firstElementChild as HTMLElement -} +HTMLElement.prototype.previous = function () { + return this.previousElementSibling as HTMLElement; +}; -HTMLElement.prototype.last = function() { - return this.lastElementChild as HTMLElement -} +HTMLElement.prototype.first = function () { + return this.firstElementChild as HTMLElement; +}; +HTMLElement.prototype.last = function () { + return this.lastElementChild as HTMLElement; +}; -export function showSmartConnector(container: HTMLElement, expandButton: HTMLElement) { +export function showSmartConnector(container: HTMLElement, expandButton: HTMLElement): void { container.style.visibility = 'visible'; expandButton.style.visibility = 'hidden'; } -function getHeaderIfGroupContainsCollapsable(group: HTMLElement) { - if (group && group.getElementsByClassName(COLLAPSABLE_CLASS).length != 0) { +function getHeaderIfGroupContainsCollapsable(group: HTMLElement): HTMLElement | undefined { + if (group && group.getElementsByClassName(COLLAPSABLE_CLASS).length !== 0) { return group.getElementsByClassName(HEADER_CLASS)[0] as HTMLElement; } - return null; + return undefined; } @injectable() export class SmartConnectorKeyListener extends KeyListener { - override keyDown(_element: GModelElement, event: KeyboardEvent) { - var container = document.getElementById(SMART_CONNECTOR_CONTAINER_ID)! - var expandButton = document.getElementById(EXPAND_BUTTON_ID) - var smartConnector = expandButton?.parentElement; - if (matchesKeystroke(event, 'Space') && smartConnector?.style.visibility == 'visible' && expandButton?.style.visibility == 'visible') { + override keyDown(_element: GModelElement, event: KeyboardEvent): Action[] { + const container = document.getElementById(SMART_CONNECTOR_CONTAINER_ID)!; + const expandButton = document.getElementById(EXPAND_BUTTON_ID); + const smartConnector = expandButton?.parentElement; + if (matchesKeystroke(event, 'Space') && smartConnector?.style.visibility === 'visible' + && expandButton?.style.visibility === 'visible') { showSmartConnector(container, expandButton); - var collapsableHeader = getHeaderIfGroupContainsCollapsable(container.firstElementChild as HTMLElement); + const collapsableHeader = getHeaderIfGroupContainsCollapsable(container.firstElementChild as HTMLElement); if (collapsableHeader) { - collapsableHeader.focus() + collapsableHeader.focus(); } - else (container.firstElementChild!.getElementsByClassName(GROUP_CLASS)[0].firstElementChild as HTMLElement).focus() + else {(container.firstElementChild!.getElementsByClassName(GROUP_CLASS)[0].firstElementChild as HTMLElement).focus();} } return []; } } - diff --git a/packages/protocol/src/action-protocol/smart-connector.ts b/packages/protocol/src/action-protocol/smart-connector.ts index 2a8fb6a0..3f0eb1e0 100644 --- a/packages/protocol/src/action-protocol/smart-connector.ts +++ b/packages/protocol/src/action-protocol/smart-connector.ts @@ -1,3 +1,18 @@ +/******************************************************************************** + * Copyright (c) 2021-2023 STMicroelectronics and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ import { hasStringProp } from '../utils/type-util'; import { Action } from './base-protocol'; @@ -23,7 +38,7 @@ export namespace OpenSmartConnectorAction { export function create(selectedElementID: string): OpenSmartConnectorAction { return { kind: KIND, - selectedElementID, + selectedElementID }; } } @@ -47,4 +62,4 @@ export namespace CloseSmartConnectorAction { kind: KIND }; } -} \ No newline at end of file +} diff --git a/packages/protocol/src/action-protocol/types.ts b/packages/protocol/src/action-protocol/types.ts index b858ffd1..465ccce9 100644 --- a/packages/protocol/src/action-protocol/types.ts +++ b/packages/protocol/src/action-protocol/types.ts @@ -203,7 +203,6 @@ export interface SmartConnectorGroupItem extends PaletteItem { readonly submenu?: boolean; /** Show either only icons or labels. Show both when not given*/ readonly showOnlyForChildren?: SmartConnectorGroupUIType; - } export namespace SmartConnectorGroupItem { From 68a3430148adfa3c3c136bd71c3364376b67bc8f Mon Sep 17 00:00:00 2001 From: yentelmanero Date: Thu, 30 Nov 2023 15:49:09 +0100 Subject: [PATCH 06/21] moved HTMLElement extension methods from smart-connector to html-utils --- .../smart-connector/smart-connector.ts | 26 ------------------- packages/client/src/utils/html-utils.ts | 26 +++++++++++++++++++ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/client/src/features/smart-connector/smart-connector.ts b/packages/client/src/features/smart-connector/smart-connector.ts index c5d09f93..85bc0ea4 100644 --- a/packages/client/src/features/smart-connector/smart-connector.ts +++ b/packages/client/src/features/smart-connector/smart-connector.ts @@ -491,32 +491,6 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle } } -// HTMLElement extensions for readability and convenience (reduce casting) -declare global { - interface HTMLElement { - next(): HTMLElement - previous(): HTMLElement - first(): HTMLElement - last(): HTMLElement - } -} - -HTMLElement.prototype.next = function () { - return this.nextElementSibling as HTMLElement; -}; - -HTMLElement.prototype.previous = function () { - return this.previousElementSibling as HTMLElement; -}; - -HTMLElement.prototype.first = function () { - return this.firstElementChild as HTMLElement; -}; - -HTMLElement.prototype.last = function () { - return this.lastElementChild as HTMLElement; -}; - export function showSmartConnector(container: HTMLElement, expandButton: HTMLElement): void { container.style.visibility = 'visible'; expandButton.style.visibility = 'hidden'; diff --git a/packages/client/src/utils/html-utils.ts b/packages/client/src/utils/html-utils.ts index 7b5c066d..7fa50924 100644 --- a/packages/client/src/utils/html-utils.ts +++ b/packages/client/src/utils/html-utils.ts @@ -23,3 +23,29 @@ export function createElementFromHTML(html: string): HTMLElement | undefined { } return undefined; } + +declare global { + interface HTMLElement { + next(): HTMLElement + previous(): HTMLElement + first(): HTMLElement + last(): HTMLElement + } +} + +// HTMLElement extensions for readability and convenience (reduce casting) +HTMLElement.prototype.next = function (): HTMLElement { + return this.nextElementSibling as HTMLElement; +}; + +HTMLElement.prototype.previous = function (): HTMLElement { + return this.previousElementSibling as HTMLElement; +}; + +HTMLElement.prototype.first = function (): HTMLElement { + return this.firstElementChild as HTMLElement; +}; + +HTMLElement.prototype.last = function (): HTMLElement { + return this.lastElementChild as HTMLElement; +}; From 6c811fcd596ec95a639a7bb7abf00c6b5a7bd539 Mon Sep 17 00:00:00 2001 From: yentelmanero Date: Wed, 27 Dec 2023 15:26:07 +0100 Subject: [PATCH 07/21] Implemented suggested changes from PR --- .../src/features/smart-connector/smart-connector-module.ts | 2 +- packages/client/src/features/smart-connector/smart-connector.ts | 2 +- packages/protocol/src/action-protocol/smart-connector.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/client/src/features/smart-connector/smart-connector-module.ts b/packages/client/src/features/smart-connector/smart-connector-module.ts index 65a81570..c709e4fc 100644 --- a/packages/client/src/features/smart-connector/smart-connector-module.ts +++ b/packages/client/src/features/smart-connector/smart-connector-module.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2023 EclipseSource and others. + * Copyright (c) 2023 Business Informatics Group (TU Wien) and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at diff --git a/packages/client/src/features/smart-connector/smart-connector.ts b/packages/client/src/features/smart-connector/smart-connector.ts index 85bc0ea4..1ead55a6 100644 --- a/packages/client/src/features/smart-connector/smart-connector.ts +++ b/packages/client/src/features/smart-connector/smart-connector.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2021-2023 STMicroelectronics and others. + * Copyright (c) 2023 Business Informatics Group (TU Wien) and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at diff --git a/packages/protocol/src/action-protocol/smart-connector.ts b/packages/protocol/src/action-protocol/smart-connector.ts index 3f0eb1e0..d2308b13 100644 --- a/packages/protocol/src/action-protocol/smart-connector.ts +++ b/packages/protocol/src/action-protocol/smart-connector.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (c) 2021-2023 STMicroelectronics and others. + * Copyright (c) 2023 Business Informatics Group (TU Wien) and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at From fae839d58170ce6a3afd8e2538dbfc7257d2f7a4 Mon Sep 17 00:00:00 2001 From: yentelmanero Date: Wed, 27 Dec 2023 16:36:33 +0100 Subject: [PATCH 08/21] added index file for smart connector --- packages/client/src/features/index.ts | 1 + .../src/features/smart-connector/index.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 packages/client/src/features/smart-connector/index.ts diff --git a/packages/client/src/features/index.ts b/packages/client/src/features/index.ts index e58d15db..f2355f6c 100644 --- a/packages/client/src/features/index.ts +++ b/packages/client/src/features/index.ts @@ -32,6 +32,7 @@ export * from './reconnect'; export * from './routing'; export * from './save'; export * from './select'; +export * from './smart-connector' export * from './source-model-watcher'; export * from './status'; export * from './svg-metadata'; diff --git a/packages/client/src/features/smart-connector/index.ts b/packages/client/src/features/smart-connector/index.ts new file mode 100644 index 00000000..547007ed --- /dev/null +++ b/packages/client/src/features/smart-connector/index.ts @@ -0,0 +1,17 @@ +/******************************************************************************** + * Copyright (c) 2023 Business Informatics Group (TU Wien) and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +export * from './smart-connector'; +export * from './smart-connector-module'; \ No newline at end of file From 3405a062ca124a6c458ed0624e9538f7818365f9 Mon Sep 17 00:00:00 2001 From: yentelmanero Date: Wed, 27 Dec 2023 18:00:18 +0100 Subject: [PATCH 09/21] fixed lint issues --- packages/client/src/features/index.ts | 2 +- packages/client/src/features/smart-connector/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/src/features/index.ts b/packages/client/src/features/index.ts index f2355f6c..32baf29e 100644 --- a/packages/client/src/features/index.ts +++ b/packages/client/src/features/index.ts @@ -32,7 +32,7 @@ export * from './reconnect'; export * from './routing'; export * from './save'; export * from './select'; -export * from './smart-connector' +export * from './smart-connector'; export * from './source-model-watcher'; export * from './status'; export * from './svg-metadata'; diff --git a/packages/client/src/features/smart-connector/index.ts b/packages/client/src/features/smart-connector/index.ts index 547007ed..e9480453 100644 --- a/packages/client/src/features/smart-connector/index.ts +++ b/packages/client/src/features/smart-connector/index.ts @@ -14,4 +14,4 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ export * from './smart-connector'; -export * from './smart-connector-module'; \ No newline at end of file +export * from './smart-connector-module'; From 238cec9cfbf5500f01e5e14992aaa57682628bbb Mon Sep 17 00:00:00 2001 From: Tobias Pellkvist Date: Thu, 1 Feb 2024 22:50:24 +0100 Subject: [PATCH 10/21] Implemented PR changes, moved HTMLElement extensions, added Action to change Smart Connector state, added top and bottom borders to Smart Connector --- packages/client/css/smart-connector.css | 3 +- .../smart-connector/smart-connector-module.ts | 4 +- .../smart-connector/smart-connector.ts | 188 +++++++++--------- packages/client/src/utils/html-utils.ts | 26 --- .../src/utils/htmlelement-extensions.ts | 42 ++++ packages/client/src/utils/index.ts | 1 + .../src/action-protocol/smart-connector.ts | 42 +++- 7 files changed, 183 insertions(+), 123 deletions(-) create mode 100644 packages/client/src/utils/htmlelement-extensions.ts diff --git a/packages/client/css/smart-connector.css b/packages/client/css/smart-connector.css index b9f3b34f..014e439f 100644 --- a/packages/client/css/smart-connector.css +++ b/packages/client/css/smart-connector.css @@ -62,8 +62,7 @@ /* seperated containers */ .smart-connector-group-container { - border-left: 1px solid black; - border-right: 1px solid black; + border: 1px solid black; background-color:rgba(1, 1, 1, 0.6); max-width: 200px; /* display: flex; */ diff --git a/packages/client/src/features/smart-connector/smart-connector-module.ts b/packages/client/src/features/smart-connector/smart-connector-module.ts index c709e4fc..e6c5a714 100644 --- a/packages/client/src/features/smart-connector/smart-connector-module.ts +++ b/packages/client/src/features/smart-connector/smart-connector-module.ts @@ -15,7 +15,8 @@ ********************************************************************************/ import { FeatureModule, TYPES, bindAsService, configureActionHandler, OpenSmartConnectorAction, CloseSmartConnectorAction, MoveAction, SetBoundsAction, SetViewportAction, - DeleteElementOperation } from '@eclipse-glsp/sprotty'; + DeleteElementOperation, + ChangeSmartConnectorStateAction} from '@eclipse-glsp/sprotty'; import '../../../css/smart-connector.css'; import { SmartConnector, SmartConnectorKeyListener } from './smart-connector'; @@ -25,6 +26,7 @@ export const smartConnectorModule = new FeatureModule((bind, unbind, isBound, re bind(TYPES.IDiagramStartup).toService(SmartConnector); configureActionHandler(context, OpenSmartConnectorAction.KIND, SmartConnector); configureActionHandler(context, CloseSmartConnectorAction.KIND, SmartConnector); + configureActionHandler(context, ChangeSmartConnectorStateAction.KIND, SmartConnector); configureActionHandler(context, MoveAction.KIND, SmartConnector); configureActionHandler(context, SetBoundsAction.KIND, SmartConnector); configureActionHandler(context, SetViewportAction.KIND, SmartConnector); diff --git a/packages/client/src/features/smart-connector/smart-connector.ts b/packages/client/src/features/smart-connector/smart-connector.ts index 1ead55a6..3234540e 100644 --- a/packages/client/src/features/smart-connector/smart-connector.ts +++ b/packages/client/src/features/smart-connector/smart-connector.ts @@ -38,24 +38,15 @@ import { SmartConnectorGroupItem, SmartConnectorNodeItem, SmartConnectorPosition, - KeyCode + KeyCode, + ChangeSmartConnectorStateAction, + SmartConnectorState } from '@eclipse-glsp/sprotty'; import { GetViewportAction } from 'sprotty-protocol/lib/actions'; import { changeCodiconClass, createIcon } from '../tool-palette/tool-palette'; import { IDiagramStartup } from '../../base/model/diagram-loader'; import { EnableDefaultToolsAction } from '../../base/tool-manager/tool'; -const CONTAINER_PADDING_PX = 20; -const MAX_HEIGHT_GROUP = '150px'; -const SEARCH_FIELD_SUFFIX = '_search_field'; -const SMART_CONNECTOR_CONTAINER_ID = 'smart-connector-container'; -const EXPAND_BUTTON_ID = 'smart-connector-expand-button'; -const GROUP_CONTAINER_CLASS = 'smart-connector-group-container'; -const HEADER_CLASS = 'smart-connector-group-header'; -const GROUP_CLASS = 'smart-connector-group'; -const TOOL_BUTTON_CLASS = 'smart-connector-button'; -const COLLAPSABLE_CLASS = 'collapsable-group'; - @injectable() export class SmartConnector extends AbstractUIExtension implements IActionHandler, IDiagramStartup { @@ -96,7 +87,8 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle protected override async onBeforeShow(_containerElement: HTMLElement, root: Readonly): Promise { const viewportResult: ViewportResult = await this.actionDispatcher.request(GetViewportAction.create()); - await this.requestAvailableOptions(root.index.getById(this.selectedElementId)); + await this.initAvailableOptions(root.index.getById(this.selectedElementId)); + this.initBody(); this.currentZoom = viewportResult.viewport.zoom; const nodeFromDom = document.getElementById(`${this.options.baseDiv}_${this.selectedElementId}`) as any as SVGGElement; const nodeBoundsFromDom = nodeFromDom.getBoundingClientRect(); @@ -116,7 +108,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle this.hideSmartConnector(); } - protected async requestAvailableOptions(contextElement?: GModelElement): Promise { + protected async initAvailableOptions(contextElement?: GModelElement): Promise { const requestAction = RequestContextActions.create({ contextId: SmartConnector.ID, editorContext: { @@ -126,7 +118,6 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle }); const response = await this.actionDispatcher.request(requestAction); this.smartConnectorItems = response.actions.map(e => e as SmartConnectorGroupItem); - this.createBody(); } protected setMainPosition(viewport: ViewportResult, nodeBounds: DOMRect): void { @@ -144,11 +135,11 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle let xDiff = -element.offsetWidth/2; let yDiff = -element.offsetHeight/2*zoom; if (position === SmartConnectorPosition.Right || position === SmartConnectorPosition.Left) { - xDiff = nodeWidth/2 + CONTAINER_PADDING_PX*zoom; + xDiff = nodeWidth/2 + SmartConnector.CONTAINER_PADDING_PX*zoom; element.style.top = `${yDiff}px`; } if (position === SmartConnectorPosition.Top || position === SmartConnectorPosition.Bottom) { - yDiff = nodeHeight/2 + CONTAINER_PADDING_PX*zoom; + yDiff = nodeHeight/2 + SmartConnector.CONTAINER_PADDING_PX*zoom; element.style.left = `${xDiff}px`; } if (position === SmartConnectorPosition.Right) { @@ -171,15 +162,15 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle } protected initializeContents(containerElement: HTMLElement): void { - this.createBody(); - this.createExpandButton(); + this.initBody(); + this.initExpandButton(); this.containerElement.appendChild(this.expandButton); containerElement.setAttribute('aria-label', 'Smart-Connector'); } - protected createBody(): void { + protected initBody(): void { const smartConnectorContainer = document.createElement('div'); - smartConnectorContainer.id = SMART_CONNECTOR_CONTAINER_ID; + smartConnectorContainer.id = SmartConnector.SMART_CONNECTOR_CONTAINER_ID; for (const item of this.smartConnectorItems) { if (item.children) { const group = this.createGroup(item); @@ -194,18 +185,18 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle this.smartConnectorContainer = smartConnectorContainer; } - protected createExpandButton(): void { + protected initExpandButton(): void { this.expandButton = document.createElement('div'); - this.expandButton.id = EXPAND_BUTTON_ID; + this.expandButton.id = SmartConnector.EXPAND_BUTTON_ID; this.expandButton.innerHTML = '+'; this.expandButton.onclick = _ev => { if(!this.editorContext.isReadonly) - {showSmartConnector(this.smartConnectorContainer, this.expandButton);} - }; - this.expandButton.onkeydown = ev => { - if(matchesKeystroke(ev, 'Space') && !this.editorContext.isReadonly) - {showSmartConnector(this.smartConnectorContainer, this.expandButton);} + {this.showSmartConnector();} }; + // this.expandButton.onkeydown = ev => { + // if(matchesKeystroke(ev, 'Space') && !this.editorContext.isReadonly) + // {this.showSmartConnector();} + // }; } // default state @@ -216,6 +207,11 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle } } + protected showSmartConnector(): void { + this.smartConnectorContainer.style.visibility = 'visible'; + this.expandButton.style.visibility = 'hidden'; + } + // to avoid onclicks on nested hidden > visible protected hideAll(): void { if (this.smartConnectorContainer && this.expandButton) { @@ -229,8 +225,8 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle const group = document.createElement('div'); if (item.children!.length === 0) {return group;} const groupItems = document.createElement('div'); - group.classList.add(GROUP_CONTAINER_CLASS); - groupItems.classList.add(GROUP_CLASS); + group.classList.add(SmartConnector.GROUP_CONTAINER_CLASS); + groupItems.classList.add(SmartConnector.GROUP_CLASS); group.id = item.id; for (const child of item.children!) { groupItems.appendChild(this.createToolButton(child)); @@ -256,7 +252,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle protected createGroupHeader(group: SmartConnectorGroupItem, groupItems: HTMLElement, searchField: HTMLElement): HTMLElement{ const header = document.createElement('div'); const headerTitle = document.createElement('div'); - header.classList.add(HEADER_CLASS); + header.classList.add(SmartConnector.HEADER_CLASS); // for same css as palette header header.classList.add('group-header'); if (group.icon) { @@ -269,7 +265,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle if (group.submenu) { const submenuIcon = group.position === SmartConnectorPosition.Top ? createIcon('chevron-up') : createIcon('chevron-down'); header.appendChild(submenuIcon); - groupItems.classList.add(COLLAPSABLE_CLASS); + groupItems.classList.add(SmartConnector.COLLAPSABLE_CLASS); header.onclick = _ev => { this.toggleSubmenu(submenuIcon, group, groupItems, searchField); }; @@ -277,7 +273,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle if (matchesKeystroke(ev, 'Enter')) { this.toggleSubmenu(submenuIcon, group, groupItems, searchField); } - this.headerNavigation(ev, groupItems, header); + this.navigateHeader(ev, groupItems, header); }; this.groupIsCollapsed[group.id] = true; } @@ -292,7 +288,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle groupItems.style.maxHeight = ''; searchField.style.maxHeight = ''; } else { - groupItems.style.maxHeight = MAX_HEIGHT_GROUP; + groupItems.style.maxHeight = SmartConnector.MAX_HEIGHT_GROUP; searchField.style.maxHeight = '50px'; } } @@ -300,29 +296,29 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle protected createToolButton(item: PaletteItem): HTMLElement { const button = document.createElement('div'); button.tabIndex = 0; - button.classList.add(TOOL_BUTTON_CLASS); + button.classList.add(SmartConnector.TOOL_BUTTON_CLASS); if (item.icon) { button.appendChild(createIcon(item.icon)); return button; } button.insertAdjacentText('beforeend', item.label); button.onclick = this.onClickCreateToolButton(button, item); - button.onkeydown = ev => this.toolButtonKeyHandler(ev, item); + button.onkeydown = ev => this.handleToolButtonKey(ev, item); button.id = item.id; return button; } protected createSearchField(itemGroup: SmartConnectorGroupItem): HTMLElement { const searchField = document.createElement('input'); - searchField.classList.add('smart-connector-search'); - searchField.id = itemGroup.id + SEARCH_FIELD_SUFFIX; + searchField.classList.add(SmartConnector.SEARCH_CLASS); + searchField.id = itemGroup.id + SmartConnector.SEARCH_FIELD_SUFFIX; searchField.type = 'text'; searchField.placeholder = ' Search...'; searchField.onkeyup = () => this.requestFilterUpdate(this.searchFields[itemGroup.id].value, itemGroup); - searchField.onkeydown = ev => this.searchFieldKeyHandler(ev, itemGroup); + searchField.onkeydown = ev => this.handleSearchFieldKey(ev, itemGroup); this.searchFields[itemGroup.id] = searchField; const searchContainer = document.createElement('div'); - const containerClass = itemGroup.submenu ? 'smart-connector-submenu-search-container' : 'smart-connector-search-container'; + const containerClass = itemGroup.submenu ? SmartConnector.SEARCH_SUBMENU_CONTAINER_CLASS : SmartConnector.SEARCH_CONTAINER_CLASS; searchContainer.classList.add(containerClass); searchContainer.appendChild(searchField); return searchContainer; @@ -333,48 +329,46 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle protected requestFilterUpdate(filter: string, itemGroup: SmartConnectorGroupItem): void { if (itemGroup.children) { const matchingChildren = itemGroup.children.filter(child => child.label.toLowerCase().includes(filter.toLowerCase())); - if (matchingChildren.length > 0) { - const items = document.getElementById(itemGroup.id)?.getElementsByClassName(TOOL_BUTTON_CLASS); - if (items) { - Array.from(items).forEach(item => { - if (matchingChildren.find(child => child.id === item.id)) {(item as HTMLElement).style.display = 'block';} - else {(item as HTMLElement).style.display = 'none';} - }); - } + const items = document.getElementById(itemGroup.id)?.getElementsByClassName(SmartConnector.TOOL_BUTTON_CLASS); + if (items) { + Array.from(items).forEach(item => { + if (matchingChildren.find(child => child.id === item.id)) {(item as HTMLElement).style.display = 'block';} + else {(item as HTMLElement).style.display = 'none';} + }); } } } - protected toolButtonKeyHandler(event: KeyboardEvent, item: PaletteItem): void { + protected handleToolButtonKey(event: KeyboardEvent, item: PaletteItem): void { if (matchesKeystroke(event, 'Escape')) { this.actionDispatcher.dispatch(EnableDefaultToolsAction.create()); } if (matchesKeystroke(event, 'Enter')) { - this.createTool(item); + this.triggerCreation(item); } if (event.ctrlKey) { // matchesKeystroke with Ctrl and Ctrl+F does not seem to work on Windows 11/Chrome // if (matchesKeystroke(event, 'ControlLeft')) { // if (matchesKeystroke(event, 'KeyF', 'ctrlCmd')) { const parentId = this.smartConnectorItems.find(e => e.children?.includes(item))!.id; - const searchFieldId = parentId + SEARCH_FIELD_SUFFIX; + const searchFieldId = parentId + SmartConnector.SEARCH_FIELD_SUFFIX; const searchField = document.getElementById(searchFieldId); if (searchField) {(searchField as HTMLElement).focus();} } - this.toolButtonNavigation(event, item); + this.navigateToolButton(event, item); } - protected searchFieldKeyHandler(event: KeyboardEvent, itemGroup: SmartConnectorGroupItem): void { + protected handleSearchFieldKey(event: KeyboardEvent, itemGroup: SmartConnectorGroupItem): void { if (matchesKeystroke(event, 'Escape')) { this.searchFields[itemGroup.id].value = ''; this.requestFilterUpdate('', itemGroup); } - this.searchFieldNavigation(event, itemGroup); + this.navigateSearchField(event, itemGroup); } // #region navigation handlers - protected searchFieldNavigation(event: KeyboardEvent, itemGroup: SmartConnectorGroupItem): void { + protected navigateSearchField(event: KeyboardEvent, itemGroup: SmartConnectorGroupItem): void { if (matchesKeystroke(event, this.previousElementKeyCode)) { (document.getElementById(itemGroup.children![itemGroup.children!.length-1].id) as HTMLElement).focus(); } @@ -383,11 +377,11 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle } } - protected headerNavigation(event: KeyboardEvent, groupItems: HTMLElement, header: HTMLElement): void { + protected navigateHeader(event: KeyboardEvent, groupItems: HTMLElement, header: HTMLElement): void { const parent = header.parentElement!; if (matchesKeystroke(event, this.previousElementKeyCode)) { if ((this.groupIsCollapsed[parent.id] || !this.groupIsTop[parent.id]) && parent.previous()) { - const collapsableHeader = getHeaderIfGroupContainsCollapsable(parent.previous()); + const collapsableHeader = this.getHeaderIfGroupContainsCollapsable(parent.previous()); if (collapsableHeader && (this.groupIsTop[parent.previous().id] || this.groupIsCollapsed[parent.previous().id])) {collapsableHeader.focus();} else {this.getPreviousGroupLastItem(parent).focus();} @@ -396,7 +390,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle } if (matchesKeystroke(event, this.nextElementKeyCode)) { if ((this.groupIsCollapsed[parent.id] || this.groupIsTop[parent.id]) && parent.next()) { - const collapsableHeader = getHeaderIfGroupContainsCollapsable(parent.next()); + const collapsableHeader = this.getHeaderIfGroupContainsCollapsable(parent.next()); if (collapsableHeader && (!this.groupIsTop[parent.next().id] || this.groupIsCollapsed[parent.next().id])) {collapsableHeader.focus();} else {this.getNextGroupFirstItem(parent).focus();} @@ -405,20 +399,20 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle } } - protected toolButtonNavigation(event: KeyboardEvent, item: PaletteItem): void { + protected navigateToolButton(event: KeyboardEvent, item: PaletteItem): void { const parentId = this.smartConnectorItems.find(e => e.children?.includes(item))!.id; const parent = document.getElementById(parentId)!; const toolButton = document.getElementById(item.id)!; - const collapsableHeader = getHeaderIfGroupContainsCollapsable(parent); + const collapsableHeader = this.getHeaderIfGroupContainsCollapsable(parent); if (matchesKeystroke(event, this.previousElementKeyCode)) { - const previousGroupCollapsableHeader = getHeaderIfGroupContainsCollapsable(parent.previous()); + const previousGroupCollapsableHeader = this.getHeaderIfGroupContainsCollapsable(parent.previous()); if (toolButton.previous()) {toolButton.previous().focus();} else if (collapsableHeader && !this.groupIsTop[parent.id]) {collapsableHeader.focus();} else if (previousGroupCollapsableHeader && this.groupIsTop[parent.previous().id]) {previousGroupCollapsableHeader.focus();} else if (parent.previous()) {this.getPreviousGroupLastItem(parent).focus();} } if (matchesKeystroke(event, this.nextElementKeyCode)) { - const nextGroupCollapsableHeader = getHeaderIfGroupContainsCollapsable(parent.next()); + const nextGroupCollapsableHeader = this.getHeaderIfGroupContainsCollapsable(parent.next()); if (toolButton.next()) {toolButton.next().focus();} else if (collapsableHeader && this.groupIsTop[parent.id]) {collapsableHeader.focus();} else if (nextGroupCollapsableHeader && !this.groupIsTop[parent.next().id]) {nextGroupCollapsableHeader.focus();} @@ -427,22 +421,29 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle } protected getNextGroupFirstItem(parent: HTMLElement): HTMLElement { - return parent.nextElementSibling!.getElementsByClassName(GROUP_CLASS)[0].firstElementChild as HTMLElement; + return parent.nextElementSibling!.getElementsByClassName(SmartConnector.GROUP_CLASS)[0].firstElementChild as HTMLElement; } protected getPreviousGroupLastItem(parent: HTMLElement): HTMLElement { - return parent.previousElementSibling!.getElementsByClassName(GROUP_CLASS)[0].lastElementChild as HTMLElement; + return parent.previousElementSibling!.getElementsByClassName(SmartConnector.GROUP_CLASS)[0].lastElementChild as HTMLElement; + } + + protected getHeaderIfGroupContainsCollapsable(group: HTMLElement): HTMLElement | undefined { + if (group && group.getElementsByClassName(SmartConnector.COLLAPSABLE_CLASS).length !== 0) { + return group.getElementsByClassName(SmartConnector.HEADER_CLASS)[0] as HTMLElement; + } + return undefined; } // #endregion protected onClickCreateToolButton(_button: HTMLElement, item: PaletteItem) { return (_ev: MouseEvent) => { - this.createTool(item); + this.triggerCreation(item); }; } - protected createTool(item: PaletteItem): void { + protected triggerCreation(item: PaletteItem): void { if (!this.editorContext.isReadonly) { item.actions.forEach(e => { let args: Args; @@ -466,11 +467,25 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle handle(action: Action): ICommand | Action | void { if (OpenSmartConnectorAction.is(action)) { - this.selectedElementId = action.selectedElementID; + this.selectedElementId = action.selectedElementId; this.actionDispatcher.dispatch( SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: true }) ); } + else if (ChangeSmartConnectorStateAction.is(action)) { + if (action.state === SmartConnectorState.Collapse) { + this.hideSmartConnector(); + } else { + this.showSmartConnector(); + const collapsableHeader = this.getHeaderIfGroupContainsCollapsable(this.smartConnectorContainer + .firstElementChild as HTMLElement); + if (collapsableHeader) { + collapsableHeader.focus(); + } + else {(this.smartConnectorContainer.firstElementChild!.getElementsByClassName(SmartConnector.GROUP_CLASS)[0] + .firstElementChild as HTMLElement).focus();} + } + } else { this.actionDispatcher.dispatch( SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: false }) @@ -491,34 +506,29 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle } } -export function showSmartConnector(container: HTMLElement, expandButton: HTMLElement): void { - container.style.visibility = 'visible'; - expandButton.style.visibility = 'hidden'; -} - -function getHeaderIfGroupContainsCollapsable(group: HTMLElement): HTMLElement | undefined { - if (group && group.getElementsByClassName(COLLAPSABLE_CLASS).length !== 0) { - return group.getElementsByClassName(HEADER_CLASS)[0] as HTMLElement; - } - return undefined; +export namespace SmartConnector { + export const CONTAINER_PADDING_PX = 20; + export const MAX_HEIGHT_GROUP = '150px'; + export const SEARCH_FIELD_SUFFIX = '_search_field'; + export const SMART_CONNECTOR_CONTAINER_ID = 'smart-connector-container'; + export const EXPAND_BUTTON_ID = 'smart-connector-expand-button'; + export const GROUP_CONTAINER_CLASS = 'smart-connector-group-container'; + export const HEADER_CLASS = 'smart-connector-group-header'; + export const GROUP_CLASS = 'smart-connector-group'; + export const TOOL_BUTTON_CLASS = 'smart-connector-button'; + export const COLLAPSABLE_CLASS = 'collapsable-group'; + export const SEARCH_CLASS = 'smart-connector-search'; + export const SEARCH_CONTAINER_CLASS = 'smart-connector-search-container'; + export const SEARCH_SUBMENU_CONTAINER_CLASS = 'smart-connector-submenu-search-container'; } @injectable() export class SmartConnectorKeyListener extends KeyListener { override keyDown(_element: GModelElement, event: KeyboardEvent): Action[] { - const container = document.getElementById(SMART_CONNECTOR_CONTAINER_ID)!; - const expandButton = document.getElementById(EXPAND_BUTTON_ID); - const smartConnector = expandButton?.parentElement; - if (matchesKeystroke(event, 'Space') && smartConnector?.style.visibility === 'visible' - && expandButton?.style.visibility === 'visible') { - showSmartConnector(container, expandButton); - const collapsableHeader = getHeaderIfGroupContainsCollapsable(container.firstElementChild as HTMLElement); - if (collapsableHeader) { - collapsableHeader.focus(); - } - else {(container.firstElementChild!.getElementsByClassName(GROUP_CLASS)[0].firstElementChild as HTMLElement).focus();} + if (matchesKeystroke(event, 'Space')) { + return [ChangeSmartConnectorStateAction.create(SmartConnectorState.Expand)]; } + // Add key to close return []; } } - diff --git a/packages/client/src/utils/html-utils.ts b/packages/client/src/utils/html-utils.ts index 31e8a032..801cc49d 100644 --- a/packages/client/src/utils/html-utils.ts +++ b/packages/client/src/utils/html-utils.ts @@ -29,29 +29,3 @@ export function createElementFromHTML(html: string): HTMLElement | undefined { export function isMouseEvent(object: unknown): object is MouseEvent { return AnyObject.is(object) && hasNumberProp(object, 'pageX') && hasNumberProp(object, 'pageY'); } - -declare global { - interface HTMLElement { - next(): HTMLElement - previous(): HTMLElement - first(): HTMLElement - last(): HTMLElement - } -} - -// HTMLElement extensions for readability and convenience (reduce casting) -HTMLElement.prototype.next = function (): HTMLElement { - return this.nextElementSibling as HTMLElement; -}; - -HTMLElement.prototype.previous = function (): HTMLElement { - return this.previousElementSibling as HTMLElement; -}; - -HTMLElement.prototype.first = function (): HTMLElement { - return this.firstElementChild as HTMLElement; -}; - -HTMLElement.prototype.last = function (): HTMLElement { - return this.lastElementChild as HTMLElement; -}; diff --git a/packages/client/src/utils/htmlelement-extensions.ts b/packages/client/src/utils/htmlelement-extensions.ts new file mode 100644 index 00000000..9461496d --- /dev/null +++ b/packages/client/src/utils/htmlelement-extensions.ts @@ -0,0 +1,42 @@ +/******************************************************************************** + * Copyright (c) 2023-2024 Business Informatics Group (TU Wien) and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +declare global { + interface HTMLElement { + next(): HTMLElement + previous(): HTMLElement + first(): HTMLElement + last(): HTMLElement + } +} + +// HTMLElement extensions for readability and convenience (reduce casting) +HTMLElement.prototype.next = function (): HTMLElement { + return this.nextElementSibling as HTMLElement; +}; + +HTMLElement.prototype.previous = function (): HTMLElement { + return this.previousElementSibling as HTMLElement; +}; + +HTMLElement.prototype.first = function (): HTMLElement { + return this.firstElementChild as HTMLElement; +}; + +HTMLElement.prototype.last = function (): HTMLElement { + return this.lastElementChild as HTMLElement; +}; + +export {}; diff --git a/packages/client/src/utils/index.ts b/packages/client/src/utils/index.ts index ffccc99a..deb42404 100644 --- a/packages/client/src/utils/index.ts +++ b/packages/client/src/utils/index.ts @@ -17,6 +17,7 @@ export * from './argument-utils'; export * from './geometry-util'; export * from './gmodel-util'; export * from './html-utils'; +export * from './htmlelement-extensions'; export * from './layout-utils'; export * from './marker'; export * from './viewpoint-util'; diff --git a/packages/protocol/src/action-protocol/smart-connector.ts b/packages/protocol/src/action-protocol/smart-connector.ts index d2308b13..419c2828 100644 --- a/packages/protocol/src/action-protocol/smart-connector.ts +++ b/packages/protocol/src/action-protocol/smart-connector.ts @@ -13,7 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { hasStringProp } from '../utils/type-util'; +import { hasNumberProp, hasStringProp } from '../utils/type-util'; import { Action } from './base-protocol'; /** @@ -25,20 +25,20 @@ export interface OpenSmartConnectorAction extends Action { /** * The identifier of the element where the smart connector is to be opened. */ - selectedElementID: string; + selectedElementId: string; } export namespace OpenSmartConnectorAction { export const KIND = 'openSmartConnector'; export function is(object: any): object is OpenSmartConnectorAction { - return Action.hasKind(object, KIND) && hasStringProp(object, 'selectedElementID'); + return Action.hasKind(object, KIND) && hasStringProp(object, 'selectedElementId'); } - export function create(selectedElementID: string): OpenSmartConnectorAction { + export function create(selectedElementId: string): OpenSmartConnectorAction { return { kind: KIND, - selectedElementID + selectedElementId }; } } @@ -63,3 +63,35 @@ export namespace CloseSmartConnectorAction { }; } } + +export enum SmartConnectorState { + Collapse, + Expand +} + +/** + * Action that closes the smart connector + */ +export interface ChangeSmartConnectorStateAction extends Action { + kind: typeof ChangeSmartConnectorStateAction.KIND; + + /** + * The smart connector state to be switched to + */ + state: SmartConnectorState; +} + +export namespace ChangeSmartConnectorStateAction { + export const KIND = 'changeSmartConnectorState'; + + export function is(object: any): object is ChangeSmartConnectorStateAction { + return Action.hasKind(object, KIND) && hasNumberProp(object, 'state'); + } + + export function create(state: SmartConnectorState): ChangeSmartConnectorStateAction { + return { + kind: KIND, + state + }; + } +} From 87326978128ac76058da8c8316c65aba7ec79013 Mon Sep 17 00:00:00 2001 From: Tobias Pellkvist Date: Mon, 12 Feb 2024 01:15:27 +0100 Subject: [PATCH 11/21] Added escape key to exit smart connector, applied prettier formatting, fixed css issues --- packages/client/css/smart-connector.css | 64 ++++--- .../smart-connector/smart-connector-module.ts | 15 +- .../smart-connector/smart-connector.ts | 181 +++++++++++------- .../src/utils/htmlelement-extensions.ts | 12 +- 4 files changed, 172 insertions(+), 100 deletions(-) diff --git a/packages/client/css/smart-connector.css b/packages/client/css/smart-connector.css index 014e439f..81dbc905 100644 --- a/packages/client/css/smart-connector.css +++ b/packages/client/css/smart-connector.css @@ -1,15 +1,3 @@ -.smart-connector { - position: absolute; - z-index: 1; -} - -/* combined container */ - -#smart-connector-container { - position: absolute; - visibility: hidden; -} - /* expand button */ #smart-connector-expand-button { @@ -19,11 +7,24 @@ border: 1px solid black; z-index: 9999; display: flex; /* or inline-flex */ - align-items: center; + align-items: center; justify-content: center; position: absolute; background: #cccccc; visibility: hidden; + cursor: pointer; +} + +.smart-connector { + position: absolute; + z-index: 1; +} + +/* combined container */ + +#smart-connector-container { + position: absolute; + visibility: hidden; } /* search bar */ @@ -32,9 +33,9 @@ background: #dfdfdf; color: black; border: #bddaef; - padding-left: 3px; width: 100%; box-sizing: border-box; + min-height: 20px; } .smart-connector-submenu-search-container { @@ -56,19 +57,20 @@ .smart-connector-group-header { justify-content: space-between; - width: 100px; + min-width: 100px; } -/* seperated containers */ +/* single containers */ .smart-connector-group-container { border: 1px solid black; - background-color:rgba(1, 1, 1, 0.6); + background: #ededee; max-width: 200px; /* display: flex; */ } -.smart-connector-group-container:hover, .smart-connector-group-container:focus-within { +.smart-connector-group-container:hover, +.smart-connector-group-container:focus-within { z-index: 10000; } @@ -79,15 +81,26 @@ /* container items */ .smart-connector-group { - overflow-y: scroll; - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ + overflow-y: auto; max-height: 150px; } /* scrollbar */ .smart-connector-group::-webkit-scrollbar { - display: none; + width: 6px; +} + +.smart-connector-group::-webkit-scrollbar-thumb:active { + background: rgba(0, 0, 0, 0.9); +} + +.smart-connector-group::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); + border-radius: 10px; +} +.smart-connector-group::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.2); + border-radius: 10px; } .collapsable-group { @@ -103,4 +116,9 @@ padding: 8px 0; z-index: 9998; cursor: pointer; -} \ No newline at end of file + font-size: 12px; +} + +.smart-connector-button:hover { + background: #dfdfdf; +} diff --git a/packages/client/src/features/smart-connector/smart-connector-module.ts b/packages/client/src/features/smart-connector/smart-connector-module.ts index e6c5a714..939cd44f 100644 --- a/packages/client/src/features/smart-connector/smart-connector-module.ts +++ b/packages/client/src/features/smart-connector/smart-connector-module.ts @@ -13,10 +13,19 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { FeatureModule, TYPES, bindAsService, configureActionHandler, OpenSmartConnectorAction, - CloseSmartConnectorAction, MoveAction, SetBoundsAction, SetViewportAction, +import { + FeatureModule, + TYPES, + bindAsService, + configureActionHandler, + OpenSmartConnectorAction, + CloseSmartConnectorAction, + MoveAction, + SetBoundsAction, + SetViewportAction, DeleteElementOperation, - ChangeSmartConnectorStateAction} from '@eclipse-glsp/sprotty'; + ChangeSmartConnectorStateAction +} from '@eclipse-glsp/sprotty'; import '../../../css/smart-connector.css'; import { SmartConnector, SmartConnectorKeyListener } from './smart-connector'; diff --git a/packages/client/src/features/smart-connector/smart-connector.ts b/packages/client/src/features/smart-connector/smart-connector.ts index 3234540e..8f13520a 100644 --- a/packages/client/src/features/smart-connector/smart-connector.ts +++ b/packages/client/src/features/smart-connector/smart-connector.ts @@ -45,11 +45,10 @@ import { import { GetViewportAction } from 'sprotty-protocol/lib/actions'; import { changeCodiconClass, createIcon } from '../tool-palette/tool-palette'; import { IDiagramStartup } from '../../base/model/diagram-loader'; -import { EnableDefaultToolsAction } from '../../base/tool-manager/tool'; +//import { EnableDefaultToolsAction } from '../../base/tool-manager/tool'; @injectable() export class SmartConnector extends AbstractUIExtension implements IActionHandler, IDiagramStartup { - static readonly ID = 'smart-connector'; protected selectedElementId: string; @@ -97,12 +96,16 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle this.setPosition(this.expandButton, this.expandButtonPosition, nodeBoundsFromDom); // set position of container(s) const sameSide = this.smartConnectorItems.every(e => e.position === this.smartConnectorItems[0].position); - if (sameSide) - {this.setPosition(this.smartConnectorContainer, this.smartConnectorItems[0].position, nodeBoundsFromDom);} - else { + if (sameSide) { + this.setPosition(this.smartConnectorContainer, this.smartConnectorItems[0].position, nodeBoundsFromDom); + } else { for (let i = 0; i < this.smartConnectorContainer.childElementCount; i++) { - this.setPosition(this.smartConnectorContainer.children[i] as HTMLElement, this.smartConnectorItems[i].position, - nodeBoundsFromDom, true); + this.setPosition( + this.smartConnectorContainer.children[i] as HTMLElement, + this.smartConnectorItems[i].position, + nodeBoundsFromDom, + true + ); } } this.hideSmartConnector(); @@ -121,8 +124,8 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle } protected setMainPosition(viewport: ViewportResult, nodeBounds: DOMRect): void { - const xCenter = nodeBounds.x + nodeBounds.width/2 - viewport.canvasBounds.x; - const yCenter = nodeBounds.y + nodeBounds.height/2 - viewport.canvasBounds.y; + const xCenter = nodeBounds.x + nodeBounds.width / 2 - viewport.canvasBounds.x; + const yCenter = nodeBounds.y + nodeBounds.height / 2 - viewport.canvasBounds.y; this.containerElement.style.left = `${xCenter}px`; this.containerElement.style.top = `${yCenter}px`; } @@ -132,14 +135,14 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle element.style.transform = `scale(${zoom})`; const nodeHeight = nodeBounds.height; const nodeWidth = nodeBounds.width; - let xDiff = -element.offsetWidth/2; - let yDiff = -element.offsetHeight/2*zoom; + let xDiff = -element.offsetWidth / 2; + let yDiff = (-element.offsetHeight / 2) * zoom; if (position === SmartConnectorPosition.Right || position === SmartConnectorPosition.Left) { - xDiff = nodeWidth/2 + SmartConnector.CONTAINER_PADDING_PX*zoom; + xDiff = nodeWidth / 2 + SmartConnector.CONTAINER_PADDING_PX * zoom; element.style.top = `${yDiff}px`; } if (position === SmartConnectorPosition.Top || position === SmartConnectorPosition.Bottom) { - yDiff = nodeHeight/2 + SmartConnector.CONTAINER_PADDING_PX*zoom; + yDiff = nodeHeight / 2 + SmartConnector.CONTAINER_PADDING_PX * zoom; element.style.left = `${xDiff}px`; } if (position === SmartConnectorPosition.Right) { @@ -158,7 +161,9 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle element.style.transformOrigin = 'top'; element.style.top = `${yDiff}px`; } - if (single) {element.style.position = 'absolute';} + if (single) { + element.style.position = 'absolute'; + } } protected initializeContents(containerElement: HTMLElement): void { @@ -190,9 +195,10 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle this.expandButton.id = SmartConnector.EXPAND_BUTTON_ID; this.expandButton.innerHTML = '+'; this.expandButton.onclick = _ev => { - if(!this.editorContext.isReadonly) - {this.showSmartConnector();} - }; + if (!this.editorContext.isReadonly) { + this.showSmartConnector(); + } + }; // this.expandButton.onkeydown = ev => { // if(matchesKeystroke(ev, 'Space') && !this.editorContext.isReadonly) // {this.showSmartConnector();} @@ -223,7 +229,9 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle protected createGroup(item: SmartConnectorGroupItem): HTMLElement { const searchField = this.createSearchField(item); const group = document.createElement('div'); - if (item.children!.length === 0) {return group;} + if (item.children!.length === 0) { + return group; + } const groupItems = document.createElement('div'); group.classList.add(SmartConnector.GROUP_CONTAINER_CLASS); groupItems.classList.add(SmartConnector.GROUP_CLASS); @@ -236,20 +244,24 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle if (item.position === SmartConnectorPosition.Top) { // the header is at the bottom on top group.appendChild(groupItems); - if (item.children!.length > 1) {group.appendChild(searchField);} + if (item.children!.length > 1) { + group.appendChild(searchField); + } group.appendChild(header); this.groupIsTop[group.id] = true; return group; } group.appendChild(header); } - if (item.children!.length > 1) {group.appendChild(searchField);} + if (item.children!.length > 1) { + group.appendChild(searchField); + } group.appendChild(groupItems); this.groupIsTop[group.id] = false; return group; } - protected createGroupHeader(group: SmartConnectorGroupItem, groupItems: HTMLElement, searchField: HTMLElement): HTMLElement{ + protected createGroupHeader(group: SmartConnectorGroupItem, groupItems: HTMLElement, searchField: HTMLElement): HTMLElement { const header = document.createElement('div'); const headerTitle = document.createElement('div'); header.classList.add(SmartConnector.HEADER_CLASS); @@ -273,7 +285,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle if (matchesKeystroke(ev, 'Enter')) { this.toggleSubmenu(submenuIcon, group, groupItems, searchField); } - this.navigateHeader(ev, groupItems, header); + this.handlerHeaderKey(ev, groupItems, header); }; this.groupIsCollapsed[group.id] = true; } @@ -332,30 +344,33 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle const items = document.getElementById(itemGroup.id)?.getElementsByClassName(SmartConnector.TOOL_BUTTON_CLASS); if (items) { Array.from(items).forEach(item => { - if (matchingChildren.find(child => child.id === item.id)) {(item as HTMLElement).style.display = 'block';} - else {(item as HTMLElement).style.display = 'none';} + if (matchingChildren.find(child => child.id === item.id)) { + (item as HTMLElement).style.display = 'block'; + } else { + (item as HTMLElement).style.display = 'none'; + } }); } } } protected handleToolButtonKey(event: KeyboardEvent, item: PaletteItem): void { - if (matchesKeystroke(event, 'Escape')) { - this.actionDispatcher.dispatch(EnableDefaultToolsAction.create()); - } if (matchesKeystroke(event, 'Enter')) { this.triggerCreation(item); } if (event.ctrlKey) { - // matchesKeystroke with Ctrl and Ctrl+F does not seem to work on Windows 11/Chrome - // if (matchesKeystroke(event, 'ControlLeft')) { - // if (matchesKeystroke(event, 'KeyF', 'ctrlCmd')) { + // matchesKeystroke with Ctrl and Ctrl+F does not seem to work on Windows 11/Chrome + // if (matchesKeystroke(event, 'ControlLeft')) { + // if (matchesKeystroke(event, 'KeyF', 'ctrlCmd')) { const parentId = this.smartConnectorItems.find(e => e.children?.includes(item))!.id; const searchFieldId = parentId + SmartConnector.SEARCH_FIELD_SUFFIX; const searchField = document.getElementById(searchFieldId); - if (searchField) {(searchField as HTMLElement).focus();} + if (searchField) { + (searchField as HTMLElement).focus(); + } } this.navigateToolButton(event, item); + this.closeOnEscapeKey(event); } protected handleSearchFieldKey(event: KeyboardEvent, itemGroup: SmartConnectorGroupItem): void { @@ -366,11 +381,25 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle this.navigateSearchField(event, itemGroup); } + protected handlerHeaderKey(event: KeyboardEvent, groupItems: HTMLElement, header: HTMLElement) { + this.navigateHeader(event, groupItems, header); + this.closeOnEscapeKey(event); + } + + protected closeOnEscapeKey(event: KeyboardEvent) { + if (matchesKeystroke(event, 'Escape')) { + this.hideSmartConnector(); + // assumes that the graph is always the last child of base div + // this focus is done to "reactivate" the key listener to re-open if needed + document.getElementById(this.options.baseDiv)?.last().focus(); + } + } + // #region navigation handlers protected navigateSearchField(event: KeyboardEvent, itemGroup: SmartConnectorGroupItem): void { if (matchesKeystroke(event, this.previousElementKeyCode)) { - (document.getElementById(itemGroup.children![itemGroup.children!.length-1].id) as HTMLElement).focus(); + (document.getElementById(itemGroup.children![itemGroup.children!.length - 1].id) as HTMLElement).focus(); } if (matchesKeystroke(event, this.nextElementKeyCode)) { (document.getElementById(itemGroup.children![0].id) as HTMLElement).focus(); @@ -382,20 +411,26 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle if (matchesKeystroke(event, this.previousElementKeyCode)) { if ((this.groupIsCollapsed[parent.id] || !this.groupIsTop[parent.id]) && parent.previous()) { const collapsableHeader = this.getHeaderIfGroupContainsCollapsable(parent.previous()); - if (collapsableHeader && - (this.groupIsTop[parent.previous().id] || this.groupIsCollapsed[parent.previous().id])) {collapsableHeader.focus();} - else {this.getPreviousGroupLastItem(parent).focus();} + if (collapsableHeader && (this.groupIsTop[parent.previous().id] || this.groupIsCollapsed[parent.previous().id])) { + collapsableHeader.focus(); + } else { + this.getPreviousGroupLastItem(parent).focus(); + } + } else if (!this.groupIsCollapsed[parent.id] && header.previous()) { + groupItems.last().focus(); } - else if (!this.groupIsCollapsed[parent.id] && header.previous()) {(groupItems.last()).focus();} } if (matchesKeystroke(event, this.nextElementKeyCode)) { if ((this.groupIsCollapsed[parent.id] || this.groupIsTop[parent.id]) && parent.next()) { const collapsableHeader = this.getHeaderIfGroupContainsCollapsable(parent.next()); - if (collapsableHeader && - (!this.groupIsTop[parent.next().id] || this.groupIsCollapsed[parent.next().id])) {collapsableHeader.focus();} - else {this.getNextGroupFirstItem(parent).focus();} + if (collapsableHeader && (!this.groupIsTop[parent.next().id] || this.groupIsCollapsed[parent.next().id])) { + collapsableHeader.focus(); + } else { + this.getNextGroupFirstItem(parent).focus(); + } + } else if (!this.groupIsCollapsed[parent.id] && header.next()) { + groupItems.first().focus(); } - else if (!this.groupIsCollapsed[parent.id] && header.next()) {(groupItems.first()).focus();} } } @@ -406,17 +441,27 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle const collapsableHeader = this.getHeaderIfGroupContainsCollapsable(parent); if (matchesKeystroke(event, this.previousElementKeyCode)) { const previousGroupCollapsableHeader = this.getHeaderIfGroupContainsCollapsable(parent.previous()); - if (toolButton.previous()) {toolButton.previous().focus();} - else if (collapsableHeader && !this.groupIsTop[parent.id]) {collapsableHeader.focus();} - else if (previousGroupCollapsableHeader && this.groupIsTop[parent.previous().id]) {previousGroupCollapsableHeader.focus();} - else if (parent.previous()) {this.getPreviousGroupLastItem(parent).focus();} + if (toolButton.previous()) { + toolButton.previous().focus(); + } else if (collapsableHeader && !this.groupIsTop[parent.id]) { + collapsableHeader.focus(); + } else if (previousGroupCollapsableHeader && this.groupIsTop[parent.previous().id]) { + previousGroupCollapsableHeader.focus(); + } else if (parent.previous()) { + this.getPreviousGroupLastItem(parent).focus(); + } } if (matchesKeystroke(event, this.nextElementKeyCode)) { const nextGroupCollapsableHeader = this.getHeaderIfGroupContainsCollapsable(parent.next()); - if (toolButton.next()) {toolButton.next().focus();} - else if (collapsableHeader && this.groupIsTop[parent.id]) {collapsableHeader.focus();} - else if (nextGroupCollapsableHeader && !this.groupIsTop[parent.next().id]) {nextGroupCollapsableHeader.focus();} - else if (parent.next()) {this.getNextGroupFirstItem(parent).focus();} + if (toolButton.next()) { + toolButton.next().focus(); + } else if (collapsableHeader && this.groupIsTop[parent.id]) { + collapsableHeader.focus(); + } else if (nextGroupCollapsableHeader && !this.groupIsTop[parent.next().id]) { + nextGroupCollapsableHeader.focus(); + } else if (parent.next()) { + this.getNextGroupFirstItem(parent).focus(); + } } } @@ -437,7 +482,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle // #endregion - protected onClickCreateToolButton(_button: HTMLElement, item: PaletteItem) { + protected onClickCreateToolButton(_button: HTMLElement, item: PaletteItem) { return (_ev: MouseEvent) => { this.triggerCreation(item); }; @@ -456,9 +501,9 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle (e as TriggerNodeCreationAction).args = args; } }); - this.actionDispatcher.dispatchAll(item.actions.concat([SetUIExtensionVisibilityAction.create( - { extensionId: SmartConnector.ID, visible: false }) - ])); + this.actionDispatcher.dispatchAll( + item.actions.concat([SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: false })]) + ); this.hideAll(); } } @@ -468,28 +513,26 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle handle(action: Action): ICommand | Action | void { if (OpenSmartConnectorAction.is(action)) { this.selectedElementId = action.selectedElementId; - this.actionDispatcher.dispatch( - SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: true }) - ); - } - else if (ChangeSmartConnectorStateAction.is(action)) { + this.actionDispatcher.dispatch(SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: true })); + } else if (ChangeSmartConnectorStateAction.is(action)) { if (action.state === SmartConnectorState.Collapse) { this.hideSmartConnector(); - } else { + } else if (action.state === SmartConnectorState.Expand && this.smartConnectorContainer.style.visibility === 'hidden') { this.showSmartConnector(); - const collapsableHeader = this.getHeaderIfGroupContainsCollapsable(this.smartConnectorContainer - .firstElementChild as HTMLElement); + const collapsableHeader = this.getHeaderIfGroupContainsCollapsable( + this.smartConnectorContainer.firstElementChild as HTMLElement + ); if (collapsableHeader) { collapsableHeader.focus(); + } else { + ( + this.smartConnectorContainer.firstElementChild!.getElementsByClassName(SmartConnector.GROUP_CLASS)[0] + .firstElementChild as HTMLElement + ).focus(); } - else {(this.smartConnectorContainer.firstElementChild!.getElementsByClassName(SmartConnector.GROUP_CLASS)[0] - .firstElementChild as HTMLElement).focus();} } - } - else { - this.actionDispatcher.dispatch( - SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: false }) - ); + } else { + this.actionDispatcher.dispatch(SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: false })); this.hideAll(); } } @@ -528,7 +571,9 @@ export class SmartConnectorKeyListener extends KeyListener { if (matchesKeystroke(event, 'Space')) { return [ChangeSmartConnectorStateAction.create(SmartConnectorState.Expand)]; } - // Add key to close + if (matchesKeystroke(event, 'Escape')) { + return [ChangeSmartConnectorStateAction.create(SmartConnectorState.Collapse)]; + } return []; } } diff --git a/packages/client/src/utils/htmlelement-extensions.ts b/packages/client/src/utils/htmlelement-extensions.ts index 9461496d..e410925f 100644 --- a/packages/client/src/utils/htmlelement-extensions.ts +++ b/packages/client/src/utils/htmlelement-extensions.ts @@ -15,10 +15,10 @@ ********************************************************************************/ declare global { interface HTMLElement { - next(): HTMLElement - previous(): HTMLElement - first(): HTMLElement - last(): HTMLElement + next(): HTMLElement; + previous(): HTMLElement; + first(): HTMLElement; + last(): HTMLElement; } } @@ -27,7 +27,7 @@ HTMLElement.prototype.next = function (): HTMLElement { return this.nextElementSibling as HTMLElement; }; -HTMLElement.prototype.previous = function (): HTMLElement { +HTMLElement.prototype.previous = function (): HTMLElement { return this.previousElementSibling as HTMLElement; }; @@ -35,7 +35,7 @@ HTMLElement.prototype.first = function (): HTMLElement { return this.firstElementChild as HTMLElement; }; -HTMLElement.prototype.last = function (): HTMLElement { +HTMLElement.prototype.last = function (): HTMLElement { return this.lastElementChild as HTMLElement; }; From 001a16efdb39b62ded2bec2198d326490cd5d81f Mon Sep 17 00:00:00 2001 From: Tobias Pellkvist Date: Mon, 12 Feb 2024 01:26:34 +0100 Subject: [PATCH 12/21] removed comment --- .../client/src/features/smart-connector/smart-connector.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/client/src/features/smart-connector/smart-connector.ts b/packages/client/src/features/smart-connector/smart-connector.ts index 8f13520a..b5f48e12 100644 --- a/packages/client/src/features/smart-connector/smart-connector.ts +++ b/packages/client/src/features/smart-connector/smart-connector.ts @@ -45,7 +45,6 @@ import { import { GetViewportAction } from 'sprotty-protocol/lib/actions'; import { changeCodiconClass, createIcon } from '../tool-palette/tool-palette'; import { IDiagramStartup } from '../../base/model/diagram-loader'; -//import { EnableDefaultToolsAction } from '../../base/tool-manager/tool'; @injectable() export class SmartConnector extends AbstractUIExtension implements IActionHandler, IDiagramStartup { @@ -381,12 +380,12 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle this.navigateSearchField(event, itemGroup); } - protected handlerHeaderKey(event: KeyboardEvent, groupItems: HTMLElement, header: HTMLElement) { + protected handlerHeaderKey(event: KeyboardEvent, groupItems: HTMLElement, header: HTMLElement): void { this.navigateHeader(event, groupItems, header); this.closeOnEscapeKey(event); } - protected closeOnEscapeKey(event: KeyboardEvent) { + protected closeOnEscapeKey(event: KeyboardEvent): void { if (matchesKeystroke(event, 'Escape')) { this.hideSmartConnector(); // assumes that the graph is always the last child of base div From 12a1f77d27ef591be73bf85b09ec34797def9848 Mon Sep 17 00:00:00 2001 From: Tobias Pellkvist Date: Thu, 22 Feb 2024 23:38:06 +0700 Subject: [PATCH 13/21] fixed bug where top and bottom containers would not align with related node --- .../smart-connector/smart-connector.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/client/src/features/smart-connector/smart-connector.ts b/packages/client/src/features/smart-connector/smart-connector.ts index b5f48e12..3b312809 100644 --- a/packages/client/src/features/smart-connector/smart-connector.ts +++ b/packages/client/src/features/smart-connector/smart-connector.ts @@ -56,7 +56,6 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle protected expandButton: HTMLElement; protected currentZoom: number; - protected smartConnectorGroups: Record = {}; protected groupIsCollapsed: Record = {}; protected groupIsTop: Record = {}; protected searchFields: Record = {}; @@ -96,14 +95,13 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle // set position of container(s) const sameSide = this.smartConnectorItems.every(e => e.position === this.smartConnectorItems[0].position); if (sameSide) { - this.setPosition(this.smartConnectorContainer, this.smartConnectorItems[0].position, nodeBoundsFromDom); + this.setPosition(this.smartConnectorContainer, this.smartConnectorItems[0].position, nodeBoundsFromDom, true); } else { for (let i = 0; i < this.smartConnectorContainer.childElementCount; i++) { this.setPosition( this.smartConnectorContainer.children[i] as HTMLElement, this.smartConnectorItems[i].position, - nodeBoundsFromDom, - true + nodeBoundsFromDom ); } } @@ -134,6 +132,15 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle element.style.transform = `scale(${zoom})`; const nodeHeight = nodeBounds.height; const nodeWidth = nodeBounds.width; + if (single) { + for (let i = 0; i < element.childElementCount; i++) { + const child = element.children[i] as HTMLElement; + child.style.position = 'static'; + if (i < element.childElementCount) { + child.style.borderBottom = '0'; + } + } + } let xDiff = -element.offsetWidth / 2; let yDiff = (-element.offsetHeight / 2) * zoom; if (position === SmartConnectorPosition.Right || position === SmartConnectorPosition.Left) { @@ -160,9 +167,6 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle element.style.transformOrigin = 'top'; element.style.top = `${yDiff}px`; } - if (single) { - element.style.position = 'absolute'; - } } protected initializeContents(containerElement: HTMLElement): void { @@ -178,7 +182,6 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle for (const item of this.smartConnectorItems) { if (item.children) { const group = this.createGroup(item); - this.smartConnectorGroups[group.id] = group; smartConnectorContainer.appendChild(group); } } @@ -231,6 +234,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle if (item.children!.length === 0) { return group; } + group.style.position = 'absolute'; const groupItems = document.createElement('div'); group.classList.add(SmartConnector.GROUP_CONTAINER_CLASS); groupItems.classList.add(SmartConnector.GROUP_CLASS); From f7bafea6351d858717d70c9bf94b04835cd3e84d Mon Sep 17 00:00:00 2001 From: Tobias Pellkvist Date: Sun, 25 Feb 2024 17:03:27 +0700 Subject: [PATCH 14/21] fixed issue where icons could not be used for tool buttons --- packages/client/css/smart-connector.css | 6 ++++++ .../client/src/features/smart-connector/smart-connector.ts | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/client/css/smart-connector.css b/packages/client/css/smart-connector.css index 81dbc905..cb1c9adc 100644 --- a/packages/client/css/smart-connector.css +++ b/packages/client/css/smart-connector.css @@ -117,6 +117,12 @@ z-index: 9998; cursor: pointer; font-size: 12px; + display: flex; + align-items: center; +} + +.smart-connector-button > i { + padding-right: 0.2em; } .smart-connector-button:hover { diff --git a/packages/client/src/features/smart-connector/smart-connector.ts b/packages/client/src/features/smart-connector/smart-connector.ts index 3b312809..68007877 100644 --- a/packages/client/src/features/smart-connector/smart-connector.ts +++ b/packages/client/src/features/smart-connector/smart-connector.ts @@ -314,7 +314,6 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle button.classList.add(SmartConnector.TOOL_BUTTON_CLASS); if (item.icon) { button.appendChild(createIcon(item.icon)); - return button; } button.insertAdjacentText('beforeend', item.label); button.onclick = this.onClickCreateToolButton(button, item); From defec5366e2158b439d50027353a299dc2c5237f Mon Sep 17 00:00:00 2001 From: Tobias Pellkvist Date: Mon, 26 Feb 2024 17:22:00 +0700 Subject: [PATCH 15/21] changed line to use display:flex instead of block after searching --- packages/client/src/features/smart-connector/smart-connector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/features/smart-connector/smart-connector.ts b/packages/client/src/features/smart-connector/smart-connector.ts index 68007877..dfac7647 100644 --- a/packages/client/src/features/smart-connector/smart-connector.ts +++ b/packages/client/src/features/smart-connector/smart-connector.ts @@ -347,7 +347,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle if (items) { Array.from(items).forEach(item => { if (matchingChildren.find(child => child.id === item.id)) { - (item as HTMLElement).style.display = 'block'; + (item as HTMLElement).style.display = 'flex'; } else { (item as HTMLElement).style.display = 'none'; } From 1bcc5132c4555d26af8b9fc7e9829b5912098474 Mon Sep 17 00:00:00 2001 From: Tobias Pellkvist Date: Mon, 1 Apr 2024 17:44:01 +0200 Subject: [PATCH 16/21] renamed smart connector to selection palette, removed open and close actions for selection palette (are now handled as onSelectionChanged listener), moved SelectionPaletteState and action to new file, removed HTMLElement extensions --- examples/workflow-standalone/src/di.config.ts | 6 +- ...rt-connector.css => selection-palette.css} | 36 +-- packages/client/src/default-modules.ts | 4 +- packages/client/src/features/index.ts | 2 +- .../index.ts | 4 +- .../selection-palette-actions.ts | 48 +++ .../selection-palette-module.ts} | 30 +- .../selection-palette.ts} | 294 ++++++++++-------- .../src/utils/htmlelement-extensions.ts | 42 --- packages/client/src/utils/index.ts | 1 - .../protocol/src/action-protocol/index.ts | 1 - .../src/action-protocol/smart-connector.ts | 97 ------ .../protocol/src/action-protocol/types.ts | 22 +- 13 files changed, 262 insertions(+), 325 deletions(-) rename packages/client/css/{smart-connector.css => selection-palette.css} (68%) rename packages/client/src/features/{smart-connector => selection-palette}/index.ts (91%) create mode 100644 packages/client/src/features/selection-palette/selection-palette-actions.ts rename packages/client/src/features/{smart-connector/smart-connector-module.ts => selection-palette/selection-palette-module.ts} (52%) rename packages/client/src/features/{smart-connector/smart-connector.ts => selection-palette/selection-palette.ts} (63%) delete mode 100644 packages/client/src/utils/htmlelement-extensions.ts delete mode 100644 packages/protocol/src/action-protocol/smart-connector.ts diff --git a/examples/workflow-standalone/src/di.config.ts b/examples/workflow-standalone/src/di.config.ts index 2e1c8ffe..be3b8337 100644 --- a/examples/workflow-standalone/src/di.config.ts +++ b/examples/workflow-standalone/src/di.config.ts @@ -23,7 +23,8 @@ import { LogLevel, STANDALONE_MODULE_CONFIG, TYPES, - toolPaletteModule + toolPaletteModule, + selectionPaletteModule } from '@eclipse-glsp/client'; import { Container } from 'inversify'; import '../css/diagram.css'; @@ -34,6 +35,9 @@ export default function createContainer(options: IDiagramOptions): Container { add: accessibilityModule, remove: toolPaletteModule }, + { + add: selectionPaletteModule + }, STANDALONE_MODULE_CONFIG ); bindOrRebind(container, TYPES.ILogger).to(ConsoleLogger).inSingletonScope(); diff --git a/packages/client/css/smart-connector.css b/packages/client/css/selection-palette.css similarity index 68% rename from packages/client/css/smart-connector.css rename to packages/client/css/selection-palette.css index 81dbc905..8d6c292c 100644 --- a/packages/client/css/smart-connector.css +++ b/packages/client/css/selection-palette.css @@ -1,6 +1,6 @@ /* expand button */ -#smart-connector-expand-button { +#selection-palette-expand-button { height: 32px; width: 32px; border-radius: 50%; @@ -15,21 +15,21 @@ cursor: pointer; } -.smart-connector { +.selection-palette { position: absolute; z-index: 1; } /* combined container */ -#smart-connector-container { +#selection-palette-container { position: absolute; visibility: hidden; } /* search bar */ -.smart-connector-search { +.selection-palette-search { background: #dfdfdf; color: black; border: #bddaef; @@ -38,12 +38,12 @@ min-height: 20px; } -.smart-connector-submenu-search-container { +.selection-palette-submenu-search-container { max-height: 0; overflow: hidden; } -.smart-connector-search-container { +.selection-palette-search-container { max-height: 50px; } @@ -55,50 +55,50 @@ padding-right: 8px; } -.smart-connector-group-header { +.selection-palette-group-header { justify-content: space-between; min-width: 100px; } /* single containers */ -.smart-connector-group-container { +.selection-palette-group-container { border: 1px solid black; background: #ededee; max-width: 200px; /* display: flex; */ } -.smart-connector-group-container:hover, -.smart-connector-group-container:focus-within { +.selection-palette-group-container:hover, +.selection-palette-group-container:focus-within { z-index: 10000; } -.smart-connector-group-container div:first-child { +.selection-palette-group-container div:first-child { border-top: 0; } /* container items */ -.smart-connector-group { +.selection-palette-group { overflow-y: auto; max-height: 150px; } /* scrollbar */ -.smart-connector-group::-webkit-scrollbar { +.selection-palette-group::-webkit-scrollbar { width: 6px; } -.smart-connector-group::-webkit-scrollbar-thumb:active { +.selection-palette-group::-webkit-scrollbar-thumb:active { background: rgba(0, 0, 0, 0.9); } -.smart-connector-group::-webkit-scrollbar-track { +.selection-palette-group::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.1); border-radius: 10px; } -.smart-connector-group::-webkit-scrollbar-thumb { +.selection-palette-group::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.2); border-radius: 10px; } @@ -110,7 +110,7 @@ /* single item */ -.smart-connector-button { +.selection-palette-button { border-top: 1px solid black; margin: 0 4px; padding: 8px 0; @@ -119,6 +119,6 @@ font-size: 12px; } -.smart-connector-button:hover { +.selection-palette-button:hover { background: #dfdfdf; } diff --git a/packages/client/src/default-modules.ts b/packages/client/src/default-modules.ts index bd123377..e260d15f 100644 --- a/packages/client/src/default-modules.ts +++ b/packages/client/src/default-modules.ts @@ -61,7 +61,6 @@ import { nodeCreationToolModule } from './features/tools/node-creation/node-crea import { toolFocusLossModule } from './features/tools/tool-focus-loss-module'; import { markerNavigatorModule, validationModule } from './features/validation/validation-modules'; import { viewportModule } from './features/viewport/viewport-modules'; -import { smartConnectorModule } from './features/smart-connector/smart-connector-module'; export const DEFAULT_MODULES = [ defaultModule, @@ -100,8 +99,7 @@ export const DEFAULT_MODULES = [ validationModule, zorderModule, svgMetadataModule, - statusModule, - smartConnectorModule + statusModule ] as const; /** diff --git a/packages/client/src/features/index.ts b/packages/client/src/features/index.ts index 32baf29e..e851df4d 100644 --- a/packages/client/src/features/index.ts +++ b/packages/client/src/features/index.ts @@ -32,7 +32,7 @@ export * from './reconnect'; export * from './routing'; export * from './save'; export * from './select'; -export * from './smart-connector'; +export * from './selection-palette'; export * from './source-model-watcher'; export * from './status'; export * from './svg-metadata'; diff --git a/packages/client/src/features/smart-connector/index.ts b/packages/client/src/features/selection-palette/index.ts similarity index 91% rename from packages/client/src/features/smart-connector/index.ts rename to packages/client/src/features/selection-palette/index.ts index e9480453..fe2fb7f5 100644 --- a/packages/client/src/features/smart-connector/index.ts +++ b/packages/client/src/features/selection-palette/index.ts @@ -13,5 +13,5 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -export * from './smart-connector'; -export * from './smart-connector-module'; +export * from './selection-palette'; +export * from './selection-palette-module'; diff --git a/packages/client/src/features/selection-palette/selection-palette-actions.ts b/packages/client/src/features/selection-palette/selection-palette-actions.ts new file mode 100644 index 00000000..82843d60 --- /dev/null +++ b/packages/client/src/features/selection-palette/selection-palette-actions.ts @@ -0,0 +1,48 @@ +/******************************************************************************** + * Copyright (c) 2023 Business Informatics Group (TU Wien) and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ +import { Action, hasNumberProp } from '@eclipse-glsp/sprotty'; + +export enum SelectionPaletteState { + Collapse, + Expand +} + +/** + * Action that changes the selection palette state + */ +export interface ChangeSelectionPaletteStateAction extends Action { + kind: typeof ChangeSelectionPaletteStateAction.KIND; + + /** + * The selection palette state to be switched to + */ + state: SelectionPaletteState; +} + +export namespace ChangeSelectionPaletteStateAction { + export const KIND = 'changeSelectionPaletteState'; + + export function is(object: any): object is ChangeSelectionPaletteStateAction { + return Action.hasKind(object, KIND) && hasNumberProp(object, 'state'); + } + + export function create(state: SelectionPaletteState): ChangeSelectionPaletteStateAction { + return { + kind: KIND, + state + }; + } +} diff --git a/packages/client/src/features/smart-connector/smart-connector-module.ts b/packages/client/src/features/selection-palette/selection-palette-module.ts similarity index 52% rename from packages/client/src/features/smart-connector/smart-connector-module.ts rename to packages/client/src/features/selection-palette/selection-palette-module.ts index 939cd44f..ba44eebc 100644 --- a/packages/client/src/features/smart-connector/smart-connector-module.ts +++ b/packages/client/src/features/selection-palette/selection-palette-module.ts @@ -18,27 +18,23 @@ import { TYPES, bindAsService, configureActionHandler, - OpenSmartConnectorAction, - CloseSmartConnectorAction, MoveAction, SetBoundsAction, SetViewportAction, - DeleteElementOperation, - ChangeSmartConnectorStateAction + DeleteElementOperation } from '@eclipse-glsp/sprotty'; -import '../../../css/smart-connector.css'; -import { SmartConnector, SmartConnectorKeyListener } from './smart-connector'; +import '../../../css/selection-palette.css'; +import { SelectionPalette, SelectionPaletteKeyListener } from './selection-palette'; +import { ChangeSelectionPaletteStateAction } from './selection-palette-actions'; -export const smartConnectorModule = new FeatureModule((bind, unbind, isBound, rebind) => { +export const selectionPaletteModule = new FeatureModule((bind, unbind, isBound, rebind) => { const context = { bind, unbind, isBound, rebind }; - bindAsService(context, TYPES.IUIExtension, SmartConnector); - bind(TYPES.IDiagramStartup).toService(SmartConnector); - configureActionHandler(context, OpenSmartConnectorAction.KIND, SmartConnector); - configureActionHandler(context, CloseSmartConnectorAction.KIND, SmartConnector); - configureActionHandler(context, ChangeSmartConnectorStateAction.KIND, SmartConnector); - configureActionHandler(context, MoveAction.KIND, SmartConnector); - configureActionHandler(context, SetBoundsAction.KIND, SmartConnector); - configureActionHandler(context, SetViewportAction.KIND, SmartConnector); - configureActionHandler(context, DeleteElementOperation.KIND, SmartConnector); - bindAsService(bind, TYPES.KeyListener, SmartConnectorKeyListener); + bindAsService(context, TYPES.IUIExtension, SelectionPalette); + bind(TYPES.IDiagramStartup).toService(SelectionPalette); + configureActionHandler(context, ChangeSelectionPaletteStateAction.KIND, SelectionPalette); + configureActionHandler(context, MoveAction.KIND, SelectionPalette); + configureActionHandler(context, SetBoundsAction.KIND, SelectionPalette); + configureActionHandler(context, SetViewportAction.KIND, SelectionPalette); + configureActionHandler(context, DeleteElementOperation.KIND, SelectionPalette); + bindAsService(bind, TYPES.KeyListener, SelectionPaletteKeyListener); }); diff --git a/packages/client/src/features/smart-connector/smart-connector.ts b/packages/client/src/features/selection-palette/selection-palette.ts similarity index 63% rename from packages/client/src/features/smart-connector/smart-connector.ts rename to packages/client/src/features/selection-palette/selection-palette.ts index b5f48e12..d73bd6b5 100644 --- a/packages/client/src/features/smart-connector/smart-connector.ts +++ b/packages/client/src/features/selection-palette/selection-palette.ts @@ -13,7 +13,7 @@ * * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { inject, injectable } from 'inversify'; +import { inject, injectable, postConstruct } from 'inversify'; import { GLSPActionDispatcher } from '../../base/action-dispatcher'; import { EditorContextService } from '../../base/editor-context-service'; import { FocusTracker } from '../../base/focus/focus-tracker'; @@ -26,7 +26,6 @@ import { GModelElement, GModelRoot, matchesKeystroke, - OpenSmartConnectorAction, SetUIExtensionVisibilityAction, ViewportResult, RequestContextActions, @@ -35,28 +34,28 @@ import { TriggerEdgeCreationAction, TriggerNodeCreationAction, Args, - SmartConnectorGroupItem, - SmartConnectorNodeItem, - SmartConnectorPosition, - KeyCode, - ChangeSmartConnectorStateAction, - SmartConnectorState + SelectionPaletteGroupItem, + SelectionPaletteNodeItem, + SelectionPalettePosition, + KeyCode } from '@eclipse-glsp/sprotty'; import { GetViewportAction } from 'sprotty-protocol/lib/actions'; import { changeCodiconClass, createIcon } from '../tool-palette/tool-palette'; import { IDiagramStartup } from '../../base/model/diagram-loader'; +import { ISelectionListener, SelectionService } from '../../base/selection-service'; +import { ChangeSelectionPaletteStateAction, SelectionPaletteState } from './selection-palette-actions'; @injectable() -export class SmartConnector extends AbstractUIExtension implements IActionHandler, IDiagramStartup { - static readonly ID = 'smart-connector'; +export class SelectionPalette extends AbstractUIExtension implements IActionHandler, IDiagramStartup, ISelectionListener { + static readonly ID = 'selection-palette'; protected selectedElementId: string; - protected smartConnectorItems: SmartConnectorGroupItem[]; - protected smartConnectorContainer: HTMLElement; + protected selectionPaletteItems: SelectionPaletteGroupItem[]; + protected selectionPaletteContainer: HTMLElement; protected expandButton: HTMLElement; protected currentZoom: number; - protected smartConnectorGroups: Record = {}; + protected selectionPaletteGroups: Record = {}; protected groupIsCollapsed: Record = {}; protected groupIsTop: Record = {}; protected searchFields: Record = {}; @@ -65,7 +64,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle protected nextElementKeyCode: KeyCode = 'ArrowDown'; // Sets the position of the expand button - protected expandButtonPosition = SmartConnectorPosition.Top; + protected expandButtonPosition = SelectionPalettePosition.Top; @inject(GLSPActionDispatcher) protected actionDispatcher: GLSPActionDispatcher; @@ -76,11 +75,29 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle @inject(FocusTracker) protected focusTracker: FocusTracker; + @inject(SelectionService) + protected selectionService: SelectionService; + + @postConstruct() + protected init(): void { + this.selectionService.onSelectionChanged(change => this.selectionChanged(change.root, change.selectedElements)); + } + + selectionChanged(root: GModelRoot, selectedElements: string[]): void { + if (selectedElements.length !== 0) { + this.selectedElementId = selectedElements[0]; + this.actionDispatcher.dispatch(SetUIExtensionVisibilityAction.create({ extensionId: SelectionPalette.ID, visible: true })); + } else { + this.actionDispatcher.dispatch(SetUIExtensionVisibilityAction.create({ extensionId: SelectionPalette.ID, visible: false })); + this.hideAll(); + } + } + override id(): string { - return SmartConnector.ID; + return SelectionPalette.ID; } override containerClass(): string { - return SmartConnector.ID; + return SelectionPalette.ID; } protected override async onBeforeShow(_containerElement: HTMLElement, root: Readonly): Promise { @@ -94,32 +111,32 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle // set position of expand button this.setPosition(this.expandButton, this.expandButtonPosition, nodeBoundsFromDom); // set position of container(s) - const sameSide = this.smartConnectorItems.every(e => e.position === this.smartConnectorItems[0].position); + const sameSide = this.selectionPaletteItems.every(e => e.position === this.selectionPaletteItems[0].position); if (sameSide) { - this.setPosition(this.smartConnectorContainer, this.smartConnectorItems[0].position, nodeBoundsFromDom); + this.setPosition(this.selectionPaletteContainer, this.selectionPaletteItems[0].position, nodeBoundsFromDom); } else { - for (let i = 0; i < this.smartConnectorContainer.childElementCount; i++) { + for (let i = 0; i < this.selectionPaletteContainer.childElementCount; i++) { this.setPosition( - this.smartConnectorContainer.children[i] as HTMLElement, - this.smartConnectorItems[i].position, + this.selectionPaletteContainer.children[i] as HTMLElement, + this.selectionPaletteItems[i].position, nodeBoundsFromDom, true ); } } - this.hideSmartConnector(); + this.hideSelectionPalette(); } protected async initAvailableOptions(contextElement?: GModelElement): Promise { const requestAction = RequestContextActions.create({ - contextId: SmartConnector.ID, + contextId: SelectionPalette.ID, editorContext: { selectedElementIds: [this.selectedElementId], args: { nodeType: contextElement!.type } } }); const response = await this.actionDispatcher.request(requestAction); - this.smartConnectorItems = response.actions.map(e => e as SmartConnectorGroupItem); + this.selectionPaletteItems = response.actions.map(e => e as SelectionPaletteGroupItem); } protected setMainPosition(viewport: ViewportResult, nodeBounds: DOMRect): void { @@ -129,34 +146,34 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle this.containerElement.style.top = `${yCenter}px`; } - protected setPosition(element: HTMLElement, position: SmartConnectorPosition, nodeBounds: DOMRect, single?: boolean): void { + protected setPosition(element: HTMLElement, position: SelectionPalettePosition, nodeBounds: DOMRect, single?: boolean): void { const zoom = this.currentZoom; element.style.transform = `scale(${zoom})`; const nodeHeight = nodeBounds.height; const nodeWidth = nodeBounds.width; let xDiff = -element.offsetWidth / 2; let yDiff = (-element.offsetHeight / 2) * zoom; - if (position === SmartConnectorPosition.Right || position === SmartConnectorPosition.Left) { - xDiff = nodeWidth / 2 + SmartConnector.CONTAINER_PADDING_PX * zoom; + if (position === SelectionPalettePosition.Right || position === SelectionPalettePosition.Left) { + xDiff = nodeWidth / 2 + SelectionPalette.CONTAINER_PADDING_PX * zoom; element.style.top = `${yDiff}px`; } - if (position === SmartConnectorPosition.Top || position === SmartConnectorPosition.Bottom) { - yDiff = nodeHeight / 2 + SmartConnector.CONTAINER_PADDING_PX * zoom; + if (position === SelectionPalettePosition.Top || position === SelectionPalettePosition.Bottom) { + yDiff = nodeHeight / 2 + SelectionPalette.CONTAINER_PADDING_PX * zoom; element.style.left = `${xDiff}px`; } - if (position === SmartConnectorPosition.Right) { + if (position === SelectionPalettePosition.Right) { element.style.transformOrigin = 'top left'; element.style.left = `${xDiff}px`; } - if (position === SmartConnectorPosition.Left) { + if (position === SelectionPalettePosition.Left) { element.style.transformOrigin = 'top right'; element.style.right = `${xDiff}px`; } - if (position === SmartConnectorPosition.Top) { + if (position === SelectionPalettePosition.Top) { element.style.transformOrigin = 'bottom'; element.style.bottom = `${yDiff}px`; } - if (position === SmartConnectorPosition.Bottom) { + if (position === SelectionPalettePosition.Bottom) { element.style.transformOrigin = 'top'; element.style.top = `${yDiff}px`; } @@ -169,78 +186,78 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle this.initBody(); this.initExpandButton(); this.containerElement.appendChild(this.expandButton); - containerElement.setAttribute('aria-label', 'Smart-Connector'); + containerElement.setAttribute('aria-label', 'Selection-Palette'); } protected initBody(): void { - const smartConnectorContainer = document.createElement('div'); - smartConnectorContainer.id = SmartConnector.SMART_CONNECTOR_CONTAINER_ID; - for (const item of this.smartConnectorItems) { + const selectionPaletteContainer = document.createElement('div'); + selectionPaletteContainer.id = SelectionPalette.SELECTION_PALETTE_CONTAINER_ID; + for (const item of this.selectionPaletteItems) { if (item.children) { const group = this.createGroup(item); - this.smartConnectorGroups[group.id] = group; - smartConnectorContainer.appendChild(group); + this.selectionPaletteGroups[group.id] = group; + selectionPaletteContainer.appendChild(group); } } - if (this.smartConnectorContainer) { - this.containerElement.removeChild(this.smartConnectorContainer); + if (this.selectionPaletteContainer) { + this.containerElement.removeChild(this.selectionPaletteContainer); } - this.containerElement.appendChild(smartConnectorContainer); - this.smartConnectorContainer = smartConnectorContainer; + this.containerElement.appendChild(selectionPaletteContainer); + this.selectionPaletteContainer = selectionPaletteContainer; } protected initExpandButton(): void { this.expandButton = document.createElement('div'); - this.expandButton.id = SmartConnector.EXPAND_BUTTON_ID; + this.expandButton.id = SelectionPalette.EXPAND_BUTTON_ID; this.expandButton.innerHTML = '+'; this.expandButton.onclick = _ev => { if (!this.editorContext.isReadonly) { - this.showSmartConnector(); + this.showSelectionPalette(); } }; // this.expandButton.onkeydown = ev => { // if(matchesKeystroke(ev, 'Space') && !this.editorContext.isReadonly) - // {this.showSmartConnector();} + // {this.showSelectionPalette();} // }; } // default state - protected hideSmartConnector(): void { - if (this.smartConnectorContainer && this.expandButton) { - this.smartConnectorContainer.style.visibility = 'hidden'; + protected hideSelectionPalette(): void { + if (this.selectionPaletteContainer && this.expandButton) { + this.selectionPaletteContainer.style.visibility = 'hidden'; this.expandButton.style.visibility = 'visible'; } } - protected showSmartConnector(): void { - this.smartConnectorContainer.style.visibility = 'visible'; + protected showSelectionPalette(): void { + this.selectionPaletteContainer.style.visibility = 'visible'; this.expandButton.style.visibility = 'hidden'; } // to avoid onclicks on nested hidden > visible protected hideAll(): void { - if (this.smartConnectorContainer && this.expandButton) { - this.smartConnectorContainer.style.visibility = 'hidden'; + if (this.selectionPaletteContainer && this.expandButton) { + this.selectionPaletteContainer.style.visibility = 'hidden'; this.expandButton.style.visibility = 'hidden'; } } - protected createGroup(item: SmartConnectorGroupItem): HTMLElement { + protected createGroup(item: SelectionPaletteGroupItem): HTMLElement { const searchField = this.createSearchField(item); const group = document.createElement('div'); if (item.children!.length === 0) { return group; } const groupItems = document.createElement('div'); - group.classList.add(SmartConnector.GROUP_CONTAINER_CLASS); - groupItems.classList.add(SmartConnector.GROUP_CLASS); + group.classList.add(SelectionPalette.GROUP_CONTAINER_CLASS); + groupItems.classList.add(SelectionPalette.GROUP_CLASS); group.id = item.id; for (const child of item.children!) { groupItems.appendChild(this.createToolButton(child)); } if (item.showTitle) { const header = this.createGroupHeader(item, groupItems, searchField); - if (item.position === SmartConnectorPosition.Top) { + if (item.position === SelectionPalettePosition.Top) { // the header is at the bottom on top group.appendChild(groupItems); if (item.children!.length > 1) { @@ -260,10 +277,10 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle return group; } - protected createGroupHeader(group: SmartConnectorGroupItem, groupItems: HTMLElement, searchField: HTMLElement): HTMLElement { + protected createGroupHeader(group: SelectionPaletteGroupItem, groupItems: HTMLElement, searchField: HTMLElement): HTMLElement { const header = document.createElement('div'); const headerTitle = document.createElement('div'); - header.classList.add(SmartConnector.HEADER_CLASS); + header.classList.add(SelectionPalette.HEADER_CLASS); // for same css as palette header header.classList.add('group-header'); if (group.icon) { @@ -274,9 +291,9 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle header.appendChild(headerTitle); header.tabIndex = 0; if (group.submenu) { - const submenuIcon = group.position === SmartConnectorPosition.Top ? createIcon('chevron-up') : createIcon('chevron-down'); + const submenuIcon = group.position === SelectionPalettePosition.Top ? createIcon('chevron-up') : createIcon('chevron-down'); header.appendChild(submenuIcon); - groupItems.classList.add(SmartConnector.COLLAPSABLE_CLASS); + groupItems.classList.add(SelectionPalette.COLLAPSABLE_CLASS); header.onclick = _ev => { this.toggleSubmenu(submenuIcon, group, groupItems, searchField); }; @@ -291,7 +308,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle return header; } - protected toggleSubmenu(icon: HTMLElement, group: SmartConnectorGroupItem, groupItems: HTMLElement, searchField: HTMLElement): void { + protected toggleSubmenu(icon: HTMLElement, group: SelectionPaletteGroupItem, groupItems: HTMLElement, searchField: HTMLElement): void { changeCodiconClass(icon, 'chevron-up'); changeCodiconClass(icon, 'chevron-down'); this.groupIsCollapsed[group.id] = !this.groupIsCollapsed[group.id]; @@ -299,7 +316,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle groupItems.style.maxHeight = ''; searchField.style.maxHeight = ''; } else { - groupItems.style.maxHeight = SmartConnector.MAX_HEIGHT_GROUP; + groupItems.style.maxHeight = SelectionPalette.MAX_HEIGHT_GROUP; searchField.style.maxHeight = '50px'; } } @@ -307,7 +324,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle protected createToolButton(item: PaletteItem): HTMLElement { const button = document.createElement('div'); button.tabIndex = 0; - button.classList.add(SmartConnector.TOOL_BUTTON_CLASS); + button.classList.add(SelectionPalette.TOOL_BUTTON_CLASS); if (item.icon) { button.appendChild(createIcon(item.icon)); return button; @@ -319,17 +336,19 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle return button; } - protected createSearchField(itemGroup: SmartConnectorGroupItem): HTMLElement { + protected createSearchField(itemGroup: SelectionPaletteGroupItem): HTMLElement { const searchField = document.createElement('input'); - searchField.classList.add(SmartConnector.SEARCH_CLASS); - searchField.id = itemGroup.id + SmartConnector.SEARCH_FIELD_SUFFIX; + searchField.classList.add(SelectionPalette.SEARCH_CLASS); + searchField.id = itemGroup.id + SelectionPalette.SEARCH_FIELD_SUFFIX; searchField.type = 'text'; searchField.placeholder = ' Search...'; searchField.onkeyup = () => this.requestFilterUpdate(this.searchFields[itemGroup.id].value, itemGroup); searchField.onkeydown = ev => this.handleSearchFieldKey(ev, itemGroup); this.searchFields[itemGroup.id] = searchField; const searchContainer = document.createElement('div'); - const containerClass = itemGroup.submenu ? SmartConnector.SEARCH_SUBMENU_CONTAINER_CLASS : SmartConnector.SEARCH_CONTAINER_CLASS; + const containerClass = itemGroup.submenu + ? SelectionPalette.SEARCH_SUBMENU_CONTAINER_CLASS + : SelectionPalette.SEARCH_CONTAINER_CLASS; searchContainer.classList.add(containerClass); searchContainer.appendChild(searchField); return searchContainer; @@ -337,10 +356,10 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle // #region event handlers - protected requestFilterUpdate(filter: string, itemGroup: SmartConnectorGroupItem): void { + protected requestFilterUpdate(filter: string, itemGroup: SelectionPaletteGroupItem): void { if (itemGroup.children) { const matchingChildren = itemGroup.children.filter(child => child.label.toLowerCase().includes(filter.toLowerCase())); - const items = document.getElementById(itemGroup.id)?.getElementsByClassName(SmartConnector.TOOL_BUTTON_CLASS); + const items = document.getElementById(itemGroup.id)?.getElementsByClassName(SelectionPalette.TOOL_BUTTON_CLASS); if (items) { Array.from(items).forEach(item => { if (matchingChildren.find(child => child.id === item.id)) { @@ -361,8 +380,8 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle // matchesKeystroke with Ctrl and Ctrl+F does not seem to work on Windows 11/Chrome // if (matchesKeystroke(event, 'ControlLeft')) { // if (matchesKeystroke(event, 'KeyF', 'ctrlCmd')) { - const parentId = this.smartConnectorItems.find(e => e.children?.includes(item))!.id; - const searchFieldId = parentId + SmartConnector.SEARCH_FIELD_SUFFIX; + const parentId = this.selectionPaletteItems.find(e => e.children?.includes(item))!.id; + const searchFieldId = parentId + SelectionPalette.SEARCH_FIELD_SUFFIX; const searchField = document.getElementById(searchFieldId); if (searchField) { (searchField as HTMLElement).focus(); @@ -372,7 +391,7 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle this.closeOnEscapeKey(event); } - protected handleSearchFieldKey(event: KeyboardEvent, itemGroup: SmartConnectorGroupItem): void { + protected handleSearchFieldKey(event: KeyboardEvent, itemGroup: SelectionPaletteGroupItem): void { if (matchesKeystroke(event, 'Escape')) { this.searchFields[itemGroup.id].value = ''; this.requestFilterUpdate('', itemGroup); @@ -387,16 +406,16 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle protected closeOnEscapeKey(event: KeyboardEvent): void { if (matchesKeystroke(event, 'Escape')) { - this.hideSmartConnector(); + this.hideSelectionPalette(); // assumes that the graph is always the last child of base div // this focus is done to "reactivate" the key listener to re-open if needed - document.getElementById(this.options.baseDiv)?.last().focus(); + last(document.getElementById(this.options.baseDiv)!).focus(); } } // #region navigation handlers - protected navigateSearchField(event: KeyboardEvent, itemGroup: SmartConnectorGroupItem): void { + protected navigateSearchField(event: KeyboardEvent, itemGroup: SelectionPaletteGroupItem): void { if (matchesKeystroke(event, this.previousElementKeyCode)) { (document.getElementById(itemGroup.children![itemGroup.children!.length - 1].id) as HTMLElement).focus(); } @@ -408,73 +427,73 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle protected navigateHeader(event: KeyboardEvent, groupItems: HTMLElement, header: HTMLElement): void { const parent = header.parentElement!; if (matchesKeystroke(event, this.previousElementKeyCode)) { - if ((this.groupIsCollapsed[parent.id] || !this.groupIsTop[parent.id]) && parent.previous()) { - const collapsableHeader = this.getHeaderIfGroupContainsCollapsable(parent.previous()); - if (collapsableHeader && (this.groupIsTop[parent.previous().id] || this.groupIsCollapsed[parent.previous().id])) { + if ((this.groupIsCollapsed[parent.id] || !this.groupIsTop[parent.id]) && previous(parent)) { + const collapsableHeader = this.getHeaderIfGroupContainsCollapsable(previous(parent)); + if (collapsableHeader && (this.groupIsTop[previous(parent).id] || this.groupIsCollapsed[previous(parent).id])) { collapsableHeader.focus(); } else { this.getPreviousGroupLastItem(parent).focus(); } - } else if (!this.groupIsCollapsed[parent.id] && header.previous()) { - groupItems.last().focus(); + } else if (!this.groupIsCollapsed[parent.id] && previous(header)) { + last(groupItems).focus(); } } if (matchesKeystroke(event, this.nextElementKeyCode)) { - if ((this.groupIsCollapsed[parent.id] || this.groupIsTop[parent.id]) && parent.next()) { - const collapsableHeader = this.getHeaderIfGroupContainsCollapsable(parent.next()); - if (collapsableHeader && (!this.groupIsTop[parent.next().id] || this.groupIsCollapsed[parent.next().id])) { + if ((this.groupIsCollapsed[parent.id] || this.groupIsTop[parent.id]) && next(parent)) { + const collapsableHeader = this.getHeaderIfGroupContainsCollapsable(next(parent)); + if (collapsableHeader && (!this.groupIsTop[next(parent).id] || this.groupIsCollapsed[next(parent).id])) { collapsableHeader.focus(); } else { this.getNextGroupFirstItem(parent).focus(); } - } else if (!this.groupIsCollapsed[parent.id] && header.next()) { - groupItems.first().focus(); + } else if (!this.groupIsCollapsed[parent.id] && next(header)) { + first(groupItems).focus(); } } } protected navigateToolButton(event: KeyboardEvent, item: PaletteItem): void { - const parentId = this.smartConnectorItems.find(e => e.children?.includes(item))!.id; + const parentId = this.selectionPaletteItems.find(e => e.children?.includes(item))!.id; const parent = document.getElementById(parentId)!; const toolButton = document.getElementById(item.id)!; const collapsableHeader = this.getHeaderIfGroupContainsCollapsable(parent); if (matchesKeystroke(event, this.previousElementKeyCode)) { - const previousGroupCollapsableHeader = this.getHeaderIfGroupContainsCollapsable(parent.previous()); - if (toolButton.previous()) { - toolButton.previous().focus(); + const previousGroupCollapsableHeader = this.getHeaderIfGroupContainsCollapsable(previous(parent)); + if (previous(toolButton)) { + previous(toolButton).focus(); } else if (collapsableHeader && !this.groupIsTop[parent.id]) { collapsableHeader.focus(); - } else if (previousGroupCollapsableHeader && this.groupIsTop[parent.previous().id]) { + } else if (previousGroupCollapsableHeader && this.groupIsTop[previous(parent).id]) { previousGroupCollapsableHeader.focus(); - } else if (parent.previous()) { + } else if (previous(parent)) { this.getPreviousGroupLastItem(parent).focus(); } } if (matchesKeystroke(event, this.nextElementKeyCode)) { - const nextGroupCollapsableHeader = this.getHeaderIfGroupContainsCollapsable(parent.next()); - if (toolButton.next()) { - toolButton.next().focus(); + const nextGroupCollapsableHeader = this.getHeaderIfGroupContainsCollapsable(next(parent)); + if (next(toolButton)) { + next(toolButton).focus(); } else if (collapsableHeader && this.groupIsTop[parent.id]) { collapsableHeader.focus(); - } else if (nextGroupCollapsableHeader && !this.groupIsTop[parent.next().id]) { + } else if (nextGroupCollapsableHeader && !this.groupIsTop[next(parent).id]) { nextGroupCollapsableHeader.focus(); - } else if (parent.next()) { + } else if (next(parent)) { this.getNextGroupFirstItem(parent).focus(); } } } protected getNextGroupFirstItem(parent: HTMLElement): HTMLElement { - return parent.nextElementSibling!.getElementsByClassName(SmartConnector.GROUP_CLASS)[0].firstElementChild as HTMLElement; + return parent.nextElementSibling!.getElementsByClassName(SelectionPalette.GROUP_CLASS)[0].firstElementChild as HTMLElement; } protected getPreviousGroupLastItem(parent: HTMLElement): HTMLElement { - return parent.previousElementSibling!.getElementsByClassName(SmartConnector.GROUP_CLASS)[0].lastElementChild as HTMLElement; + return parent.previousElementSibling!.getElementsByClassName(SelectionPalette.GROUP_CLASS)[0].lastElementChild as HTMLElement; } protected getHeaderIfGroupContainsCollapsable(group: HTMLElement): HTMLElement | undefined { - if (group && group.getElementsByClassName(SmartConnector.COLLAPSABLE_CLASS).length !== 0) { - return group.getElementsByClassName(SmartConnector.HEADER_CLASS)[0] as HTMLElement; + if (group && group.getElementsByClassName(SelectionPalette.COLLAPSABLE_CLASS).length !== 0) { + return group.getElementsByClassName(SelectionPalette.HEADER_CLASS)[0] as HTMLElement; } return undefined; } @@ -493,15 +512,15 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle let args: Args; if (TriggerEdgeCreationAction.is(e)) { args = { source: this.selectedElementId }; - (e as TriggerEdgeCreationAction).args = args; + e.args = args; } if (TriggerNodeCreationAction.is(e)) { - args = { createEdge: true, source: this.selectedElementId, edgeType: (item as SmartConnectorNodeItem).edgeType }; - (e as TriggerNodeCreationAction).args = args; + args = { createEdge: true, source: this.selectedElementId, edgeType: (item as SelectionPaletteNodeItem).edgeType }; + e.args = args; } }); this.actionDispatcher.dispatchAll( - item.actions.concat([SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: false })]) + item.actions.concat([SetUIExtensionVisibilityAction.create({ extensionId: SelectionPalette.ID, visible: false })]) ); this.hideAll(); } @@ -510,68 +529,81 @@ export class SmartConnector extends AbstractUIExtension implements IActionHandle // #endregion handle(action: Action): ICommand | Action | void { - if (OpenSmartConnectorAction.is(action)) { - this.selectedElementId = action.selectedElementId; - this.actionDispatcher.dispatch(SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: true })); - } else if (ChangeSmartConnectorStateAction.is(action)) { - if (action.state === SmartConnectorState.Collapse) { - this.hideSmartConnector(); - } else if (action.state === SmartConnectorState.Expand && this.smartConnectorContainer.style.visibility === 'hidden') { - this.showSmartConnector(); + if (ChangeSelectionPaletteStateAction.is(action)) { + if (action.state === SelectionPaletteState.Collapse) { + this.hideSelectionPalette(); + } else if (action.state === SelectionPaletteState.Expand && this.selectionPaletteContainer.style.visibility === 'hidden') { + this.showSelectionPalette(); const collapsableHeader = this.getHeaderIfGroupContainsCollapsable( - this.smartConnectorContainer.firstElementChild as HTMLElement + this.selectionPaletteContainer.firstElementChild as HTMLElement ); if (collapsableHeader) { collapsableHeader.focus(); } else { ( - this.smartConnectorContainer.firstElementChild!.getElementsByClassName(SmartConnector.GROUP_CLASS)[0] + this.selectionPaletteContainer.firstElementChild!.getElementsByClassName(SelectionPalette.GROUP_CLASS)[0] .firstElementChild as HTMLElement ).focus(); } } } else { - this.actionDispatcher.dispatch(SetUIExtensionVisibilityAction.create({ extensionId: SmartConnector.ID, visible: false })); + this.actionDispatcher.dispatch(SetUIExtensionVisibilityAction.create({ extensionId: SelectionPalette.ID, visible: false })); this.hideAll(); } } async preRequestModel(): Promise { const requestAction = RequestContextActions.create({ - contextId: SmartConnector.ID, + contextId: SelectionPalette.ID, editorContext: { selectedElementIds: [] } }); const response = await this.actionDispatcher.request(requestAction); - this.smartConnectorItems = response.actions.map(e => e as SmartConnectorGroupItem); + this.selectionPaletteItems = response.actions.map(e => e as SelectionPaletteGroupItem); } } -export namespace SmartConnector { +function next(element: HTMLElement): HTMLElement { + return element.nextElementSibling as HTMLElement; +} + +function previous(element: HTMLElement): HTMLElement { + return element.previousElementSibling as HTMLElement; +} + +function first(element: HTMLElement): HTMLElement { + return element.firstElementChild as HTMLElement; +} + +function last(element: HTMLElement): HTMLElement { + return element.lastElementChild as HTMLElement; +} + +export namespace SelectionPalette { export const CONTAINER_PADDING_PX = 20; export const MAX_HEIGHT_GROUP = '150px'; export const SEARCH_FIELD_SUFFIX = '_search_field'; - export const SMART_CONNECTOR_CONTAINER_ID = 'smart-connector-container'; - export const EXPAND_BUTTON_ID = 'smart-connector-expand-button'; - export const GROUP_CONTAINER_CLASS = 'smart-connector-group-container'; - export const HEADER_CLASS = 'smart-connector-group-header'; - export const GROUP_CLASS = 'smart-connector-group'; - export const TOOL_BUTTON_CLASS = 'smart-connector-button'; + export const SELECTION_PALETTE_CONTAINER_ID = 'selection-palette-container'; + export const EXPAND_BUTTON_ID = 'selection-palette-expand-button'; + export const GROUP_CONTAINER_CLASS = 'selection-palette-group-container'; + export const HEADER_CLASS = 'selection-palette-group-header'; + export const GROUP_CLASS = 'selection-palette-group'; + export const TOOL_BUTTON_CLASS = 'selection-palette-button'; export const COLLAPSABLE_CLASS = 'collapsable-group'; - export const SEARCH_CLASS = 'smart-connector-search'; - export const SEARCH_CONTAINER_CLASS = 'smart-connector-search-container'; - export const SEARCH_SUBMENU_CONTAINER_CLASS = 'smart-connector-submenu-search-container'; + export const SEARCH_CLASS = 'selection-palette-search'; + export const SEARCH_CONTAINER_CLASS = 'selection-palette-search-container'; + export const SEARCH_SUBMENU_CONTAINER_CLASS = 'selection-palette-submenu-search-container'; } @injectable() -export class SmartConnectorKeyListener extends KeyListener { +export class SelectionPaletteKeyListener extends KeyListener { override keyDown(_element: GModelElement, event: KeyboardEvent): Action[] { if (matchesKeystroke(event, 'Space')) { - return [ChangeSmartConnectorStateAction.create(SmartConnectorState.Expand)]; + return [ChangeSelectionPaletteStateAction.create(SelectionPaletteState.Expand)]; } if (matchesKeystroke(event, 'Escape')) { - return [ChangeSmartConnectorStateAction.create(SmartConnectorState.Collapse)]; + return [ChangeSelectionPaletteStateAction.create(SelectionPaletteState.Collapse)]; } return []; } diff --git a/packages/client/src/utils/htmlelement-extensions.ts b/packages/client/src/utils/htmlelement-extensions.ts deleted file mode 100644 index e410925f..00000000 --- a/packages/client/src/utils/htmlelement-extensions.ts +++ /dev/null @@ -1,42 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2023-2024 Business Informatics Group (TU Wien) and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ -declare global { - interface HTMLElement { - next(): HTMLElement; - previous(): HTMLElement; - first(): HTMLElement; - last(): HTMLElement; - } -} - -// HTMLElement extensions for readability and convenience (reduce casting) -HTMLElement.prototype.next = function (): HTMLElement { - return this.nextElementSibling as HTMLElement; -}; - -HTMLElement.prototype.previous = function (): HTMLElement { - return this.previousElementSibling as HTMLElement; -}; - -HTMLElement.prototype.first = function (): HTMLElement { - return this.firstElementChild as HTMLElement; -}; - -HTMLElement.prototype.last = function (): HTMLElement { - return this.lastElementChild as HTMLElement; -}; - -export {}; diff --git a/packages/client/src/utils/index.ts b/packages/client/src/utils/index.ts index deb42404..ffccc99a 100644 --- a/packages/client/src/utils/index.ts +++ b/packages/client/src/utils/index.ts @@ -17,7 +17,6 @@ export * from './argument-utils'; export * from './geometry-util'; export * from './gmodel-util'; export * from './html-utils'; -export * from './htmlelement-extensions'; export * from './layout-utils'; export * from './marker'; export * from './viewpoint-util'; diff --git a/packages/protocol/src/action-protocol/index.ts b/packages/protocol/src/action-protocol/index.ts index b024a317..4913dd14 100644 --- a/packages/protocol/src/action-protocol/index.ts +++ b/packages/protocol/src/action-protocol/index.ts @@ -30,7 +30,6 @@ export * from './model-edit-mode'; export * from './model-layout'; export * from './model-saving'; export * from './node-modification'; -export * from './smart-connector'; export * from './tool-palette'; export * from './types'; export * from './undo-redo'; diff --git a/packages/protocol/src/action-protocol/smart-connector.ts b/packages/protocol/src/action-protocol/smart-connector.ts deleted file mode 100644 index 419c2828..00000000 --- a/packages/protocol/src/action-protocol/smart-connector.ts +++ /dev/null @@ -1,97 +0,0 @@ -/******************************************************************************** - * Copyright (c) 2023 Business Informatics Group (TU Wien) and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0. - * - * This Source Code may also be made available under the following Secondary - * Licenses when the conditions for such availability set forth in the Eclipse - * Public License v. 2.0 are satisfied: GNU General Public License, version 2 - * with the GNU Classpath Exception which is available at - * https://www.gnu.org/software/classpath/license.html. - * - * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 - ********************************************************************************/ -import { hasNumberProp, hasStringProp } from '../utils/type-util'; -import { Action } from './base-protocol'; - -/** - * Action that opens the smart connector at the position of the element - */ -export interface OpenSmartConnectorAction extends Action { - kind: typeof OpenSmartConnectorAction.KIND; - - /** - * The identifier of the element where the smart connector is to be opened. - */ - selectedElementId: string; -} - -export namespace OpenSmartConnectorAction { - export const KIND = 'openSmartConnector'; - - export function is(object: any): object is OpenSmartConnectorAction { - return Action.hasKind(object, KIND) && hasStringProp(object, 'selectedElementId'); - } - - export function create(selectedElementId: string): OpenSmartConnectorAction { - return { - kind: KIND, - selectedElementId - }; - } -} - -/** - * Action that closes the smart connector - */ -export interface CloseSmartConnectorAction extends Action { - kind: typeof CloseSmartConnectorAction.KIND; -} - -export namespace CloseSmartConnectorAction { - export const KIND = 'closeSmartConnector'; - - export function is(object: any): object is CloseSmartConnectorAction { - return Action.hasKind(object, KIND); - } - - export function create(): CloseSmartConnectorAction { - return { - kind: KIND - }; - } -} - -export enum SmartConnectorState { - Collapse, - Expand -} - -/** - * Action that closes the smart connector - */ -export interface ChangeSmartConnectorStateAction extends Action { - kind: typeof ChangeSmartConnectorStateAction.KIND; - - /** - * The smart connector state to be switched to - */ - state: SmartConnectorState; -} - -export namespace ChangeSmartConnectorStateAction { - export const KIND = 'changeSmartConnectorState'; - - export function is(object: any): object is ChangeSmartConnectorStateAction { - return Action.hasKind(object, KIND) && hasNumberProp(object, 'state'); - } - - export function create(state: SmartConnectorState): ChangeSmartConnectorStateAction { - return { - kind: KIND, - state - }; - } -} diff --git a/packages/protocol/src/action-protocol/types.ts b/packages/protocol/src/action-protocol/types.ts index 465ccce9..4b59c98d 100644 --- a/packages/protocol/src/action-protocol/types.ts +++ b/packages/protocol/src/action-protocol/types.ts @@ -179,12 +179,12 @@ export namespace PaletteItem { } } -export enum SmartConnectorGroupUIType { +export enum SelectionPaletteGroupUIType { Icons, Labels } -export enum SmartConnectorPosition { +export enum SelectionPalettePosition { Left, Right, Top, @@ -192,32 +192,32 @@ export enum SmartConnectorPosition { } /** - * Represents a group of the smart connector, which can be positioned around the clicked node + * Represents a group of the selection palette, which can be positioned around the clicked node */ -export interface SmartConnectorGroupItem extends PaletteItem { +export interface SelectionPaletteGroupItem extends PaletteItem { /** Position of the group */ - readonly position: SmartConnectorPosition; + readonly position: SelectionPalettePosition; /** Shows the title of a group */ readonly showTitle: boolean; /** Shows a group as a collapsed submenu if true, open if false */ readonly submenu?: boolean; /** Show either only icons or labels. Show both when not given*/ - readonly showOnlyForChildren?: SmartConnectorGroupUIType; + readonly showOnlyForChildren?: SelectionPaletteGroupUIType; } -export namespace SmartConnectorGroupItem { - export function is(object: any): object is SmartConnectorGroupItem { +export namespace SelectionPaletteGroupItem { + export function is(object: any): object is SelectionPaletteGroupItem { return PaletteItem.is(object) && hasObjectProp(object, 'position') && hasBooleanProp(object, 'showTitle'); } } -export interface SmartConnectorNodeItem extends PaletteItem { +export interface SelectionPaletteNodeItem extends PaletteItem { /** default edge when creating an outgoing edge */ readonly edgeType: string; } -export namespace SmartConnectorNodeItem { - export function is(object: any): object is SmartConnectorNodeItem { +export namespace SelectionPaletteNodeItem { + export function is(object: any): object is SelectionPaletteNodeItem { return PaletteItem.is(object) && hasStringProp(object, 'edgeType'); } } From b0774d7e3761936c2918e961f0fae89ccc80f03e Mon Sep 17 00:00:00 2001 From: Tobias Pellkvist Date: Mon, 1 Apr 2024 18:34:51 +0200 Subject: [PATCH 17/21] fixed merge issues happening due to name change --- packages/client/css/selection-palette.css | 2 +- .../selection-palette/selection-palette.ts | 23 +++++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/client/css/selection-palette.css b/packages/client/css/selection-palette.css index 4f03fb72..a8bfa544 100644 --- a/packages/client/css/selection-palette.css +++ b/packages/client/css/selection-palette.css @@ -121,7 +121,7 @@ align-items: center; } -.smart-connector-button > i { +.selection-palette-button > i { padding-right: 0.2em; } diff --git a/packages/client/src/features/selection-palette/selection-palette.ts b/packages/client/src/features/selection-palette/selection-palette.ts index d73bd6b5..f5f81b2b 100644 --- a/packages/client/src/features/selection-palette/selection-palette.ts +++ b/packages/client/src/features/selection-palette/selection-palette.ts @@ -55,7 +55,6 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand protected expandButton: HTMLElement; protected currentZoom: number; - protected selectionPaletteGroups: Record = {}; protected groupIsCollapsed: Record = {}; protected groupIsTop: Record = {}; protected searchFields: Record = {}; @@ -113,14 +112,13 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand // set position of container(s) const sameSide = this.selectionPaletteItems.every(e => e.position === this.selectionPaletteItems[0].position); if (sameSide) { - this.setPosition(this.selectionPaletteContainer, this.selectionPaletteItems[0].position, nodeBoundsFromDom); + this.setPosition(this.selectionPaletteContainer, this.selectionPaletteItems[0].position, nodeBoundsFromDom, true); } else { for (let i = 0; i < this.selectionPaletteContainer.childElementCount; i++) { this.setPosition( this.selectionPaletteContainer.children[i] as HTMLElement, this.selectionPaletteItems[i].position, - nodeBoundsFromDom, - true + nodeBoundsFromDom ); } } @@ -151,6 +149,15 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand element.style.transform = `scale(${zoom})`; const nodeHeight = nodeBounds.height; const nodeWidth = nodeBounds.width; + if (single) { + for (let i = 0; i < element.childElementCount; i++) { + const child = element.children[i] as HTMLElement; + child.style.position = 'static'; + if (i < element.childElementCount) { + child.style.borderBottom = '0'; + } + } + } let xDiff = -element.offsetWidth / 2; let yDiff = (-element.offsetHeight / 2) * zoom; if (position === SelectionPalettePosition.Right || position === SelectionPalettePosition.Left) { @@ -177,9 +184,6 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand element.style.transformOrigin = 'top'; element.style.top = `${yDiff}px`; } - if (single) { - element.style.position = 'absolute'; - } } protected initializeContents(containerElement: HTMLElement): void { @@ -195,7 +199,6 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand for (const item of this.selectionPaletteItems) { if (item.children) { const group = this.createGroup(item); - this.selectionPaletteGroups[group.id] = group; selectionPaletteContainer.appendChild(group); } } @@ -248,6 +251,7 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand if (item.children!.length === 0) { return group; } + group.style.position = 'absolute'; const groupItems = document.createElement('div'); group.classList.add(SelectionPalette.GROUP_CONTAINER_CLASS); groupItems.classList.add(SelectionPalette.GROUP_CLASS); @@ -327,7 +331,6 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand button.classList.add(SelectionPalette.TOOL_BUTTON_CLASS); if (item.icon) { button.appendChild(createIcon(item.icon)); - return button; } button.insertAdjacentText('beforeend', item.label); button.onclick = this.onClickCreateToolButton(button, item); @@ -363,7 +366,7 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand if (items) { Array.from(items).forEach(item => { if (matchingChildren.find(child => child.id === item.id)) { - (item as HTMLElement).style.display = 'block'; + (item as HTMLElement).style.display = 'flex'; } else { (item as HTMLElement).style.display = 'none'; } From 3cc4e584eaaf305b17d11b56535c3bdd213832d8 Mon Sep 17 00:00:00 2001 From: Tobias Pellkvist Date: Tue, 2 Apr 2024 18:55:31 +0200 Subject: [PATCH 18/21] selection palette expand button does not close when panning and zooming anymore --- .../selection-palette/selection-palette.ts | 81 ++++++++++++------- 1 file changed, 51 insertions(+), 30 deletions(-) diff --git a/packages/client/src/features/selection-palette/selection-palette.ts b/packages/client/src/features/selection-palette/selection-palette.ts index f5f81b2b..8ec87dc9 100644 --- a/packages/client/src/features/selection-palette/selection-palette.ts +++ b/packages/client/src/features/selection-palette/selection-palette.ts @@ -37,13 +37,17 @@ import { SelectionPaletteGroupItem, SelectionPaletteNodeItem, SelectionPalettePosition, - KeyCode + KeyCode, + Viewport, + Bounds, + GNode } from '@eclipse-glsp/sprotty'; import { GetViewportAction } from 'sprotty-protocol/lib/actions'; import { changeCodiconClass, createIcon } from '../tool-palette/tool-palette'; import { IDiagramStartup } from '../../base/model/diagram-loader'; import { ISelectionListener, SelectionService } from '../../base/selection-service'; import { ChangeSelectionPaletteStateAction, SelectionPaletteState } from './selection-palette-actions'; +import { SetViewportAction } from '@eclipse-glsp/sprotty'; @injectable() export class SelectionPalette extends AbstractUIExtension implements IActionHandler, IDiagramStartup, ISelectionListener { @@ -53,7 +57,6 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand protected selectionPaletteItems: SelectionPaletteGroupItem[]; protected selectionPaletteContainer: HTMLElement; protected expandButton: HTMLElement; - protected currentZoom: number; protected groupIsCollapsed: Record = {}; protected groupIsTop: Record = {}; @@ -101,28 +104,10 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand protected override async onBeforeShow(_containerElement: HTMLElement, root: Readonly): Promise { const viewportResult: ViewportResult = await this.actionDispatcher.request(GetViewportAction.create()); - await this.initAvailableOptions(root.index.getById(this.selectedElementId)); + let node = root.index.getById(this.selectedElementId) as GNode; + await this.initAvailableOptions(node); this.initBody(); - this.currentZoom = viewportResult.viewport.zoom; - const nodeFromDom = document.getElementById(`${this.options.baseDiv}_${this.selectedElementId}`) as any as SVGGElement; - const nodeBoundsFromDom = nodeFromDom.getBoundingClientRect(); - this.setMainPosition(viewportResult, nodeBoundsFromDom); - // set position of expand button - this.setPosition(this.expandButton, this.expandButtonPosition, nodeBoundsFromDom); - // set position of container(s) - const sameSide = this.selectionPaletteItems.every(e => e.position === this.selectionPaletteItems[0].position); - if (sameSide) { - this.setPosition(this.selectionPaletteContainer, this.selectionPaletteItems[0].position, nodeBoundsFromDom, true); - } else { - for (let i = 0; i < this.selectionPaletteContainer.childElementCount; i++) { - this.setPosition( - this.selectionPaletteContainer.children[i] as HTMLElement, - this.selectionPaletteItems[i].position, - nodeBoundsFromDom - ); - } - } - this.hideSelectionPalette(); + this.setPosition(viewportResult.viewport, viewportResult.canvasBounds, node); } protected async initAvailableOptions(contextElement?: GModelElement): Promise { @@ -137,18 +122,38 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand this.selectionPaletteItems = response.actions.map(e => e as SelectionPaletteGroupItem); } - protected setMainPosition(viewport: ViewportResult, nodeBounds: DOMRect): void { - const xCenter = nodeBounds.x + nodeBounds.width / 2 - viewport.canvasBounds.x; - const yCenter = nodeBounds.y + nodeBounds.height / 2 - viewport.canvasBounds.y; + protected setPosition(viewport: Viewport, canvasBounds: Bounds, node: GNode) { + this.setMainPosition(canvasBounds, viewport, node); + // set position of expand button + this.setContainerPosition(this.expandButton, this.expandButtonPosition, node.bounds, viewport.zoom); + // set position of container(s) + const sameSide = this.selectionPaletteItems.every(e => e.position === this.selectionPaletteItems[0].position); + if (sameSide) { + this.setContainerPosition(this.selectionPaletteContainer, this.selectionPaletteItems[0].position, node.bounds, viewport.zoom, true); + } else { + for (let i = 0; i < this.selectionPaletteContainer.childElementCount; i++) { + this.setContainerPosition( + this.selectionPaletteContainer.children[i] as HTMLElement, + this.selectionPaletteItems[i].position, + node.bounds, + viewport.zoom + ); + } + } + this.hideSelectionPalette(); + } + + protected setMainPosition(canvasBounds: Bounds, viewport: Viewport, node: GNode): void { + let zoom = viewport.zoom; + const calculatedBounds = { x: (-viewport.scroll.x + node.bounds.x)*zoom, y: (-viewport.scroll.y + node.bounds.y)*zoom, width: node.bounds.width*zoom, height: node.bounds.height*zoom }; + const xCenter = calculatedBounds.x + calculatedBounds.width / 2 - canvasBounds.x; + const yCenter = calculatedBounds.y + calculatedBounds.height / 2 - canvasBounds.y; this.containerElement.style.left = `${xCenter}px`; this.containerElement.style.top = `${yCenter}px`; } - protected setPosition(element: HTMLElement, position: SelectionPalettePosition, nodeBounds: DOMRect, single?: boolean): void { - const zoom = this.currentZoom; + protected setContainerPosition(element: HTMLElement, position: SelectionPalettePosition, nodeBounds: Bounds, zoom: number, single?: boolean): void { element.style.transform = `scale(${zoom})`; - const nodeHeight = nodeBounds.height; - const nodeWidth = nodeBounds.width; if (single) { for (let i = 0; i < element.childElementCount; i++) { const child = element.children[i] as HTMLElement; @@ -158,6 +163,12 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand } } } + this.setDirectionalProperties(element, position, nodeBounds, zoom); + } + + protected setDirectionalProperties(element: HTMLElement, position: SelectionPalettePosition, nodeBounds: Bounds, zoom: number) { + const nodeHeight = nodeBounds.height * zoom; + const nodeWidth = nodeBounds.width * zoom; let xDiff = -element.offsetWidth / 2; let yDiff = (-element.offsetHeight / 2) * zoom; if (position === SelectionPalettePosition.Right || position === SelectionPalettePosition.Left) { @@ -549,12 +560,22 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand ).focus(); } } + } else if (SetViewportAction.is(action)) { + this.handleSetViewportAction(action.newViewport); } else { this.actionDispatcher.dispatch(SetUIExtensionVisibilityAction.create({ extensionId: SelectionPalette.ID, visible: false })); this.hideAll(); } } + protected handleSetViewportAction(viewport: Viewport) { + const root = this.selectionService.getModelRoot(); + const canvasBounds = root.canvasBounds; + if (!this.selectedElementId) return + const node = root.index.getById(this.selectedElementId) as GNode; + this.setPosition(viewport, canvasBounds, node); + } + async preRequestModel(): Promise { const requestAction = RequestContextActions.create({ contextId: SelectionPalette.ID, From 152a9fffa1766a9254a8809257b2a476ba9e1112 Mon Sep 17 00:00:00 2001 From: Tobias Pellkvist Date: Tue, 2 Apr 2024 19:05:59 +0200 Subject: [PATCH 19/21] fixed lint problems --- .../selection-palette/selection-palette.ts | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/packages/client/src/features/selection-palette/selection-palette.ts b/packages/client/src/features/selection-palette/selection-palette.ts index 8ec87dc9..2f22f6bb 100644 --- a/packages/client/src/features/selection-palette/selection-palette.ts +++ b/packages/client/src/features/selection-palette/selection-palette.ts @@ -40,14 +40,14 @@ import { KeyCode, Viewport, Bounds, - GNode + GNode, + SetViewportAction } from '@eclipse-glsp/sprotty'; import { GetViewportAction } from 'sprotty-protocol/lib/actions'; import { changeCodiconClass, createIcon } from '../tool-palette/tool-palette'; import { IDiagramStartup } from '../../base/model/diagram-loader'; import { ISelectionListener, SelectionService } from '../../base/selection-service'; import { ChangeSelectionPaletteStateAction, SelectionPaletteState } from './selection-palette-actions'; -import { SetViewportAction } from '@eclipse-glsp/sprotty'; @injectable() export class SelectionPalette extends AbstractUIExtension implements IActionHandler, IDiagramStartup, ISelectionListener { @@ -104,7 +104,7 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand protected override async onBeforeShow(_containerElement: HTMLElement, root: Readonly): Promise { const viewportResult: ViewportResult = await this.actionDispatcher.request(GetViewportAction.create()); - let node = root.index.getById(this.selectedElementId) as GNode; + const node = root.index.getById(this.selectedElementId) as GNode; await this.initAvailableOptions(node); this.initBody(); this.setPosition(viewportResult.viewport, viewportResult.canvasBounds, node); @@ -122,14 +122,20 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand this.selectionPaletteItems = response.actions.map(e => e as SelectionPaletteGroupItem); } - protected setPosition(viewport: Viewport, canvasBounds: Bounds, node: GNode) { + protected setPosition(viewport: Viewport, canvasBounds: Bounds, node: GNode): void { this.setMainPosition(canvasBounds, viewport, node); // set position of expand button this.setContainerPosition(this.expandButton, this.expandButtonPosition, node.bounds, viewport.zoom); // set position of container(s) const sameSide = this.selectionPaletteItems.every(e => e.position === this.selectionPaletteItems[0].position); if (sameSide) { - this.setContainerPosition(this.selectionPaletteContainer, this.selectionPaletteItems[0].position, node.bounds, viewport.zoom, true); + this.setContainerPosition( + this.selectionPaletteContainer, + this.selectionPaletteItems[0].position, + node.bounds, + viewport.zoom, + true + ); } else { for (let i = 0; i < this.selectionPaletteContainer.childElementCount; i++) { this.setContainerPosition( @@ -144,15 +150,26 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand } protected setMainPosition(canvasBounds: Bounds, viewport: Viewport, node: GNode): void { - let zoom = viewport.zoom; - const calculatedBounds = { x: (-viewport.scroll.x + node.bounds.x)*zoom, y: (-viewport.scroll.y + node.bounds.y)*zoom, width: node.bounds.width*zoom, height: node.bounds.height*zoom }; + const zoom = viewport.zoom; + const calculatedBounds = { + x: (-viewport.scroll.x + node.bounds.x) * zoom, + y: (-viewport.scroll.y + node.bounds.y) * zoom, + width: node.bounds.width * zoom, + height: node.bounds.height * zoom + }; const xCenter = calculatedBounds.x + calculatedBounds.width / 2 - canvasBounds.x; const yCenter = calculatedBounds.y + calculatedBounds.height / 2 - canvasBounds.y; this.containerElement.style.left = `${xCenter}px`; this.containerElement.style.top = `${yCenter}px`; } - protected setContainerPosition(element: HTMLElement, position: SelectionPalettePosition, nodeBounds: Bounds, zoom: number, single?: boolean): void { + protected setContainerPosition( + element: HTMLElement, + position: SelectionPalettePosition, + nodeBounds: Bounds, + zoom: number, + single?: boolean + ): void { element.style.transform = `scale(${zoom})`; if (single) { for (let i = 0; i < element.childElementCount; i++) { @@ -166,7 +183,7 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand this.setDirectionalProperties(element, position, nodeBounds, zoom); } - protected setDirectionalProperties(element: HTMLElement, position: SelectionPalettePosition, nodeBounds: Bounds, zoom: number) { + protected setDirectionalProperties(element: HTMLElement, position: SelectionPalettePosition, nodeBounds: Bounds, zoom: number): void { const nodeHeight = nodeBounds.height * zoom; const nodeWidth = nodeBounds.width * zoom; let xDiff = -element.offsetWidth / 2; @@ -568,10 +585,12 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand } } - protected handleSetViewportAction(viewport: Viewport) { + protected handleSetViewportAction(viewport: Viewport): void { const root = this.selectionService.getModelRoot(); const canvasBounds = root.canvasBounds; - if (!this.selectedElementId) return + if (!this.selectedElementId) { + return; + } const node = root.index.getById(this.selectedElementId) as GNode; this.setPosition(viewport, canvasBounds, node); } From b3ad8c70a12dd0b13a2c8235eeceb2f74486da35 Mon Sep 17 00:00:00 2001 From: Tobias Pellkvist Date: Tue, 16 Apr 2024 12:15:32 +0200 Subject: [PATCH 20/21] fixed bug where edge selection would also bring up selection palette --- .../src/features/selection-palette/selection-palette.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/client/src/features/selection-palette/selection-palette.ts b/packages/client/src/features/selection-palette/selection-palette.ts index 2f22f6bb..2711afb9 100644 --- a/packages/client/src/features/selection-palette/selection-palette.ts +++ b/packages/client/src/features/selection-palette/selection-palette.ts @@ -87,7 +87,11 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand selectionChanged(root: GModelRoot, selectedElements: string[]): void { if (selectedElements.length !== 0) { - this.selectedElementId = selectedElements[0]; + const filteredNodes = root.children.filter(element => element instanceof GNode && element.id === selectedElements[0]); + if (filteredNodes.length === 0) { + return; + } + this.selectedElementId = filteredNodes[0].id; this.actionDispatcher.dispatch(SetUIExtensionVisibilityAction.create({ extensionId: SelectionPalette.ID, visible: true })); } else { this.actionDispatcher.dispatch(SetUIExtensionVisibilityAction.create({ extensionId: SelectionPalette.ID, visible: false })); From b4705309caa722e7eaedabc83d3c5ff158d87160 Mon Sep 17 00:00:00 2001 From: Tobias Pellkvist Date: Thu, 18 Apr 2024 19:31:12 +0200 Subject: [PATCH 21/21] selection palette now moves when resizing and moving node instead of disappearing --- .../selection-palette-module.ts | 4 +- .../selection-palette/selection-palette.ts | 80 +++++++++++++------ 2 files changed, 57 insertions(+), 27 deletions(-) diff --git a/packages/client/src/features/selection-palette/selection-palette-module.ts b/packages/client/src/features/selection-palette/selection-palette-module.ts index ba44eebc..23573e8b 100644 --- a/packages/client/src/features/selection-palette/selection-palette-module.ts +++ b/packages/client/src/features/selection-palette/selection-palette-module.ts @@ -21,7 +21,8 @@ import { MoveAction, SetBoundsAction, SetViewportAction, - DeleteElementOperation + DeleteElementOperation, + ChangeBoundsOperation } from '@eclipse-glsp/sprotty'; import '../../../css/selection-palette.css'; import { SelectionPalette, SelectionPaletteKeyListener } from './selection-palette'; @@ -36,5 +37,6 @@ export const selectionPaletteModule = new FeatureModule((bind, unbind, isBound, configureActionHandler(context, SetBoundsAction.KIND, SelectionPalette); configureActionHandler(context, SetViewportAction.KIND, SelectionPalette); configureActionHandler(context, DeleteElementOperation.KIND, SelectionPalette); + configureActionHandler(context, ChangeBoundsOperation.KIND, SelectionPalette); bindAsService(bind, TYPES.KeyListener, SelectionPaletteKeyListener); }); diff --git a/packages/client/src/features/selection-palette/selection-palette.ts b/packages/client/src/features/selection-palette/selection-palette.ts index 2711afb9..032f4733 100644 --- a/packages/client/src/features/selection-palette/selection-palette.ts +++ b/packages/client/src/features/selection-palette/selection-palette.ts @@ -41,7 +41,11 @@ import { Viewport, Bounds, GNode, - SetViewportAction + SetViewportAction, + Point, + ChangeBoundsOperation, + Dimension, + MoveAction } from '@eclipse-glsp/sprotty'; import { GetViewportAction } from 'sprotty-protocol/lib/actions'; import { changeCodiconClass, createIcon } from '../tool-palette/tool-palette'; @@ -65,6 +69,9 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand protected previousElementKeyCode: KeyCode = 'ArrowUp'; protected nextElementKeyCode: KeyCode = 'ArrowDown'; + private viewport: Viewport; + private canvasBounds: Bounds; + // Sets the position of the expand button protected expandButtonPosition = SelectionPalettePosition.Top; @@ -86,7 +93,7 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand } selectionChanged(root: GModelRoot, selectedElements: string[]): void { - if (selectedElements.length !== 0) { + if (selectedElements.length === 1) { const filteredNodes = root.children.filter(element => element instanceof GNode && element.id === selectedElements[0]); if (filteredNodes.length === 0) { return; @@ -110,8 +117,10 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand const viewportResult: ViewportResult = await this.actionDispatcher.request(GetViewportAction.create()); const node = root.index.getById(this.selectedElementId) as GNode; await this.initAvailableOptions(node); + this.viewport = viewportResult.viewport; + this.canvasBounds = viewportResult.canvasBounds; this.initBody(); - this.setPosition(viewportResult.viewport, viewportResult.canvasBounds, node); + this.setPosition(node.bounds, node.position); } protected async initAvailableOptions(contextElement?: GModelElement): Promise { @@ -126,18 +135,17 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand this.selectionPaletteItems = response.actions.map(e => e as SelectionPaletteGroupItem); } - protected setPosition(viewport: Viewport, canvasBounds: Bounds, node: GNode): void { - this.setMainPosition(canvasBounds, viewport, node); + protected setPosition(nodeDimension: Dimension, position: Point): void { + this.setMainPosition(nodeDimension, position); // set position of expand button - this.setContainerPosition(this.expandButton, this.expandButtonPosition, node.bounds, viewport.zoom); + this.setContainerPosition(this.expandButton, this.expandButtonPosition, nodeDimension); // set position of container(s) const sameSide = this.selectionPaletteItems.every(e => e.position === this.selectionPaletteItems[0].position); if (sameSide) { this.setContainerPosition( this.selectionPaletteContainer, this.selectionPaletteItems[0].position, - node.bounds, - viewport.zoom, + nodeDimension, true ); } else { @@ -145,24 +153,23 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand this.setContainerPosition( this.selectionPaletteContainer.children[i] as HTMLElement, this.selectionPaletteItems[i].position, - node.bounds, - viewport.zoom + nodeDimension ); } } this.hideSelectionPalette(); } - protected setMainPosition(canvasBounds: Bounds, viewport: Viewport, node: GNode): void { - const zoom = viewport.zoom; + protected setMainPosition(nodeDimension: Dimension, position: Point): void { + const zoom = this.viewport.zoom; const calculatedBounds = { - x: (-viewport.scroll.x + node.bounds.x) * zoom, - y: (-viewport.scroll.y + node.bounds.y) * zoom, - width: node.bounds.width * zoom, - height: node.bounds.height * zoom + x: (-this.viewport.scroll.x + position.x) * zoom, + y: (-this.viewport.scroll.y + position.y) * zoom, + width: nodeDimension.width * zoom, + height: nodeDimension.height * zoom }; - const xCenter = calculatedBounds.x + calculatedBounds.width / 2 - canvasBounds.x; - const yCenter = calculatedBounds.y + calculatedBounds.height / 2 - canvasBounds.y; + const xCenter = calculatedBounds.x + calculatedBounds.width / 2 - this.canvasBounds.x; + const yCenter = calculatedBounds.y + calculatedBounds.height / 2 - this.canvasBounds.y; this.containerElement.style.left = `${xCenter}px`; this.containerElement.style.top = `${yCenter}px`; } @@ -170,10 +177,10 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand protected setContainerPosition( element: HTMLElement, position: SelectionPalettePosition, - nodeBounds: Bounds, - zoom: number, + nodeDimension: Dimension, single?: boolean ): void { + const zoom = this.viewport.zoom; element.style.transform = `scale(${zoom})`; if (single) { for (let i = 0; i < element.childElementCount; i++) { @@ -184,12 +191,17 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand } } } - this.setDirectionalProperties(element, position, nodeBounds, zoom); + this.setDirectionalProperties(element, position, nodeDimension, zoom); } - protected setDirectionalProperties(element: HTMLElement, position: SelectionPalettePosition, nodeBounds: Bounds, zoom: number): void { - const nodeHeight = nodeBounds.height * zoom; - const nodeWidth = nodeBounds.width * zoom; + protected setDirectionalProperties( + element: HTMLElement, + position: SelectionPalettePosition, + nodeDimension: Dimension, + zoom: number + ): void { + const nodeHeight = nodeDimension.height * zoom; + const nodeWidth = nodeDimension.width * zoom; let xDiff = -element.offsetWidth / 2; let yDiff = (-element.offsetHeight / 2) * zoom; if (position === SelectionPalettePosition.Right || position === SelectionPalettePosition.Left) { @@ -583,20 +595,36 @@ export class SelectionPalette extends AbstractUIExtension implements IActionHand } } else if (SetViewportAction.is(action)) { this.handleSetViewportAction(action.newViewport); + } else if (MoveAction.is(action) && action.moves && action.moves.length === 1) { + this.handleMoveAction(action.moves[0].toPosition); + } else if (ChangeBoundsOperation.is(action) && action.newBounds && action.newBounds.length === 1) { + this.handleChangeBoundsOperation(action.newBounds[0].newSize); } else { this.actionDispatcher.dispatch(SetUIExtensionVisibilityAction.create({ extensionId: SelectionPalette.ID, visible: false })); this.hideAll(); } } + protected handleMoveAction(position: Point): void { + const root = this.selectionService.getModelRoot(); + const node = root.index.getById(this.selectedElementId) as GNode; + this.setPosition(node.bounds, position); + } + + protected handleChangeBoundsOperation(dimension: Dimension): void { + const root = this.selectionService.getModelRoot(); + const node = root.index.getById(this.selectedElementId) as GNode; + this.setPosition(dimension, node.position); + } + protected handleSetViewportAction(viewport: Viewport): void { + this.viewport = viewport; const root = this.selectionService.getModelRoot(); - const canvasBounds = root.canvasBounds; if (!this.selectedElementId) { return; } const node = root.index.getById(this.selectedElementId) as GNode; - this.setPosition(viewport, canvasBounds, node); + this.setPosition(node.bounds, node.position); } async preRequestModel(): Promise {