From 5655bb1556d8afbe41dff1938d0ca5d5b8f39ae4 Mon Sep 17 00:00:00 2001 From: wuda-io Date: Mon, 10 Jan 2022 22:28:57 +0100 Subject: [PATCH 01/10] refactor(select): changed the internal value storage to an array instead of an object --- js/select.js | 293 +++++++++++++-------------------------------------- 1 file changed, 76 insertions(+), 217 deletions(-) diff --git a/js/select.js b/js/select.js index f22da1fb88..96a04adfb6 100644 --- a/js/select.js +++ b/js/select.js @@ -6,72 +6,36 @@ dropdownOptions: {} }; - /** - * @class - * - */ class FormSelect extends Component { - /** - * Construct FormSelect instance - * @constructor - * @param {Element} el - * @param {Object} options - */ constructor(el, options) { super(FormSelect, el, options); - - // Don't init if browser default version - if (this.$el.hasClass('browser-default')) { - return; - } - + if (this.$el.hasClass('browser-default')) return; this.el.M_FormSelect = this; - - /** - * Options for the select - * @member FormSelect#options - */ this.options = $.extend({}, FormSelect.defaults, options); - this.isMultiple = this.$el.prop('multiple'); - - // Setup this.el.tabIndex = -1; - this._keysSelected = {}; - this._valueDict = {}; // Maps key to original and generated option element. - this._setupDropdown(); + this._selectedValues = []; + this._values = []; + + this._setupDropdown(); this._setupEventHandlers(); } - static get defaults() { return _defaults; } - static init(els, options) { return super.init(this, els, options); } - - /** - * Get Instance - */ static getInstance(el) { let domElem = !!el.jquery ? el[0] : el; return domElem.M_FormSelect; } - - /** - * Teardown component - */ destroy() { this._removeEventHandlers(); this._removeDropdown(); this.el.M_FormSelect = undefined; } - - /** - * Setup Event Handlers - */ _setupEventHandlers() { this._handleSelectChangeBound = this._handleSelectChange.bind(this); this._handleOptionClickBound = this._handleOptionClick.bind(this); @@ -85,10 +49,6 @@ this.el.addEventListener('change', this._handleSelectChangeBound); this.input.addEventListener('click', this._handleInputClickBound); } - - /** - * Remove Event Handlers - */ _removeEventHandlers() { $(this.dropdownOptions) .find('li:not(.optgroup)') @@ -98,66 +58,50 @@ this.el.removeEventListener('change', this._handleSelectChangeBound); this.input.removeEventListener('click', this._handleInputClickBound); } - - /** - * Handle Select Change - * @param {Event} e - */ _handleSelectChange(e) { this._setValueToInput(); } - - /** - * Handle Option Click - * @param {Event} e - */ _handleOptionClick(e) { e.preventDefault(); let optionEl = $(e.target).closest('li')[0]; - this._selectOption(optionEl); + this._selectOptionElement(optionEl); e.stopPropagation(); } - _selectOption(optionEl) { + _selectOptionElement(optionEl) { let key = optionEl.id; if (!$(optionEl).hasClass('disabled') && !$(optionEl).hasClass('optgroup') && key.length) { - let selected = true; - + // let selected = true; if (this.isMultiple) { // Deselect placeholder option if still selected. - let placeholderOption = $(this.dropdownOptions).find('li.disabled.selected'); - if (placeholderOption.length) { - placeholderOption.removeClass('selected'); - placeholderOption.find('input[type="checkbox"]').prop('checked', false); - this._toggleEntryFromArray(placeholderOption[0].id); - } - selected = this._toggleEntryFromArray(key); + // let placeholderOption = $(this.dropdownOptions).find('li.disabled.selected'); + // if (placeholderOption.length) { + // placeholderOption.removeClass('selected'); + // placeholderOption.find('input[type="checkbox"]').prop('checked', false); + // this._toggleEntryFromArray(placeholderOption[0].id); + // } + // selected = + this._toggleEntryFromArray(key); } else { + // Single-Select $(this.dropdownOptions) .find('li') .removeClass('selected'); $(optionEl).toggleClass('selected', selected); - this._keysSelected = {}; - this._keysSelected[optionEl.id] = true; + this._selectedValues = []; + this._selectedValues.push(optionEl); } - // Set selected on original select option // Only trigger if selected state changed - let prevSelected = $(this._valueDict[key].el).prop('selected'); - if (prevSelected !== selected) { - $(this._valueDict[key].el).prop('selected', selected); - this.$el.trigger('change'); - } - } - - if (!this.isMultiple) { - this.dropdown.close(); + // let prevSelected = $(this._values[key].el).prop('selected'); + // if (prevSelected !== selected) { + // $(this._values[key].el).prop('selected', selected); + // this.$el.trigger('change'); + // } } + if (!this.isMultiple) this.dropdown.close(); } - /** - * Handle Input Click - */ _handleInputClick() { if (this.dropdown && this.dropdown.isOpen) { this._setValueToInput(); @@ -165,21 +109,17 @@ } } - /** - * Setup dropdown - */ _setupDropdown() { this.wrapper = document.createElement('div'); $(this.wrapper).addClass('select-wrapper ' + this.options.classes); this.$el.before($(this.wrapper)); + // Move actual select element into overflow hidden wrapper let $hideSelect = $('
'); $(this.wrapper).append($hideSelect); $hideSelect[0].appendChild(this.el); - if (this.el.disabled) { - this.wrapper.classList.add('disabled'); - } + if (this.el.disabled) this.wrapper.classList.add('disabled'); // Create dropdown this.$selectOptions = this.$el.children('option, optgroup'); @@ -189,34 +129,26 @@ 'dropdown-content select-dropdown ' + (this.isMultiple ? 'multiple-select-dropdown' : '') ); - // Create dropdown structure. + // Create dropdown structure if (this.$selectOptions.length) { this.$selectOptions.each((el) => { if ($(el).is('option')) { - // Direct descendant option. - let optionEl; - if (this.isMultiple) { - optionEl = this._appendOptionWithIcon(this.$el, el, 'multiple'); - } else { - optionEl = this._appendOptionWithIcon(this.$el, el); - } - - this._addOptionToValueDict(el, optionEl); + // Direct descendant option + const option = this._createOptionWithIcon(el, this.isMultiple ? 'multiple' : undefined); + this._addOptionToValues(el, option); } else if ($(el).is('optgroup')) { - // Optgroup. + // Optgroup let selectOptions = $(el).children('option'); $(this.dropdownOptions).append( $('
  • ' + el.getAttribute('label') + '
  • ')[0] ); - selectOptions.each((el) => { - let optionEl = this._appendOptionWithIcon(this.$el, el, 'optgroup-option'); - this._addOptionToValueDict(el, optionEl); + const option = this._createOptionWithIcon(el, 'optgroup-option'); + this._addOptionToValues(el, option); }); } }); } - $(this.wrapper).append(this.dropdownOptions); // Add input dropdown @@ -225,9 +157,7 @@ this.input.setAttribute('type', 'text'); this.input.setAttribute('readonly', 'true'); this.input.setAttribute('data-target', this.dropdownOptions.id); - if (this.el.disabled) { - $(this.input).prop('disabled', 'true'); - } + if (this.el.disabled) $(this.input).prop('disabled', 'true'); $(this.wrapper).prepend(this.input); this._setValueToInput(); @@ -237,25 +167,21 @@ '' ); $(this.wrapper).prepend(dropdownIcon[0]); - // Initialize dropdown if (!this.el.disabled) { let dropdownOptions = $.extend({}, this.options.dropdownOptions); let userOnOpenEnd = dropdownOptions.onOpenEnd; - // Add callback for centering selected option when dropdown content is scrollable dropdownOptions.onOpenEnd = (el) => { let selectedOption = $(this.dropdownOptions) .find('.selected') .first(); - if (selectedOption.length) { // Focus selected option in dropdown M.keyDown = true; this.dropdown.focusedIndex = selectedOption.index(); this.dropdown._focusFocusedItem(); M.keyDown = false; - // Handle scrolling to selected option if (this.dropdown.isScrollable) { let scrollOffset = @@ -265,42 +191,25 @@ this.dropdownOptions.scrollTop = scrollOffset; } } - // Handle user declared onOpenEnd if needed - if (userOnOpenEnd && typeof userOnOpenEnd === 'function') { + if (userOnOpenEnd && typeof userOnOpenEnd === 'function') userOnOpenEnd.call(this.dropdown, this.el); - } }; - // Prevent dropdown from closing too early dropdownOptions.closeOnClick = false; - this.dropdown = M.Dropdown.init(this.input, dropdownOptions); } - // Add initial selections this._setSelectedStates(); } - /** - * Add option to value dict - * @param {Element} el original option element - * @param {Element} optionEl generated option element - */ - _addOptionToValueDict(el, optionEl) { - let index = Object.keys(this._valueDict).length; + _addOptionToValues(el, optionEl) { + let index = this._values.length; let key = this.dropdownOptions.id + index; - let obj = {}; optionEl.id = key; - - obj.el = el; - obj.optionEl = optionEl; - this._valueDict[key] = obj; + this._values.push({ el, optionEl }); } - /** - * Remove dropdown - */ _removeDropdown() { $(this.wrapper) .find('.caret') @@ -311,141 +220,91 @@ $(this.wrapper).remove(); } - /** - * Setup dropdown - * @param {Element} select select element - * @param {Element} option option element from select - * @param {String} type - * @return {Element} option element added - */ - _appendOptionWithIcon(select, option, type) { - // Add disabled attr if disabled + _createOptionWithIcon(option, type) { let disabledClass = option.disabled ? 'disabled ' : ''; let optgroupClass = type === 'optgroup-option' ? 'optgroup-option ' : ''; let multipleCheckbox = this.isMultiple ? `` : option.innerHTML; - let liEl = $('
  • '); - let spanEl = $(''); - spanEl.html(multipleCheckbox); - liEl.addClass(`${disabledClass} ${optgroupClass}`); - liEl.append(spanEl); - + let li = $('
  • '); + let span = $(''); + span.html(multipleCheckbox); + li.addClass(`${disabledClass} ${optgroupClass}`); + li.append(span); // add icons let iconUrl = option.getAttribute('data-icon'); let classes = option.getAttribute('class'); - if (!!iconUrl) { - let imgEl = $(``); - liEl.prepend(imgEl); + if (iconUrl) { + const img = $(``); + li.prepend(img); } - // Check for multiple type. - $(this.dropdownOptions).append(liEl[0]); - return liEl[0]; + $(this.dropdownOptions).append(li[0]); + return li[0]; } - /** - * Toggle entry from option - * @param {String} key Option key - * @return {Boolean} if entry was added or removed - */ _toggleEntryFromArray(key) { - let notAdded = !this._keysSelected.hasOwnProperty(key); - let $optionLi = $(this._valueDict[key].optionEl); - - if (notAdded) { - this._keysSelected[key] = true; + const li = this._values.filter((value) => value.optionEl.id === key)[0]; + const isNotSelected = + this._selectedValues.filter((value) => value.optionEl.id === key).length === 0; + if (isNotSelected) { + this._selectedValues.push(li); } else { - delete this._keysSelected[key]; + this._selectedValues = this._selectedValues.filter((value) => value.optionEl.id !== key); } - - $optionLi.toggleClass('selected', notAdded); - - // Set checkbox checked value - $optionLi.find('input[type="checkbox"]').prop('checked', notAdded); - + li.toggleClass('selected', isNotSelected); + li.find('input[type="checkbox"]').prop('checked', isNotSelected); // use notAdded instead of true (to detect if the option is selected or not) - $optionLi.prop('selected', notAdded); - - return notAdded; + li.prop('selected', isNotSelected); + return isNotSelected; } - /** - * Set text value to input - */ _setValueToInput() { let values = []; let options = this.$el.find('option'); - options.each((el) => { if ($(el).prop('selected')) { - let text = $(el).text().trim(); + let text = $(el) + .text() + .trim(); values.push(text); } }); - if (!values.length) { let firstDisabled = this.$el.find('option:disabled').eq(0); - if (firstDisabled.length && firstDisabled[0].value === '') { + if (firstDisabled.length && firstDisabled[0].value === '') values.push(firstDisabled.text()); - } } - this.input.value = values.join(', '); } - /** - * Set selected state of dropdown to match actual select element - */ _setSelectedStates() { - this._keysSelected = {}; - - for (let key in this._valueDict) { - let option = this._valueDict[key]; - let optionIsSelected = $(option.el).prop('selected'); + this._selectedValues = []; + this._values.forEach((option) => { + const optionIsSelected = $(option.el).prop('selected'); $(option.optionEl) .find('input[type="checkbox"]') .prop('checked', optionIsSelected); if (optionIsSelected) { this._activateOption($(this.dropdownOptions), $(option.optionEl)); - this._keysSelected[key] = true; - } else { - $(option.optionEl).removeClass('selected'); - } - } + this._selectedValues.push(option); + } else $(option.optionEl).removeClass('selected'); + }); } - /** - * Make option as selected and scroll to selected position - * @param {jQuery} collection Select options jQuery element - * @param {Element} newOption element of the new option - */ + // Make option as selected and scroll to selected position _activateOption(collection, newOption) { - if (newOption) { - if (!this.isMultiple) { - collection.find('li.selected').removeClass('selected'); - } - let option = $(newOption); - option.addClass('selected'); - } + if (!newOption) return; + if (!this.isMultiple) collection.find('li.selected').removeClass('selected'); + $(newOption).addClass('selected'); } - /** - * Get Selected Values - * @return {Array} Array of selected values - */ getSelectedValues() { - let selectedValues = []; - for (let key in this._keysSelected) { - selectedValues.push(this._valueDict[key].el.value); - } - return selectedValues; + return this._selectedValues; } } M.FormSelect = FormSelect; - if (M.jQueryLoaded) { - M.initializeJqueryWrapper(FormSelect, 'formSelect', 'M_FormSelect'); - } + if (M.jQueryLoaded) M.initializeJqueryWrapper(FormSelect, 'formSelect', 'M_FormSelect'); })(cash); From 9f622674946b77eceef5e804d93317cc0ad74c63 Mon Sep 17 00:00:00 2001 From: wuda-io Date: Mon, 10 Jan 2022 23:56:11 +0100 Subject: [PATCH 02/10] fix(select): select is now working again after refactoring --- js/select.js | 187 +++++++++++++++++++++++++++++---------------------- 1 file changed, 107 insertions(+), 80 deletions(-) diff --git a/js/select.js b/js/select.js index 96a04adfb6..d5974a8e9e 100644 --- a/js/select.js +++ b/js/select.js @@ -14,10 +14,7 @@ this.options = $.extend({}, FormSelect.defaults, options); this.isMultiple = this.$el.prop('multiple'); this.el.tabIndex = -1; - - this._selectedValues = []; this._values = []; - this._setupDropdown(); this._setupEventHandlers(); } @@ -40,7 +37,6 @@ this._handleSelectChangeBound = this._handleSelectChange.bind(this); this._handleOptionClickBound = this._handleOptionClick.bind(this); this._handleInputClickBound = this._handleInputClick.bind(this); - $(this.dropdownOptions) .find('li:not(.optgroup)') .each((el) => { @@ -61,17 +57,27 @@ _handleSelectChange(e) { this._setValueToInput(); } + _handleOptionClick(e) { e.preventDefault(); - let optionEl = $(e.target).closest('li')[0]; - this._selectOptionElement(optionEl); + let virtualOption = $(e.target).closest('li')[0]; + this._selectOptionElement(virtualOption); e.stopPropagation(); } + _selectOptionElement(virtualOption) { + console.log('Selected an Option Element'); + console.log(virtualOption); + //const key = optionElement.id; + //console.log(key); + if ( + !$(virtualOption).hasClass('disabled') && + !$(virtualOption).hasClass('optgroup') /* && key.length*/ + ) { + //console.log(">>>"); + const value = this._values.filter((value) => value.optionEl === virtualOption)[0]; + //console.log(value); - _selectOptionElement(optionEl) { - let key = optionEl.id; - if (!$(optionEl).hasClass('disabled') && !$(optionEl).hasClass('optgroup') && key.length) { - // let selected = true; + let selected = true; if (this.isMultiple) { // Deselect placeholder option if still selected. // let placeholderOption = $(this.dropdownOptions).find('li.disabled.selected'); @@ -80,20 +86,22 @@ // placeholderOption.find('input[type="checkbox"]').prop('checked', false); // this._toggleEntryFromArray(placeholderOption[0].id); // } - // selected = - this._toggleEntryFromArray(key); + selected = this._toggleEntryFromArray(value); + this._setValueToInput(); } else { // Single-Select - $(this.dropdownOptions) - .find('li') - .removeClass('selected'); - $(optionEl).toggleClass('selected', selected); - this._selectedValues = []; - this._selectedValues.push(optionEl); + //console.log("Single-Select"); + //console.log(optionElement); + //$(this.dropdownOptions).find('li').removeClass('selected'); + //$(virtualOption).toggleClass('selected', selected); + this._deselectAll(); + value.el.setAttribute('selected', 'selected'); + // TODO: Mark as selected + this._setValueToInput(); } // Set selected on original select option // Only trigger if selected state changed - // let prevSelected = $(this._values[key].el).prop('selected'); + // let prevSelected = $(this._values.filter(value => value.el === key)).prop('selected'); // if (prevSelected !== selected) { // $(this._values[key].el).prop('selected', selected); // this.$el.trigger('change'); @@ -103,6 +111,7 @@ } _handleInputClick() { + //console.log("Clicked on SelectBox.") if (this.dropdown && this.dropdown.isOpen) { this._setValueToInput(); this._setSelectedStates(); @@ -133,8 +142,11 @@ if (this.$selectOptions.length) { this.$selectOptions.each((el) => { if ($(el).is('option')) { - // Direct descendant option - const option = this._createOptionWithIcon(el, this.isMultiple ? 'multiple' : undefined); + // Option + const option = this._createAndAppendOptionWithIcon( + el, + this.isMultiple ? 'multiple' : undefined + ); this._addOptionToValues(el, option); } else if ($(el).is('optgroup')) { // Optgroup @@ -143,7 +155,7 @@ $('
  • ' + el.getAttribute('label') + '
  • ')[0] ); selectOptions.each((el) => { - const option = this._createOptionWithIcon(el, 'optgroup-option'); + const option = this._createAndAppendOptionWithIcon(el, 'optgroup-option'); this._addOptionToValues(el, option); }); } @@ -203,11 +215,8 @@ this._setSelectedStates(); } - _addOptionToValues(el, optionEl) { - let index = this._values.length; - let key = this.dropdownOptions.id + index; - optionEl.id = key; - this._values.push({ el, optionEl }); + _addOptionToValues(el, optionElement) { + this._values.push({ el, optionEl: optionElement }); } _removeDropdown() { @@ -220,87 +229,105 @@ $(this.wrapper).remove(); } - _createOptionWithIcon(option, type) { - let disabledClass = option.disabled ? 'disabled ' : ''; - let optgroupClass = type === 'optgroup-option' ? 'optgroup-option ' : ''; - let multipleCheckbox = this.isMultiple - ? `` - : option.innerHTML; - let li = $('
  • '); - let span = $(''); - span.html(multipleCheckbox); - li.addClass(`${disabledClass} ${optgroupClass}`); - li.append(span); - // add icons - let iconUrl = option.getAttribute('data-icon'); - let classes = option.getAttribute('class'); + _createAndAppendOptionWithIcon(option, type) { + const li = document.createElement('li'); + if (option.disabled) li.classList.add('disabled'); + if (type === 'optgroup-option') li.classList.add(type); + // Text / Checkbox + const span = document.createElement('span'); + if (this.isMultiple) + span.innerHTML = ``; + else span.innerHTML = option.innerHTML; + li.appendChild(span); + // add Icon + const iconUrl = option.getAttribute('data-icon'); + const classes = option.getAttribute('class'); if (iconUrl) { const img = $(``); - li.prepend(img); + li.prepend(img[0]); } - // Check for multiple type. - $(this.dropdownOptions).append(li[0]); - return li[0]; + // Check for multiple type + $(this.dropdownOptions).append(li); + return li; } - _toggleEntryFromArray(key) { - const li = this._values.filter((value) => value.optionEl.id === key)[0]; - const isNotSelected = - this._selectedValues.filter((value) => value.optionEl.id === key).length === 0; - if (isNotSelected) { - this._selectedValues.push(li); + _deselectValue(value) { + value.optionEl.classList.remove('selected'); + value.el.removeAttribute('selected'); + } + _deselectAll() { + this._values.forEach((value) => { + this._deselectValue(value); + }); + } + + _toggleEntryFromArray(value) { + const li = value.optionEl; + const isSelected = li.classList.contains('selected'); + //console.log("Toggle", value, isSelected); + if (isSelected) { + value.el.removeAttribute('selected'); + li.classList.remove('selected'); + li.removeAttribute('selected'); } else { - this._selectedValues = this._selectedValues.filter((value) => value.optionEl.id !== key); + value.el.setAttribute('selected', 'selected'); + li.classList.add('selected'); + li.setAttribute('selected', 'selected'); } - li.toggleClass('selected', isNotSelected); - li.find('input[type="checkbox"]').prop('checked', isNotSelected); - // use notAdded instead of true (to detect if the option is selected or not) - li.prop('selected', isNotSelected); - return isNotSelected; + li.querySelector('input[type="checkbox"]').checked = !isSelected; + return isSelected; } _setValueToInput() { - let values = []; - let options = this.$el.find('option'); - options.each((el) => { - if ($(el).prop('selected')) { - let text = $(el) - .text() - .trim(); - values.push(text); - } - }); - if (!values.length) { - let firstDisabled = this.$el.find('option:disabled').eq(0); - if (firstDisabled.length && firstDisabled[0].value === '') - values.push(firstDisabled.text()); - } - this.input.value = values.join(', '); + // console.log('👉 Set Value to Input!'); + const texts = this._values + .filter((value) => value.el.hasAttribute('selected') && !value.el.hasAttribute('disabled')) + .map((value) => value.optionEl.querySelector('span').innerText.trim()); + this.input.value = texts.join(', '); + // let values = []; + // let options = this.$el.find('option'); + // console.log(options); + // options.each((el) => { + // if ($(el).prop('selected')) { + // let text = $(el) + // .text() + // .trim(); + // values.push(text); + // } + // }); + // if (!values.length) { + // let firstDisabled = this.$el.find('option:disabled').eq(0); + // if (firstDisabled.length && firstDisabled[0].value === '') + // values.push(firstDisabled.text()); + // } + // this.input.value = values.join(', '); } _setSelectedStates() { - this._selectedValues = []; this._values.forEach((option) => { const optionIsSelected = $(option.el).prop('selected'); $(option.optionEl) .find('input[type="checkbox"]') .prop('checked', optionIsSelected); + if (optionIsSelected) { this._activateOption($(this.dropdownOptions), $(option.optionEl)); - this._selectedValues.push(option); + //this._selectedValues.push(option); } else $(option.optionEl).removeClass('selected'); }); } // Make option as selected and scroll to selected position - _activateOption(collection, newOption) { - if (!newOption) return; - if (!this.isMultiple) collection.find('li.selected').removeClass('selected'); - $(newOption).addClass('selected'); + _activateOption(ul, option) { + if (!option) return; + if (!this.isMultiple) ul.find('li.selected').removeClass('selected'); + $(option).addClass('selected'); } getSelectedValues() { - return this._selectedValues; + return this._values.filter((value) => value.isSelected); } } From 0622fb0e3d119036dd6a6d8fcb177b0a3ac99ab4 Mon Sep 17 00:00:00 2001 From: Daniel Wurzer Date: Tue, 11 Jan 2022 10:29:54 +0100 Subject: [PATCH 03/10] fix(select): tested and working in Select-Testbed --- js/select.js | 154 +++++++++++++++++++++------------------------------ 1 file changed, 63 insertions(+), 91 deletions(-) diff --git a/js/select.js b/js/select.js index d5974a8e9e..d63fc8eb30 100644 --- a/js/select.js +++ b/js/select.js @@ -64,54 +64,40 @@ this._selectOptionElement(virtualOption); e.stopPropagation(); } + + _arraysEqual(a, b) { + if (a === b) return true; + if (a == null || b == null) return false; + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; ++i) if (a[i] !== b[i]) return false; + return true; + } + _selectOptionElement(virtualOption) { - console.log('Selected an Option Element'); - console.log(virtualOption); - //const key = optionElement.id; - //console.log(key); - if ( - !$(virtualOption).hasClass('disabled') && - !$(virtualOption).hasClass('optgroup') /* && key.length*/ - ) { - //console.log(">>>"); + if (!$(virtualOption).hasClass('disabled') && !$(virtualOption).hasClass('optgroup')) { const value = this._values.filter((value) => value.optionEl === virtualOption)[0]; - //console.log(value); - - let selected = true; + const previousSelectedValues = this.getSelectedValues(); if (this.isMultiple) { - // Deselect placeholder option if still selected. - // let placeholderOption = $(this.dropdownOptions).find('li.disabled.selected'); - // if (placeholderOption.length) { - // placeholderOption.removeClass('selected'); - // placeholderOption.find('input[type="checkbox"]').prop('checked', false); - // this._toggleEntryFromArray(placeholderOption[0].id); - // } - selected = this._toggleEntryFromArray(value); + // Multi-Select + this._toggleEntryFromArray(value); this._setValueToInput(); } else { // Single-Select - //console.log("Single-Select"); - //console.log(optionElement); - //$(this.dropdownOptions).find('li').removeClass('selected'); - //$(virtualOption).toggleClass('selected', selected); this._deselectAll(); value.el.setAttribute('selected', 'selected'); - // TODO: Mark as selected this._setValueToInput(); } - // Set selected on original select option - // Only trigger if selected state changed - // let prevSelected = $(this._values.filter(value => value.el === key)).prop('selected'); - // if (prevSelected !== selected) { - // $(this._values[key].el).prop('selected', selected); - // this.$el.trigger('change'); - // } + const actualSelectedValues = this.getSelectedValues(); + const selectionHasChanged = !this._arraysEqual( + previousSelectedValues, + actualSelectedValues + ); + if (selectionHasChanged) this.$el.trigger('change'); } if (!this.isMultiple) this.dropdown.close(); } _handleInputClick() { - //console.log("Clicked on SelectBox.") if (this.dropdown && this.dropdown.isOpen) { this._setValueToInput(); this._setSelectedStates(); @@ -140,23 +126,28 @@ // Create dropdown structure if (this.$selectOptions.length) { - this.$selectOptions.each((el) => { - if ($(el).is('option')) { + this.$selectOptions.each((realOption) => { + if ($(realOption).is('option')) { // Option - const option = this._createAndAppendOptionWithIcon( - el, + const virtualOption = this._createAndAppendOptionWithIcon( + realOption, this.isMultiple ? 'multiple' : undefined ); - this._addOptionToValues(el, option); - } else if ($(el).is('optgroup')) { + this._addOptionToValues(realOption, virtualOption); + } else if ($(realOption).is('optgroup')) { // Optgroup - let selectOptions = $(el).children('option'); + const selectOptions = $(realOption).children('option'); $(this.dropdownOptions).append( - $('
  • ' + el.getAttribute('label') + '
  • ')[0] + $( + '
  • ' + realOption.getAttribute('label') + '
  • ' + )[0] ); - selectOptions.each((el) => { - const option = this._createAndAppendOptionWithIcon(el, 'optgroup-option'); - this._addOptionToValues(el, option); + selectOptions.each((realOption) => { + const virtualOption = this._createAndAppendOptionWithIcon( + realOption, + 'optgroup-option' + ); + this._addOptionToValues(realOption, virtualOption); }); } }); @@ -215,8 +206,8 @@ this._setSelectedStates(); } - _addOptionToValues(el, optionElement) { - this._values.push({ el, optionEl: optionElement }); + _addOptionToValues(realOption, virtualOption) { + this._values.push({ el: realOption, optionEl: virtualOption }); } _removeDropdown() { @@ -229,21 +220,21 @@ $(this.wrapper).remove(); } - _createAndAppendOptionWithIcon(option, type) { + _createAndAppendOptionWithIcon(realOption, type) { const li = document.createElement('li'); - if (option.disabled) li.classList.add('disabled'); + if (realOption.disabled) li.classList.add('disabled'); if (type === 'optgroup-option') li.classList.add(type); // Text / Checkbox const span = document.createElement('span'); if (this.isMultiple) span.innerHTML = ``; - else span.innerHTML = option.innerHTML; + realOption.disabled ? ' disabled="disabled"' : '' + }>${realOption.innerHTML}`; + else span.innerHTML = realOption.innerHTML; li.appendChild(span); // add Icon - const iconUrl = option.getAttribute('data-icon'); - const classes = option.getAttribute('class'); + const iconUrl = realOption.getAttribute('data-icon'); + const classes = realOption.getAttribute('class'); if (iconUrl) { const img = $(``); li.prepend(img[0]); @@ -262,72 +253,53 @@ this._deselectValue(value); }); } - _toggleEntryFromArray(value) { const li = value.optionEl; const isSelected = li.classList.contains('selected'); - //console.log("Toggle", value, isSelected); if (isSelected) { value.el.removeAttribute('selected'); li.classList.remove('selected'); - li.removeAttribute('selected'); } else { value.el.setAttribute('selected', 'selected'); li.classList.add('selected'); - li.setAttribute('selected', 'selected'); } li.querySelector('input[type="checkbox"]').checked = !isSelected; return isSelected; } - _setValueToInput() { - // console.log('👉 Set Value to Input!'); const texts = this._values .filter((value) => value.el.hasAttribute('selected') && !value.el.hasAttribute('disabled')) .map((value) => value.optionEl.querySelector('span').innerText.trim()); + // Set input-text to first Option with empty value which indicates a description like "choose your option" + if (texts.length === 0) { + const firstDisabledOption = this.$el.find('option:disabled').eq(0); + if (firstDisabledOption.length > 0 && firstDisabledOption[0].value === '') { + this.input.value = firstDisabledOption.text(); + return; + } + } this.input.value = texts.join(', '); - // let values = []; - // let options = this.$el.find('option'); - // console.log(options); - // options.each((el) => { - // if ($(el).prop('selected')) { - // let text = $(el) - // .text() - // .trim(); - // values.push(text); - // } - // }); - // if (!values.length) { - // let firstDisabled = this.$el.find('option:disabled').eq(0); - // if (firstDisabled.length && firstDisabled[0].value === '') - // values.push(firstDisabled.text()); - // } - // this.input.value = values.join(', '); } - _setSelectedStates() { - this._values.forEach((option) => { - const optionIsSelected = $(option.el).prop('selected'); - $(option.optionEl) + this._values.forEach((value) => { + const optionIsSelected = $(value.el).prop('selected'); + $(value.optionEl) .find('input[type="checkbox"]') .prop('checked', optionIsSelected); - if (optionIsSelected) { - this._activateOption($(this.dropdownOptions), $(option.optionEl)); - //this._selectedValues.push(option); - } else $(option.optionEl).removeClass('selected'); + this._activateOption($(this.dropdownOptions), $(value.optionEl)); + } else $(value.optionEl).removeClass('selected'); }); } - - // Make option as selected and scroll to selected position - _activateOption(ul, option) { - if (!option) return; + _activateOption(ul, li) { + if (!li) return; if (!this.isMultiple) ul.find('li.selected').removeClass('selected'); - $(option).addClass('selected'); + $(li).addClass('selected'); } - getSelectedValues() { - return this._values.filter((value) => value.isSelected); + return this._values.filter( + (value) => value.el.hasAttribute('selected') && !value.el.hasAttribute('disabled') + ); } } From 8db1b99af316d46ef1c18b5bb06353f01e5a6ec5 Mon Sep 17 00:00:00 2001 From: Daniel Wurzer Date: Tue, 11 Jan 2022 10:38:46 +0100 Subject: [PATCH 04/10] build(select): improved Testbed for Select --- test/html/select.html | 48 +++++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/test/html/select.html b/test/html/select.html index 1f8f344732..3cef44449d 100644 --- a/test/html/select.html +++ b/test/html/select.html @@ -25,7 +25,7 @@ - +
    @@ -35,7 +35,7 @@ - +
    @@ -49,7 +49,7 @@ - +
    @@ -59,8 +59,9 @@ - +
    +
    - + +
    + +
    + +
    @@ -79,16 +91,30 @@ +
    + + +
    - From 698429f86c42cefe653ed7b543836c099fabfaa1 Mon Sep 17 00:00:00 2001 From: Daniel Wurzer Date: Tue, 11 Jan 2022 10:41:01 +0100 Subject: [PATCH 05/10] fix(select): removed double-rendering of Input, done in change-event --- js/select.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/js/select.js b/js/select.js index d63fc8eb30..9e6e502829 100644 --- a/js/select.js +++ b/js/select.js @@ -80,12 +80,10 @@ if (this.isMultiple) { // Multi-Select this._toggleEntryFromArray(value); - this._setValueToInput(); } else { // Single-Select this._deselectAll(); value.el.setAttribute('selected', 'selected'); - this._setValueToInput(); } const actualSelectedValues = this.getSelectedValues(); const selectionHasChanged = !this._arraysEqual( From ac09fcf58c71b7c7195d483018f282ea0c325559 Mon Sep 17 00:00:00 2001 From: Daniel Wurzer Date: Tue, 11 Jan 2022 10:58:32 +0100 Subject: [PATCH 06/10] refactor(select): cleaned unnecessary spaces --- js/select.js | 10 +--------- test/html/select.html | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/js/select.js b/js/select.js index 9e6e502829..9b0dd99544 100644 --- a/js/select.js +++ b/js/select.js @@ -57,14 +57,12 @@ _handleSelectChange(e) { this._setValueToInput(); } - _handleOptionClick(e) { e.preventDefault(); let virtualOption = $(e.target).closest('li')[0]; this._selectOptionElement(virtualOption); e.stopPropagation(); } - _arraysEqual(a, b) { if (a === b) return true; if (a == null || b == null) return false; @@ -72,7 +70,6 @@ for (let i = 0; i < a.length; ++i) if (a[i] !== b[i]) return false; return true; } - _selectOptionElement(virtualOption) { if (!$(virtualOption).hasClass('disabled') && !$(virtualOption).hasClass('optgroup')) { const value = this._values.filter((value) => value.optionEl === virtualOption)[0]; @@ -94,14 +91,12 @@ } if (!this.isMultiple) this.dropdown.close(); } - _handleInputClick() { if (this.dropdown && this.dropdown.isOpen) { this._setValueToInput(); this._setSelectedStates(); } } - _setupDropdown() { this.wrapper = document.createElement('div'); $(this.wrapper).addClass('select-wrapper ' + this.options.classes); @@ -203,11 +198,9 @@ // Add initial selections this._setSelectedStates(); } - _addOptionToValues(realOption, virtualOption) { this._values.push({ el: realOption, optionEl: virtualOption }); } - _removeDropdown() { $(this.wrapper) .find('.caret') @@ -217,7 +210,6 @@ $(this.wrapper).before(this.$el); $(this.wrapper).remove(); } - _createAndAppendOptionWithIcon(realOption, type) { const li = document.createElement('li'); if (realOption.disabled) li.classList.add('disabled'); @@ -241,7 +233,6 @@ $(this.dropdownOptions).append(li); return li; } - _deselectValue(value) { value.optionEl.classList.remove('selected'); value.el.removeAttribute('selected'); @@ -294,6 +285,7 @@ if (!this.isMultiple) ul.find('li.selected').removeClass('selected'); $(li).addClass('selected'); } + getSelectedValues() { return this._values.filter( (value) => value.el.hasAttribute('selected') && !value.el.hasAttribute('disabled') diff --git a/test/html/select.html b/test/html/select.html index 3cef44449d..ed57d9420a 100644 --- a/test/html/select.html +++ b/test/html/select.html @@ -103,7 +103,7 @@ elems[0].addEventListener('change', e => console.log("First Select changed!")); elems[1].addEventListener('change', e => console.log("Second Select changed!")); - + var instances = M.FormSelect.init(elems, { // specify options here }); From f37253e3bfdf368a6ec5e385f9e7b608100a9b77 Mon Sep 17 00:00:00 2001 From: Daniel Wurzer Date: Tue, 11 Jan 2022 11:09:29 +0100 Subject: [PATCH 07/10] fix(select): had to fix getSelectedValues to return values only so that the tests pass --- js/select.js | 6 +++--- test/html/select.html | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/js/select.js b/js/select.js index 9b0dd99544..5d4992d94f 100644 --- a/js/select.js +++ b/js/select.js @@ -287,9 +287,9 @@ } getSelectedValues() { - return this._values.filter( - (value) => value.el.hasAttribute('selected') && !value.el.hasAttribute('disabled') - ); + return this._values + .filter((value) => value.el.hasAttribute('selected') && !value.el.hasAttribute('disabled')) + .map((value) => value.el.value); } } diff --git a/test/html/select.html b/test/html/select.html index ed57d9420a..3cac52ae99 100644 --- a/test/html/select.html +++ b/test/html/select.html @@ -109,10 +109,10 @@ }); // Methods document.querySelector('.btn1').addEventListener('click', e => { - alert(instances[0].getSelectedValues().map(value => value.el.value)); + alert(instances[0].getSelectedValues()); }); document.querySelector('.btn2').addEventListener('click', e => { - alert(instances[1].getSelectedValues().map(value => value.el.value)); + alert(instances[1].getSelectedValues()); }); }); From cb52a6e14ba263bac5ddb9e2daf79b3857f02440 Mon Sep 17 00:00:00 2001 From: Daniel Wurzer Date: Mon, 17 Jan 2022 16:41:16 +0100 Subject: [PATCH 08/10] fix(select): shows text in select-box when creating options dynamically --- js/select.js | 8 ++- test/html/select.html | 118 +++++++++++++++++++++++++++++++----------- 2 files changed, 95 insertions(+), 31 deletions(-) diff --git a/js/select.js b/js/select.js index 5d4992d94f..69f6a07bb7 100644 --- a/js/select.js +++ b/js/select.js @@ -255,9 +255,13 @@ li.querySelector('input[type="checkbox"]').checked = !isSelected; return isSelected; } + _isOptionChosen(realOption) { + if (realOption.hasAttribute('disabled')) return false; + return realOption.selected || realOption.hasAttribute('selected'); + } _setValueToInput() { const texts = this._values - .filter((value) => value.el.hasAttribute('selected') && !value.el.hasAttribute('disabled')) + .filter((value) => this._isOptionChosen(value.el)) .map((value) => value.optionEl.querySelector('span').innerText.trim()); // Set input-text to first Option with empty value which indicates a description like "choose your option" if (texts.length === 0) { @@ -288,7 +292,7 @@ getSelectedValues() { return this._values - .filter((value) => value.el.hasAttribute('selected') && !value.el.hasAttribute('disabled')) + .filter((value) => this._isOptionChosen(value.el)) .map((value) => value.el.value); } } diff --git a/test/html/select.html b/test/html/select.html index 3cac52ae99..5d47e194a7 100644 --- a/test/html/select.html +++ b/test/html/select.html @@ -38,6 +38,11 @@ +
    + + +
    +
    - - - - - - +
    +
    Selects with Images
    +
    + + +
    +
    + + +
    -
    - - +
    +
    Select with selected Options
    +
    + + +
    +
    + + +
    -
    - - +
    +
    Dynamically generated Select-Options
    +
    + + +
    +
    + + +
    @@ -91,10 +124,6 @@ -
    - - -
    From 1087e38215334ea69452e9357cc8509a708bbe19 Mon Sep 17 00:00:00 2001 From: Daniel Wurzer Date: Tue, 18 Jan 2022 14:41:26 +0100 Subject: [PATCH 09/10] fix(select): fixed pre-select bug in multi-select and improved code-structure --- js/select.js | 59 +++++++++++++++++----------------- test/html/select.html | 73 ++++++++++++++++++++++++++----------------- 2 files changed, 73 insertions(+), 59 deletions(-) diff --git a/js/select.js b/js/select.js index 69f6a07bb7..c672ab3f13 100644 --- a/js/select.js +++ b/js/select.js @@ -80,8 +80,11 @@ } else { // Single-Select this._deselectAll(); - value.el.setAttribute('selected', 'selected'); + this._selectValue(value); } + // Refresh Input-Text + this._setValueToInput(); + // Trigger Change-Event only when data is different const actualSelectedValues = this.getSelectedValues(); const selectionHasChanged = !this._arraysEqual( previousSelectedValues, @@ -166,6 +169,7 @@ // Initialize dropdown if (!this.el.disabled) { let dropdownOptions = $.extend({}, this.options.dropdownOptions); + dropdownOptions.coverTrigger = false; let userOnOpenEnd = dropdownOptions.onOpenEnd; // Add callback for centering selected option when dropdown content is scrollable dropdownOptions.onOpenEnd = (el) => { @@ -233,44 +237,41 @@ $(this.dropdownOptions).append(li); return li; } + + _selectValue(value) { + value.el.selected = true; + value.optionEl.classList.add('selected'); + const checkbox = value.optionEl.querySelector('input[type="checkbox"]'); + if (checkbox) checkbox.checked = true; + } _deselectValue(value) { + value.el.selected = false; value.optionEl.classList.remove('selected'); - value.el.removeAttribute('selected'); + const checkbox = value.optionEl.querySelector('input[type="checkbox"]'); + if (checkbox) checkbox.checked = false; } _deselectAll() { this._values.forEach((value) => { this._deselectValue(value); }); } + _isValueSelected(value) { + const realValues = this.getSelectedValues(); + return realValues.some((realValue) => realValue === value.el.value); + } _toggleEntryFromArray(value) { - const li = value.optionEl; - const isSelected = li.classList.contains('selected'); - if (isSelected) { - value.el.removeAttribute('selected'); - li.classList.remove('selected'); - } else { - value.el.setAttribute('selected', 'selected'); - li.classList.add('selected'); - } - li.querySelector('input[type="checkbox"]').checked = !isSelected; - return isSelected; + const isSelected = this._isValueSelected(value); + if (isSelected) this._deselectValue(value); + else this._selectValue(value); } - _isOptionChosen(realOption) { - if (realOption.hasAttribute('disabled')) return false; - return realOption.selected || realOption.hasAttribute('selected'); + _getSelectedOptions() { + return Array.prototype.map.call(this.el.selectedOptions, (realOption) => realOption); } + _setValueToInput() { - const texts = this._values - .filter((value) => this._isOptionChosen(value.el)) - .map((value) => value.optionEl.querySelector('span').innerText.trim()); - // Set input-text to first Option with empty value which indicates a description like "choose your option" - if (texts.length === 0) { - const firstDisabledOption = this.$el.find('option:disabled').eq(0); - if (firstDisabledOption.length > 0 && firstDisabledOption[0].value === '') { - this.input.value = firstDisabledOption.text(); - return; - } - } + const realOptions = this._getSelectedOptions(); + const values = this._values.filter((value) => realOptions.indexOf(value.el) >= 0); + const texts = values.map((value) => value.optionEl.querySelector('span').innerText.trim()); this.input.value = texts.join(', '); } _setSelectedStates() { @@ -291,9 +292,7 @@ } getSelectedValues() { - return this._values - .filter((value) => this._isOptionChosen(value.el)) - .map((value) => value.el.value); + return this._getSelectedOptions().map((realOption) => realOption.value); } } diff --git a/test/html/select.html b/test/html/select.html index 5d47e194a7..63a2cbab45 100644 --- a/test/html/select.html +++ b/test/html/select.html @@ -18,30 +18,31 @@ -
    - - -
    - -
    - - +
    +
    + + +
    + +
    +
    +
    + + +
    + +
    +
    -
    - - -
    - - - - - +
    +
    + + +
    +
    + + +
    +
    + From 0ff6233df7cff8ebb36d0f1a27edc795aa1d3011 Mon Sep 17 00:00:00 2001 From: Daniel Wurzer Date: Tue, 18 Jan 2022 15:02:42 +0100 Subject: [PATCH 10/10] test(select): had to fix and add the default option to Choose your option for tests --- js/select.js | 10 +++++++++- test/html/select.html | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/js/select.js b/js/select.js index c672ab3f13..80966f4e64 100644 --- a/js/select.js +++ b/js/select.js @@ -265,13 +265,21 @@ else this._selectValue(value); } _getSelectedOptions() { - return Array.prototype.map.call(this.el.selectedOptions, (realOption) => realOption); + return Array.prototype.filter.call(this.el.selectedOptions, (realOption) => realOption); } _setValueToInput() { const realOptions = this._getSelectedOptions(); const values = this._values.filter((value) => realOptions.indexOf(value.el) >= 0); const texts = values.map((value) => value.optionEl.querySelector('span').innerText.trim()); + // Set input-text to first Option with empty value which indicates a description like "choose your option" + if (texts.length === 0) { + const firstDisabledOption = this.$el.find('option:disabled').eq(0); + if (firstDisabledOption.length > 0 && firstDisabledOption[0].value === '') { + this.input.value = firstDisabledOption.text(); + return; + } + } this.input.value = texts.join(', '); } _setSelectedStates() { diff --git a/test/html/select.html b/test/html/select.html index 63a2cbab45..03ad231625 100644 --- a/test/html/select.html +++ b/test/html/select.html @@ -21,6 +21,7 @@
    +