1/** 2 * The "I'm interested in" dropdown â progressive enhancement over the native 3 * <select name="mlsimport_interest"> in the property lead forms. 4 * 5 * A native select's option list is drawn by the operating system and cannot be 6 * styled, so it never matched the rest of the site however the closed box was 7 * themed. This replaces the closed box with a button and the list with ordinary 8 * markup, reusing the .mlsimport-location__list / __option panel the search 9 * form's Location autocomplete already uses â so the two dropdowns match by 10 * construction rather than by hand-tuned values. 11 * 12 * The native select STAYS in the DOM (hidden once enhanced) and remains the 13 * submitted control: every pick writes through to it. So the lead handler is 14 * untouched, and with JS off the form degrades to the plain select. Enhancement 15 * is keyed off the wrapper gaining .is-enhanced, which is also what hides the 16 * native control â CSS alone never hides a control the user might still need. 17 * 18 * No jQuery. 19 */ 20( function () { 21 'use strict'; 22 23 // Wrapper emitted by mlsimport_property_lead_interest_field(). 24 var ROOT = '.mlsimport-interest'; 25 26 /** 27 * Build the button + listbox for one wrapper and wire its behaviour. 28 * @param {Element} root The .mlsimport-interest wrapper. 29 */ 30 function enhance( root ) { 31 // Enhance once, and only when there is a native select to mirror. 32 if ( root.classList.contains( 'is-enhanced' ) ) { 33 return; 34 } 35 var select = root.querySelector( 'select' ); 36 if ( ! select ) { 37 return; 38 } 39 40 // The trigger reports the current choice; the placeholder option's text is 41 // the resting label, so the control reads the same before and after JS. 42 var trigger = document.createElement( 'button' ); 43 trigger.type = 'button'; 44 trigger.className = 'mlsimport-interest__trigger'; 45 trigger.setAttribute( 'aria-haspopup', 'listbox' ); 46 trigger.setAttribute( 'aria-expanded', 'false' ); 47 48 var label = document.createElement( 'span' ); 49 label.className = 'mlsimport-interest__label'; 50 trigger.appendChild( label ); 51 52 // The panel reuses the Location autocomplete's classes verbatim. 53 var list = document.createElement( 'ul' ); 54 list.className = 'mlsimport-location__list mlsimport-interest__list'; 55 list.setAttribute( 'role', 'listbox' ); 56 list.hidden = true; 57 58 // One option per <option>, carrying its index so selection is a plain lookup. 59 var options = []; 60 Array.prototype.forEach.call( select.options, function ( opt, i ) { 61 var li = document.createElement( 'li' ); 62 li.className = 'mlsimport-location__option'; 63 li.setAttribute( 'role', 'option' ); 64 li.setAttribute( 'data-index', String( i ) ); 65 li.textContent = opt.textContent; 66 list.appendChild( li ); 67 options.push( li ); 68 } ); 69 70 /** 71 * Mirror the select's current value onto the trigger and the listbox. 72 */ 73 function paint() { 74 var opt = select.options[ select.selectedIndex ]; 75 label.textContent = opt ? opt.textContent : ''; 76 // The placeholder option has an empty value â style it as unfilled. 77 root.classList.toggle( 'is-placeholder', ! select.value ); 78 options.forEach( function ( li, i ) { 79 var on = i === select.selectedIndex; 80 li.classList.toggle( 'is-selected', on ); 81 li.setAttribute( 'aria-selected', on ? 'true' : 'false' ); 82 } ); 83 } 84 85 /** 86 * Open or close the panel. 87 * @param {boolean} open Whether the panel should be open. 88 */ 89 function setOpen( open ) { 90 list.hidden = ! open; 91 root.classList.toggle( 'is-open', open ); 92 trigger.setAttribute( 'aria-expanded', open ? 'true' : 'false' ); 93 if ( open ) { 94 // Start the keyboard walk from the current choice. 95 highlight( select.selectedIndex ); 96 } 97 } 98 99 // Index currently highlighted by the keyboard (-1 = none). 100 var active = -1; 101 102 /** 103 * Move the keyboard highlight, clamped to the option range. 104 * @param {number} i Target index. 105 */ 106 function highlight( i ) { 107 if ( active > -1 && options[ active ] ) { 108 options[ active ].classList.remove( 'is-active' ); 109 } 110 active = Math.max( 0, Math.min( options.length - 1, i ) ); 111 options[ active ].classList.add( 'is-active' ); 112 // Keep the highlighted row inside the scrollable panel. 113 if ( options[ active ].scrollIntoView ) { 114 options[ active ].scrollIntoView( { block: 'nearest' } ); 115 } 116 } 117 118 /** 119 * Commit an option: write through to the native select, repaint, close. 120 * @param {number} i Option index to select. 121 */ 122 function choose( i ) { 123 select.selectedIndex = i; 124 // Fire change so anything listening on the real control still hears it. 125 select.dispatchEvent( new Event( 'change', { bubbles: true } ) );
126 paint(); 127 setOpen( false ); 128 trigger.focus(); 129 } 130 131 trigger.addEventListener( 'click', function () { 132 setOpen( list.hidden ); 133 } ); 134 135 // Pointer selection. Using mousedown would fire before the click that closes 136 // an already-open panel, so click is the one that behaves. 137 list.addEventListener( 'click', function ( e ) { 138 var li = e.target.closest ? e.target.closest( '.mlsimport-location__option' ) : null; 139 if ( li ) { 140 choose( parseInt( li.getAttribute( 'data-index' ), 10 ) || 0 ); 141 } 142 } ); 143 144 // Keyboard: the listbox conventions a native select already answers to. 145 trigger.addEventListener( 'keydown', function ( e ) { 146 if ( 'ArrowDown' === e.key || 'ArrowUp' === e.key ) { 147 e.preventDefault(); 148 if ( list.hidden ) { 149 setOpen( true ); 150 return; 151 } 152 highlight( active + ( 'ArrowDown' === e.key ? 1 : -1 ) ); 153 } else if ( 'Enter' === e.key || ' ' === e.key ) { 154 // Enter on a closed control opens it; on an open one it commits. 155 e.preventDefault(); 156 if ( list.hidden ) { 157 setOpen( true ); 158 } else { 159 choose( active ); 160 } 161 } else if ( 'Escape' === e.key && ! list.hidden ) { 162 e.preventDefault(); 163 setOpen( false ); 164 } 165 } ); 166 167 // Any click outside closes the panel. 168 document.addEventListener( 'click', function ( e ) { 169 if ( ! root.contains( e.target ) ) { 170 setOpen( false ); 171 } 172 } ); 173 174 // Insert the widget, then flag the wrapper â the flag is what hides the 175 // native select, so it is set only once the replacement is really in place. 176 root.appendChild( trigger ); 177 root.appendChild( list ); 178 paint(); 179 root.classList.add( 'is-enhanced' ); 180 } 181 182 /** 183 * Enhance every interest dropdown inside a scope. 184 * @param {Element|Document} [scope] Root to search; defaults to document. 185 */ 186 function init( scope ) { 187 var node = scope && scope.querySelectorAll ? scope : document; 188 Array.prototype.forEach.call( node.querySelectorAll( ROOT ), enhance ); 189 } 190 191 if ( 'loading' === document.readyState ) { 192 document.addEventListener( 'DOMContentLoaded', function () { 193 init( document ); 194 } ); 195 } else { 196 init( document ); 197 } 198} )();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.