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
|
, invoice_number
|
||||||
, contact.business_name
|
, contact.business_name
|
||||||
, contact.slug
|
, contact.slug
|
||||||
, array_agg(tag.name::text)
|
, array_agg(coalesce(tag.name::text, ''))
|
||||||
, invoice.invoice_status
|
, invoice.invoice_status
|
||||||
, isi18n.name
|
, isi18n.name
|
||||||
, to_price(total, decimal_digits)
|
, to_price(total, decimal_digits)
|
||||||
|
@ -583,7 +583,7 @@ func (form *invoiceForm) MustFillFromDatabase(ctx context.Context, conn *Conn, s
|
||||||
, invoice_date
|
, invoice_date
|
||||||
, notes
|
, notes
|
||||||
, payment_method_id
|
, payment_method_id
|
||||||
, string_agg(tag.name, ', ')
|
, string_agg(tag.name, ',')
|
||||||
from invoice
|
from invoice
|
||||||
left join invoice_tag using (invoice_id)
|
left join invoice_tag using (invoice_id)
|
||||||
left join tag using(tag_id) where slug = $1
|
left join tag using(tag_id) where slug = $1
|
||||||
|
@ -609,7 +609,11 @@ func mustGetTaxOptions(ctx context.Context, conn *Conn, company *Company) []*Sel
|
||||||
|
|
||||||
func (form *invoiceForm) SplitTags() []string {
|
func (form *invoiceForm) SplitTags() []string {
|
||||||
reg := regexp.MustCompile("[^a-z0-9-]+")
|
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 {
|
type invoiceProductForm struct {
|
||||||
|
|
|
@ -588,12 +588,12 @@ main > nav {
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Multiselect */
|
/* Multiselect, tags */
|
||||||
[is="numerus-multiselect"] {
|
[is="numerus-multiselect"] {
|
||||||
max-width: 35rem;
|
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;
|
font-size: 1em;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
color: var(--numerus--text-color);
|
color: var(--numerus--text-color);
|
||||||
|
@ -602,15 +602,23 @@ main > nav {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[is="numerus-multiselect"] .tags {
|
[is="numerus-multiselect"] .tags, [is="numerus-tags"] .tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 1rem 4rem 1rem 2rem;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
min-width: 20rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[is="numerus-multiselect"] .tags {
|
||||||
|
padding: 1rem 4rem 1rem 2rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
min-height: calc(1.5em + 2rem);
|
min-height: calc(1.5em + 2rem);
|
||||||
min-width: 20rem;
|
}
|
||||||
|
|
||||||
|
[is="numerus-tags"] .tags {
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
max-width: 40rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
[is="numerus-multiselect"] .tags:after {
|
[is="numerus-multiselect"] .tags:after {
|
||||||
|
@ -625,22 +633,22 @@ main > nav {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
[is="numerus-multiselect"] .tag {
|
[is="numerus-multiselect"] .tag, [is="numerus-tags"] .tag {
|
||||||
background-color: var(--numerus--color--hay);
|
background-color: var(--numerus--color--hay);
|
||||||
}
|
}
|
||||||
|
|
||||||
[is="numerus-multiselect"] .tag button {
|
[is="numerus-multiselect"] .tag button, [is="numerus-tags"] .tag button {
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
min-width: initial;
|
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);
|
background: rgba(255, 255, 255, .4);
|
||||||
}
|
}
|
||||||
|
|
||||||
[is="numerus-multiselect"] .tags input {
|
[is="numerus-multiselect"] .tags input, [is="numerus-tags"] .tags input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
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-multiselect', Multiselect, {extends: 'div'});
|
||||||
|
customElements.define('numerus-tags', Tags, {extends: 'div'});
|
||||||
|
|
|
@ -29,6 +29,23 @@
|
||||||
</div>
|
</div>
|
||||||
{{- end }}
|
{{- 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" -}}
|
{{ define "hidden-select-field" -}}
|
||||||
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.SelectField*/ -}}
|
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.SelectField*/ -}}
|
||||||
{{- range $selected := .Selected }}
|
{{- range $selected := .Selected }}
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
{{ template "select-field" .Customer }}
|
{{ template "select-field" .Customer }}
|
||||||
{{ template "hidden-field" .Number }}
|
{{ template "hidden-field" .Number }}
|
||||||
{{ template "hidden-field" .Date }}
|
{{ template "hidden-field" .Date }}
|
||||||
{{ template "input-field" .Tags }}
|
{{ template "tags-field" .Tags }}
|
||||||
{{ template "select-field" .PaymentMethod }}
|
{{ template "select-field" .PaymentMethod }}
|
||||||
{{ template "select-field" .InvoiceStatus }}
|
{{ template "select-field" .InvoiceStatus }}
|
||||||
{{ template "input-field" .Notes }}
|
{{ template "input-field" .Notes }}
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
{{ template "select-field" .Customer }}
|
{{ template "select-field" .Customer }}
|
||||||
{{ template "input-field" .Number }}
|
{{ template "input-field" .Number }}
|
||||||
{{ template "input-field" .Date }}
|
{{ template "input-field" .Date }}
|
||||||
{{ template "input-field" .Tags }}
|
{{ template "tags-field" .Tags }}
|
||||||
{{ template "select-field" .PaymentMethod }}
|
{{ template "select-field" .PaymentMethod }}
|
||||||
{{ template "input-field" .Notes }}
|
{{ template "input-field" .Notes }}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue