PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 6.3.6
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v6.3.6
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 6.3.6, at admin/js/mlsimport-searchable-select.js

177 lines 5.3 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 var node = input.nextElementSibling;
25 while ( node && 'SELECT' !== node.tagName ) {
26 node = node.nextElementSibling;
27 }
28 return node;
29 }
30
31 /**
32 * Find the chips container that mirrors a given <select> (a preceding sibling).
33 *
34 * @param {HTMLSelectElement} select The searchable select.
35 * @return {Element|null}
36 */
37 function chipsFor( select ) {
38 var node = select.previousElementSibling;
39 while ( node && ! node.classList.contains( 'mlsimport-selected-chips' ) ) {
40 node = node.previousElementSibling;
41 }
42 return node;
43 }
44
45 /**
46 * Show only options whose label contains the typed term.
47 *
48 * @param {Element} input The search input that fired.
49 */
50 function filter( input ) {
51 var select = selectFor( input );
52 if ( ! select ) {
53 return;
54 }
55 var term = input.value.toLowerCase();
56 var options = select.options;
57 for ( var i = 0; i < options.length; i++ ) {
58 var match = -1 !== options[ i ].text.toLowerCase().indexOf( term );
59 options[ i ].hidden = ! match;
60 options[ i ].style.display = match ? '' : 'none';
61 }
62 }
63
64 /**
65 * Rebuild the chip row from the select's current selection.
66 *
67 * @param {HTMLSelectElement} select The searchable select.
68 */
69 function renderChips( select ) {
70 var box = chipsFor( select );
71 if ( ! box ) {
72 return;
73 }
74 box.innerHTML = '';
75 var chosen = select.selectedOptions;
76 for ( var i = 0; i < chosen.length; i++ ) {
77 var option = chosen[ i ];
78
79 var chip = document.createElement( 'span' );
80 chip.className = 'mlsimport-chip';
81 chip.setAttribute( 'data-value', option.value );
82
83 var label = document.createElement( 'span' );
84 label.className = 'mlsimport-chip-label';
85 label.textContent = option.text;
86
87 var remove = document.createElement( 'button' );
88 remove.type = 'button';
89 remove.className = 'mlsimport-chip-remove';
90 remove.setAttribute( 'aria-label', 'Remove ' + option.text );
91 remove.textContent = '×';
92
93 chip.appendChild( label );
94 chip.appendChild( remove );
95 box.appendChild( chip );
96 }
97 }
98
99 /** Render chips for every searchable select on the page. */
100 function renderAll() {
101 var selects = document.querySelectorAll( 'select.mlsimport-searchable-select' );
102 for ( var i = 0; i < selects.length; i++ ) {
103 renderChips( selects[ i ] );
104 }
105 }
106
107 document.addEventListener( 'input', function ( event ) {
108 var target = event.target;
109 if ( target && target.classList && target.classList.contains( 'mlsimport-select-search' ) ) {
110 filter( target );
111 }
112 } );
113
114 // Plain click toggles an option — no Ctrl/Cmd needed for multi-select.
115 document.addEventListener( 'mousedown', function ( event ) {
116 var option = event.target;
117 if ( ! option || 'OPTION' !== option.tagName ) {
118 return;
119 }
120 var select = option.closest( 'select.mlsimport-searchable-select' );
121 if ( ! select ) {
122 return;
123 }
124 // Block the native "replace selection" behaviour, then toggle.
125 event.preventDefault();
126 option.selected = ! option.selected;
127 var scroll = select.scrollTop;
128 select.focus();
129 select.scrollTop = scroll;
130 select.dispatchEvent( new Event( 'change', { bubbles: true } ) );
131 } );
132
133 // Removing a chip deselects its option in the underlying select.
134 document.addEventListener( 'click', function ( event ) {
135 var button = event.target;
136 if ( ! button || ! button.classList || ! button.classList.contains( 'mlsimport-chip-remove' ) ) {
137 return;
138 }
139 var box = button.closest( '.mlsimport-selected-chips' );
140 var chip = button.closest( '.mlsimport-chip' );
141 if ( ! box || ! chip ) {
142 return;
143 }
144 var select = box.nextElementSibling;
145 while ( select && ! ( 'SELECT' === select.tagName && select.classList.contains( 'mlsimport-searchable-select' ) ) ) {
146 select = select.nextElementSibling;
147 }
148 if ( ! select ) {
149 return;
150 }
151 var value = chip.getAttribute( 'data-value' );
152 var options = select.options;
153 for ( var i = 0; i < options.length; i++ ) {
154 if ( options[ i ].value === value ) {
155 options[ i ].selected = false;
156 break;
157 }
158 }
159 select.dispatchEvent( new Event( 'change', { bubbles: true } ) );
160 } );
161
162 // Keep chips in sync with any selection change (toggle, Ctrl+click, removal).
163 document.addEventListener( 'change', function ( event ) {
164 var select = event.target;
165 if ( select && 'SELECT' === select.tagName && select.classList &&
166 select.classList.contains( 'mlsimport-searchable-select' ) ) {
167 renderChips( select );
168 }
169 } );
170
171 if ( 'loading' === document.readyState ) {
172 document.addEventListener( 'DOMContentLoaded', renderAll );
173 } else {
174 renderAll();
175 }
176 }() );
177