diff --git a/web/static/numerus.css b/web/static/numerus.css index fbe5c95..31a5ac6 100644 --- a/web/static/numerus.css +++ b/web/static/numerus.css @@ -588,12 +588,12 @@ main > nav { right: 0; } -/* Multiselect */ +/* Multiselect, tags */ [is="numerus-multiselect"] { max-width: 35rem; } -[is="numerus-multiselect"] .tags, [is="numerus-multiselect"] .options { +[is="numerus-multiselect"] .tags, [is="numerus-tags"] .tags, [is="numerus-multiselect"] .options { font-size: 1em; list-style: none; color: var(--numerus--text-color); @@ -602,15 +602,23 @@ main > nav { border-radius: 0; } -[is="numerus-multiselect"] .tags { +[is="numerus-multiselect"] .tags, [is="numerus-tags"] .tags { display: flex; flex-wrap: wrap; gap: 1rem; - padding: 1rem 4rem 1rem 2rem; position: relative; + min-width: 20rem; +} + +[is="numerus-multiselect"] .tags { + padding: 1rem 4rem 1rem 2rem; cursor: pointer; min-height: calc(1.5em + 2rem); - min-width: 20rem; +} + +[is="numerus-tags"] .tags { + padding: 1rem 2rem; + max-width: 40rem; } [is="numerus-multiselect"] .tags:after { @@ -625,22 +633,22 @@ main > nav { cursor: pointer; } -[is="numerus-multiselect"] .tag { +[is="numerus-multiselect"] .tag, [is="numerus-tags"] .tag { background-color: var(--numerus--color--hay); } -[is="numerus-multiselect"] .tag button { +[is="numerus-multiselect"] .tag button, [is="numerus-tags"] .tag button { border: none; cursor: pointer; background: transparent; min-width: initial; } -[is="numerus-multiselect"] .tag button:hover { +[is="numerus-multiselect"] .tag button:hover, [is="numerus-tags"] .tag button:hover { background: rgba(255, 255, 255, .4); } -[is="numerus-multiselect"] .tags input { +[is="numerus-multiselect"] .tags input, [is="numerus-tags"] .tags input { flex: 1; width: 100%; border: 0; diff --git a/web/static/numerus.js b/web/static/numerus.js index c51eebe..d668082 100644 --- a/web/static/numerus.js +++ b/web/static/numerus.js @@ -273,4 +273,130 @@ class Multiselect extends HTMLDivElement { } } +class Tags extends HTMLDivElement { + constructor() { + super(); + this.initialized = false; + this.tags = []; + } + + connectedCallback() { + if (!this.initialized) { + for (const child of this.children) { + switch (child.nodeName) { + case 'INPUT': + this.input = child; + break; + case 'LABEL': + this.label = child; + break; + } + } + if (this.label && this.input) { + this.init(); + } + } + } + + init() { + this.initialized = true; + + this.tagList = document.createElement('div'); + this.append(this.tagList); + this.tagList.classList.add('tags'); + + this.input.type = 'hidden'; + const tagsText = this.input.value.split(','); + for (const tagText of tagsText) { + const tagNormalized = Tags.normalize(tagText); + if (tagNormalized !== '' && this.tags.indexOf(tagNormalized) === -1) { + this.tags.push(tagNormalized); + } + } + + this.search = document.createElement('input'); + this.tagList.append(this.search); + this.search.id = this.input.id; + this.input.removeAttribute('id'); + this.input.setAttribute('aria-hidden', 'true'); + this.search.setAttribute('spellcheck', 'false'); + this.search.setAttribute('autocomplete', 'false'); + this.search.addEventListener('keydown', (e) => { + switch (e.code) { + case "Space": + e.preventDefault(); + // fallthrough + case "Enter": + if (e.target.value.trim() !== '') { + e.preventDefault(); + this.createTag(); + } + break; + case "Backspace": + if (e.target.value === '') { + this.removeLastTag(); + } + break; + } + }); + + // Must come after the search input + this.tagList.append(this.label); + + this.rebuild(); + } + + rebuild() { + this.input.value = this.tags.join(','); + this.tagList.querySelectorAll('.tag').forEach((tag) => tag.remove()); + if (this.tags.length === 0) { + this.search.setAttribute('placeholder', this.label.textContent); + } else { + this.search.removeAttribute('placeholder'); + for (const tagText of this.tags) { + const tag = document.createElement('div'); + this.tagList.insertBefore(tag, this.search); + tag.classList.add('tag'); + + const span = document.createElement('span'); + tag.append(span); + span.textContent = tagText; + + const button = document.createElement('button'); + tag.append(button); + button.type = 'button'; + button.textContent = '×'; + button.addEventListener('click', (e) => { + e.stopPropagation(); + this.removeTag(tagText) + }); + } + } + } + + createTag() { + const tagText = Tags.normalize(this.search.value); + this.search.value = ''; + if (this.tags.indexOf(tagText) === -1) { + this.tags.push(tagText); + this.rebuild(); + } + } + + static normalize(s) { + return s.normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase().trim(); + } + + removeLastTag() { + this.tags.pop(); + this.rebuild(); + } + + removeTag(tagText) { + this.tags.splice(this.tags.indexOf(tagText), 1); + this.rebuild(); + } +} + customElements.define('numerus-multiselect', Multiselect, {extends: 'div'}); +customElements.define('numerus-tags', Tags, {extends: 'div'}); diff --git a/web/template/form.gohtml b/web/template/form.gohtml index 32f88f6..6cb34f2 100644 --- a/web/template/form.gohtml +++ b/web/template/form.gohtml @@ -29,6 +29,23 @@ {{- end }} +{{ define "tags-field" -}} + {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.InputField*/ -}} +
+ + + {{- if .Errors }} + + {{- end }} +
+{{- end }} + {{ define "hidden-select-field" -}} {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.SelectField*/ -}} {{- range $selected := .Selected }} diff --git a/web/template/invoices/edit.gohtml b/web/template/invoices/edit.gohtml index e2875c3..fff36c1 100644 --- a/web/template/invoices/edit.gohtml +++ b/web/template/invoices/edit.gohtml @@ -20,7 +20,7 @@ {{ template "select-field" .Customer }} {{ template "hidden-field" .Number }} {{ template "hidden-field" .Date }} - {{ template "input-field" .Tags }} + {{ template "tags-field" .Tags }} {{ template "select-field" .PaymentMethod }} {{ template "select-field" .InvoiceStatus }} {{ template "input-field" .Notes }} diff --git a/web/template/invoices/new.gohtml b/web/template/invoices/new.gohtml index 8eb9e87..afbdde3 100644 --- a/web/template/invoices/new.gohtml +++ b/web/template/invoices/new.gohtml @@ -21,7 +21,7 @@ {{ template "select-field" .Customer }} {{ template "input-field" .Number }} {{ template "input-field" .Date }} - {{ template "input-field" .Tags }} + {{ template "tags-field" .Tags }} {{ template "select-field" .PaymentMethod }} {{ template "input-field" .Notes }}