PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 7.1.1
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v7.1.1
7.2.1 7.2 7.1.2 7.1.1 7.1 7.0.4 7.0.6 7.0.7 6.3.8 6.3.7 6.3.6 6.3.5 6.3.4 6.3.3 6.3.1 trunk 5.7.3 5.7.5 5.8.1 5.8.2 5.8.3 5.8.4 5.8.6 6.0.4 6.0.5 All 36 releases
mlsimport / admin / js / mlsimport-searchable-select.js

mlsimport-searchable-select.js in MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings 7.1.1, at admin/js/mlsimport-searchable-select.js

202 lines 6.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Searchable City/County multi-select.
3 *
4 * The field stays a native <select multiple> listbox showing the full list (as
5 * it was before). Three enhancements:
6 * - a text input above it filters the options live;
7 * - a plain click toggles an option, so multiple items can be picked without
8 * holding Ctrl/Cmd (the native listbox default);
9 * - the currently selected items are mirrored as removable chips above the list.
10 *
11 * The underlying <select multiple> is the source of truth — it submits the same
12 * name[]-array payload and no save logic changes.
13 */
14 ( function () {
15 'use strict';
16
17 /**
18 * Find the native <select> that a search input controls (its next element).
19 *
20 * @param {Element} input The .mlsimport-select-search text input.
21 * @return {HTMLSelectElement|null}
22 */
23 function selectFor( input ) {
24 // Walk forward through siblings until the <select> is found
25 var node = input.nextElementSibling;
26 while ( node && 'SELECT' !== node.tagName ) {
27 node = node.nextElementSibling;
28 }
29 return node;
30 }
31
32 /**
33 * Find the chips container that mirrors a given <select> (a preceding sibling).
34 *
35 * @param {HTMLSelectElement} select The searchable select.
36 * @return {Element|null}
37 */
38 function chipsFor( select ) {
39 // Walk backward through siblings until the chips container is found
40 var node = select.previousElementSibling;
41 while ( node && ! node.classList.contains( 'mlsimport-selected-chips' ) ) {
42 node = node.previousElementSibling;
43 }
44 return node;
45 }
46
47 /**
48 * Show only options whose label contains the typed term.
49 *
50 * @param {Element} input The search input that fired.
51 */
52 function filter( input ) {
53 // Resolve the controlled select; nothing to do without it
54 var select = selectFor( input );
55 if ( ! select ) {
56 return;
57 }
58 // Normalise the search term and test each option's label
59 var term = input.value.toLowerCase();
60 var options = select.options;
61 for ( var i = 0; i < options.length; i++ ) {
62 // Show options whose text contains the term; hide the rest (two ways, for cross-browser)
63 var match = -1 !== options[ i ].text.toLowerCase().indexOf( term );
64 options[ i ].hidden = ! match;
65 options[ i ].style.display = match ? '' : 'none';
66 }
67 }
68
69 /**
70 * Rebuild the chip row from the select's current selection.
71 *
72 * @param {HTMLSelectElement} select The searchable select.
73 */
74 function renderChips( select ) {
75 // Resolve the chips container; nothing to do without it
76 var box = chipsFor( select );
77 if ( ! box ) {
78 return;
79 }
80 // Clear the row, then rebuild one chip per selected option
81 box.innerHTML = '';
82 var chosen = select.selectedOptions;
83 for ( var i = 0; i < chosen.length; i++ ) {
84 var option = chosen[ i ];
85
86 // Chip wrapper, tagged with the option value for later removal
87 var chip = document.createElement( 'span' );
88 chip.className = 'mlsimport-chip';
89 chip.setAttribute( 'data-value', option.value );
90
91 // Visible label text
92 var label = document.createElement( 'span' );
93 label.className = 'mlsimport-chip-label';
94 label.textContent = option.text;
95
96 // "×" remove button (accessible label)
97 var remove = document.createElement( 'button' );
98 remove.type = 'button';
99 remove.className = 'mlsimport-chip-remove';
100 remove.setAttribute( 'aria-label', 'Remove ' + option.text );
101 remove.textContent = '×';
102
103 // Assemble the chip and append it to the row
104 chip.appendChild( label );
105 chip.appendChild( remove );
106 box.appendChild( chip );
107 }
108 }
109
110 /** Render chips for every searchable select on the page. */
111 function renderAll() {
112 // Refresh chips for each searchable select present in the DOM
113 var selects = document.querySelectorAll( 'select.mlsimport-searchable-select' );
114 for ( var i = 0; i < selects.length; i++ ) {
115 renderChips( selects[ i ] );
116 }
117 }
118
119 // Live-filter options as the user types in a search box
120 document.addEventListener( 'input', function ( event ) {
121 // Only react to the searchable-select search inputs
122 var target = event.target;
123 if ( target && target.classList && target.classList.contains( 'mlsimport-select-search' ) ) {
124 filter( target );
125 }
126 } );
127
128 // Plain click toggles an option — no Ctrl/Cmd needed for multi-select.
129 document.addEventListener( 'mousedown', function ( event ) {
130 // Only react to clicks on <option> elements
131 var option = event.target;
132 if ( ! option || 'OPTION' !== option.tagName ) {
133 return;
134 }
135 // Ensure the option belongs to a searchable select
136 var select = option.closest( 'select.mlsimport-searchable-select' );
137 if ( ! select ) {
138 return;
139 }
140 // Block the native "replace selection" behaviour, then toggle.
141 event.preventDefault();
142 option.selected = ! option.selected;
143 // Preserve scroll position across the focus() that the toggle needs
144 var scroll = select.scrollTop;
145 select.focus();
146 select.scrollTop = scroll;
147 // Notify listeners (chip renderer, form) that the selection changed
148 select.dispatchEvent( new Event( 'change', { bubbles: true } ) );
149 } );
150
151 // Removing a chip deselects its option in the underlying select.
152 document.addEventListener( 'click', function ( event ) {
153 // Only react to clicks on a chip's remove button
154 var button = event.target;
155 if ( ! button || ! button.classList || ! button.classList.contains( 'mlsimport-chip-remove' ) ) {
156 return;
157 }
158 // Resolve the chip and its container
159 var box = button.closest( '.mlsimport-selected-chips' );
160 var chip = button.closest( '.mlsimport-chip' );
161 if ( ! box || ! chip ) {
162 return;
163 }
164 // Walk forward from the chips box to the associated searchable select
165 var select = box.nextElementSibling;
166 while ( select && ! ( 'SELECT' === select.tagName && select.classList.contains( 'mlsimport-searchable-select' ) ) ) {
167 select = select.nextElementSibling;
168 }
169 if ( ! select ) {
170 return;
171 }
172 // Deselect the option matching the chip's stored value
173 var value = chip.getAttribute( 'data-value' );
174 var options = select.options;
175 for ( var i = 0; i < options.length; i++ ) {
176 if ( options[ i ].value === value ) {
177 options[ i ].selected = false;
178 break;
179 }
180 }
181 // Notify listeners so the chip row re-renders without this item
182 select.dispatchEvent( new Event( 'change', { bubbles: true } ) );
183 } );
184
185 // Keep chips in sync with any selection change (toggle, Ctrl+click, removal).
186 document.addEventListener( 'change', function ( event ) {
187 // Only rebuild chips for searchable selects
188 var select = event.target;
189 if ( select && 'SELECT' === select.tagName && select.classList &&
190 select.classList.contains( 'mlsimport-searchable-select' ) ) {
191 renderChips( select );
192 }
193 } );
194
195 // Initial chip render: run now, or wait for DOM ready if still parsing
196 if ( 'loading' === document.readyState ) {
197 document.addEventListener( 'DOMContentLoaded', renderAll );
198 } else {
199 renderAll();
200 }
201 }() );
202