{ "version": 3, "sources": ["../src/dual-listbox.js"], "sourcesContent": ["const MAIN_BLOCK = \"dual-listbox\";\n\nconst CONTAINER_ELEMENT = \"dual-listbox__container\";\nconst AVAILABLE_ELEMENT = \"dual-listbox__available\";\nconst SELECTED_ELEMENT = \"dual-listbox__selected\";\nconst TITLE_ELEMENT = \"dual-listbox__title\";\nconst ITEM_ELEMENT = \"dual-listbox__item\";\nconst BUTTONS_ELEMENT = \"dual-listbox__buttons\";\nconst BUTTON_ELEMENT = \"dual-listbox__button\";\nconst SEARCH_ELEMENT = \"dual-listbox__search\";\n\nconst SELECTED_MODIFIER = \"dual-listbox__item--selected\";\n\nconst DIRECTION_UP = \"up\";\nconst DIRECTION_DOWN = \"down\";\n\n/**\n * Dual select interface allowing the user to select items from a list of provided options.\n * @class\n */\nclass DualListbox {\n constructor(selector, options = {}) {\n this.setDefaults();\n this.selected = [];\n this.available = [];\n\n if (DualListbox.isDomElement(selector)) {\n this.select = selector;\n } else {\n this.select = document.querySelector(selector);\n }\n\n this._initOptions(options);\n this._initReusableElements();\n this._splitOptions(this.select.options);\n if (options.options !== undefined) {\n this._splitOptions(options.options);\n }\n this._buildDualListbox(this.select.parentNode);\n this._addActions();\n\n if (this.sortable) {\n this._initializeSortButtons();\n }\n\n this.redraw();\n }\n\n /**\n * Sets the default values that can be overwritten.\n */\n setDefaults() {\n this.addEvent = null; // TODO: Remove in favor of eventListener\n this.removeEvent = null; // TODO: Remove in favor of eventListener\n this.availableTitle = \"Available options\";\n this.selectedTitle = \"Selected options\";\n\n this.showAddButton = true;\n this.addButtonText = \"add\";\n\n this.showRemoveButton = true;\n this.removeButtonText = \"remove\";\n\n this.showAddAllButton = true;\n this.addAllButtonText = \"add all\";\n\n this.showRemoveAllButton = true;\n this.removeAllButtonText = \"remove all\";\n\n this.searchPlaceholder = \"Search\";\n\n this.sortable = false;\n this.upButtonText = \"up\";\n this.downButtonText = \"down\";\n }\n\n /**\n * Add eventListener to the dualListbox element.\n *\n * @param {String} eventName\n * @param {function} callback\n */\n addEventListener(eventName, callback) {\n this.dualListbox.addEventListener(eventName, callback);\n }\n\n /**\n * Add the listItem to the selected list.\n *\n * @param {NodeElement} listItem\n */\n addSelected(listItem) {\n let index = this.available.indexOf(listItem);\n if (index > -1) {\n this.available.splice(index, 1);\n this.selected.push(listItem);\n this._selectOption(listItem.dataset.id);\n this.redraw();\n\n setTimeout(() => {\n let event = document.createEvent(\"HTMLEvents\");\n event.initEvent(\"added\", false, true);\n event.addedElement = listItem;\n this.dualListbox.dispatchEvent(event);\n }, 0);\n }\n }\n\n /**\n * Redraws the Dual listbox content\n */\n redraw() {\n this.updateAvailableListbox();\n this.updateSelectedListbox();\n }\n\n /**\n * Removes the listItem from the selected list.\n *\n * @param {NodeElement} listItem\n */\n removeSelected(listItem) {\n let index = this.selected.indexOf(listItem);\n if (index > -1) {\n this.selected.splice(index, 1);\n this.available.push(listItem);\n this._deselectOption(listItem.dataset.id);\n this.redraw();\n\n setTimeout(() => {\n let event = document.createEvent(\"HTMLEvents\");\n event.initEvent(\"removed\", false, true);\n event.removedElement = listItem;\n this.dualListbox.dispatchEvent(event);\n }, 0);\n }\n }\n\n /**\n * Filters the listboxes with the given searchString.\n *\n * @param {Object} searchString\n * @param dualListbox\n */\n searchLists(searchString, dualListbox) {\n let items = dualListbox.querySelectorAll(`.${ITEM_ELEMENT}`);\n let lowerCaseSearchString = searchString.toLowerCase();\n\n for (let i = 0; i < items.length; i++) {\n let item = items[i];\n if (\n item.textContent\n .toLowerCase()\n .indexOf(lowerCaseSearchString) === -1\n ) {\n item.style.display = \"none\";\n } else {\n item.style.display = \"list-item\";\n }\n }\n }\n\n /**\n * Update the elements in the available listbox;\n */\n updateAvailableListbox() {\n this._updateListbox(this.availableList, this.available);\n }\n\n /**\n * Update the elements in the selected listbox;\n */\n updateSelectedListbox() {\n this._updateListbox(this.selectedList, this.selected);\n }\n\n //\n //\n // PRIVATE FUNCTIONS\n //\n //\n\n /**\n * Action to set all listItems to selected.\n */\n _actionAllSelected(event) {\n event.preventDefault();\n\n let selected = this.available.filter(\n (item) => item.style.display !== \"none\"\n );\n selected.forEach((item) => this.addSelected(item));\n }\n\n /**\n * Update the elements in the listbox;\n */\n _updateListbox(list, elements) {\n while (list.firstChild) {\n list.removeChild(list.firstChild);\n }\n\n for (let i = 0; i < elements.length; i++) {\n let listItem = elements[i];\n list.appendChild(listItem);\n }\n }\n\n /**\n * Action to set one listItem to selected.\n */\n _actionItemSelected(event) {\n event.preventDefault();\n\n let selected = this.dualListbox.querySelector(`.${SELECTED_MODIFIER}`);\n if (selected) {\n this.addSelected(selected);\n }\n }\n\n /**\n * Action to set all listItems to available.\n */\n _actionAllDeselected(event) {\n event.preventDefault();\n\n let deselected = this.selected.filter(\n (item) => item.style.display !== \"none\"\n );\n deselected.forEach((item) => this.removeSelected(item));\n }\n\n /**\n * Action to set one listItem to available.\n */\n _actionItemDeselected(event) {\n event.preventDefault();\n\n let selected = this.dualListbox.querySelector(`.${SELECTED_MODIFIER}`);\n if (selected) {\n this.removeSelected(selected);\n }\n }\n\n /**\n * Action when double clicked on a listItem.\n */\n _actionItemDoubleClick(listItem, event = null) {\n if (event) {\n event.preventDefault();\n event.stopPropagation();\n }\n\n if (this.selected.indexOf(listItem) > -1) {\n this.removeSelected(listItem);\n } else {\n this.addSelected(listItem);\n }\n }\n\n /**\n * Action when single clicked on a listItem.\n */\n _actionItemClick(listItem, dualListbox, event = null) {\n if (event) {\n event.preventDefault();\n }\n\n let items = dualListbox.querySelectorAll(`.${ITEM_ELEMENT}`);\n\n for (let i = 0; i < items.length; i++) {\n let value = items[i];\n if (value !== listItem) {\n value.classList.remove(SELECTED_MODIFIER);\n }\n }\n\n if (listItem.classList.contains(SELECTED_MODIFIER)) {\n listItem.classList.remove(SELECTED_MODIFIER);\n } else {\n listItem.classList.add(SELECTED_MODIFIER);\n }\n }\n\n /**\n * @Private\n * Adds the needed actions to the elements.\n */\n _addActions() {\n this._addButtonActions();\n this._addSearchActions();\n }\n\n /**\n * Adds the actions to the buttons that are created.\n */\n _addButtonActions() {\n this.add_all_button.addEventListener(\"click\", (event) =>\n this._actionAllSelected(event)\n );\n this.add_button.addEventListener(\"click\", (event) =>\n this._actionItemSelected(event)\n );\n this.remove_button.addEventListener(\"click\", (event) =>\n this._actionItemDeselected(event)\n );\n this.remove_all_button.addEventListener(\"click\", (event) =>\n this._actionAllDeselected(event)\n );\n }\n\n /**\n * Adds the click items to the listItem.\n *\n * @param {Object} listItem\n */\n _addClickActions(listItem) {\n listItem.addEventListener(\"dblclick\", (event) =>\n this._actionItemDoubleClick(listItem, event)\n );\n listItem.addEventListener(\"click\", (event) =>\n this._actionItemClick(listItem, this.dualListbox, event)\n );\n return listItem;\n }\n\n /**\n * @Private\n * Adds the actions to the search input.\n */\n _addSearchActions() {\n this.search_left.addEventListener(\"change\", (event) =>\n this.searchLists(event.target.value, this.availableList)\n );\n this.search_left.addEventListener(\"keyup\", (event) =>\n this.searchLists(event.target.value, this.availableList)\n );\n this.search_right.addEventListener(\"change\", (event) =>\n this.searchLists(event.target.value, this.selectedList)\n );\n this.search_right.addEventListener(\"keyup\", (event) =>\n this.searchLists(event.target.value, this.selectedList)\n );\n }\n\n /**\n * @Private\n * Builds the Dual listbox and makes it visible to the user.\n */\n _buildDualListbox(container) {\n this.select.style.display = \"none\";\n\n this.dualListBoxContainer.appendChild(\n this._createList(\n this.search_left,\n this.availableListTitle,\n this.availableList\n )\n );\n this.dualListBoxContainer.appendChild(this.buttons);\n this.dualListBoxContainer.appendChild(\n this._createList(\n this.search_right,\n this.selectedListTitle,\n this.selectedList\n )\n );\n\n this.dualListbox.appendChild(this.dualListBoxContainer);\n\n container.insertBefore(this.dualListbox, this.select);\n }\n\n /**\n * Creates list with the header.\n */\n _createList(search, header, list) {\n let result = document.createElement(\"div\");\n result.appendChild(search);\n result.appendChild(header);\n result.appendChild(list);\n return result;\n }\n\n /**\n * Creates the buttons to add/remove the selected item.\n */\n _createButtons() {\n this.buttons = document.createElement(\"div\");\n this.buttons.classList.add(BUTTONS_ELEMENT);\n\n this.add_all_button = document.createElement(\"button\");\n this.add_all_button.innerHTML = this.addAllButtonText;\n\n this.add_button = document.createElement(\"button\");\n this.add_button.innerHTML = this.addButtonText;\n\n this.remove_button = document.createElement(\"button\");\n this.remove_button.innerHTML = this.removeButtonText;\n\n this.remove_all_button = document.createElement(\"button\");\n this.remove_all_button.innerHTML = this.removeAllButtonText;\n\n const options = {\n showAddAllButton: this.add_all_button,\n showAddButton: this.add_button,\n showRemoveButton: this.remove_button,\n showRemoveAllButton: this.remove_all_button,\n };\n\n for (let optionName in options) {\n if (optionName) {\n const option = this[optionName];\n const button = options[optionName];\n\n button.setAttribute(\"type\", \"button\");\n button.classList.add(BUTTON_ELEMENT);\n\n if (option) {\n this.buttons.appendChild(button);\n }\n }\n }\n }\n\n /**\n * @Private\n * Creates the listItem out of the option.\n */\n _createListItem(option) {\n let listItem = document.createElement(\"li\");\n\n listItem.classList.add(ITEM_ELEMENT);\n listItem.innerHTML = option.text;\n listItem.dataset.id = option.value;\n\n this._addClickActions(listItem);\n\n return listItem;\n }\n\n /**\n * @Private\n * Creates the search input.\n */\n _createSearchLeft() {\n this.search_left = document.createElement(\"input\");\n this.search_left.classList.add(SEARCH_ELEMENT);\n this.search_left.placeholder = this.searchPlaceholder;\n }\n\n /**\n * @Private\n * Creates the search input.\n */\n _createSearchRight() {\n this.search_right = document.createElement(\"input\");\n this.search_right.classList.add(SEARCH_ELEMENT);\n this.search_right.placeholder = this.searchPlaceholder;\n }\n\n /**\n * @Private\n * Deselects the option with the matching value\n *\n * @param {Object} value\n */\n _deselectOption(value) {\n let options = this.select.options;\n\n for (let i = 0; i < options.length; i++) {\n let option = options[i];\n if (option.value === value) {\n option.selected = false;\n option.removeAttribute(\"selected\");\n }\n }\n\n if (this.removeEvent) {\n this.removeEvent(value);\n }\n }\n\n /**\n * @Private\n * Set the option variables to this.\n */\n _initOptions(options) {\n for (let key in options) {\n if (options.hasOwnProperty(key)) {\n this[key] = options[key];\n }\n }\n }\n\n /**\n * @Private\n * Creates all the static elements for the Dual listbox.\n */\n _initReusableElements() {\n this.dualListbox = document.createElement(\"div\");\n this.dualListbox.classList.add(MAIN_BLOCK);\n if (this.select.id) {\n this.dualListbox.classList.add(this.select.id);\n }\n\n this.dualListBoxContainer = document.createElement(\"div\");\n this.dualListBoxContainer.classList.add(CONTAINER_ELEMENT);\n\n this.availableList = document.createElement(\"ul\");\n this.availableList.classList.add(AVAILABLE_ELEMENT);\n\n this.selectedList = document.createElement(\"ul\");\n this.selectedList.classList.add(SELECTED_ELEMENT);\n\n this.availableListTitle = document.createElement(\"div\");\n this.availableListTitle.classList.add(TITLE_ELEMENT);\n this.availableListTitle.innerText = this.availableTitle;\n\n this.selectedListTitle = document.createElement(\"div\");\n this.selectedListTitle.classList.add(TITLE_ELEMENT);\n this.selectedListTitle.innerText = this.selectedTitle;\n\n this._createButtons();\n this._createSearchLeft();\n this._createSearchRight();\n }\n\n /**\n * @Private\n * Selects the option with the matching value\n *\n * @param {Object} value\n */\n _selectOption(value) {\n let options = this.select.options;\n\n for (let i = 0; i < options.length; i++) {\n let option = options[i];\n if (option.value === value) {\n option.selected = true;\n option.setAttribute(\"selected\", \"\");\n }\n }\n\n if (this.addEvent) {\n this.addEvent(value);\n }\n }\n\n /**\n * @Private\n * Splits the options and places them in the correct list.\n */\n _splitOptions(options) {\n for (let i = 0; i < options.length; i++) {\n let option = options[i];\n if (DualListbox.isDomElement(option)) {\n this._addOption({\n text: option.innerHTML,\n value: option.value,\n selected: option.attributes.selected,\n });\n } else {\n this._addOption(option);\n }\n }\n }\n\n /**\n * @Private\n * Adds option to the selected of available list (depending on the data).\n */\n _addOption(option) {\n let listItem = this._createListItem(option);\n\n if (option.selected) {\n this.selected.push(listItem);\n } else {\n this.available.push(listItem);\n }\n }\n\n /**\n * @private\n * @return {void}\n */\n _initializeSortButtons() {\n const sortUpButton = document.createElement(\"button\");\n sortUpButton.classList.add(\"dual-listbox__button\");\n sortUpButton.innerText = this.upButtonText;\n sortUpButton.addEventListener(\"click\", (event) =>\n this._onSortButtonClick(event, DIRECTION_UP)\n );\n\n const sortDownButton = document.createElement(\"button\");\n sortDownButton.classList.add(\"dual-listbox__button\");\n sortDownButton.innerText = this.downButtonText;\n sortDownButton.addEventListener(\"click\", (event) =>\n this._onSortButtonClick(event, DIRECTION_DOWN)\n );\n\n const buttonContainer = document.createElement(\"div\");\n buttonContainer.classList.add(\"dual-listbox__buttons\");\n buttonContainer.appendChild(sortUpButton);\n buttonContainer.appendChild(sortDownButton);\n\n this.dualListBoxContainer.appendChild(buttonContainer);\n }\n\n /**\n * @private\n * @param {MouseEvent} event\n * @param {string} direction\n * @return {void}\n */\n _onSortButtonClick(event, direction) {\n event.preventDefault();\n\n const [oldIndex, newIndex] = this._findSelected(direction);\n if (oldIndex !== newIndex) {\n this._sortUnderlyingSelectOptions(oldIndex, newIndex);\n this._sortSelected(oldIndex, newIndex);\n this.redraw();\n }\n }\n\n /**\n * Returns an array where the first element is the old index of the currently\n * selected item in the right box and the second element is the new index.\n *\n * @private\n * @param {string} direction\n * @return {int[]}\n */\n _findSelected(direction) {\n const oldIndex = this.selected.findIndex((element) =>\n element.classList.contains(\"dual-listbox__item--selected\")\n );\n\n let newIndex = oldIndex;\n if (DIRECTION_UP === direction && oldIndex > 0) {\n newIndex -= 1;\n } else if (\n DIRECTION_DOWN === direction &&\n oldIndex < this.selected.length - 1\n ) {\n newIndex += 1;\n }\n\n return [oldIndex, newIndex];\n }\n\n /**\n * Sorts the