From 1e8d4df2c940f77596dc466465024da6ffa3ecf1 Mon Sep 17 00:00:00 2001 From: Alex | Kronox Date: Thu, 21 Nov 2024 07:03:21 +0100 Subject: [PATCH] add basic constraint menu --- .../constraintMenu/ConstraintMenu.tsx | 127 ++++++++++++++++ .../constraintMenu/constraintMenu.css | 138 ++++++++++++++++++ src/features/constraintMenu/di.config.ts | 14 ++ src/index.ts | 2 + 4 files changed, 281 insertions(+) create mode 100644 src/features/constraintMenu/ConstraintMenu.tsx create mode 100644 src/features/constraintMenu/constraintMenu.css create mode 100644 src/features/constraintMenu/di.config.ts diff --git a/src/features/constraintMenu/ConstraintMenu.tsx b/src/features/constraintMenu/ConstraintMenu.tsx new file mode 100644 index 0000000..b83ca92 --- /dev/null +++ b/src/features/constraintMenu/ConstraintMenu.tsx @@ -0,0 +1,127 @@ +import { injectable } from "inversify"; +import "./constraintMenu.css"; +import { AbstractUIExtension } from "sprotty"; +import { calculateTextSize } from "../../utils"; + +@injectable() +export class ConstraintMenu extends AbstractUIExtension { + static readonly ID = "constraint-menu"; + + id(): string { + return ConstraintMenu.ID; + } + containerClass(): string { + return ConstraintMenu.ID; + } + protected initializeContents(containerElement: HTMLElement): void { + containerElement.classList.add("ui-float"); + containerElement.innerHTML = ` + + + `; + containerElement.appendChild(this.buildConstraintInputWrapper()); + containerElement.appendChild(this.buildConstraintListWrapper(["Test123", "Test456", "Test789"])); + containerElement.appendChild(this.buildRunButton()); + + // Set the first item as selected + setTimeout(() => this.selectConstraintListItem("Test123"), 0); + } + + private buildConstraintInputWrapper(): HTMLElement { + const wrapper = document.createElement("div"); + wrapper.id = "constraint-menu-input"; + wrapper.innerHTML = ` + + `; + return wrapper; + } + + private buildConstraintListWrapper(constrains: string[]): HTMLElement { + const wrapper = document.createElement("div"); + wrapper.id = "constraint-menu-list"; + + constrains.forEach((constraint) => { + wrapper.appendChild(this.buildConstraintListItem(constraint)); + }); + + return wrapper; + } + + private buildConstraintListItem(constraint: string): HTMLElement { + const valueElement = document.createElement("div"); + valueElement.classList.add("constrain-label"); + + valueElement.onclick = () => { + const elements = document.getElementsByClassName("constraint-label"); + for (let i = 0; i < elements.length; i++) { + elements[i].classList.remove("selected"); + } + valueElement.classList.add("selected"); + this.selectConstraintListItem(constraint); + }; + + const valueInput = document.createElement("input"); + valueInput.value = constraint; + valueInput.placeholder = "Name"; + this.dynamicallySetInputSize(valueInput); + + valueElement.appendChild(valueInput); + + const deleteButton = document.createElement("button"); + deleteButton.innerHTML = ''; + deleteButton.onclick = () => { + console.log("Delete button clicked"); + }; + valueElement.appendChild(deleteButton); + return valueElement; + } + + private selectConstraintListItem(constraint: string): void { + const input = document.getElementById("constraint-input") as HTMLInputElement; + input.value = constraint; + } + + /** + * Sets and dynamically updates the size property of the passed input element. + * When the text is zero the width is set to the placeholder length to make place for it. + * When the text is changed the size gets updated with the keyup event. + * @param inputElement the html dom input element to set the size property for + */ + private dynamicallySetInputSize(inputElement: HTMLInputElement): void { + const handleResize = () => { + const displayText = inputElement.value || inputElement.placeholder; + const { width } = calculateTextSize(displayText, window.getComputedStyle(inputElement).font); + + // Values have higher padding for the rounded border + const widthPadding = 8; + const finalWidth = width + widthPadding; + + inputElement.style.width = finalWidth + "px"; + }; + + inputElement.onkeyup = handleResize; + + // The inputElement is not added to the DOM yet, so we cannot set the size now. + // Wait for next JS tick, after which the element has been added to the DOM and we can set the initial size + setTimeout(handleResize, 0); + } + + private buildRunButton(): HTMLElement { + const wrapper = document.createElement("div"); + wrapper.id = "run-button-container"; + + const button = document.createElement("button"); + button.id = "run-button"; + button.innerHTML = "Run"; + button.onclick = () => { + console.log("Run button clicked"); + }; + + wrapper.appendChild(button); + return wrapper; + } +} diff --git a/src/features/constraintMenu/constraintMenu.css b/src/features/constraintMenu/constraintMenu.css new file mode 100644 index 0000000..432bc41 --- /dev/null +++ b/src/features/constraintMenu/constraintMenu.css @@ -0,0 +1,138 @@ +div.constraint-menu { + right: 20px; + bottom: 20px; + padding: 10px 10px; + display: grid; + grid-template-columns: auto 1fr; + grid-template-rows: 1fr; + grid-auto-rows: 0; + overflow: hidden; + gap: 8px; +} + +div.constraint-menu:has(> input:checked) { + grid-template-rows: 1fr auto 1fr; +} + +div.constraint-menu > * { + grid-column-start: 1; + grid-column-end: 2; + grid-row-start: 1; + grid-row-end: 2; +} + +#run-button { + background-color: green; + color: white; + border: none; + border-radius: 8px; + padding: 5px 10px; + text-align: center; + text-decoration: none; + display: inline-block; + width: fit-content; +} + +#run-button-container { + grid-column-start: 2; + grid-column-end: 3; + grid-row-start: 1; + grid-row-end: 2; +} + +#expand-state-constraint:checked ~ #run-button-container { + grid-column-start: 2; + grid-column-end: 3; + grid-row-start: 3; + grid-row-end: 4; +} + +#expand-state-constraint:checked ~ #run-button-container > #run-button { + width: 100%; +} + +#run-button::before { + content: ""; + background-image: url("@fortawesome/fontawesome-free/svgs/solid/play.svg"); + display: inline-block; + filter: invert(var(--dark-mode)); + height: 16px; + width: 16px; + background-size: 16px 16px; + vertical-align: text-top; +} + +#constraint-menu-input { + grid-row-start: 2; + grid-row-end: 4; + grid-column-start: 1; + grid-column-end: 2; + display: none; +} + +#expand-state-constraint:checked ~ #constraint-menu-input { + display: block; +} + +#constraint-menu-list { + grid-row-start: 2; + grid-row-end: 3; + grid-column-start: 2; + grid-column-end: 3; + display: none; +} + +#expand-state-constraint:checked ~ #constraint-menu-list { + display: block; +} + +#constraint-menu-expand-label { + padding-right: 2em; + position: relative; + display: flex; + grid-column-start: 1; + grid-column-end: 2; + align-items: center; +} + +#expand-state-constraint:checked ~ #constraint-menu-expand-label { + grid-column-end: 3; +} + +#constraint-menu-expand-label::after { + content: ""; + background-image: url("@fortawesome/fontawesome-free/svgs/solid/chevron-up.svg"); + right: 0.5em; + position: absolute; + display: inline-block; + + /* only filter=invert(1) if dark mode is enabled aka --dark-mode is set to 1 */ + filter: invert(var(--dark-mode)); + + width: 16px; + height: 16px; + background-size: 16px 16px; + + transition: transform 500ms ease; + transform: scaleY(1); +} + +#expand-state-constraint:checked ~ #constraint-menu-expand-label::after { + transform: scaleY(-1); +} + +.constrain-label input { + background-color: var(--color-background); + text-align: center; + border: 1px solid var(--color-foreground); + border-radius: 15px; + padding: 3px; + margin: 4px; +} + +.constrain-label button { + background-color: transparent; + border: none; + cursor: pointer; + padding: 0; +} diff --git a/src/features/constraintMenu/di.config.ts b/src/features/constraintMenu/di.config.ts new file mode 100644 index 0000000..f8061f5 --- /dev/null +++ b/src/features/constraintMenu/di.config.ts @@ -0,0 +1,14 @@ +import { ContainerModule } from "inversify"; +import { EDITOR_TYPES } from "../../utils"; +import { ConstraintMenu } from "./ConstraintMenu"; +import { TYPES } from "sprotty"; + +// This module contains an UI extension that adds a tool palette to the editor. +// This tool palette allows the user to create new nodes and edges. +// Additionally it contains the tools that are used to create the nodes and edges. + +export const constraintMenuModule = new ContainerModule((bind) => { + bind(ConstraintMenu).toSelf().inSingletonScope(); + bind(TYPES.IUIExtension).toService(ConstraintMenu); + bind(EDITOR_TYPES.DefaultUIElement).toService(ConstraintMenu); +}); diff --git a/src/index.ts b/src/index.ts index b91bd30..797fca7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ import { dfdElementsModule } from "./features/dfdElements/di.config"; import { copyPasteModule } from "./features/copyPaste/di.config"; import { EDITOR_TYPES } from "./utils"; import { editorModeModule } from "./features/editorMode/di.config"; +import { constraintMenuModule } from "./features/constraintMenu/di.config"; import "sprotty/css/sprotty.css"; import "sprotty/css/edit-label.css"; @@ -52,6 +53,7 @@ container.load( editorModeModule, toolPaletteModule, copyPasteModule, + constraintMenuModule, ); const dispatcher = container.get(TYPES.IActionDispatcher);