From 44978a6f23df61629163e5e2799eb64a964d8187 Mon Sep 17 00:00:00 2001 From: McRaZick Date: Wed, 2 Apr 2025 13:42:35 +0100 Subject: [PATCH 01/21] Improved keyboard accessibility for DynamicSelect + Added support for Bootstrap .form-control styling (corrected border). + Added support for `tabulation` navigation. + Added focus style for selecting options with newly implemented keyboard navigation functionality. --- DynamicSelect.scss | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/DynamicSelect.scss b/DynamicSelect.scss index 97cdfd4..69e696e 100644 --- a/DynamicSelect.scss +++ b/DynamicSelect.scss @@ -1,3 +1,6 @@ +.dynamic-select.dynamic-select-bootstrap .dynamic-select-header { + border: 0; + } .dynamic-select { display: flex; box-sizing: border-box; @@ -5,6 +8,11 @@ position: relative; width: 100%; user-select: none; + &.dynamic-select-bootstrap { + .dynamic-select-header { + border: 0; + } + } .dynamic-select-header { border: 1px solid #dee2e6; padding: 7px 30px 7px 12px; @@ -63,7 +71,7 @@ } .dynamic-select-option { padding: 7px 12px; - &:hover, &:active { + &:hover, &:active, .dynamic-select-option-focus { background-color: #f3f4f7; } } @@ -110,4 +118,4 @@ font-size: inherit; } } -} \ No newline at end of file +} From 84e9d666da5b608d6d93df897c2aa30f6fd23e03 Mon Sep 17 00:00:00 2001 From: McRaZick Date: Wed, 2 Apr 2025 13:56:08 +0100 Subject: [PATCH 02/21] fixed focus for keyboard accessibility RENAMED dynamic-selected-option-focus TO dynamic-selected-focus AND placed it appropriately. --- DynamicSelect.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DynamicSelect.scss b/DynamicSelect.scss index 69e696e..8fd7c77 100644 --- a/DynamicSelect.scss +++ b/DynamicSelect.scss @@ -71,7 +71,7 @@ } .dynamic-select-option { padding: 7px 12px; - &:hover, &:active, .dynamic-select-option-focus { + &:hover, &:active, &.dynamic-select-focus { background-color: #f3f4f7; } } From 36936f5506045b634715444df62c01d7b4f9d358 Mon Sep 17 00:00:00 2001 From: McRaZick Date: Wed, 2 Apr 2025 14:05:46 +0100 Subject: [PATCH 03/21] Update DynamicSelect.css --- DynamicSelect.css | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/DynamicSelect.css b/DynamicSelect.css index 552ff2c..be8322c 100644 --- a/DynamicSelect.css +++ b/DynamicSelect.css @@ -6,6 +6,9 @@ width: 100%; user-select: none; } +.dynamic-select.dynamic-select-bootstrap .dynamic-select-header { + border: 0; +} .dynamic-select .dynamic-select-header { border: 1px solid #dee2e6; padding: 7px 30px 7px 12px; @@ -66,7 +69,9 @@ .dynamic-select .dynamic-select-options .dynamic-select-option { padding: 7px 12px; } -.dynamic-select .dynamic-select-options .dynamic-select-option:hover, .dynamic-select .dynamic-select-options .dynamic-select-option:active { +.dynamic-select .dynamic-select-options .dynamic-select-option:hover, +.dynamic-select .dynamic-select-options .dynamic-select-option:active, +.dynamic-select .dynamic-select-options .dynamic-select-option.focus { background-color: #f3f4f7; } .dynamic-select .dynamic-select-header, .dynamic-select .dynamic-select-option { @@ -110,4 +115,4 @@ white-space: nowrap; color: inherit; font-size: inherit; -} \ No newline at end of file +} From ab3f751e5199dad779fbb11fc5b4ff23876ba944 Mon Sep 17 00:00:00 2001 From: McRaZick Date: Wed, 2 Apr 2025 14:06:37 +0100 Subject: [PATCH 04/21] Renamed style --- DynamicSelect.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DynamicSelect.scss b/DynamicSelect.scss index 8fd7c77..c4ce641 100644 --- a/DynamicSelect.scss +++ b/DynamicSelect.scss @@ -71,7 +71,7 @@ } .dynamic-select-option { padding: 7px 12px; - &:hover, &:active, &.dynamic-select-focus { + &:hover, &:active, &.focus { background-color: #f3f4f7; } } From 4c9264b1d1fcb0567a3b20b7047a1e1400eea142 Mon Sep 17 00:00:00 2001 From: McRaZick Date: Wed, 2 Apr 2025 13:22:43 +0000 Subject: [PATCH 05/21] Implemented keyboard navigability/accessibility for DynamicSelect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit + Added support for Bootstrap .form-control styling (Set DynamicSelect's option = { bootstrapForm: true }). + Added support for forward `tabulation` navigation. + Added support for backward `tabulation` navigation with `Shift+Tab`. + Added support for `Up & Down Arrow keys` navigation. + Added support for `forward & backward` search with other keyboard keys + Clients can also escape DynamicSelect's options dropdown, using `Esc` (Escape) keyboard button. + Clients can also toggle DynamicSelect's options dropdown, using `Space` (Space bar) keyboard button. + Client can also escape DynamicSelect's options dropdown, using both: forward and backward `tabulation` navigation, when there are no more options to iterate through. + UNICODE is supported. Hence, by hitting [Alt Gr] + [L] (using Polish Programmers Keyboard) you can search through options with textContent, where the first letter is: `ł` or `Ł` (This feature is NOT case-sensitive)) You can also perform a backward search to find specific options, by holding `Shift` + `Any UNICODE Key`, keyboard buttons. NOTE: Most added features work only when the client is focused on the DynamicSelect selection. --- DynamicSelect.js | 153 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 138 insertions(+), 15 deletions(-) diff --git a/DynamicSelect.js b/DynamicSelect.js index 601ba9e..8b6c70e 100644 --- a/DynamicSelect.js +++ b/DynamicSelect.js @@ -1,14 +1,20 @@ /* - * Created by David Adams + * Created by "David Adams" * https://codeshack.io/dynamic-select-images-html-javascript/ * * Released under the MIT license + * + * Modified by 'Gabriel Książek' (McRaZick) | https://github.com/gubrus50 + * Modification: 'Implemented keyboard navigability/accessibility for DynamicSelect' + * Date of last modification: '02/April/2025' (Timezone: UTC-0) */ class DynamicSelect { constructor(element, options = {}) { let defaults = { + bootstrapForm: false, /* Requires bootstrap5 library */ placeholder: 'Select an option', + tabindex: 0, columns: 1, name: '', width: '', @@ -40,6 +46,7 @@ class DynamicSelect { } this.element = this._template(); this.selectElement.replaceWith(this.element); + this.optionElement = this.element.querySelector('.dynamic-select-selected'); this._updateSelected(); this._eventHandlers(); } @@ -64,33 +71,35 @@ class DynamicSelect { `; } let template = ` -
- +
+
${this.placeholder}
${optionsHTML}
`; let element = document.createElement('div'); + + if (this.options.bootstrapForm) + element.classList.add('form-control', 'px-0'); + + element.setAttribute('tabindex', this.options.tabindex); element.innerHTML = template; return element; } _eventHandlers() { this.element.querySelectorAll('.dynamic-select-option').forEach(option => { - option.onclick = () => { - this.element.querySelectorAll('.dynamic-select-selected').forEach(selected => selected.classList.remove('dynamic-select-selected')); - option.classList.add('dynamic-select-selected'); - this.element.querySelector('.dynamic-select-header').innerHTML = option.innerHTML; - this.element.querySelector('input').value = option.getAttribute('data-value'); - this.data.forEach(data => data.selected = false); - this.data.filter(data => data.value == option.getAttribute('data-value'))[0].selected = true; - this.element.querySelector('.dynamic-select-header').classList.remove('dynamic-select-header-active'); - this.options.onChange(option.getAttribute('data-value'), option.querySelector('.dynamic-select-option-text') ? option.querySelector('.dynamic-select-option-text').innerHTML : '', option); - }; + option.onclick = () => this.optionElement = option; }); - this.element.querySelector('.dynamic-select-header').onclick = () => { + this.element.querySelector('.dynamic-select-header').onclick = event => { + event.stopPropagation(); this.element.querySelector('.dynamic-select-header').classList.toggle('dynamic-select-header-active'); - }; + }; + this.element.addEventListener('focus', () => { + if (!this.element.querySelector('.dynamic-select-header').classList.contains('dynamic-select-header-active')) + this.element.querySelector('.dynamic-select-header').classList.add('dynamic-select-header-active'); + this.optionElement = this.optionElement; + }); if (this.selectElement.id && document.querySelector('label[for="' + this.selectElement.id + '"]')) { document.querySelector('label[for="' + this.selectElement.id + '"]').onclick = () => { this.element.querySelector('.dynamic-select-header').classList.toggle('dynamic-select-header-active'); @@ -101,6 +110,79 @@ class DynamicSelect { this.element.querySelector('.dynamic-select-header').classList.remove('dynamic-select-header-active'); } }); + document.addEventListener('keydown', event => { + if (event.target.querySelector('.dynamic-select-header') && event.code == 'Space') + this.element.querySelector('.dynamic-select-header').classList.toggle('dynamic-select-header-active'); + if (!event.target.querySelector('.dynamic-select-header-active')) return; + + if (['Escape', 'Shift', 'Tab', 'ArrowUp', 'ArrowDown'].includes(event.key)) event.preventDefault(); + + let value = this.element.querySelector('input').value; + let index = this.data.findIndex(data => data.value == value); + + const exit = () => this.element.querySelector('.dynamic-select-header').classList.remove('dynamic-select-header-active'); + + const nextIndex = () => { index++; + if (index >= this.data.length && event.key == 'ArrowDown') index = 0; + else if (index >= this.data.length && event.key == 'Tab') exit(); + }; + + const prevIndex = () => { index--; + if (index < 0 && event.key == 'ArrowUp') index = this.data.length - 1; + else if (index < 0 && (event.key == 'Tab' && event.shiftKey == true)) exit(); + }; + + const nextOptionByLetter = (letter) => { + letter = letter.toLowerCase().trim(); + let option = this.optionElement.nextElementSibling; + let text; + + for (let i = 0; i < 2; i++) { + while (option) { + text = option.querySelector('.dynamic-select-option-text')?.textContent.toLowerCase().trim(); + if (text.startsWith(letter)) return option; + else option = option.nextElementSibling; + } + option = this.element.querySelector('.dynamic-select-options .dynamic-select-option'); + } + return null; + }; + + const prevOptionByLetter = (letter) => { + letter = letter.toLowerCase().trim(); + let option = this.optionElement.previousElementSibling; + let text; + + for (let i = 0; i < 2; i++) { + while (option) { + text = option.querySelector('.dynamic-select-option-text')?.textContent.toLowerCase().trim(); + if (text.startsWith(letter)) return option; + else option = option.previousElementSibling; + } + option = this.element.querySelector('.dynamic-select-options .dynamic-select-option:last-child'); + } + return null; + }; + + + if (event.key == 'Escape') return exit(); + else if (event.key == 'ArrowUp' || (event.key == 'Tab' && event.shiftKey == true)) prevIndex(); + else if (event.key == 'ArrowDown'|| event.key == 'Tab') nextIndex(); + else if (event.key.length === 1 && event.key.match(/^\p{Letter}/u)) + { + let option = (event.key && event.shiftKey == true) + ? prevOptionByLetter(event.key) + : nextOptionByLetter(event.key); + + if (option && option !== this.optionElement) this.optionElement = option; + return; + } + else return; + + + let newOption = this.element.querySelectorAll('.dynamic-select-option')[index]; + if (newOption) this.optionElement = newOption; + }); } _updateSelected() { @@ -131,6 +213,39 @@ class DynamicSelect { return this.options.selectElement; } + set optionElement(value) { + this.options.optionElement = value; + + this.element.querySelectorAll('.dynamic-select-selected').forEach(selected => selected.classList.remove('dynamic-select-selected')); + this.options.optionElement.classList.add('dynamic-select-selected'); + this.element.querySelector('.dynamic-select-header').innerHTML = this.options.optionElement.innerHTML; + this.element.querySelector('input').value = this.options.optionElement.getAttribute('data-value'); + this.data.forEach(data => data.selected = false); + this.data.filter(data => data.value == this.options.optionElement.getAttribute('data-value'))[0].selected = true; + this.options.onChange( + this.options.optionElement.getAttribute('data-value'), + this.options.optionElement.querySelector('.dynamic-select-option-text') + ? this.options.optionElement.querySelector('.dynamic-select-option-text').innerHTML + : '', this.options.optionElement + ); + + const containerRect = this.element.querySelector('.dynamic-select-options').getBoundingClientRect(); + const optionRect = this.options.optionElement.getBoundingClientRect(); + + if (optionRect.top < containerRect.top + || optionRect.bottom > containerRect.bottom + || optionRect.left < containerRect.left + || optionRect.right > containerRect.right) + this.options.optionElement.scrollIntoView({ block: "nearest" }); + + this.element.querySelectorAll('.dynamic-select-option').forEach(option => option.classList.remove('focus')); + this.optionElement.classList.toggle('focus'); + } + + get optionElement() { + return this.options.optionElement; + } + set element(value) { this.options.element = value; } @@ -147,6 +262,14 @@ class DynamicSelect { return this.options.placeholder; } + set tabindex(value) { + this.options.tabindex = value; + } + + get tabindex() { + return this.options.tabindex; + } + set columns(value) { this.options.columns = value; } From 47633ee1b9bc8b8bd80f3d355a5723dd6f518f61 Mon Sep 17 00:00:00 2001 From: McRaZick Date: Wed, 2 Apr 2025 13:25:52 +0000 Subject: [PATCH 06/21] Removed mistake duplicate style for bootstrap support --- DynamicSelect.scss | 3 --- 1 file changed, 3 deletions(-) diff --git a/DynamicSelect.scss b/DynamicSelect.scss index c4ce641..761a54f 100644 --- a/DynamicSelect.scss +++ b/DynamicSelect.scss @@ -1,6 +1,3 @@ -.dynamic-select.dynamic-select-bootstrap .dynamic-select-header { - border: 0; - } .dynamic-select { display: flex; box-sizing: border-box; From cdaee1d93d560ab7b101d601344b3fe8fc92b4c5 Mon Sep 17 00:00:00 2001 From: McRaZick Date: Wed, 2 Apr 2025 15:12:23 +0100 Subject: [PATCH 07/21] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f308316..b115dd9 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ To use this dynamic select with images in your project, follow these steps: ```javascript new DynamicSelect('#dynamic-select', { placeholder: 'Select an option', + tabindex: 0, columns: 1, width: '300px', height: '40px', @@ -96,6 +97,7 @@ You can also use custom HTML content for the options: ```javascript new DynamicSelect('#custom-select', { + bootstrapForm: true, placeholder: 'Select an option', data: [ { value: '1', html: 'Option 1Option 1' }, From 30d4b2874a6d42b3a068e9b080fcdd366cb69333 Mon Sep 17 00:00:00 2001 From: McRaZick Date: Wed, 2 Apr 2025 15:13:18 +0100 Subject: [PATCH 08/21] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b115dd9..b210770 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ To use this dynamic select with images in your project, follow these steps: }); ``` -### Advanced Example with Custom HTML Content +### Advanced Example with Custom HTML Content + Bootstrap CSS You can also use custom HTML content for the options: @@ -97,7 +97,7 @@ You can also use custom HTML content for the options: ```javascript new DynamicSelect('#custom-select', { - bootstrapForm: true, + bootstrapForm: true, // Requires Bootstrap5 library placeholder: 'Select an option', data: [ { value: '1', html: 'Option 1Option 1' }, From 2396462b0f70f180b8069684ece28dfe0b9ecd02 Mon Sep 17 00:00:00 2001 From: McRaZick Date: Thu, 3 Apr 2025 17:30:36 +0000 Subject: [PATCH 09/21] Fixed bugs --- DynamicSelect.js | 95 +++++++++++++++++++++++++++++------------------- README.md | 27 ++++++++++++-- 2 files changed, 81 insertions(+), 41 deletions(-) diff --git a/DynamicSelect.js b/DynamicSelect.js index 8b6c70e..88f125f 100644 --- a/DynamicSelect.js +++ b/DynamicSelect.js @@ -6,7 +6,7 @@ * * Modified by 'Gabriel Książek' (McRaZick) | https://github.com/gubrus50 * Modification: 'Implemented keyboard navigability/accessibility for DynamicSelect' - * Date of last modification: '02/April/2025' (Timezone: UTC-0) + * Date of last modification: '03/April/2025' (Timezone: UTC-0) */ class DynamicSelect { @@ -47,6 +47,7 @@ class DynamicSelect { this.element = this._template(); this.selectElement.replaceWith(this.element); this.optionElement = this.element.querySelector('.dynamic-select-selected'); + this._handlerState = 0; // 0 = not focused and closed, 1 = focused and open, 2 = focused and closed this._updateSelected(); this._eventHandlers(); } @@ -91,35 +92,50 @@ class DynamicSelect { this.element.querySelectorAll('.dynamic-select-option').forEach(option => { option.onclick = () => this.optionElement = option; }); - this.element.querySelector('.dynamic-select-header').onclick = event => { - event.stopPropagation(); - this.element.querySelector('.dynamic-select-header').classList.toggle('dynamic-select-header-active'); - }; + this.element.addEventListener('click', () => { + (this._handlerState > 1) + ? this._handlerState = 1 + : this._handlerState++; + if ((this._handlerState > 1) && (document.activeElement === this.element)) + this.element.querySelector('.dynamic-select-header').classList.toggle('dynamic-select-header-active'); + else this.element.querySelector('.dynamic-select-header').classList.add('dynamic-select-header-active'); + }); this.element.addEventListener('focus', () => { if (!this.element.querySelector('.dynamic-select-header').classList.contains('dynamic-select-header-active')) - this.element.querySelector('.dynamic-select-header').classList.add('dynamic-select-header-active'); + this.element.querySelector('.dynamic-select-header').classList.add('dynamic-select-header-active'); this.optionElement = this.optionElement; }); if (this.selectElement.id && document.querySelector('label[for="' + this.selectElement.id + '"]')) { document.querySelector('label[for="' + this.selectElement.id + '"]').onclick = () => { this.element.querySelector('.dynamic-select-header').classList.toggle('dynamic-select-header-active'); - }; + } } + document.addEventListener('click', event => { - if (!event.target.closest('.' + this.name) && !event.target.closest('label[for="' + this.selectElement.id + '"]')) { + if (!event.target.closest('.' + this.name) + && !event.target.closest('label[for="' + this.selectElement.id + '"]') + && !event.target.closest('div:has(.' + this.name + ')')?.children[0].classList.contains(this.name)) + { this.element.querySelector('.dynamic-select-header').classList.remove('dynamic-select-header-active'); + this._handlerState = 0; } }); + document.addEventListener('keydown', event => { - if (event.target.querySelector('.dynamic-select-header') && event.code == 'Space') + + if (event.target.querySelector('.dynamic-select-header') && event.code == 'Space') { this.element.querySelector('.dynamic-select-header').classList.toggle('dynamic-select-header-active'); + (this._handlerState > 1) + ? this._handlerState = 1 + : this._handlerState++; + } if (!event.target.querySelector('.dynamic-select-header-active')) return; - if (['Escape', 'Shift', 'Tab', 'ArrowUp', 'ArrowDown'].includes(event.key)) event.preventDefault(); - + let value = this.element.querySelector('input').value; let index = this.data.findIndex(data => data.value == value); + const exit = () => this.element.querySelector('.dynamic-select-header').classList.remove('dynamic-select-header-active'); const nextIndex = () => { index++; @@ -132,35 +148,40 @@ class DynamicSelect { else if (index < 0 && (event.key == 'Tab' && event.shiftKey == true)) exit(); }; - const nextOptionByLetter = (letter) => { + const findOptionByLetter = (letter, direction) => { + let option, text; + try { letter = letter.toLowerCase().trim(); - let option = this.optionElement.nextElementSibling; - let text; - - for (let i = 0; i < 2; i++) { - while (option) { - text = option.querySelector('.dynamic-select-option-text')?.textContent.toLowerCase().trim(); - if (text.startsWith(letter)) return option; - else option = option.nextElementSibling; + option = (direction === 'next') + ? this.optionElement.nextElementSibling + : this.optionElement.previousElementSibling; + } + catch (error) { + if (!option) { + if (this.optionElement) + throw ReferenceError(`Failed to get next/previous option adjacent to optionElement ${console.warn(this.optionElement) || ''}`); + if (this.selectedValue !== '') + throw ReferenceError(`Invalid DynamicSelect option was selected ${console.warn(this.optionElement) || ''}`); + option = this.element.querySelector('.dynamic-select-options .dynamic-select-option') ?? (() => { + throw ReferenceError('No valid DynamicSelect options available in .dynamic-select-options') + })(); } - option = this.element.querySelector('.dynamic-select-options .dynamic-select-option'); + else throw new Error(error); } - return null; - }; - - const prevOptionByLetter = (letter) => { - letter = letter.toLowerCase().trim(); - let option = this.optionElement.previousElementSibling; - let text; - + for (let i = 0; i < 2; i++) { while (option) { text = option.querySelector('.dynamic-select-option-text')?.textContent.toLowerCase().trim(); + if (!text) throw ReferenceError(`Missing '.dynamic-select-option-text' child in DynamicSelect option ${console.warn(option) || ''}`); + if (text.startsWith(letter)) return option; - else option = option.previousElementSibling; + option = (direction === 'next') ? option.nextElementSibling : option.previousElementSibling; } - option = this.element.querySelector('.dynamic-select-options .dynamic-select-option:last-child'); + option = (direction === 'next') + ? this.element.querySelector('.dynamic-select-options .dynamic-select-option') + : this.element.querySelector('.dynamic-select-options .dynamic-select-option:last-child'); } + return null; }; @@ -170,10 +191,7 @@ class DynamicSelect { else if (event.key == 'ArrowDown'|| event.key == 'Tab') nextIndex(); else if (event.key.length === 1 && event.key.match(/^\p{Letter}/u)) { - let option = (event.key && event.shiftKey == true) - ? prevOptionByLetter(event.key) - : nextOptionByLetter(event.key); - + let option = findOptionByLetter(event.key, event.shiftKey ? 'prev' : 'next'); if (option && option !== this.optionElement) this.optionElement = option; return; } @@ -181,7 +199,7 @@ class DynamicSelect { let newOption = this.element.querySelectorAll('.dynamic-select-option')[index]; - if (newOption) this.optionElement = newOption; + if (newOption) this.optionElement = newOption; }); } @@ -214,6 +232,8 @@ class DynamicSelect { } set optionElement(value) { + if (!value?.classList.contains('dynamic-select-option')) return; + this.options.optionElement = value; this.element.querySelectorAll('.dynamic-select-selected').forEach(selected => selected.classList.remove('dynamic-select-selected')); @@ -222,6 +242,7 @@ class DynamicSelect { this.element.querySelector('input').value = this.options.optionElement.getAttribute('data-value'); this.data.forEach(data => data.selected = false); this.data.filter(data => data.value == this.options.optionElement.getAttribute('data-value'))[0].selected = true; + this.options.onChange( this.options.optionElement.getAttribute('data-value'), this.options.optionElement.querySelector('.dynamic-select-option-text') @@ -236,7 +257,7 @@ class DynamicSelect { || optionRect.bottom > containerRect.bottom || optionRect.left < containerRect.left || optionRect.right > containerRect.right) - this.options.optionElement.scrollIntoView({ block: "nearest" }); + this.options.optionElement.scrollIntoView({ block: 'nearest' }); this.element.querySelectorAll('.dynamic-select-option').forEach(option => option.classList.remove('focus')); this.optionElement.classList.toggle('focus'); diff --git a/README.md b/README.md index b210770..5a1a066 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ To use this dynamic select with images in your project, follow these steps: }); ``` -### Advanced Example with Custom HTML Content + Bootstrap CSS +### Advanced Example with Custom HTML Content You can also use custom HTML content for the options: @@ -100,9 +100,9 @@ new DynamicSelect('#custom-select', { bootstrapForm: true, // Requires Bootstrap5 library placeholder: 'Select an option', data: [ - { value: '1', html: 'Option 1Option 1' }, - { value: '2', html: 'Option 2Option 2' }, - { value: '3', html: 'Option 3Option 3' } + { value: '1', html: 'Option 1Option 1' }, + { value: '2', html: 'Option 2Option 2' }, + { value: '3', html: 'Option 3Option 3' } ], onChange: function(value, text, option) { console.log(value, text, option); @@ -110,6 +110,21 @@ new DynamicSelect('#custom-select', { }); ``` +**data** list containing object with a **html** option, requires strictly one element with **class** `.dynamic-select-option-text`. In the above example, that would be for each `` element. This enhances search functionality by allowing clients to filter options based on the first letter, making selection more efficient. + +```html +Option N +``` + +**bootstrapForm** option styles the **DynamicSelect** `
` wrapper like so: + +```html +
+``` +This modified `
` wrapper makes it easier to apply **Bootstrap** styles to the **DynamicSelect** element. Additionally, it provides `.dynamic-select-bootstrap` class, removing default border style from a `.dynamic-select-header` element. + + + ### Example with Multiple Columns For dropdown options to be displayed in multiple columns: @@ -145,7 +160,9 @@ It is useful if you want to populate images in a grid-like view. To customize the dynamic select with images, you can modify the HTML and JavaScript as needed. The following options are available: +- `bootstrapForm`: Converts select element to bootstrap's form-control. - `placeholder`: Placeholder text for the select element. +- `tabindex`: Tabindex attribute for the select element's div wrapper. - `columns`: Number of columns in the dropdown. - `name`: Name attribute for the select element. - `width`: Width of the select element. @@ -153,6 +170,8 @@ To customize the dynamic select with images, you can modify the HTML and JavaScr - `data`: Array of objects representing the select options. - `onChange`: Callback function when the selected option changes. +NOTE: **bootstrapForm** option requires library: Bootstrap (v5 / latest). + Example configuration: ```javascript new DynamicSelect('#dynamic-select', { From b30e6c5a6bb002260699a6e3bbbd403879d784a0 Mon Sep 17 00:00:00 2001 From: McRaZick Date: Thu, 3 Apr 2025 18:35:31 +0100 Subject: [PATCH 10/21] Update README.md Included new options to the Example configuration --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5a1a066..3b291d0 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,9 @@ NOTE: **bootstrapForm** option requires library: Bootstrap (v5 / latest). Example configuration: ```javascript new DynamicSelect('#dynamic-select', { + bootstrapForm: true, placeholder: 'Select an option', + tabindex: 0, columns: 2, width: '300px', height: '50px', From c89188067a9b929665096708a6e1a879d432bf31 Mon Sep 17 00:00:00 2001 From: McRaZick Date: Thu, 3 Apr 2025 20:21:27 +0100 Subject: [PATCH 11/21] Update DynamicSelect.js Fixed issue where many objects used to change options of one another unintensionally --- DynamicSelect.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DynamicSelect.js b/DynamicSelect.js index 88f125f..0eeef20 100644 --- a/DynamicSelect.js +++ b/DynamicSelect.js @@ -122,7 +122,7 @@ class DynamicSelect { }); document.addEventListener('keydown', event => { - + if (document.activeElement !== this.element) return; if (event.target.querySelector('.dynamic-select-header') && event.code == 'Space') { this.element.querySelector('.dynamic-select-header').classList.toggle('dynamic-select-header-active'); (this._handlerState > 1) @@ -324,4 +324,4 @@ class DynamicSelect { } } -document.querySelectorAll('[data-dynamic-select]').forEach(select => new DynamicSelect(select)); \ No newline at end of file +document.querySelectorAll('[data-dynamic-select]').forEach(select => new DynamicSelect(select)); From fb55b0843dfe2679ba8eec924f2e00f3bf6d12f2 Mon Sep 17 00:00:00 2001 From: McRaZick Date: Thu, 3 Apr 2025 20:39:29 +0100 Subject: [PATCH 12/21] Update DynamicSelect.js "focus" event When multiple dropdowns are open, clicking a new one should: Close all other open dropdowns. and Keep only the clicked dropdown open --- DynamicSelect.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DynamicSelect.js b/DynamicSelect.js index 0eeef20..0169331 100644 --- a/DynamicSelect.js +++ b/DynamicSelect.js @@ -101,6 +101,8 @@ class DynamicSelect { else this.element.querySelector('.dynamic-select-header').classList.add('dynamic-select-header-active'); }); this.element.addEventListener('focus', () => { + if (document.activeElement === this.element) + document.querySelectorAll('.dynamic-select-header').forEach(header => header.classList.remove('dynamic-select-header-active')) if (!this.element.querySelector('.dynamic-select-header').classList.contains('dynamic-select-header-active')) this.element.querySelector('.dynamic-select-header').classList.add('dynamic-select-header-active'); this.optionElement = this.optionElement; From 5573ec9af80519563528035a737397a7e35dcb51 Mon Sep 17 00:00:00 2001 From: McRaZick Date: Sun, 6 Apr 2025 01:45:40 +0000 Subject: [PATCH 13/21] Fixed bugs, improved accessibility and disabled attribute functionality. Removed previouslly added support for Bootstrap, and more --- DynamicSelect.css | 32 ++++-- DynamicSelect.js | 284 ++++++++++++++++++++++++++++------------------ 2 files changed, 198 insertions(+), 118 deletions(-) diff --git a/DynamicSelect.css b/DynamicSelect.css index be8322c..6e513f7 100644 --- a/DynamicSelect.css +++ b/DynamicSelect.css @@ -1,13 +1,25 @@ -.dynamic-select { - display: flex; - box-sizing: border-box; - flex-direction: column; - position: relative; - width: 100%; - user-select: none; +dynamic-select, +dynamic-select selection, +dynamic-select selected, +dynamic-select dropdown, +dynamic-select item { + all: unset; + display: block; +} +dynamic-select[disabled], +.dynamic-select .dynamic-select-options .dynamic-select-option[disabled] { + opacity: 0.5; + pointer-events: none; + background-color: transparent; + pointer-events: none; } -.dynamic-select.dynamic-select-bootstrap .dynamic-select-header { - border: 0; +.dynamic-select { + display: flex; + box-sizing: border-box; + flex-direction: column; + position: relative; + width: 100%; + user-select: none; } .dynamic-select .dynamic-select-header { border: 1px solid #dee2e6; @@ -115,4 +127,4 @@ white-space: nowrap; color: inherit; font-size: inherit; -} +} \ No newline at end of file diff --git a/DynamicSelect.js b/DynamicSelect.js index 0169331..a2ebe62 100644 --- a/DynamicSelect.js +++ b/DynamicSelect.js @@ -5,30 +5,62 @@ * Released under the MIT license * * Modified by 'Gabriel Książek' (McRaZick) | https://github.com/gubrus50 - * Modification: 'Implemented keyboard navigability/accessibility for DynamicSelect' - * Date of last modification: '03/April/2025' (Timezone: UTC-0) + * Contribution date: 'From 02/April/2025 To 06/April/2025 (Timezone: UTC-0)' + * Implementation: + * + added keyboard navigability/accessibility for DynamicSelect. + * + added support for "disabled" attribute. + * -/+ replaced primary elements with custom semantic elements: + * this.element = + * .dynamic-select = + * .dynamic-select-selected = + * .dynamic-select-options = + * .dynamic-select-option = + * + * -/+ moved ID attribute FROM TO + * + added support for extending classes and styles for most semantic elements. + * + 'new DynamicSelect()' object now inherits options from target -
${this.placeholder}
-
${optionsHTML}
-
+ ${this.placeholder} + ${optionsHTML} + `; - let element = document.createElement('div'); - if (this.options.bootstrapForm) - element.classList.add('form-control', 'px-0'); - - element.setAttribute('tabindex', this.options.tabindex); + let element = document.createElement('dynamic-select'); + element.setAttribute('id', this.selectElement.id && this.selectElement.id); + element.setAttribute('class', this.options.class); + element.setAttribute('style', this.options.style); + element.setAttribute('tabindex', this.tabindex); + (this.disabled == true) && + element.setAttribute('disabled', ''); element.innerHTML = template; return element; } _eventHandlers() { - this.element.querySelectorAll('.dynamic-select-option').forEach(option => { - option.onclick = () => this.optionElement = option; - }); - this.element.addEventListener('click', () => { - (this._handlerState > 1) - ? this._handlerState = 1 - : this._handlerState++; - if ((this._handlerState > 1) && (document.activeElement === this.element)) - this.element.querySelector('.dynamic-select-header').classList.toggle('dynamic-select-header-active'); - else this.element.querySelector('.dynamic-select-header').classList.add('dynamic-select-header-active'); - }); - this.element.addEventListener('focus', () => { - if (document.activeElement === this.element) - document.querySelectorAll('.dynamic-select-header').forEach(header => header.classList.remove('dynamic-select-header-active')) - if (!this.element.querySelector('.dynamic-select-header').classList.contains('dynamic-select-header-active')) - this.element.querySelector('.dynamic-select-header').classList.add('dynamic-select-header-active'); - this.optionElement = this.optionElement; - }); - if (this.selectElement.id && document.querySelector('label[for="' + this.selectElement.id + '"]')) { - document.querySelector('label[for="' + this.selectElement.id + '"]').onclick = () => { - this.element.querySelector('.dynamic-select-header').classList.toggle('dynamic-select-header-active'); - } + + const getContextOfClickedTargetDynamicOption = (eventTarget) => { + const parentOptions = eventTarget.closest('.dynamic-select-options'); + const clickedOption = eventTarget.closest('.dynamic-select-option'); + return !parentOptions + && !clickedOption + ? { clicked: false } + : { clicked: !!parentOptions, disabled: !clickedOption } } + new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.attributeName === 'disabled'); + this.disabled = this.element.hasAttribute('disabled'); + })}).observe(this.element, { attributes: true }); + + const header = this.element.querySelector('.dynamic-select-header'); + this.open = () => (!this.disabled) && header.classList.add('dynamic-select-header-active'); + this.close = () => (!this.disabled) && header.classList.remove('dynamic-select-header-active'); + this.toggle = () => (!this.disabled) && header.classList.toggle('dynamic-select-header-active'); + + this.element.querySelectorAll('.dynamic-select-option').forEach(option => + option.addEventListener('click', () => this.optionElement = option)); + + this.element.addEventListener('click', (event) => { + if (document.activeElement !== this.element) return; + if (!header.classList.contains('dynamic-select-header-active')) return this.open(); + if (event.target.closest('dynamic-select')) + { + const option = getContextOfClickedTargetDynamicOption(event.target); + if (!option.clicked) return this.toggle(); + else (option.clicked && !option.disabled) && this.close(); + } + }); + + this.element.addEventListener('blur', () => setTimeout(() => + (!this.element.contains(document.activeElement)) && this.close(), 50)); + + if (this.selectElement.id && document.querySelector(`label[for="${this.selectElement.id}"]`)) + document.querySelector(`label[for="${this.selectElement.id}"]`).onclick = () => this.toggle(); + document.addEventListener('click', event => { if (!event.target.closest('.' + this.name) && !event.target.closest('label[for="' + this.selectElement.id + '"]') - && !event.target.closest('div:has(.' + this.name + ')')?.children[0].classList.contains(this.name)) + && !event.target.closest('div:has(.' + this.name + ')')?.children[0].classList.contains(this.name) + && event.target.closest('dynamic-select') !== this.element) { - this.element.querySelector('.dynamic-select-header').classList.remove('dynamic-select-header-active'); - this._handlerState = 0; + const option = getContextOfClickedTargetDynamicOption(event.target); + if (option.clicked && !option.disabled) this.close(); } }); - document.addEventListener('keydown', event => { - if (document.activeElement !== this.element) return; - if (event.target.querySelector('.dynamic-select-header') && event.code == 'Space') { - this.element.querySelector('.dynamic-select-header').classList.toggle('dynamic-select-header-active'); - (this._handlerState > 1) - ? this._handlerState = 1 - : this._handlerState++; - } - if (!event.target.querySelector('.dynamic-select-header-active')) return; - if (['Escape', 'Shift', 'Tab', 'ArrowUp', 'ArrowDown'].includes(event.key)) event.preventDefault(); - - let value = this.element.querySelector('input').value; - let index = this.data.findIndex(data => data.value == value); - + const reservedKeys = ['Escape', 'Enter', ' ', 'Shift', 'Tab', 'ArrowUp', 'ArrowDown']; - const exit = () => this.element.querySelector('.dynamic-select-header').classList.remove('dynamic-select-header-active'); + document.addEventListener('keydown', event => { + if (document.activeElement !== this.element + || !event.target.querySelector('.dynamic-select-header.dynamic-select-header-active') && event.key == 'Tab') return; + if (reservedKeys.includes(event.key) || event.code == 'Space') event.preventDefault(); + + if (event.key == 'Enter') this.toggle(); + else if (event.key == ' ' + || event.code == 'Space') this.open(); + + let newOption, direction, + value = this.element.querySelector(`input[name="${this.name}"]`).value, + index = this.data.findIndex(data => data.value == value), + options = [...this.element.querySelectorAll('.dynamic-select-option')]; + if (!options) throw new Error('No valid DynamicSelect options available'); + const nextIndex = () => { index++; if (index >= this.data.length && event.key == 'ArrowDown') index = 0; - else if (index >= this.data.length && event.key == 'Tab') exit(); + else if (index >= this.data.length && event.key == 'Tab') this.close(); }; const prevIndex = () => { index--; if (index < 0 && event.key == 'ArrowUp') index = this.data.length - 1; - else if (index < 0 && (event.key == 'Tab' && event.shiftKey == true)) exit(); + else if (index < 0 && (event.key == 'Tab' && event.shiftKey == true)) this.close(); }; const findOptionByLetter = (letter, direction) => { - let option, text; - try { - letter = letter.toLowerCase().trim(); - option = (direction === 'next') - ? this.optionElement.nextElementSibling - : this.optionElement.previousElementSibling; - } - catch (error) { - if (!option) { - if (this.optionElement) - throw ReferenceError(`Failed to get next/previous option adjacent to optionElement ${console.warn(this.optionElement) || ''}`); - if (this.selectedValue !== '') - throw ReferenceError(`Invalid DynamicSelect option was selected ${console.warn(this.optionElement) || ''}`); - option = this.element.querySelector('.dynamic-select-options .dynamic-select-option') ?? (() => { - throw ReferenceError('No valid DynamicSelect options available in .dynamic-select-options') - })(); - } - else throw new Error(error); - } + letter = letter.toLowerCase().trim(); - for (let i = 0; i < 2; i++) { - while (option) { - text = option.querySelector('.dynamic-select-option-text')?.textContent.toLowerCase().trim(); - if (!text) throw ReferenceError(`Missing '.dynamic-select-option-text' child in DynamicSelect option ${console.warn(option) || ''}`); - - if (text.startsWith(letter)) return option; - option = (direction === 'next') ? option.nextElementSibling : option.previousElementSibling; + const currentIndex = options.findIndex(opt => opt === this.optionElement); + const step = direction === 'next' ? 1 : -1; + + // Start from current position (or beginning/end if not found) + let start = currentIndex !== -1 ? currentIndex : (step === 1 ? -1 : options.length); + + for (let i = 1; i <= options.length; i++) { + const index = (start + step * i + options.length) % options.length; + const option = options[index]; + const text = option.querySelector('.dynamic-select-option-text')?.textContent.toLowerCase().trim(); + + if (!text) { + console.warn('Missing .dynamic-select-option-text in option', option); + continue; } - option = (direction === 'next') - ? this.element.querySelector('.dynamic-select-options .dynamic-select-option') - : this.element.querySelector('.dynamic-select-options .dynamic-select-option:last-child'); - } + if (text.startsWith(letter) && !option.hasAttribute('disabled')) return option; + } return null; }; - if (event.key == 'Escape') return exit(); - else if (event.key == 'ArrowUp' || (event.key == 'Tab' && event.shiftKey == true)) prevIndex(); - else if (event.key == 'ArrowDown'|| event.key == 'Tab') nextIndex(); - else if (event.key.length === 1 && event.key.match(/^\p{Letter}/u)) - { - let option = findOptionByLetter(event.key, event.shiftKey ? 'prev' : 'next'); - if (option && option !== this.optionElement) this.optionElement = option; + if (event.key == 'Escape') { + return this.close(); + } + else if (event.key == 'ArrowUp' || (event.key == 'Tab' && event.shiftKey == true)) { + direction = 'prev'; prevIndex(); + } + else if (event.key == 'ArrowDown' || event.key == 'Tab') { + direction = 'next'; nextIndex(); + } + else if (event.key.length === 1 && event.key.match(/^\p{Letter}/u)) { + direction = event.shiftKey ? 'prev' : 'next'; + newOption = findOptionByLetter(event.key, direction); + if (newOption + && newOption !== this.optionElement + && !newOption.hasAttribute('disabled')) this.optionElement = newOption; return; } else return; - - - let newOption = this.element.querySelectorAll('.dynamic-select-option')[index]; - if (newOption) this.optionElement = newOption; + + + for (let i = 0; i < options.length; i++) { + newOption = options[index]; + if (newOption + && !newOption.hasAttribute('disabled')) return this.optionElement = newOption; + + else direction === 'next' ? nextIndex() : prevIndex(); + } }); } @@ -234,14 +289,16 @@ class DynamicSelect { } set optionElement(value) { - if (!value?.classList.contains('dynamic-select-option')) return; - + if (!value?.classList.contains('dynamic-select-option') + || value.hasAttribute('disabled') + || this.element.hasAttribute('disabled')) return; + this.options.optionElement = value; this.element.querySelectorAll('.dynamic-select-selected').forEach(selected => selected.classList.remove('dynamic-select-selected')); this.options.optionElement.classList.add('dynamic-select-selected'); this.element.querySelector('.dynamic-select-header').innerHTML = this.options.optionElement.innerHTML; - this.element.querySelector('input').value = this.options.optionElement.getAttribute('data-value'); + this.element.querySelector(`input[name="${this.name}"]`).value = this.options.optionElement.getAttribute('data-value'); this.data.forEach(data => data.selected = false); this.data.filter(data => data.value == this.options.optionElement.getAttribute('data-value'))[0].selected = true; @@ -325,5 +382,16 @@ class DynamicSelect { return this.options.height; } + set disabled(value) { + this.options.disabled = value; + (this.disabled == true) + ? this.element.querySelector(`input[name="${this.name}"]`).setAttribute('disabled', '') + : this.element.querySelector(`input[name="${this.name}"]`).removeAttribute('disabled'); + } + + get disabled() { + return this.options.disabled; + } } -document.querySelectorAll('[data-dynamic-select]').forEach(select => new DynamicSelect(select)); + +document.querySelectorAll('[data-dynamic-select]').forEach(select => new DynamicSelect(select)); \ No newline at end of file From 91e9485b5cb33e8b3126dd56ae3dff77d08a74a0 Mon Sep 17 00:00:00 2001 From: McRaZick Date: Sun, 6 Apr 2025 02:14:24 +0000 Subject: [PATCH 14/21] Updated SCSS and CSS --- DynamicSelect.css | 39 +++++++++++++++++++-------------------- DynamicSelect.scss | 24 +++++++++++++++++++----- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/DynamicSelect.css b/DynamicSelect.css index 6e513f7..4a94525 100644 --- a/DynamicSelect.css +++ b/DynamicSelect.css @@ -1,32 +1,27 @@ -dynamic-select, -dynamic-select selection, -dynamic-select selected, -dynamic-select dropdown, -dynamic-select item { +dynamic-select selection, dynamic-select selected, dynamic-select dropdown, dynamic-select item { all: unset; display: block; } -dynamic-select[disabled], -.dynamic-select .dynamic-select-options .dynamic-select-option[disabled] { +dynamic-select[disabled], .dynamic-select .dynamic-select-options .dynamic-select-option[disabled] { opacity: 0.5; pointer-events: none; background-color: transparent; pointer-events: none; } .dynamic-select { - display: flex; - box-sizing: border-box; - flex-direction: column; - position: relative; - width: 100%; - user-select: none; + display: flex; + box-sizing: border-box; + flex-direction: column; + position: relative; + width: 100%; + user-select: none; } .dynamic-select .dynamic-select-header { border: 1px solid #dee2e6; padding: 7px 30px 7px 12px; } .dynamic-select .dynamic-select-header::after { - content: ""; + content: ''; display: block; position: absolute; top: 50%; @@ -81,11 +76,15 @@ dynamic-select[disabled], .dynamic-select .dynamic-select-options .dynamic-select-option { padding: 7px 12px; } -.dynamic-select .dynamic-select-options .dynamic-select-option:hover, -.dynamic-select .dynamic-select-options .dynamic-select-option:active, -.dynamic-select .dynamic-select-options .dynamic-select-option.focus { +.dynamic-select .dynamic-select-options .dynamic-select-option:hover, .dynamic-select .dynamic-select-options .dynamic-select-option:active, .dynamic-select .dynamic-select-options .dynamic-select-option.focus { background-color: #f3f4f7; } +.dynamic-select .dynamic-select-options .dynamic-select-option[disabled] { + opacity: 0.5; + pointer-events: none; + background-color: transparent; + pointer-events: none; +} .dynamic-select .dynamic-select-header, .dynamic-select .dynamic-select-option { display: flex; box-sizing: border-box; @@ -109,14 +108,14 @@ dynamic-select[disabled], max-height: none; max-width: none; } -.dynamic-select .dynamic-select-header img, .dynamic-select .dynamic-select-header svg, .dynamic-select .dynamic-select-header i, .dynamic-select .dynamic-select-header span, .dynamic-select .dynamic-select-option img, .dynamic-select .dynamic-select-option svg, .dynamic-select .dynamic-select-option i, .dynamic-select .dynamic-select-option span { +.dynamic-select .dynamic-select-header img, .dynamic-select .dynamic-select-option img, .dynamic-select .dynamic-select-header svg, .dynamic-select .dynamic-select-option svg, .dynamic-select .dynamic-select-header i, .dynamic-select .dynamic-select-option i, .dynamic-select .dynamic-select-header span, .dynamic-select .dynamic-select-option span { box-sizing: border-box; margin-right: 10px; } .dynamic-select .dynamic-select-header.dynamic-select-no-text, .dynamic-select .dynamic-select-option.dynamic-select-no-text { justify-content: center; } -.dynamic-select .dynamic-select-header.dynamic-select-no-text img, .dynamic-select .dynamic-select-header.dynamic-select-no-text svg, .dynamic-select .dynamic-select-header.dynamic-select-no-text i, .dynamic-select .dynamic-select-header.dynamic-select-no-text span, .dynamic-select .dynamic-select-option.dynamic-select-no-text img, .dynamic-select .dynamic-select-option.dynamic-select-no-text svg, .dynamic-select .dynamic-select-option.dynamic-select-no-text i, .dynamic-select .dynamic-select-option.dynamic-select-no-text span { +.dynamic-select .dynamic-select-header.dynamic-select-no-text img, .dynamic-select .dynamic-select-option.dynamic-select-no-text img, .dynamic-select .dynamic-select-header.dynamic-select-no-text svg, .dynamic-select .dynamic-select-option.dynamic-select-no-text svg, .dynamic-select .dynamic-select-header.dynamic-select-no-text i, .dynamic-select .dynamic-select-option.dynamic-select-no-text i, .dynamic-select .dynamic-select-header.dynamic-select-no-text span, .dynamic-select .dynamic-select-option.dynamic-select-no-text span { margin-right: 0; } .dynamic-select .dynamic-select-header .dynamic-select-option-text, .dynamic-select .dynamic-select-option .dynamic-select-option-text { @@ -127,4 +126,4 @@ dynamic-select[disabled], white-space: nowrap; color: inherit; font-size: inherit; -} \ No newline at end of file +} diff --git a/DynamicSelect.scss b/DynamicSelect.scss index 761a54f..6823a0d 100644 --- a/DynamicSelect.scss +++ b/DynamicSelect.scss @@ -1,3 +1,16 @@ +dynamic-select { + selection, selected, dropdown, item { + all: unset; + display: block; + } +} +dynamic-select[disabled], +.dynamic-select .dynamic-select-options .dynamic-select-option[disabled] { + opacity: 0.5; + pointer-events: none; + background-color: transparent; + pointer-events: none; +} .dynamic-select { display: flex; box-sizing: border-box; @@ -5,11 +18,6 @@ position: relative; width: 100%; user-select: none; - &.dynamic-select-bootstrap { - .dynamic-select-header { - border: 0; - } - } .dynamic-select-header { border: 1px solid #dee2e6; padding: 7px 30px 7px 12px; @@ -71,6 +79,12 @@ &:hover, &:active, &.focus { background-color: #f3f4f7; } + &[disabled] { + opacity: 0.5; + pointer-events: none; + background-color: transparent; + pointer-events: none; + } } } .dynamic-select-header, .dynamic-select-option { From e7f456e9a60b70487fe1b2da94dcd21955b134d4 Mon Sep 17 00:00:00 2001 From: McRaZick Date: Sun, 6 Apr 2025 02:23:18 +0000 Subject: [PATCH 15/21] Updated SCSS CSS (correction) --- DynamicSelect.css | 6 ------ DynamicSelect.scss | 6 ------ 2 files changed, 12 deletions(-) diff --git a/DynamicSelect.css b/DynamicSelect.css index 4a94525..f4de0ce 100644 --- a/DynamicSelect.css +++ b/DynamicSelect.css @@ -79,12 +79,6 @@ dynamic-select[disabled], .dynamic-select .dynamic-select-options .dynamic-selec .dynamic-select .dynamic-select-options .dynamic-select-option:hover, .dynamic-select .dynamic-select-options .dynamic-select-option:active, .dynamic-select .dynamic-select-options .dynamic-select-option.focus { background-color: #f3f4f7; } -.dynamic-select .dynamic-select-options .dynamic-select-option[disabled] { - opacity: 0.5; - pointer-events: none; - background-color: transparent; - pointer-events: none; -} .dynamic-select .dynamic-select-header, .dynamic-select .dynamic-select-option { display: flex; box-sizing: border-box; diff --git a/DynamicSelect.scss b/DynamicSelect.scss index 6823a0d..b3d2373 100644 --- a/DynamicSelect.scss +++ b/DynamicSelect.scss @@ -79,12 +79,6 @@ dynamic-select[disabled], &:hover, &:active, &.focus { background-color: #f3f4f7; } - &[disabled] { - opacity: 0.5; - pointer-events: none; - background-color: transparent; - pointer-events: none; - } } } .dynamic-select-header, .dynamic-select-option { From 9f1f4c66e82c7bbdbfc4d974121da0725959e49a Mon Sep 17 00:00:00 2001 From: McRaZick Date: Tue, 8 Apr 2025 02:56:59 +0000 Subject: [PATCH 16/21] Updated documentation, fixed some bugs with placeholder and more --- DynamicSelect.js | 69 +++++++++++++++------------- README.md | 117 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 129 insertions(+), 57 deletions(-) diff --git a/DynamicSelect.js b/DynamicSelect.js index a2ebe62..34d7a08 100644 --- a/DynamicSelect.js +++ b/DynamicSelect.js @@ -5,7 +5,7 @@ * Released under the MIT license * * Modified by 'Gabriel Książek' (McRaZick) | https://github.com/gubrus50 - * Contribution date: 'From 02/April/2025 To 06/April/2025 (Timezone: UTC-0)' + * Contribution date: 'From 02/April/2025 To 08/April/2025 (Timezone: UTC-0)' * Implementation: * + added keyboard navigability/accessibility for DynamicSelect. * + added support for "disabled" attribute. @@ -31,19 +31,21 @@ class DynamicSelect { tabindex: 0, columns: 1, name: '', - style: '', - class: '', width: '', height: '', + style: '', + class: '', disabled: false, + selectionStyle: '', + selectionClass: '', + selectedStyle: '', + selectedClass: '', dropdownWidth: '', dropdownHeight: '', dropdownStyle: '', dropdownClass: '', - selectedStyle: '', - selectedClass: '', - selectionStyle: '', - selectionClass: '', + itemsStyle: '', + itemsClass: '', data: [], onChange: function() {} }; @@ -69,18 +71,18 @@ class DynamicSelect { value: options[i].value, text: options[i].innerHTML, img: options[i].getAttribute('data-img'), - selected: options[i].selected, html: options[i].getAttribute('data-html'), imgWidth: options[i].getAttribute('data-img-width'), imgHeight: options[i].getAttribute('data-img-height'), - disabled: options[i].hasAttribute('disabled') + selected: options[i].hasAttribute('selected'), + disabled: options[i].hasAttribute('disabled'), }); } } this.element = this._template(); this.selectElement.replaceWith(this.element); - this.optionElement = this.element.querySelector('.dynamic-select-selected'); + this.optionElement = this.optionElement = this.selectedValue ? this.element.querySelector(`.dynamic-select-option[data-value="${this.selectedValue}"]`) : null; this.disabled = this.disabled; this._updateSelected(); this._eventHandlers(); @@ -100,7 +102,7 @@ class DynamicSelect { `; } optionsHTML += ` - + ${optionContent} `; @@ -142,9 +144,9 @@ class DynamicSelect { })}).observe(this.element, { attributes: true }); const header = this.element.querySelector('.dynamic-select-header'); - this.open = () => (!this.disabled) && header.classList.add('dynamic-select-header-active'); - this.close = () => (!this.disabled) && header.classList.remove('dynamic-select-header-active'); - this.toggle = () => (!this.disabled) && header.classList.toggle('dynamic-select-header-active'); + this.open = () => {(!this.disabled) && header.classList.add('dynamic-select-header-active'); this.scrollToSelectedOption()} + this.close = () => (!this.disabled) && header.classList.remove('dynamic-select-header-active'); + this.toggle = () => {(!this.disabled) && header.classList.toggle('dynamic-select-header-active'); this.scrollToSelectedOption()} this.element.querySelectorAll('.dynamic-select-option').forEach(option => option.addEventListener('click', () => this.optionElement = option)); @@ -170,8 +172,9 @@ class DynamicSelect { if (!event.target.closest('.' + this.name) && !event.target.closest('label[for="' + this.selectElement.id + '"]') && !event.target.closest('div:has(.' + this.name + ')')?.children[0].classList.contains(this.name) - && event.target.closest('dynamic-select') !== this.element) + && event.target.closest('dynamic-select') !== this.element) { + const option = getContextOfClickedTargetDynamicOption(event.target); if (option.clicked && !option.disabled) this.close(); } @@ -249,7 +252,7 @@ class DynamicSelect { } else return; - + for (let i = 0; i < options.length; i++) { newOption = options[index]; if (newOption @@ -266,6 +269,18 @@ class DynamicSelect { } } + scrollToSelectedOption() { + if (!this.options.optionElement) return; + const containerRect = this.element.querySelector('.dynamic-select-options').getBoundingClientRect(); + const optionRect = this.options.optionElement.getBoundingClientRect(); + + if (optionRect.top < containerRect.top + || optionRect.bottom > containerRect.bottom + || optionRect.left < containerRect.left + || optionRect.right > containerRect.right) + this.options.optionElement.scrollIntoView({ block: 'nearest' }); + } + get selectedValue() { let selected = this.data.filter(option => option.selected); selected = selected.length ? selected[0].value : ''; @@ -301,23 +316,15 @@ class DynamicSelect { this.element.querySelector(`input[name="${this.name}"]`).value = this.options.optionElement.getAttribute('data-value'); this.data.forEach(data => data.selected = false); this.data.filter(data => data.value == this.options.optionElement.getAttribute('data-value'))[0].selected = true; - + this.options.onChange( this.options.optionElement.getAttribute('data-value'), - this.options.optionElement.querySelector('.dynamic-select-option-text') - ? this.options.optionElement.querySelector('.dynamic-select-option-text').innerHTML - : '', this.options.optionElement + this.options.optionElement.querySelector('.dynamic-select-option-text') ? + this.options.optionElement.querySelector('.dynamic-select-option-text').innerHTML : '', + this.options.optionElement ); - const containerRect = this.element.querySelector('.dynamic-select-options').getBoundingClientRect(); - const optionRect = this.options.optionElement.getBoundingClientRect(); - - if (optionRect.top < containerRect.top - || optionRect.bottom > containerRect.bottom - || optionRect.left < containerRect.left - || optionRect.right > containerRect.right) - this.options.optionElement.scrollIntoView({ block: 'nearest' }); - + this.scrollToSelectedOption(); this.element.querySelectorAll('.dynamic-select-option').forEach(option => option.classList.remove('focus')); this.optionElement.classList.toggle('focus'); } @@ -384,8 +391,8 @@ class DynamicSelect { set disabled(value) { this.options.disabled = value; - (this.disabled == true) - ? this.element.querySelector(`input[name="${this.name}"]`).setAttribute('disabled', '') + this.disabled == true + ? this.element.querySelector(`input[name="${this.name}"]`).setAttribute('disabled','') : this.element.querySelector(`input[name="${this.name}"]`).removeAttribute('disabled'); } diff --git a/README.md b/README.md index 3b291d0..bd9d4d6 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,13 @@ The complete guide and reference is available here: [https://codeshack.io/multi- - Customizable placeholder text - Multiple columns for dropdown options - Custom HTML content for options +- Semantic naming convension - Easy integration with existing forms +- Navigability with tabulation and UP/DOWN arrow keys +- Toggle, Open & Close with keyboard buttons: **Enter**, **Spacebar** & **Esc** +- Search by first letter using any keyboard buttons (a-z, 0-9 + supports UNICODE) +- Reverse search by holding **shift** key +- Disable functionality of dynamic select element and any option from the dropdown in realtime - Lightweight and fast ## Screenshot @@ -30,9 +36,9 @@ The complete guide and reference is available here: [https://codeshack.io/multi- 3. Initialize the dynamic select element in your HTML file: ```html ``` @@ -49,6 +55,11 @@ The complete guide and reference is available here: [https://codeshack.io/multi- }); ``` +`new DynamicSelect()` object inherits attribute data from the target element. Hence property: **placeholder**, overwrites dataset attribute: **data-placeholder**. + +Additionally, **class** and **style** attributes are reserved for the `` element. +- `disabled`: Disabled attribute for the `` element. +
+ +**NOTE** - Added `disabled` **attribute** to `` also disables ``'s ``.
Additionally, `disabled` **property** requires a boolean value for it to be initialized and updated. +
+ +- `style`: Style attribute for the `` element. +- `class`: Class attribute for the `` element. +- `width`: Width attribute for the `` element. +- `height`: Height attribute for the `` element. +- `selectionStyle`: Style attribute for the `` element. +- `selectionClass`: Class attribute for the `` element. +- `selectedStyle`: Style attribute for the `` element. +- `selectedClass`: Class attribute for the `` element. +
+ +- `dropdownWidth`: Width attribute for the `` element. +- `dropdownHeight`: Height attribute for the `` element. +- `dropdownStyle`: Style attribute for the `` element. +- `dropdownClass`: Class attribute for the `` element. +- `itemsStyle`: Style attribute for all instances of the `` element. +- `itemsClass`: Class attribute for all instances of the `` element. +
+ +**NOTE** - Any **style** or **class** option, for example: **dropdownClass**, overwrites its defined properties: width, height, + and any styles from the *"DynamicSelect.css"* stylesheet. +
+ +- `data`: Array of objects representing the select options: `` element. + +
+ +**NOTE** - Present ` - - + + + ``` @@ -231,23 +231,9 @@ new DynamicSelect('#dynamic-select', { dropdownStyle: 'background-color: #E8E8E8', // Customize options ( elements of parent ) data: [ - { - value: '1', - text: 'Option 1', - img: 'path/to/image1.jpg' - }, - { - value: '2', - text: 'Option 2', - img: 'path/to/image2.jpg', - disabled: true - }, - { - value: '3', - text: 'Option 3', - img: 'path/to/image3.jpg', - selected: true - } + { value: '1', text: 'Option 1', img: 'path/to/image1.jpg' }, + { value: '2', text: 'Option 2', img: 'path/to/image2.jpg', disabled: true }, + { value: '3', text: 'Option 3', img: 'path/to/image3.jpg', selected: true } ], onChange: function(value, text, option) { console.log(value, text, option); From 86ab05db8a63692a8abe54fd880b1ad0bf4b12f6 Mon Sep 17 00:00:00 2001 From: McRaZick Date: Tue, 8 Apr 2025 04:46:35 +0100 Subject: [PATCH 18/21] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c07f6d..484e77f 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,7 @@ To customize the dynamic select with images, you can modify the HTML and JavaScr - `onChange`: Callback function when the selected option changes. - +
Example configuration: ```javascript From 9bc987e8a18f39d962827a24c01c7e8abe53471c Mon Sep 17 00:00:00 2001 From: McRaZick Date: Tue, 8 Apr 2025 04:48:08 +0100 Subject: [PATCH 19/21] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 484e77f..aaab879 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ Custom defined HTML context data of `` element requires an instance of `.d **More Advanced Examples** - https://jsfiddle.net/sat6h1r4/ +
### Example with Multiple Columns @@ -242,6 +243,7 @@ new DynamicSelect('#dynamic-select', { ``` **More Advanced Examples** - https://jsfiddle.net/sat6h1r4/ +
## License From f61fbfdfce806564dcb78d5fac4d9a5e24612989 Mon Sep 17 00:00:00 2001 From: McRaZick Date: Tue, 8 Apr 2025 04:50:52 +0100 Subject: [PATCH 20/21] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index aaab879..1766c2c 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ The complete guide and reference is available here: [https://codeshack.io/multi- Additionally, **class** and **style** attributes are reserved for the ` element, + * and can modify them. For example, by adding extra options or overwriting existing ones. + */ + +class DynamicSelect { + + constructor(element, options = {}) { + + this.options = { + placeholder: 'Select an option', + tabindex: 0, + columns: 1, + name: '', + width: '', + height: '', + style: '', + class: '', + disabled: false, + selectionStyle: '', + selectionClass: '', + selectedStyle: '', + selectedClass: '', + dropdownWidth: '', + dropdownHeight: '', + dropdownStyle: '', + dropdownClass: '', + itemsStyle: '', + itemsClass: '', + data: [], + onChange: function() {} + }; + + this.selectElement = typeof element === 'string' ? document.querySelector(element) : element; + this.options.disabled = (this.selectElement.hasAttribute('disabled') || this.selectElement.dataset.hasOwnProperty('disabled')); + + for (const prop in this.selectElement.dataset) { + if (this.options.hasOwnProperty(prop)) { + if (prop === 'disabled') this.options[prop] = true; + else this.options[prop] = this.selectElement.dataset[prop]; + } + } + + // Prioritize user defined options from DynamicSelect object over those directly defined by data attributes + Object.assign(this.options, options); + + this.name = this.selectElement.getAttribute('name') ? this.selectElement.getAttribute('name') : 'dynamic-select-' + Math.floor(Math.random() * 1000000); + if (!this.options.data.length) { + let options = this.selectElement.querySelectorAll('option'); + for (let i = 0; i < options.length; i++) { + this.options.data.push({ + value: options[i].value, + text: options[i].innerHTML, + img: options[i].getAttribute('data-img'), + html: options[i].getAttribute('data-html'), + imgWidth: options[i].getAttribute('data-img-width'), + imgHeight: options[i].getAttribute('data-img-height'), + selected: options[i].hasAttribute('selected'), + disabled: options[i].hasAttribute('disabled'), + }); + } + } + + this.element = this._template(); + this.selectElement.replaceWith(this.element); + this.optionElement = this.optionElement = this.selectedValue ? this.element.querySelector(`.dynamic-select-option[data-value="${this.selectedValue}"]`) : null; + this.disabled = this.disabled; + this._updateSelected(); + this._eventHandlers(); + } + + _template() { + let optionsHTML = ''; + for (let i = 0; i < this.data.length; i++) { + let optionWidth = 100 / this.columns; + let optionContent = ''; + if (this.data[i].html) { + optionContent = this.data[i].html; + } else { + optionContent = ` + ${this.data[i].img ? `${this.data[i].text}` : ''} + ${this.data[i].text ? '' + this.data[i].text + '' : ''} + `; + } + optionsHTML += ` + + ${optionContent} + + `; + } + let template = ` + + + ${this.placeholder} + ${optionsHTML} + + `; + + let element = document.createElement('dynamic-select'); + element.setAttribute('id', this.selectElement.id && this.selectElement.id); + element.setAttribute('class', this.options.class); + element.setAttribute('style', this.options.style); + element.setAttribute('tabindex', this.tabindex); + (this.disabled == true) && + element.setAttribute('disabled', ''); + element.innerHTML = template; + return element; + } + + _eventHandlers() { + + const getContextOfClickedTargetDynamicOption = (eventTarget) => { + const parentOptions = eventTarget.closest('.dynamic-select-options'); + const clickedOption = eventTarget.closest('.dynamic-select-option'); + return !parentOptions + && !clickedOption + ? { clicked: false } + : { clicked: !!parentOptions, disabled: !clickedOption } + } + + new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.attributeName === 'disabled'); + this.disabled = this.element.hasAttribute('disabled'); + })}).observe(this.element, { attributes: true }); + + const header = this.element.querySelector('.dynamic-select-header'); + this.open = () => {(!this.disabled) && header.classList.add('dynamic-select-header-active'); this.scrollToSelectedOption()} + this.close = () => (!this.disabled) && header.classList.remove('dynamic-select-header-active'); + this.toggle = () => {(!this.disabled) && header.classList.toggle('dynamic-select-header-active'); this.scrollToSelectedOption()} + + this.element.querySelectorAll('.dynamic-select-option').forEach(option => + option.addEventListener('click', () => this.optionElement = option)); + + this.element.addEventListener('click', (event) => { + if (document.activeElement !== this.element) return; + if (!header.classList.contains('dynamic-select-header-active')) return this.open(); + if (event.target.closest('dynamic-select')) + { + const option = getContextOfClickedTargetDynamicOption(event.target); + if (!option.clicked) return this.toggle(); + else (option.clicked && !option.disabled) && this.close(); + } + }); + + this.element.addEventListener('blur', () => setTimeout(() => + (!this.element.contains(document.activeElement)) && this.close(), 50)); + + if (this.selectElement.id && document.querySelector(`label[for="${this.selectElement.id}"]`)) + document.querySelector(`label[for="${this.selectElement.id}"]`).onclick = () => this.toggle(); + + document.addEventListener('click', event => { + if (!event.target.closest('.' + this.name) + && !event.target.closest('label[for="' + this.selectElement.id + '"]') + && !event.target.closest('div:has(.' + this.name + ')')?.children[0].classList.contains(this.name) + && event.target.closest('dynamic-select') !== this.element) + { + + const option = getContextOfClickedTargetDynamicOption(event.target); + if (option.clicked && !option.disabled) this.close(); + } + }); + + const reservedKeys = ['Escape', 'Enter', ' ', 'Shift', 'Tab', 'ArrowUp', 'ArrowDown']; + + document.addEventListener('keydown', event => { + if (document.activeElement !== this.element + || !event.target.querySelector('.dynamic-select-header.dynamic-select-header-active') && event.key == 'Tab') return; + if (reservedKeys.includes(event.key) || event.code == 'Space') event.preventDefault(); + + if (event.key == 'Enter') this.toggle(); + else if (event.key == ' ' + || event.code == 'Space') this.open(); + + let newOption, direction, + value = this.element.querySelector(`input[name="${this.name}"]`).value, + index = this.data.findIndex(data => data.value == value), + options = [...this.element.querySelectorAll('.dynamic-select-option')]; + if (!options) throw new Error('No valid DynamicSelect options available'); + + + const nextIndex = () => { index++; + if (index >= this.data.length && event.key == 'ArrowDown') index = 0; + else if (index >= this.data.length && event.key == 'Tab') this.close(); + }; + + const prevIndex = () => { index--; + if (index < 0 && event.key == 'ArrowUp') index = this.data.length - 1; + else if (index < 0 && (event.key == 'Tab' && event.shiftKey == true)) this.close(); + }; + + const findOptionByLetter = (letter, direction) => { + letter = letter.toLowerCase().trim(); + + const currentIndex = options.findIndex(opt => opt === this.optionElement); + const step = direction === 'next' ? 1 : -1; + + // Start from current position (or beginning/end if not found) + let start = currentIndex !== -1 ? currentIndex : (step === 1 ? -1 : options.length); + + for (let i = 1; i <= options.length; i++) { + const index = (start + step * i + options.length) % options.length; + const option = options[index]; + const text = option.querySelector('.dynamic-select-option-text')?.textContent.toLowerCase().trim(); + + if (!text) { + console.warn('Missing .dynamic-select-option-text in option', option); + continue; + } + + if (text.startsWith(letter) && !option.hasAttribute('disabled')) return option; + } + return null; + }; + + + if (event.key == 'Escape') { + return this.close(); + } + else if (event.key == 'ArrowUp' || (event.key == 'Tab' && event.shiftKey == true)) { + direction = 'prev'; prevIndex(); + } + else if (event.key == 'ArrowDown' || event.key == 'Tab') { + direction = 'next'; nextIndex(); + } + else if (event.key.length === 1 && event.key.match(/^\p{Letter}/u)) { + direction = event.shiftKey ? 'prev' : 'next'; + newOption = findOptionByLetter(event.key, direction); + if (newOption + && newOption !== this.optionElement + && !newOption.hasAttribute('disabled')) this.optionElement = newOption; + return; + } + else return; + + + for (let i = 0; i < options.length; i++) { + newOption = options[index]; + if (newOption + && !newOption.hasAttribute('disabled')) return this.optionElement = newOption; + + else direction === 'next' ? nextIndex() : prevIndex(); + } + }); + } + + _updateSelected() { + if (this.selectedValue) { + this.element.querySelector('.dynamic-select-header').innerHTML = this.element.querySelector('.dynamic-select-selected').innerHTML; + } + } + + scrollToSelectedOption() { + if (!this.options.optionElement) return; + const containerRect = this.element.querySelector('.dynamic-select-options').getBoundingClientRect(); + const optionRect = this.options.optionElement.getBoundingClientRect(); + + if (optionRect.top < containerRect.top + || optionRect.bottom > containerRect.bottom + || optionRect.left < containerRect.left + || optionRect.right > containerRect.right) + this.options.optionElement.scrollIntoView({ block: 'nearest' }); + } + + get selectedValue() { + let selected = this.data.filter(option => option.selected); + selected = selected.length ? selected[0].value : ''; + return selected; + } + + set data(value) { + this.options.data = value; + } + + get data() { + return this.options.data; + } + + set selectElement(value) { + this.options.selectElement = value; + } + + get selectElement() { + return this.options.selectElement; + } + + set optionElement(value) { + if (!value?.classList.contains('dynamic-select-option') + || value.hasAttribute('disabled') + || this.element.hasAttribute('disabled')) return; + + this.options.optionElement = value; + + this.element.querySelectorAll('.dynamic-select-selected').forEach(selected => selected.classList.remove('dynamic-select-selected')); + this.options.optionElement.classList.add('dynamic-select-selected'); + this.element.querySelector('.dynamic-select-header').innerHTML = this.options.optionElement.innerHTML; + this.element.querySelector(`input[name="${this.name}"]`).value = this.options.optionElement.getAttribute('data-value'); + this.data.forEach(data => data.selected = false); + this.data.filter(data => data.value == this.options.optionElement.getAttribute('data-value'))[0].selected = true; + + this.options.onChange( + this.options.optionElement.getAttribute('data-value'), + this.options.optionElement.querySelector('.dynamic-select-option-text') ? + this.options.optionElement.querySelector('.dynamic-select-option-text').innerHTML : '', + this.options.optionElement + ); + + this.scrollToSelectedOption(); + this.element.querySelectorAll('.dynamic-select-option').forEach(option => option.classList.remove('focus')); + this.optionElement.classList.toggle('focus'); + } + + get optionElement() { + return this.options.optionElement; + } + + set element(value) { + this.options.element = value; + } + + get element() { + return this.options.element; + } + + set placeholder(value) { + this.options.placeholder = value; + } + + get placeholder() { + return this.options.placeholder; + } + + set tabindex(value) { + this.options.tabindex = value; + } + + get tabindex() { + return this.options.tabindex; + } + + set columns(value) { + this.options.columns = value; + } + + get columns() { + return this.options.columns; + } + + set name(value) { + this.options.name = value; + } + + get name() { + return this.options.name; + } + + set width(value) { + this.options.width = value; + } + + get width() { + return this.options.width; + } + + set height(value) { + this.options.height = value; + } + + get height() { + return this.options.height; + } + + set disabled(value) { + this.options.disabled = value; + this.disabled == true + ? this.element.querySelector(`input[name="${this.name}"]`).setAttribute('disabled','') + : this.element.querySelector(`input[name="${this.name}"]`).removeAttribute('disabled'); + } + + get disabled() { + return this.options.disabled; + } +} + +document.querySelectorAll('[data-dynamic-select]').forEach(select => new DynamicSelect(select)); \ No newline at end of file diff --git a/advanced examples/cz.svg b/advanced examples/cz.svg new file mode 100644 index 0000000..dcd0a6b --- /dev/null +++ b/advanced examples/cz.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/advanced examples/ee.svg b/advanced examples/ee.svg new file mode 100644 index 0000000..5a6a7e3 --- /dev/null +++ b/advanced examples/ee.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/advanced examples/fi.svg b/advanced examples/fi.svg new file mode 100644 index 0000000..aba2ef3 --- /dev/null +++ b/advanced examples/fi.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/advanced examples/hu.svg b/advanced examples/hu.svg new file mode 100644 index 0000000..088242d --- /dev/null +++ b/advanced examples/hu.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/advanced examples/index.html b/advanced examples/index.html new file mode 100644 index 0000000..4980192 --- /dev/null +++ b/advanced examples/index.html @@ -0,0 +1,118 @@ + + + + + + Document + + + + + + + + +
+ +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/advanced examples/lt.svg b/advanced examples/lt.svg new file mode 100644 index 0000000..52ada94 --- /dev/null +++ b/advanced examples/lt.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/advanced examples/lv.svg b/advanced examples/lv.svg new file mode 100644 index 0000000..5af883c --- /dev/null +++ b/advanced examples/lv.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/advanced examples/md.svg b/advanced examples/md.svg new file mode 100644 index 0000000..f204511 --- /dev/null +++ b/advanced examples/md.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/advanced examples/pl.svg b/advanced examples/pl.svg new file mode 100644 index 0000000..8c43577 --- /dev/null +++ b/advanced examples/pl.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/advanced examples/ro.svg b/advanced examples/ro.svg new file mode 100644 index 0000000..e6cf0f6 --- /dev/null +++ b/advanced examples/ro.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/advanced examples/sk.svg b/advanced examples/sk.svg new file mode 100644 index 0000000..4846127 --- /dev/null +++ b/advanced examples/sk.svg @@ -0,0 +1,9 @@ + + + + + + + + +