From d4f76b964d37ed695d7c25bf7ff94da831401754 Mon Sep 17 00:00:00 2001 From: Jae Aeich Date: Fri, 9 Feb 2024 14:42:55 +0530 Subject: [PATCH 1/4] feat: add code component --- apps/documentation/docs/.vitepress/config.mts | 4 + .../docs/design/components/code.md | 119 +++++++ package-lock.json | 6 +- .../ecc-utils-design/demo/code/index.html | 23 ++ packages/ecc-utils-design/demo/index.html | 40 ++- packages/ecc-utils-design/package.json | 1 + .../src/components/code/code.styles.ts | 12 + .../src/components/code/code.test.ts | 0 .../src/components/code/code.ts | 325 ++++++++++++++++++ .../src/components/code/index.ts | 12 + .../ecc-utils-design/src/components/index.ts | 1 + .../src/events/ecc-utils-change.ts | 7 + packages/ecc-utils-design/src/events/index.ts | 1 + packages/ecc-utils-design/tsconfig.json | 2 +- 14 files changed, 530 insertions(+), 23 deletions(-) create mode 100644 apps/documentation/docs/design/components/code.md create mode 100644 packages/ecc-utils-design/demo/code/index.html create mode 100644 packages/ecc-utils-design/src/components/code/code.styles.ts create mode 100644 packages/ecc-utils-design/src/components/code/code.test.ts create mode 100644 packages/ecc-utils-design/src/components/code/code.ts create mode 100644 packages/ecc-utils-design/src/components/code/index.ts create mode 100644 packages/ecc-utils-design/src/events/ecc-utils-change.ts diff --git a/apps/documentation/docs/.vitepress/config.mts b/apps/documentation/docs/.vitepress/config.mts index 5e28ee68..6b709293 100644 --- a/apps/documentation/docs/.vitepress/config.mts +++ b/apps/documentation/docs/.vitepress/config.mts @@ -91,6 +91,10 @@ export default defineConfig({ text: 'Details', link: '/design/components/details', }, + { + text: 'Code', + link: '/design/components/code', + }, ], }, ], diff --git a/apps/documentation/docs/design/components/code.md b/apps/documentation/docs/design/components/code.md new file mode 100644 index 00000000..16ab84a7 --- /dev/null +++ b/apps/documentation/docs/design/components/code.md @@ -0,0 +1,119 @@ +# Collection Component + +
<ecc-utils-design-code>
+Simple code editor to handle Yaml, JSON and multiline text input. + +
+ + +::: details Code Blocks +::: code-group + +```js [HTML] +import "@elixir-cloud/design/dist/components/code/index.js"; +``` + + + +::: + +
+
+ +## Importing + +```js [HTML] +import "@elixir-cloud/design/dist/components/code/index.js"; +``` + +## Properties + +| Property | Required | Default | Type | Description | +| ------------- | -------- | ------- | -------- | ---------------------------------------------------------------------- | +| `code` | `false` | | `String` | Specifies the code to be rendered in the editor during initialization. | +| `label` | `Code` | | `String` | Label for code editor input field. | +| `language` | `false` | | `String` | Specifies the language interpreter for syntax highlighting. | +| `indentation` | `2` | | `Number` | Specifies number of spaces that should be considered for 1 Tab space. | +| `blurDelay` | `150` | | `Number` | Time in ms between 2 Tab key presses that should move the focus. | + +## Events + +| Event Name | Description | +| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ecc-utils-change` | This event is triggered when there is a change in the code within the editor. The event details include the language set by the package author, the code as value, and an error attribute indicating whether the entered code is valid based on the specified language. | + +## Methods + +| Method Name | Arguments | Description | +| ----------- | --------- | ------------------ | +| `getCode()` | | Returns the input. | + +## Slots + +## Parts + +## Examples + +### JSON + +
<ecc-utils-design-code>
+Simple code editor to handle Yaml, JSON and multiline text input. + +
+ + +::: details Code Blocks +::: code-group + +```js [HTML] +import "@elixir-cloud/design/dist/components/code/index.js"; +``` + + + +::: + +
+
+ +### Indentation + +
<ecc-utils-design-code>
+Simple code editor to handle Yaml, JSON and multiline text input. + +
+ + +::: details Code Blocks +::: code-group + +```js [HTML] +import "@elixir-cloud/design/dist/components/code/index.js"; + +; +``` + + + +::: + +
+
+ + + + diff --git a/package-lock.json b/package-lock.json index a2bb0863..2c2a4b47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14454,7 +14454,6 @@ }, "node_modules/argparse": { "version": "2.0.1", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -24948,8 +24947,8 @@ }, "node_modules/js-yaml": { "version": "4.1.0", - "dev": true, - "license": "MIT", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dependencies": { "argparse": "^2.0.1" }, @@ -38714,6 +38713,7 @@ "license": "ISC", "dependencies": { "@shoelace-style/shoelace": "^2.8.0", + "js-yaml": "^4.1.0", "lit": "^2.8.0", "lodash-es": "^4.17.21" }, diff --git a/packages/ecc-utils-design/demo/code/index.html b/packages/ecc-utils-design/demo/code/index.html new file mode 100644 index 00000000..78507f8b --- /dev/null +++ b/packages/ecc-utils-design/demo/code/index.html @@ -0,0 +1,23 @@ + + + + + + ecc-utils-design + + + +
+
+
+ + + diff --git a/packages/ecc-utils-design/demo/index.html b/packages/ecc-utils-design/demo/index.html index ae99db6d..63d81cc1 100644 --- a/packages/ecc-utils-design/demo/index.html +++ b/packages/ecc-utils-design/demo/index.html @@ -1,22 +1,24 @@ - - - - ecc-utils-design - - - - - + + + + ecc-utils-design + + + + + diff --git a/packages/ecc-utils-design/package.json b/packages/ecc-utils-design/package.json index fd4e7ea5..fd9a487e 100644 --- a/packages/ecc-utils-design/package.json +++ b/packages/ecc-utils-design/package.json @@ -60,6 +60,7 @@ }, "dependencies": { "@shoelace-style/shoelace": "^2.8.0", + "js-yaml": "^4.1.0", "lit": "^2.8.0", "lodash-es": "^4.17.21" } diff --git a/packages/ecc-utils-design/src/components/code/code.styles.ts b/packages/ecc-utils-design/src/components/code/code.styles.ts new file mode 100644 index 00000000..4a653f04 --- /dev/null +++ b/packages/ecc-utils-design/src/components/code/code.styles.ts @@ -0,0 +1,12 @@ +import { css } from "lit"; + +const codeStyles = css` + #label { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--sl-spacing-x-small); + } +`; + +export default codeStyles; diff --git a/packages/ecc-utils-design/src/components/code/code.test.ts b/packages/ecc-utils-design/src/components/code/code.test.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/ecc-utils-design/src/components/code/code.ts b/packages/ecc-utils-design/src/components/code/code.ts new file mode 100644 index 00000000..761ec482 --- /dev/null +++ b/packages/ecc-utils-design/src/components/code/code.ts @@ -0,0 +1,325 @@ +import { LitElement, html } from "lit"; +import { property, state } from "lit/decorators.js"; +import "@shoelace-style/shoelace/dist/components/input/input.js"; +import "@shoelace-style/shoelace/dist/components/badge/badge.js"; +import "@shoelace-style/shoelace/dist/components/textarea/textarea.js"; +import "@shoelace-style/shoelace/dist/components/copy-button/copy-button.js"; +import jsyaml from "js-yaml"; +import { hostStyles } from "../../styles/host.styles.js"; +import getShoelaceStyles from "../../styles/shoelace.styles.js"; +import codeStyles from "./code.styles.js"; + +type Language = "YAML" | "JSON" | "Text"; + +export default class EccUtilsDesignCode extends LitElement { + static styles = [ + getShoelaceStyles( + document.querySelector("html")?.classList.contains("dark") + ), + hostStyles, + codeStyles, + ]; + + @property({ type: String }) code = ""; + @property({ type: String }) label = "Code"; + @property({ type: String }) language: Language = "YAML"; + @property({ type: Number }) indentation = 2; + @property({ type: Number }) blurDelay = 150; + + @state() elTextarea: HTMLTextAreaElement | null = null; + @state() error = false; + @state() indent = ""; + @state() currentIndentation = ""; + @state() opening = ["(", "{", "[", "'", '"']; + @state() closing = [")", "}", "]", "'", '"']; + @state() lastTabPressTime = 0; + @state() errorLanguage: Language = "Text"; + + private cssParts = {}; + + private _getTextAreaEle(): HTMLTextAreaElement { + const slTextarea = this.shadowRoot?.querySelector("sl-textarea"); + const ele = slTextarea?.shadowRoot?.querySelector("textarea"); + if (!ele || !(ele instanceof HTMLTextAreaElement)) { + throw new Error("Could not find textarea element"); + } + return ele; + } + + firstUpdated() { + this.indent = " ".repeat(this.indentation); + this._updateTextarea(); + } + + public getCode() { + return this.code; + } + + private _setCursor(pos: number) { + this.elTextarea = this._getTextAreaEle(); + this.elTextarea.setSelectionRange(pos, pos); + } + + private _setSelect(from: number, to: number) { + this.elTextarea = this._getTextAreaEle(); + this.elTextarea.setSelectionRange(from, to); + } + + private _getCurrentLineIndent() { + const slTextarea = this.shadowRoot?.querySelector("sl-textarea"); + const ele = slTextarea?.shadowRoot?.querySelector("textarea"); + if (!ele || !(ele instanceof HTMLTextAreaElement)) { + throw new Error("Could not find textarea element"); + } + this.elTextarea = ele; + + const selStart = this.elTextarea.selectionStart; + const selEnd = this.elTextarea.selectionEnd; + + const indentStart = this.code.lastIndexOf("\n", selStart - 1) + 1; + const spaces = (() => { + let pos = indentStart; + while (this.code[pos] === " " && pos < selEnd) pos += 1; + return pos - indentStart; + })(); + return " ".repeat(spaces); + } + + private _updateTextarea() { + if (!this.elTextarea) return; + this.elTextarea.value = this.code; + + this.dispatchEvent( + new CustomEvent("ecc-utils-change", { detail: this.code }) + ); + } + + private _insertCode(pos: number, text: string, placeCursor = true) { + this.code = this.code.substring(0, pos) + text + this.code.substring(pos); + this._updateTextarea(); + if (placeCursor) this._setCursor(pos + text.length); + } + + private _replaceCode( + posFrom: number, + posTo: number, + text = "", + placeCursor = true + ) { + this.code = + this.code.substring(0, posFrom) + text + this.code.substring(posTo); + this._updateTextarea(); + if (placeCursor) this._setCursor(posFrom + text.length); + } + + private _handleInput(e: InputEvent) { + this.code = (e.target as HTMLInputElement).value; + this._validateCode(); + this.dispatchEvent( + new CustomEvent("ecc-utils-change", { detail: this.code }) + ); + } + + private _handleTabs(e: KeyboardEvent) { + e.preventDefault(); + const slTextarea = this.shadowRoot?.querySelector("sl-textarea"); + const ele = slTextarea?.shadowRoot?.querySelector("textarea"); + if (!ele || !(ele instanceof HTMLTextAreaElement)) { + throw new Error("Could not find textarea element"); + } + this.elTextarea = ele; + const selStart = this.elTextarea.selectionStart; + const selEnd = this.elTextarea.selectionEnd; + + if (selStart !== selEnd) { + // multiline indent + const selLineStart = Math.max( + 0, + this.code.lastIndexOf("\n", selStart - 1) + ); + const selLineEnd = Math.max(this.code.indexOf("\n", selEnd), selEnd); + + let linesInChunk = 0; + let codeChunk = this.code.substring(selLineStart, selLineEnd); + let lenShift = this.indent.length; + if (selLineStart === 0) codeChunk = `\n${codeChunk}`; + + if (e.shiftKey) { + // Unindent + lenShift = -lenShift; + linesInChunk = ( + codeChunk.match(new RegExp(`\n${this.indent}`, "g")) || [] + ).length; + codeChunk = codeChunk.replaceAll(`\n${this.indent}`, "\n"); + } else { + // Indent + linesInChunk = (codeChunk.match(/\n/g) || []).length; + codeChunk = codeChunk.replaceAll("\n", `\n${this.indent}`); + } + + if (selLineStart === 0) codeChunk = codeChunk.replace(/^\n/, ""); + this._replaceCode(selLineStart, selLineEnd, codeChunk, false); + + const newStart = Math.max(selLineStart + 1, selStart + lenShift); + const newEnd = selEnd + linesInChunk * lenShift; + this._setSelect(newStart, newEnd); + } else { + this._insertCode(selStart, this.indent, true); + } + } + + private _handleBackspace(e: KeyboardEvent) { + this.elTextarea = this._getTextAreaEle(); + const selStart = this.elTextarea.selectionStart; + const selEnd = this.elTextarea.selectionEnd; + if (e.ctrlKey || selStart !== selEnd) return; + + e.preventDefault(); + + const prevSymbol = this.code[selStart - 1]; + const curSymbol = this.code[selStart]; + const isInPairs = + this.opening.includes(prevSymbol) && this.closing.includes(curSymbol); + const isPair = this.closing[this.opening.indexOf(prevSymbol)] === curSymbol; + + if (isInPairs && isPair) { + this._replaceCode(selStart - 1, selStart + 1); + } else { + const chunkStart = selStart - this.indent.length; + const chunkEnd = selStart; + const chunk = this.code.substring(chunkStart, chunkEnd); + + if (chunk === this.indent) this._replaceCode(chunkStart, chunkEnd); + else this._replaceCode(selStart - 1, selStart); + } + } + + private _handleAutoClose(e: KeyboardEvent) { + this.elTextarea = this._getTextAreaEle(); + const selStart = this.elTextarea.selectionStart; + const selEnd = this.elTextarea.selectionEnd; + if (this.code[selStart] === "'" || this.code[selStart] === '"') { + this._handleAutoSkip(e); + return; + } + e.preventDefault(); + + if (selStart === selEnd) { + const opening = e.key; + const closing = this.closing[this.opening.indexOf(opening)]; + + if ( + opening === "{" && + (this.code[selStart] === "\n" || this.code.length === selStart) + ) { + const lineShift = `\n${this._getCurrentLineIndent()}`; + this._insertCode( + selStart, + opening + lineShift + this.indent + lineShift + closing + ); + this._setCursor(selStart + lineShift.length + this.indent.length + 1); + } else { + this._insertCode(selStart, opening + closing); + this._setCursor(selStart + 1); + } + } + } + + private _handleAutoSkip(e: KeyboardEvent): void { + this.elTextarea = this._getTextAreaEle(); + const selStart = this.elTextarea.selectionStart; + + if (this.code[selStart] === e.key) { + e.preventDefault(); + this._setCursor(selStart + 1); + } + } + + private _handleNewLine(e: KeyboardEvent) { + e.preventDefault(); + this.elTextarea = this._getTextAreaEle(); + this._insertCode( + this.elTextarea.selectionStart, + `\n${this._getCurrentLineIndent()}` + ); + } + + private _handleKeys(e: KeyboardEvent) { + console.log("hello", this.indent, "hi"); + const currentTime = new Date().getTime(); + const timeSinceLastTabPress = currentTime - this.lastTabPressTime; + + switch (e.code) { + case "Tab": + if (timeSinceLastTabPress > this.blurDelay) { + this._handleTabs(e); + } else { + this.shadowRoot?.querySelector("sl-textarea")?.blur(); + e.preventDefault(); + } + + this.lastTabPressTime = currentTime; + break; + case "Enter": + this._handleNewLine(e); + break; + case "Backspace": + this._handleBackspace(e); + break; + default: + if (this.opening.includes(e.key)) this._handleAutoClose(e); + else if (this.closing.includes(e.key)) this._handleAutoSkip(e); + } + this._validateCode(); + this.dispatchEvent( + new CustomEvent("ecc-utils-change", { detail: this.code }) + ); + } + + private _validateCode(): void { + if (this.language === "JSON") { + try { + JSON.parse(this.code); + this.error = false; + } catch (error) { + try { + jsyaml.loadAll(this.code); + this.errorLanguage = "YAML"; + } catch (err) { + this.errorLanguage = "Text"; + } + this.error = true; + } + } else if (this.language === "YAML") { + try { + jsyaml.loadAll(this.code); + this.error = false; + } catch (error) { + this.error = true; + this.errorLanguage = "Text"; + } + } else this.error = false; + } + + render() { + return html` + +
+ ${this.label} + ${this.language} + ${this.error + ? html`${this.errorLanguage}` + : html``} + +
+
+ `; + } +} diff --git a/packages/ecc-utils-design/src/components/code/index.ts b/packages/ecc-utils-design/src/components/code/index.ts new file mode 100644 index 00000000..b0361664 --- /dev/null +++ b/packages/ecc-utils-design/src/components/code/index.ts @@ -0,0 +1,12 @@ +import EccUtilsDesignCode from "./code.js"; + +export * from "./code.js"; +export default EccUtilsDesignCode; + +window.customElements.define("ecc-utils-design-code", EccUtilsDesignCode); + +declare global { + interface HTMLElementTagNameMap { + "ecc-utils-design-code": EccUtilsDesignCode; + } +} diff --git a/packages/ecc-utils-design/src/components/index.ts b/packages/ecc-utils-design/src/components/index.ts index cfdcd695..d4a4d22c 100644 --- a/packages/ecc-utils-design/src/components/index.ts +++ b/packages/ecc-utils-design/src/components/index.ts @@ -1,3 +1,4 @@ export { default as EccUtilsDesignForm } from "./form/index.js"; export { default as EccUtilsDesignCollection } from "./collection/index.js"; export { default as EccUtilsDesignDetails } from "./details/index.js"; +export { default as EccUtilsDesignCode } from "./code/index.js"; diff --git a/packages/ecc-utils-design/src/events/ecc-utils-change.ts b/packages/ecc-utils-design/src/events/ecc-utils-change.ts new file mode 100644 index 00000000..5b0f615f --- /dev/null +++ b/packages/ecc-utils-design/src/events/ecc-utils-change.ts @@ -0,0 +1,7 @@ +export type EccUtilsChangeEvent = CustomEvent>; + +declare global { + interface GlobalEventHandlersEventMap { + "ecc-utils-change": EccUtilsChangeEvent; + } +} diff --git a/packages/ecc-utils-design/src/events/index.ts b/packages/ecc-utils-design/src/events/index.ts index 9035e409..0dbf6d83 100644 --- a/packages/ecc-utils-design/src/events/index.ts +++ b/packages/ecc-utils-design/src/events/index.ts @@ -3,3 +3,4 @@ export type { EccUtilsExpandEvent } from "./ecc-utils-expand.js"; export type { EccUtilsFilterEvent } from "./ecc-utils-filter.js"; export type { EccUtilsPageChangeEvent } from "./ecc-utils-page-change.js"; export type { EccUtilsButtonClickEvent } from "./ecc-utils-button-click.js"; +export type { EccUtilsChangeEvent } from "./ecc-utils-change.js"; diff --git a/packages/ecc-utils-design/tsconfig.json b/packages/ecc-utils-design/tsconfig.json index efc71f5c..d69e3587 100644 --- a/packages/ecc-utils-design/tsconfig.json +++ b/packages/ecc-utils-design/tsconfig.json @@ -5,7 +5,7 @@ "module": "esnext", "moduleResolution": "node", "noEmitOnError": true, - "lib": ["es2017", "dom"], + "lib": ["es2017", "dom", "ES2021.String"], "strict": true, "esModuleInterop": false, "allowSyntheticDefaultImports": true, From bfaca0ecc0ed982dac7f2d5a5ed2bd1c1809217f Mon Sep 17 00:00:00 2001 From: Jae Aeich Date: Fri, 9 Feb 2024 18:08:05 +0530 Subject: [PATCH 2/4] fix: workaround for ci --- .../src/components/tes-create-run/create-run.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ecc-client-lit-ga4gh-tes/src/components/tes-create-run/create-run.ts b/packages/ecc-client-lit-ga4gh-tes/src/components/tes-create-run/create-run.ts index 4c0f123a..65ebc39d 100644 --- a/packages/ecc-client-lit-ga4gh-tes/src/components/tes-create-run/create-run.ts +++ b/packages/ecc-client-lit-ga4gh-tes/src/components/tes-create-run/create-run.ts @@ -495,7 +495,7 @@ export class TESCreateRun extends LitElement { }); }; - // Process env data + // eslint-disable-next-line private _processEnv = ( envArray: Record[] ): Record => @@ -507,11 +507,11 @@ export class TESCreateRun extends LitElement { {} ); - // Process volume data + // eslint-disable-next-line private _processVolumes = (value: Array<{ volume: string }>) => value.map((vol) => vol.volume); - // Process tags data + // eslint-disable-next-line private _processTags = ( tagArray: Array<{ name: string; value: string }> ): Record => From f8bb5a915d7e765b9e0fee807acbb8c990407f78 Mon Sep 17 00:00:00 2001 From: Jae Aeich Date: Fri, 9 Feb 2024 18:19:19 +0530 Subject: [PATCH 3/4] fix: docs --- apps/documentation/docs/design/components/code.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/documentation/docs/design/components/code.md b/apps/documentation/docs/design/components/code.md index 79edfa67..64b547ba 100644 --- a/apps/documentation/docs/design/components/code.md +++ b/apps/documentation/docs/design/components/code.md @@ -107,6 +107,8 @@ const renderComponent = ref(false); onMounted(() => { import("@elixir-cloud/design/dist/components/code/index.js").then((module) => { + renderComponent.value = false; + renderComponent.value = true; }); }); From 4035aaa9c818e3f108113728472767aee587bc58 Mon Sep 17 00:00:00 2001 From: Jae Aeich Date: Fri, 9 Feb 2024 18:24:27 +0530 Subject: [PATCH 4/4] fix: docs --- apps/documentation/docs/design/components/code.md | 2 -- apps/documentation/package.json | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/documentation/docs/design/components/code.md b/apps/documentation/docs/design/components/code.md index 64b547ba..79edfa67 100644 --- a/apps/documentation/docs/design/components/code.md +++ b/apps/documentation/docs/design/components/code.md @@ -107,8 +107,6 @@ const renderComponent = ref(false); onMounted(() => { import("@elixir-cloud/design/dist/components/code/index.js").then((module) => { - renderComponent.value = false; - renderComponent.value = true; }); }); diff --git a/apps/documentation/package.json b/apps/documentation/package.json index f5a1b07d..8e52205f 100644 --- a/apps/documentation/package.json +++ b/apps/documentation/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "vitepress dev docs", "build": "vitepress build docs", - "start": "vitepress preview docs", + "start": "npm run build && vitepress preview docs", "lint": "prettier .", "lint:fix": "prettier --write ." },