Compare commits
2 Commits
041017adc3
...
82eb8a2733
Author | SHA1 | Date |
---|---|---|
jordi fita mas | 82eb8a2733 | |
jordi fita mas | 356d0a0892 |
|
@ -56,7 +56,7 @@ func mustCollectInvoiceEntries(ctx context.Context, conn *Conn, company *Company
|
|||
, invoice_number
|
||||
, contact.business_name
|
||||
, contact.slug
|
||||
, array_agg(tag.name::text)
|
||||
, array_agg(coalesce(tag.name::text, ''))
|
||||
, invoice.invoice_status
|
||||
, isi18n.name
|
||||
, to_price(total, decimal_digits)
|
||||
|
@ -609,7 +609,11 @@ func mustGetTaxOptions(ctx context.Context, conn *Conn, company *Company) []*Sel
|
|||
|
||||
func (form *invoiceForm) SplitTags() []string {
|
||||
reg := regexp.MustCompile("[^a-z0-9-]+")
|
||||
return strings.Split(reg.ReplaceAllString(form.Tags.Val, " "), " ")
|
||||
tags := strings.Split(reg.ReplaceAllString(form.Tags.Val, ","), ",")
|
||||
if len(tags) == 1 && len(tags[0]) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
type invoiceProductForm struct {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'});
|
||||
|
|
|
@ -29,6 +29,23 @@
|
|||
</div>
|
||||
{{- end }}
|
||||
|
||||
{{ define "tags-field" -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.InputField*/ -}}
|
||||
<div class="input {{ if .Errors }}has-errors{{ end }}" is="numerus-tags">
|
||||
<input type="text" name="{{ .Name }}" id="{{ .Name }}-field"
|
||||
{{- range $attribute := .Attributes }} {{$attribute}} {{ end }}
|
||||
{{ if .Required }}required="required"{{ end }} value="{{ .Val }}" placeholder="{{ .Label }}">
|
||||
<label for="{{ .Name }}-field">{{ .Label }}</label>
|
||||
{{- if .Errors }}
|
||||
<ul>
|
||||
{{- range $error := .Errors }}
|
||||
<li>{{ . }}</li>
|
||||
{{- end }}
|
||||
</ul>
|
||||
{{- end }}
|
||||
</div>
|
||||
{{- end }}
|
||||
|
||||
{{ define "hidden-select-field" -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.SelectField*/ -}}
|
||||
{{- range $selected := .Selected }}
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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 }}
|
||||
|
||||
|
|
Loading…
Reference in New Issue