diff --git a/dev/html/multiple-select.html b/dev/html/multiple-select.html
new file mode 100644
index 0000000..13b2e41
--- /dev/null
+++ b/dev/html/multiple-select.html
@@ -0,0 +1,21 @@
+
+ Multiple select
+
+
+
\ No newline at end of file
diff --git a/dev/js/test-multiple-select.js b/dev/js/test-multiple-select.js
new file mode 100644
index 0000000..d9a428a
--- /dev/null
+++ b/dev/js/test-multiple-select.js
@@ -0,0 +1,8 @@
+import html from "../html/multiple-select.html";
+
+export function testMultipleSelect(root){
+ root.insertAdjacentHTML('beforeend', html);
+
+ // Init: default layout
+ EasySelect.init();
+}
\ No newline at end of file
diff --git a/dev/script.js b/dev/script.js
index 5ca610c..5bb21b8 100644
--- a/dev/script.js
+++ b/dev/script.js
@@ -12,6 +12,7 @@ import {testLayout} from "./js/test-layout";
import {testMethods} from "./js/test-methods";
import {testDisabled} from "./js/test-disabled";
import {testSearch} from "./js/test-search";
+import {testMultipleSelect} from "./js/test-multiple-select";
// import package info
const packageInfo = require('../package.json');
@@ -34,4 +35,5 @@ testMethods(root);
testSearch(root);
testLayout(root);
testInit(root);
-testDisabled(root);
\ No newline at end of file
+testDisabled(root);
+testMultipleSelect(root);
\ No newline at end of file
diff --git a/dev/style.scss b/dev/style.scss
index ff43a60..0492b35 100644
--- a/dev/style.scss
+++ b/dev/style.scss
@@ -16,8 +16,9 @@ section {
}
.col-right {
- width: 200px;
+ width: 400px;
display: block;
+ text-align: right;
}
code {
diff --git a/src/_index.js b/src/_index.js
index 21f855f..9a1babf 100644
--- a/src/_index.js
+++ b/src/_index.js
@@ -1,7 +1,7 @@
import {getSelectData, val} from "./data";
import {fireOnChangeEvent, init} from "./methods";
-import {getOptionHTML, updateDropdownHTML} from "./layout";
-import {findObjectInArray, getSelectTag} from "./utils";
+import {getCurrentInnerHTML, getOptionHTML, updateDropdownHTML} from "./layout";
+import {findObjectInArray, getOptionByValue, getSelectTag} from "./utils";
import {EventsManager, getOptionsFromAttribute} from "@phucbm/os-util";
import {CLASSES, ATTRS, DEFAULTS} from './configs'
@@ -114,7 +114,7 @@ class EasySelect{
* @param disabled
*/
disableOption(optionValue, disabled){
- const option = this.selectTag.querySelector(`option[value="${optionValue}"]`);
+ const option = getOptionByValue(this, optionValue);
if(!option){
console.warn(`Option with value "${optionValue}" is not found.`);
@@ -144,7 +144,7 @@ class EasySelect{
if(this.selectTagData.length){
// update current
- this.current.innerHTML = getOptionHTML(this);
+ this.current.innerHTML = getCurrentInnerHTML(this);
// if not native select
if(!this.options.nativeSelect){
@@ -183,18 +183,52 @@ class EasySelect{
select(value){
if(this.isDisabled) return;
- // skip duplicate value
- if(value === val(this)) return;
+ // todo: create isSelectedOption()
+ const isSelected = val(this, 'array').includes(value);
- // value exists in data object => update value
- if(typeof findObjectInArray(this.selectTagData, 'value', value) !== 'undefined'){
- this.selectTag.value = value;
- fireOnChangeEvent(this);
+ // treat selected option
+ if(isSelected){
+ if(this.options.multiple) this.deselect(value);
return;
}
- // warning
- if(this.options.warning) console.warn(`Option[value="${value}"] is not found in this select!`);
+
+ // value not exists in data object => update value
+ if(typeof findObjectInArray(this.selectTagData, 'value', value) === 'undefined'){
+ // warning
+ if(this.options.warning) console.warn(`Option[value="${value}"] is not found in this select!`);
+ return;
+ }
+
+
+ // set selected value to select tag (single select only)
+ // with multi select, update select tag will lead to missing previous selected values
+ if(!this.options.multiple) this.selectTag.value = value;
+
+ // make the option selected
+ const option = getOptionByValue(this, value);
+ option.selected = true;
+
+ fireOnChangeEvent(this);
+ }
+
+
+ /**
+ * Deselect by value
+ * @param value
+ */
+ deselect(value){
+ // todo: bug multi deselect
+ if(this.isDisabled) return;
+
+ // get option
+ const option = getOptionByValue(this, value);
+ if(!option) return;
+
+ // deselect
+ option.selected = false;
+
+ fireOnChangeEvent(this);
}
/**
@@ -222,19 +256,31 @@ class EasySelect{
if(this.isDisabled) return;
// update current HTML
- this.current.innerHTML = getOptionHTML(this);
+ this.current.innerHTML = getCurrentInnerHTML(this);
const newValue = val(this);
+ const newValueArray = val(this, 'array');
/** Dropdown **/
if(!this.options.nativeSelect){
- // active option
- this.dropdown.querySelectorAll(`[${ATTRS.optionAttr}]`).forEach(item => {
- item.classList.remove(CLASSES.active);
+ // todo: this.selectTagData not updated
+ //console.log(this.selectTagData)
+
+ // update active class
+ this.selectTagData.forEach(option => {
+ const isSelected = newValueArray.includes(option.value);
+ if(isSelected){
+ // activate selected values
+ // todo: save dropdown el to selectTagData
+ this.dropdown.querySelector(`[${ATTRS.optionAttr}="${option.value}"]`).classList.add(CLASSES.active);
+ }else{
+ this.dropdown.querySelector(`[${ATTRS.optionAttr}="${option.value}"]`).classList.remove(CLASSES.active);
+ }
});
- this.dropdown.querySelector(`[${ATTRS.optionAttr}="${newValue}"]`).classList.add(CLASSES.active);
// close on change
- if(this.options.closeOnChange) this.close();
+ let isCloseOnChange = this.options.closeOnChange;
+ if(this.options.multiple) isCloseOnChange = false; // not close in multi select
+ if(isCloseOnChange) this.close();
}
// update value attribute
diff --git a/src/_style.scss b/src/_style.scss
index 175be85..1d13812 100644
--- a/src/_style.scss
+++ b/src/_style.scss
@@ -274,3 +274,48 @@
min-height: var(--es-height);
padding: 5px 20px;
}
+
+
+/****************************
+ * Checkbox
+****************************/
+.easy-select.es-multi-select {
+ .es-current-label {
+ max-width: var(--es-dropdown-width);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .es-dropdown {
+ .es-option {
+ position: relative;
+ padding-left: 45px;
+
+ &:not(.es-active) {
+ .es-checkbox:before {
+ opacity: 0;
+ }
+ }
+ }
+
+ .es-checkbox {
+ position: absolute;
+ top: .65em;
+ left: 20px;
+ width: 18px;
+ aspect-ratio: 1;
+ background-color: #e0e7ee;
+ border-radius: 3px;
+
+ &:before {
+ content: '✓';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-size: .7em;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/configs.js b/src/configs.js
index 97edb15..dadbdad 100644
--- a/src/configs.js
+++ b/src/configs.js
@@ -18,6 +18,9 @@ export const CLASSES = {
searchEnabled: 'es-search-enabled',
searchWrapper: 'es-search-wrapper',
searchEmpty: 'es-search-empty',
+
+ // multi select
+ multipleSelect: 'es-multi-select'
}
/**
* Attributes
@@ -39,6 +42,9 @@ export const DEFAULTS = {
closeOnChange: true,
align: "left",
+ multiple: false,
+ multipleLabel: "Select multiple options",
+
// show search input inside dropdown
search: false,
emptySearchText: "There are no options", // optional, text appear when search empty
diff --git a/src/data.js b/src/data.js
index a778ca5..2cdf304 100644
--- a/src/data.js
+++ b/src/data.js
@@ -1,4 +1,4 @@
-import {getIndex, getSelectedOption, stringToSlug} from "./utils";
+import {getIndex, getMultipleSelectedValues, getSelectedOption, stringToSlug} from "./utils";
/****************************************************
********************** Data *********************
@@ -6,10 +6,30 @@ import {getIndex, getSelectedOption, stringToSlug} from "./utils";
/**
* Get value
+ * @param context
+ * @param type
* @returns {*}
*/
-export function val(context){
- context.value = context.selectTag.value;
+export function val(context, type = 'string'){
+ const valueArray = getMultipleSelectedValues(context.selectTag);
+ let value;
+
+ switch(type){
+ case "array":
+ value = valueArray;
+ break;
+ default:
+ // string
+ if(valueArray.length === 1){
+ value = valueArray[0]; // => "value"
+ }else if(valueArray.length > 1){
+ value = valueArray.join(','); // => "value1,value2"
+ }else{
+ value = ''; // => ""
+ }
+ }
+
+ context.value = value;
return context.value;
}
@@ -27,7 +47,9 @@ export function getSelectData(context){
/**
* Get option data
- * @returns {{isSelected: boolean, index: *, id: string, label: *, value: (*|string|number|string[])}}
+ * @param context
+ * @param option
+ * @returns {{el: *, isSelected: *, index: *, id: string, label: string, isDisabled: (*|(() => boolean)|((setting: string) => boolean)|string|boolean), value: *}}
*/
export function getOptionData(context, option = undefined){
if(typeof option === 'undefined'){
@@ -35,13 +57,13 @@ export function getOptionData(context, option = undefined){
option = getSelectedOption(context.selectTag);
}
- const label = option.innerText;
- const value = option.value;
+ const label = option?.innerText;
+ const value = option?.value;
const index = getIndex(option);
const id = stringToSlug(value) + '-' + index;
- const isSelected = value === val(context);
+ const isSelected = val(context, 'array').includes(value); // tested with multi select
const el = option;
- const isDisabled = option.disabled;
+ const isDisabled = option?.disabled;
return {id, label, value, isSelected, isDisabled, index, el};
}
\ No newline at end of file
diff --git a/src/layout.js b/src/layout.js
index 5649c9e..37713df 100644
--- a/src/layout.js
+++ b/src/layout.js
@@ -1,3 +1,4 @@
+import {getOptionByValue} from "./utils";
import {getOptionData, val} from "./data";
import {CLASSES, ATTRS} from "./configs"
@@ -10,10 +11,30 @@ import {CLASSES, ATTRS} from "./configs"
* @returns {string}
*/
export function getCurrentHTML(context){
+ return `
${getCurrentInnerHTML(context)}
`;
+}
+
+export function getCurrentInnerHTML(context){
let html = '';
- html += ``;
- html += getOptionHTML(context);
- html += `
`;
+
+ if(context.options.multiple){
+ // current multiple
+ const selectedValues = val(context, 'array');
+ const labels = [];
+ selectedValues.forEach(value => {
+ const option = getOptionData(context, getOptionByValue(context, value));
+ labels.push(option.label);
+ });
+ html += ``;
+ html += ``;
+ html += labels.length ? labels.join(', ') : context.options.multipleLabel;
+ html += ``;
+ html += `
`;
+ }else{
+ // current single
+ html += getOptionHTML(context);
+ }
+
return html;
}
@@ -75,7 +96,8 @@ export function getDropdownHTML(context){
*/
export function getOptionHTML(context, option = undefined){
// is active
- const isActive = typeof option !== 'undefined' && option['value'] === val(context);
+ // tested with multi select
+ const isActive = typeof option !== 'undefined' && val(context, 'array').includes(option['value']);
// return selected option
if(typeof option === 'undefined'){
@@ -100,11 +122,22 @@ export function getOptionHTML(context, option = undefined){
* @returns {string}
*/
export function getOptionInnerHTML(context, option){
- let html = context.options.customDropDownOptionHTML(option);
+ // return custom HTML if any
+ let customHTML = context.options.customDropDownOptionHTML(option);
+ if(customHTML) return customHTML;
- if(typeof html === 'undefined'){
- html = `${option['label']}`;
+ let html = '';
+
+ // multiple select
+ if(context.options.multiple){
+ // option
+ html += ``;
+ html += `${option['label']}`;
+ return html;
}
+ // single select
+ html = `${option['label']}`;
+
return html;
}
\ No newline at end of file
diff --git a/src/methods.js b/src/methods.js
index c40d96b..237d9d0 100644
--- a/src/methods.js
+++ b/src/methods.js
@@ -1,3 +1,4 @@
+import {initMultiSelect} from "./multi-select";
import {createEl, insertAfter, wrapAll} from "./utils";
import {getCurrentHTML, updateDropdownHTML} from "./layout";
import {val} from "./data";
@@ -25,7 +26,13 @@ export function init(context){
initSearchDropdown(context);
}
+ // init multi select
+ if(context.options.multiple && !context.options.nativeSelect){
+ initMultiSelect(context);
+ }
+
// update value attribute
+ // tested with multi select
context.selectTag.setAttribute(ATTRS.value, val(context));
// Event: onInit
diff --git a/src/multi-select.js b/src/multi-select.js
new file mode 100644
index 0000000..59710d8
--- /dev/null
+++ b/src/multi-select.js
@@ -0,0 +1,9 @@
+import {CLASSES} from "./configs";
+
+export function initMultiSelect(context){
+ // add wrapper class
+ context.wrapper.classList.add(CLASSES.multipleSelect);
+
+ // set multiple attribute
+ context.selectTag.setAttribute('multiple', 'true');
+}
\ No newline at end of file
diff --git a/src/utils.js b/src/utils.js
index 0a0a4aa..926a68e 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -149,4 +149,34 @@ export function removeAccents(str){
return str.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/đ/g, 'd').replace(/Đ/g, 'D');
+}
+
+
+/**
+ * Get selected values from a select tag (support multiple select)
+ * @param selectTag
+ * @returns {*[]}
+ */
+export function getMultipleSelectedValues(selectTag){
+ let result = [];
+ let options = selectTag && selectTag.options;
+
+ for(const option of options){
+ if(option.selected){
+ result.push(option.value || option.text);
+ }
+ }
+
+ return result;
+}
+
+
+/**
+ * Get option element by value
+ * @param context
+ * @param optionValue
+ * @returns {Element}
+ */
+export function getOptionByValue(context, optionValue){
+ return context.selectTag.querySelector(`option[value="${optionValue}"]`);
}
\ No newline at end of file