From 0a43b6c3e9ac675c1d6d03aa9a08f6aecb8bfcc0 Mon Sep 17 00:00:00 2001 From: Sam Lee Date: Mon, 29 Mar 2021 00:19:04 +0200 Subject: [PATCH 1/3] Add bwc-combox component --- main.js | 25 +- src/components/bwc-combobox.js | 416 +++++++++++++++++++++++++++++++++ 2 files changed, 429 insertions(+), 12 deletions(-) mode change 100644 => 100755 main.js create mode 100755 src/components/bwc-combobox.js diff --git a/main.js b/main.js old mode 100644 new mode 100755 index e6ad15e..d6bedf5 --- a/main.js +++ b/main.js @@ -1,12 +1,13 @@ -import { createApp } from 'vue' -import App from './App.vue' -import router from './router.js' -import store from './store.js' -import './public/index.css' -import './src/components/bwc-autocomplete.js' -import './src/components/bwc-table.js' - -const app = createApp(App) -app.use(router) -app.use(store) -app.mount('#app') +import { createApp } from 'vue' +import App from './App.vue' +import router from './router.js' +import store from './store.js' +import './public/index.css' +import './src/components/bwc-autocomplete.js' +import './src/components/bwc-table.js' +import './src/components/bwc-combobox.js' + +const app = createApp(App) +app.use(router) +app.use(store) +app.mount('#app') diff --git a/src/components/bwc-combobox.js b/src/components/bwc-combobox.js new file mode 100755 index 0000000..3bb11a0 --- /dev/null +++ b/src/components/bwc-combobox.js @@ -0,0 +1,416 @@ +// Combobox with autocomplete component using input, datalist and tags +// TBD fix multi select init in bwc-t4t-form.js +// TBD Initially if no data for the list, please fetch some +// TBD allow configurable classnames for tag and tag wrapper, clear icons (for bootstrap, muicss) +// TBD single select clear value if not found and custom tags not allowed +// TBD use ul/li instead of datalist (big change) +// OR https://stackoverflow.com/questions/30022728/perform-action-when-clicking-html5-datalist-option + +/* +attributes: +- value (via v-model), at the text input +- required +- disabled +- listid (needed if using more than 2 components on the same page) +- input-class (style the input) +- multiple (v2) +- repeat (v2) for multiple selects allow same item to be selected many times +- allow-custom-tag (v2) - allow user defined tags +- object-key +- object-text +- tag-limit - maximum allowed tags +properties: +- items [string] or [{ key, text }] +- tags [string] or [{ key, text }] +methods: +- _setList(items) // should be private, called when items property changes +- _setTags(tags) // should be private, called with tags property changes +events emitted: +- @input (via v-model) - e.target.value +- @search - e.detail String +- @select - e.detail String or Object or null +if selected data is null (no match found, else match found) +Usage with (VueJS): + +// string version +const ac = reactive({ value: 'a', items: ['aa9','aa5'] }) +const autoComplete = (e) => { + const list = ['aa1', 'aa15', 'aa16', 'aa17', 'aa18', 'aa19', 'aa20', 'aa21', 'aa22', 'aa23'] + const result = [] + for (let i = 0; i < list.length; i++) { + if (list[i].includes(e.detail)) result.push(list[i]) + } + ac.items = result +} +// object version +[ + { key: 'unique', text: 'longer description' } +] +*/ + +const template = document.createElement('template') +template.innerHTML = /*html*/ ` + + + + + +` + +class BwcCombobox extends HTMLElement { + // local properties + #items = [] // list of items + #tags = [] // multi-select + #selected = null // single-select + #key = '' // must have both, other wise string is assumed? + #text = '' + + #elTags = null // div.tags element + #elInput = null // input element + #elList = null // datalist element + #elClearBtn = null // clear button + + #multiple = false // hold readonly attributes + #repeat = false // for multiselect, tag can be added multiple times + #allowCustomTag = false // can add new items + #tagLimit = 0 // unlimited tags + + constructor() { + super() + this._onInput = this._onInput.bind(this) + } + + connectedCallback() { + this.appendChild(template.content.cloneNode(true)) + + this.#elInput = this.querySelector('input') + this.#elList = this.querySelector('datalist') + this.#elClearBtn = this.querySelector('.clear-btn') + + this.#elList.id = this.listid // console.log('listid', this.listid) + this.#elInput.setAttribute('list', this.listid) + this.#elInput.addEventListener('input', this._onInput) + + this.#allowCustomTag = this.hasAttribute('allow-custom-tag') + this.#multiple = this.hasAttribute('multiple') + this.#repeat = this.hasAttribute('repeat') + + if (this.hasAttribute('tag-limit')) + this.#tagLimit = Number(this.getAttribute('tag-limit')) + if (this.hasAttribute('object-key')) + this.#key = this.getAttribute('object-key') + if (this.hasAttribute('object-text')) + this.#text = this.getAttribute('object-text') + + if (this.#multiple) { + // if multiple... use tags + this.#elTags = document.createElement('div') + this.#elTags.className = 'tags' + // this.prepend(this.#elTags) + this.append(this.#elTags) + } + + this.#elClearBtn.onclick = (e) => { + this.#elInput.value = '' + if (!this.#multiple) { + // console.log('clear button click') + this.#selected = null + this.dispatchEvent( + new CustomEvent('select', { detail: this.#selected }) + ) + } + } + this.#elInput.onblur = (e) => { + // console.log('onblur', e) + const found = this.items.find((item) => this._itemMatchInput(item)) + if (this.#multiple) { + // multiple + if (!found) { + // not found + if (this.#allowCustomTag) { + // can add new + this._addTag(this._makeItemFromValue()) + this.dispatchEvent( + new CustomEvent('select', { detail: this.#tags }) + ) + } + } else { + // if repeatable? set tags list if not there already + this._addTag(found) + this.dispatchEvent(new CustomEvent('select', { detail: this.#tags })) + } + this.value = '' + } else { + // single + if (!found) { + // not found + if (this.#selected) { + console.log( + 'onBlur - single select - not found and this.#selected truthy' + ) + this.#selected = null + this.dispatchEvent( + new CustomEvent('select', { detail: this.#selected }) + ) + } + } else { + if (!this.#selected) { + console.log( + 'onBlur - single select - found and this.#selected falsy' + ) + this.#selected = found + this.dispatchEvent( + new CustomEvent('select', { detail: this.#selected }) + ) + } + } + } + } + + console.log( + 'combo box connected', + this.required, + this.disabled, + this.inputClass + ) + this.#elInput.value = this.value + this.#elInput.className = this.inputClass || 'input' // default to bulma - // if (this.hasAttribute('input-class')) el.setAttribute('class', this.getAttribute('input-class')) + this.required + ? this.#elInput.setAttribute('required', '') + : this.#elInput.removeAttribute('required') + this.disabled + ? this.#elInput.setAttribute('disabled', '') + : this.#elInput.removeAttribute('disabled') + this._setList(this.items) + + this.dispatchEvent(new CustomEvent('load')) + } + + disconnectedCallback() { + this.#elInput.removeEventListener('input', this._onInput) + } + + attributeChangedCallback(name, oldVal, newVal) { + const el = this.#elInput + switch (name) { + case 'value': { + if (el) el.value = newVal // v-model affects this + this.dispatchEvent(new CustomEvent('input', { detail: newVal })) + break + } + case 'required': { + el && el.setAttribute('required', '') + break + } + case 'disabled': { + el && el.setAttribute('disabled', '') + break + } + case 'input-class': { + if (el) el.className = newVal + break + } + default: + break + } + } + + static get observedAttributes() { + return ['value', 'required', 'listid', 'disabled', 'input-class'] + } + + get value() { + return this.getAttribute('value') + } + set value(val) { + this.setAttribute('value', val) + } + + get required() { + return this.hasAttribute('required') + } + set required(val) { + val ? this.setAttribute('required', '') : this.removeAttribute('required') + } + + get listid() { + return this.getAttribute('listid') + } + set listid(val) { + this.setAttribute('listid', val) + } + + get disabled() { + return this.hasAttribute('disabled') + } + set disabled(val) { + val ? this.setAttribute('disabled', '') : this.removeAttribute('disabled') + } + + get inputClass() { + return this.getAttribute('input-class') + } + set inputClass(val) { + this.setAttribute('input-class', val) + } + + // properties + get items() { + return this.#items + } + set items(val) { + // console.log('set items', val.length) + this.#items = val + this._setList(val) + } + + // multi-select + get tags() { + return this.#tags + } + set tags(val) { + this._setTags(val) + } // this.#tags will be set in _setTags() + + // single-select + get selected() { + return this.#selected + } + set selected(val) { + this.#selected = val + } // TBD set it correctly + + _isStringType() { + // is list item and selected values string ? + return !(this.#key && this.#text) // console.log('_isStringType', !(this.#key && this.#text)) + } + _itemMatchInput(item) { + // item match to text input + return this._isStringType() + ? item === this.value + : item[this.#key] === this.value || item[this.#text] === this.value + } + _matchItems(item1, item2) { + // item match to another item + if (item1 === null && item2 === null) return true + else if (item1 === null) return false + else if (item2 === null) return false + return this._isStringType() + ? item1 === item2 + : item1[this.#key] === item2[this.#key] || + item1[this.#text] === item2[this.#text] + } + _makeItemFromValue() { + // TBD if all spaces only... return? trim white spaces? + return this._isStringType() + ? this.value + : { [this.#key]: this.value, [this.#text]: this.value } + } + + _tagLimitReached() { + return this.#tagLimit && this.#elTags.children.length >= this.#tagLimit + } + _addTag(item) { + if (this._tagLimitReached()) return + const itemExists = this.#tags.find((tag) => + this._isStringType() + ? tag === item + : tag[this.#key] === item[this.#key] && + tag[this.#text] === item[this.#text] + ) + if (!this.#repeat && itemExists) return // duplicates not allowed + const span = document.createElement('span') + span.className = 'tag is-black' + span.innerText = this._isStringType() ? item : item[this.#text] + span.value = this._isStringType() ? item : item[this.#key] + span.onclick = (e) => { + // e.target.innerText, e.target.value + this._removeTag(span) + this.dispatchEvent(new CustomEvent('select', { detail: this.#tags })) + } + this.#elTags.appendChild(span) + this._updateTags() + if (this._tagLimitReached()) this.#elInput.setAttribute('disabled', '') + } + _updateTags() { + let tags = [...this.#elTags.children] + this.#tags = tags.map((tag) => + this._isStringType() + ? tag.innerText + : { [this.#key]: tag.value, [this.#text]: tag.innerText } + ) + // console.log('_updateTags', this.#tags) + } + _removeTag(span) { + this.#elTags.removeChild(span) + this._updateTags() + if (!this._tagLimitReached() && !this.disabled) + this.#elInput.removeAttribute('disabled') + } + + _onInput(e) { + // whether clicked or typed + // console.log('_onInput', e.target.value, this.items.length) + const prevItem = this.#selected + this.value = this.#elInput.value + + const found = this.items.find((item) => this._itemMatchInput(item)) + if (!found) { + // not found + this.#selected = null + this.dispatchEvent(new CustomEvent('search', { detail: this.value })) + } else { + this.#selected = found + } + if (!this._matchItems(prevItem, this.#selected) && !this.#multiple) { + console.log('_onInput - selected && provItem not match this.#selected') + if (!this.#selected && this.allowCustomTag) { + this.#selected = this._makeItemFromValue() + } + this.dispatchEvent(new CustomEvent('select', { detail: this.#selected })) + } + } + + _setTags(_tags) { + // console.log('_setTags', _tags, this.#elTags) + if (!this.#elTags) return + this.#elTags.innerHTML = '' + this.#tags = [] + _tags.forEach((tag) => this._addTag(tag)) + this.dispatchEvent(new CustomEvent('select', { detail: this.#tags })) + } + + _setList(_items) { + // set list items + // console.log('items', _items, this.value) + const dd = this.#elList + if (!dd) return + while (dd.firstChild) { + dd.removeChild(dd.lastChild) + } + if (typeof _items !== 'object') return + + // EVENT TEST START + // dd.style.pointerEvents = 'all' + // dd.style.cursor = 'pointer' + // dd.onclick = (e) => console.log('whwhwhwh22a') + // dd.onmousedown = (e) => console.log('whwhwhwh22b') + // EVENT TEST END + _items.forEach((item) => { + const li = document.createElement('option') + li.innerHTML = typeof item === 'string' ? item : item[this.#key] + li.value = typeof item === 'string' ? item : item[this.#text] + // EVENT TEST START + // li.style.pointerEvents = 'all' + // li.style.cursor = 'pointer' + // li.onclick = (e) => console.log('whwhwhwha') + // li.onmousedown = (e) => console.log('whwhwhwhb') + // li.addEventListener('click', (e) => console.log('whwhwhwh'), true) + // li.onmousedown // useless on a datalist with listid... + // EVENT TEST END + dd.appendChild(li) + }) + } +} + +customElements.define('bwc-combobox', BwcCombobox) From ca1410a9195b238ba90027df0a75627ed3bb4346 Mon Sep 17 00:00:00 2001 From: Sam Lee Date: Sat, 24 Apr 2021 01:25:40 +0200 Subject: [PATCH 2/3] Add Tags --- src/components/CaseDetails.vue | 72 +++++++++++++++++++++++++++++++++- src/components/bwc-combobox.js | 1 - 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/components/CaseDetails.vue b/src/components/CaseDetails.vue index 4c256d9..694e950 100644 --- a/src/components/CaseDetails.vue +++ b/src/components/CaseDetails.vue @@ -272,6 +272,24 @@ +
+
+ +
+
+ -
-
-
Amount requested
- - -
-
- - -
-
-
ONE (Singapore) P.O.C.
- - -
-
-
-
-
Application date
- - -
-
- -
- -
-
-
- -
- -
-
-
Request Type
-
- -
-
-
- - - -
-
-
Fulfilment
-
- -
-
-
- -
-
-
Description
- -
-
- -
-
- -
- -
-
-
- -
- -
-
-
- - - - - - - - + + + + + +