From dbde3a583419d5cae01aceea76ecb0fba1e2e617 Mon Sep 17 00:00:00 2001 From: Reimar Date: Mon, 19 Jan 2026 09:39:51 +0100 Subject: [PATCH] Implement fields having multiple items in creator --- assets/scripts/Field.js | 6 +++ assets/scripts/FieldInput.js | 63 ++++++++++++++++++++----- assets/scripts/FieldInputItem.js | 60 +++++++++++++++++++++++ assets/scripts/records/SpfRecord.js | 8 ++-- assets/scripts/records/TagListRecord.js | 8 ++-- assets/scripts/spf/Term.js | 6 --- assets/scripts/ui/creator.js | 7 ++- assets/styles/main.css | 20 ++++++++ 8 files changed, 150 insertions(+), 28 deletions(-) create mode 100644 assets/scripts/FieldInputItem.js diff --git a/assets/scripts/Field.js b/assets/scripts/Field.js index 87bc400..dee58db 100644 --- a/assets/scripts/Field.js +++ b/assets/scripts/Field.js @@ -10,6 +10,7 @@ export class Field { categoryName = null; isHidden = false; isDisabled = false; + allowMultiple = false; constructor(key) { this.key = key; @@ -55,4 +56,9 @@ export class Field { this.isDisabled = true; return this; } + + multiple() { + this.allowMultiple = true; + return this; + } } diff --git a/assets/scripts/FieldInput.js b/assets/scripts/FieldInput.js index 20ba004..70c46c1 100644 --- a/assets/scripts/FieldInput.js +++ b/assets/scripts/FieldInput.js @@ -1,29 +1,66 @@ +import { FieldInputItem } from "./FieldInputItem.js"; + /** * Represents the actual input element on the creator tool - * A field may have multiple inputs if it allows multiple values + * May contain multiple items if the field allows multiple values */ export class FieldInput { constructor(field, parentElem) { this.field = field; this.id = "field-" + field.key; + this.items = []; + this.parent = parentElem; - if (!field.isHidden) - parentElem.innerHTML += ` - -

${field.description ?? ""}

- ${field.getInputHtml(this.id)} - `; + this.appendHtml(parentElem); } - isValid() { - return this.field.isValidInput(this.id); + appendHtml(parent) { + const fieldContainer = document.createElement("div"); + fieldContainer.className = "field-container"; + + const item = new FieldInputItem(this, fieldContainer); + + if (!this.field.isHidden) { + parent.insertAdjacentHTML("beforeend", ` + +

${this.field.description ?? ""}

+ `); + + parent.appendChild(fieldContainer); + + if (this.field.allowMultiple && this.items.length === 0) { + const addBtn = document.createElement("button"); + addBtn.className = "text-button"; + addBtn.type = "button"; + addBtn.innerText = "+ Add"; + addBtn.onclick = () => this.addItem(fieldContainer); + parent.appendChild(addBtn); + } + } + + this.items.push(item); + this.updateItems(); } - getValue() { - return this.field.getInputValue(this.id); + updateItems() { + for (const item of this.items) item.update(); } - toString() { - return this.field.inputToString(this.id); + addItem(parent) { + const item = new FieldInputItem(this, parent); + this.items.push(item); + + this.updateItems(); + } + + removeItem(id) { + const i = this.items.findIndex(item => item.id === id); + + this.items[i].remove(); + this.items.splice(i, 1); + + this.updateItems(); + + this.parent.closest("form").onchange(); } } diff --git a/assets/scripts/FieldInputItem.js b/assets/scripts/FieldInputItem.js new file mode 100644 index 0000000..5cd7910 --- /dev/null +++ b/assets/scripts/FieldInputItem.js @@ -0,0 +1,60 @@ +export class FieldInputItem { + container = null; + removeBtn = null; + + constructor(input, parentElem) { + this.input = input; + this.field = input.field; + this.id = input.id + (input.field.allowMultiple ? "-" + Math.random().toString().slice(2, 5) : ""); + + this.appendHtml(parentElem); + } + + appendHtml(parent) { + const container = document.createElement("div"); + container.style.display = "flex"; + container.style.gap = "0.5rem"; + + if (this.field.allowMultiple) { + const wrapper = document.createElement("div"); + wrapper.innerHTML = this.field.getInputHtml(this.id); + container.appendChild(wrapper); + + const removeBtn = document.createElement("button"); + removeBtn.type = "button"; + removeBtn.className = "text-button"; + removeBtn.innerHTML = "× Remove"; + removeBtn.onclick = () => this.input.removeItem(this.id); + container.appendChild(removeBtn); + + this.removeBtn = removeBtn; + } else { + container.innerHTML = this.field.getInputHtml(this.id); + } + + parent.appendChild(container); + this.container = container; + } + + update() { + if (this.removeBtn) { + this.removeBtn.style.display = this.input.items.length > 1 ? "inline-block" : "none"; + } + } + + remove() { + this.container.remove(); + } + + isValid() { + return this.field.isValidInput(this.id); + } + + getValue() { + return this.field.getInputValue(this.id); + } + + toString() { + return this.field.inputToString(this.id); + } +} diff --git a/assets/scripts/records/SpfRecord.js b/assets/scripts/records/SpfRecord.js index fd17a4b..e964685 100644 --- a/assets/scripts/records/SpfRecord.js +++ b/assets/scripts/records/SpfRecord.js @@ -86,10 +86,10 @@ export class SpfRecord { this.text = text; } - static createFromFieldInputs(fieldInputs) { - const tokens = fieldInputs - .filter(input => !input.field.isDisabled && input.isValid()) - .map(input => input.toString()); + static createFromFieldInputItems(items) { + const tokens = items + .filter(item => !item.field.isDisabled && item.isValid()) + .map(item => item.toString()); const text = tokens.join(" "); diff --git a/assets/scripts/records/TagListRecord.js b/assets/scripts/records/TagListRecord.js index 5dbb2cc..526e664 100644 --- a/assets/scripts/records/TagListRecord.js +++ b/assets/scripts/records/TagListRecord.js @@ -8,10 +8,10 @@ export class TagListRecord { this.text = text; } - static createFromFieldInputs(fieldInputs) { - const tokens = fieldInputs - .filter(input => !input.field.isDisabled && input.isValid()) - .filter(input => !input.field.defaultValue || input.getValue() !== input.field.defaultValue) + static createFromFieldInputItems(items) { + const tokens = items + .filter(item => !item.field.isDisabled && item.isValid()) + .filter(item => !item.field.defaultValue || item.getValue() !== item.field.defaultValue) .map(input => input.toString()); const text = tokens.join("; "); diff --git a/assets/scripts/spf/Term.js b/assets/scripts/spf/Term.js index 33ac449..32c6cd3 100644 --- a/assets/scripts/spf/Term.js +++ b/assets/scripts/spf/Term.js @@ -5,7 +5,6 @@ export class Term extends Field { separator = null; isRequired = false; position = null; - allowMultiple = false; valueRequirement = ValueRequirement.REQUIRED; constructor(key) { @@ -52,11 +51,6 @@ export class Term extends Field { return this; } - multiple() { - this.allowMultiple = true; - return this; - } - value(requirement) { this.valueRequirement = requirement; return this; diff --git a/assets/scripts/ui/creator.js b/assets/scripts/ui/creator.js index bacbb45..1004a02 100644 --- a/assets/scripts/ui/creator.js +++ b/assets/scripts/ui/creator.js @@ -36,7 +36,12 @@ document.getElementById("form").onchange = generate; generate(); function generate() { - const record = Record.createFromFieldInputs(inputs); + const items = []; + for (const input of inputs) { + items.push(...input.items); + } + + const record = Record.createFromFieldInputItems(items); document.getElementById("record").value = record.text; } diff --git a/assets/styles/main.css b/assets/styles/main.css index 2ff2666..7b389c9 100644 --- a/assets/styles/main.css +++ b/assets/styles/main.css @@ -164,3 +164,23 @@ summary { summary:hover { color: black; } + +.field-container { + display: flex; + flex-direction: column; + align-items: start; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.text-button { + background: none; + border: none; + cursor: pointer; + color: #757575; + transition: color 200ms; +} + +.text-button:hover { + color: black; +}