1/*! 2 * jobsearch-select2-shim.js 3 * 4 * Migration shim: Selectize.js â Select2 4.1.x 5 * 6 * The plugin had ~50 `$el.selectize(opts)` init sites scattered across PHP 7 * + JS files. Rather than rewrite every one, this file exposes a thin 8 * `$.fn.selectize` that: 9 * 10 * 1. Translates Selectize options to their Select2 equivalents, 11 * 2. Calls `.select2(translated)` under the hood, 12 * 3. Re-emits Selectize-style event callbacks (`onChange`, `onItemAdd`, 13 * `onItemRemove`, `onInitialize`) by binding to the Select2 events, 14 * 4. Attaches a Selectize-compat handle (`element.selectize`) so the 15 * legacy `$el.selectize()[0].selectize.destroy()` pattern still works. 16 * 17 * New code should call `$el.select2(...)` directly. This shim only exists 18 * so the migration didn't have to touch 50 files at once. 19 */ 20(function ($) { 21 if (!$ || typeof $.fn !== 'object') { 22 return; 23 } 24 if (typeof $.fn.select2 !== 'function') { 25 if (typeof window !== 'undefined' && window.console && window.console.warn) { 26 window.console.warn('jobsearch-select2-shim: $.fn.select2 not found; loaded order issue?'); 27 } 28 return; 29 } 30 if ($.fn.selectize && $.fn.selectize._jobsearchShim) { 31 return; // already installed 32 } 33 34 // Map Selectize options to Select2 options. 35 function translate(opts) { 36 opts = opts || {}; 37 // Default allowClear on for every dropdown. Select2 only shows the 38 // clear (Ã) on single-selects with a placeholder; multi-selects 39 // ignore it and already have per-tag remove buttons. 40 var s2 = { width: opts.width || '100%', allowClear: true }; 41 42 // Placeholder is handled later in the per-element pass so we can 43 // pick the text up from the first empty <option> (already wrapped 44 // in __() / esc_html__() by the PHP template, so it's translated 45 // automatically â no JS i18n needed). 46 if (typeof opts.placeholder !== 'undefined') { 47 s2.placeholder = opts.placeholder; 48 } 49 if (opts.allowClear === false) { 50 s2.allowClear = false; 51 } 52 if (typeof opts.maxItems === 'number' && opts.maxItems > 0) { 53 s2.maximumSelectionLength = opts.maxItems; 54 } 55 if (opts.create === true) { 56 s2.tags = true; 57 } 58 if (typeof opts.delimiter === 'string') { 59 s2.tokenSeparators = [opts.delimiter]; 60 } 61 if (typeof opts.maxOptions === 'number') { 62 s2.maximumInputLength = opts.maxOptions; // best-effort, not 1:1 63 } 64 if (opts.closeAfterSelect === true) { 65 s2.closeOnSelect = true; 66 } 67 if (opts.dropdownParent) { 68 s2.dropdownParent = opts.dropdownParent; 69 } 70 71 // Selectize: render.option(data, escape) â Select2: templateResult(state) 72 if (opts.render && typeof opts.render.option === 'function') { 73 s2.templateResult = function (state) { 74 if (!state.id) { 75 return state.text; 76 } 77 var data = { text: state.text, value: state.id }; 78 if (state.element && state.element.attributes) { 79 for (var i = 0; i < state.element.attributes.length; i++) { 80 var a = state.element.attributes[i]; 81 if (a.name.indexOf('data-') === 0) { 82 data[a.name.slice(5)] = a.value; 83 } 84 } 85 } 86 // Selectize's `escape` callback is a function; pass identity 87 // (the rendered HTML is trusted from server template). 88 var html = opts.render.option(data, function (s) { return s; }); 89 return $(html); 90 }; 91 // Keep raw HTML from templateResult instead of having Select2 92 // re-escape it. 93 s2.escapeMarkup = function (m) { return m; }; 94 } 95 if (opts.render && typeof opts.render.item === 'function') { 96 s2.templateSelection = function (state) { 97 if (!state.id) { 98 return state.text; 99 } 100 var data = { text: state.text, value: state.id }; 101 var html = opts.render.item(data, function (s) { return s; }); 102 return $(html); 103 }; 104 } 105 106 return s2; 107 } 108 109 // Build the Selectize-compat instance attached to each DOM element. 110 function buildInstance($el, opts) { 111 return { 112 destroy: function () { 113 if ($el.data('select2')) { 114 $el.select2('destroy'); 115 } 116 }, 117 setValue: function (val, silent) { 118 $el.val(val); 119 if (!silent) { $el.trigger('change'); } 120 }, 121 getValue: function () { 122 return $el.val(); 123 }, 124 addItem: function (val, silent) { 125 var existing = $el.find('option[value="' + String(val).replace(/"/g, '\\"') + '"]'); 126 if (!existing.length) { 127 $el.append(new Option(val, val, true, true)); 128 } 129 var cur = $el.val(); 130 if ($el.prop('multiple')) { 131 cur = Array.isArray(cur) ? cur.slice() : (cur ? [cur] : []); 132 if (cur.indexOf(String(val)) === -1) { cur.push(String(val)); } 133 $el.val(cur); 134 } else { 135 $el.val(val); 136 } 137 if (!silent) { $el.trigger('change'); } 138 }, 139 removeItem: function (val, silent) { 140 var cur = $el.val(); 141 if (Array.isArray(cur)) { 142 cur = cur.filter(function (v) { return v !== String(val); }); 143 } else if (cur === String(val)) { 144 cur = null; 145 } 146 $el.val(cur); 147 if (!silent) { $el.trigger('change'); } 148 }, 149 clear: function (silent) { 150 $el.val(null); 151 if (!silent) { $el.trigger('change'); } 152 }, 153 refreshOptions: function () { /* no-op for Select2 */ }, 154 updateOption: function () { /* no-op for Select2 */ }, 155 $input: $el, 156 }; 157 } 158 159 // Count how many leading "indent" characters (NBSP, space, tab) the 160 // option text begins with. 161 function countLeadingIndentChars(text) { 162 if (typeof text !== 'string') { 163 return 0; 164 } 165 var n = 0; 166 for (var i = 0; i < text.length; i++) { 167 var ch = text.charCodeAt(i); 168 // 0xA0 = NBSP, 0x20 = space, 0x09 = tab 169 if (ch === 0xA0 || ch === 0x20 || ch === 0x09) { 170 n++; 171 } else { 172 break; 173 } 174 } 175 return n; 176 } 177 178 // Read the option's depth (0 = top-level). Two signals supported: 179 // 1. WP's Walker_CategoryDropdown emits class="level-N" on each 180 // <option> when wp_dropdown_categories(hierarchical=>true) renders. 181 // 2. Plugins / themes (incl. this one's sector dropdown) indent 182 // children with leading " " or " " inside 183 // the option text itself, no class involved. Count those chars 184 // and bucket every ~4 chars as one level (handles WP's 3-NBSPs 185 // pattern and this plugin's 4-char-per-level pattern). 186 // Read the raw <option>.textContent directly. We CAN'T trust state.text: 187 // Selectize-era addons stamp a `data-data='{...}'` JSON attribute on 188 // options, and Select2's option-data adapter parses that JSON via 189 // jQuery.data() and replaces the entire state object â wiping state.id 190 // and state.text. element.textContent is always present and reliable. 191 function getOptionRawText(state) { 192 if (state && state.element && typeof state.element.textContent === 'string') { 193 return state.element.textContent; 194 } 195 return typeof (state && state.text) === 'string' ? state.text : ''; 196 } 197 198 function getOptionLevel(state) { 199 if (!state || !state.element) { 200 return 0; 201 } 202 // 1. Explicit signal: data-level="N" on the <option>. Stamped by 203 // the hierarchy walkers on the PHP side (jobsearch_sector_terms_hierarchical 204 // and friends). Most reliable. 205 var dlvl = state.element.getAttribute && state.element.getAttribute('data-level'); 206 if (dlvl !== null && dlvl !== '' && !isNaN(parseInt(dlvl, 10))) { 207 return parseInt(dlvl, 10); 208 } 209 // 2. WP's Walker_CategoryDropdown emits class="level-N". 210 if (state.element.className) { 211 var m = /\blevel-(\d+)\b/.exec(state.element.className); 212 if (m) { 213 return parseInt(m[1], 10); 214 } 215 } 216 // 3. Fallback: count leading NBSP/space chars in the option text. 217 var lead = countLeadingIndentChars(getOptionRawText(state)); 218 if (lead >= 2) { 219 return Math.ceil(lead / 4); 220 } 221 return 0; 222 } 223
224 // Strip leading non-breaking spaces (WP walker's indentation) from a 225 // visible string so the indentation is purely driven by CSS padding, 226 // not text whitespace that can be collapsed by browser rules. 227 function trimLeadingNbsp(text) { 228 if (typeof text !== 'string') { 229 return text; 230 } 231 //  = NBSP, \s = regular whitespace 232 return text.replace(/^[ \s]+/, ''); 233 } 234 235 // Wrap a user-supplied templateResult so we (a) hide the empty 236 // placeholder option from the open dropdown list, and (b) re-render 237 // hierarchy indentation via CSS padding using the `level-N` class WP 238 // adds to hierarchical category options. Without this the parent/ 239 // child indentation Selectize used to show is lost. 240 // True only when this state represents the EMPTY placeholder option. 241 // Detect via the underlying <option>.value being empty â not state.id, 242 // because `data-data` JSON attributes (Selectize convention used by 243 // several addons) cause state.id to be falsy on real child options too. 244 function isPlaceholderOption(state) { 245 if (!state) { 246 return true; 247 } 248 if (state.element && typeof state.element.value === 'string') { 249 return state.element.value === ''; 250 } 251 return !state.id; 252 } 253 254 function withHiddenPlaceholderRow(userTemplate) { 255 return function (state) { 256 if (isPlaceholderOption(state)) { 257 return null; 258 } 259 var rawText = getOptionRawText(state); 260 var inner = userTemplate 261 ? userTemplate(state) 262 : trimLeadingNbsp(rawText); 263 var level = getOptionLevel(state); 264 if (level > 0) { 265 var $wrap = $('<span class="jobsearch-tax-row" style="display:block;padding-left:' + (level * 14) + 'px;"></span>'); 266 if (inner && inner.jquery) { 267 $wrap.append(inner); 268 } else { 269 $wrap.text(inner == null ? '' : String(inner)); 270 } 271 return $wrap; 272 } 273 if (inner && inner.jquery) { 274 return inner; 275 } 276 return inner == null ? '' : String(inner); 277 }; 278 } 279 280 // Default templateSelection: strips the leading-NBSP indentation from 281 // the selected value chip / single-line display so the chip just shows 282 // "Child Term" not " Child Term". 283 function defaultTemplateSelection(state) { 284 if (isPlaceholderOption(state)) { 285 return state.text || ''; 286 } 287 return trimLeadingNbsp(getOptionRawText(state)); 288 } 289 290 $.fn.selectize = function (opts) { 291 var s2 = translate(opts); 292 return this.each(function () { 293 var $el = $(this); 294 // Re-init: destroy any previous Select2 first so callers that 295 // toggle the dropdown content + re-call .selectize() (the 296 // legacy AJAX-cascade pattern) keep working. 297 if ($el.data('select2')) { 298 $el.select2('destroy'); 299 } 300 301 // Legacy Selectize `data-data='{"depend":"true","optid":"5"}'` 302 // attributes are used by other code (e.g. the dependent-fields 303 // change handler) to read per-option metadata. Select2 4.x reads 304 // data-data via jQuery.data('data') and REPLACES the entire 305 // state object with it â wiping state.id/text/element. Before 306 // letting Select2 see these options we (1) flatten any JSON 307 // keys onto the option as real data-<key> attributes so the 308 // metadata is still accessible via $(option).attr('data-key'), 309 // then (2) drop the data-data attribute so it doesn't sabotage 310 // Select2's state. 311 $el.find('option').each(function () { 312 if (!this.hasAttribute || !this.hasAttribute('data-data')) { 313 return; 314 } 315 var raw = this.getAttribute('data-data'); 316 if (raw) { 317 try { 318 var parsed = JSON.parse(raw); 319 if (parsed && typeof parsed === 'object') { 320 for (var k in parsed) { 321 if (Object.prototype.hasOwnProperty.call(parsed, k)) { 322 var attrName = 'data-' + String(k).toLowerCase(); 323 if (!this.hasAttribute(attrName)) { 324 this.setAttribute(attrName, String(parsed[k])); 325 } 326 } 327 } 328 } 329 } catch (e) { /* malformed JSON â fall through */ } 330 } 331 this.removeAttribute('data-data'); 332 $(this).removeData('data'); 333 }); 334 335 // Resolve placeholder text. Precedence: 336 // 1. opts.placeholder (caller-supplied) 337 // 2. text of the first <option value=""> in the markup 338 // (translatable on the PHP side via __() / _e()) 339 // 3. data-placeholder attribute on the <select> 340 // 4. placeholder attribute on the <select> (Selectize convention; 341 // this is how multi-selects carry their placeholder, since a 342 // multiple <select> has no empty <option> to read from) 343 // 5. nothing â no placeholder 344 var perElementS2 = $.extend({}, s2); 345 if (typeof perElementS2.placeholder === 'undefined') { 346 var $emptyOpt = $el.find('option[value=""]').first(); 347 var phText = ''; 348 if ($emptyOpt.length) { 349 phText = $.trim($emptyOpt.text()); 350 } 351 if (!phText && $el.attr('data-placeholder')) { 352 phText = $el.attr('data-placeholder'); 353 } 354 if (!phText && $el.attr('placeholder')) { 355 phText = $el.attr('placeholder'); 356 } 357 if (phText) { 358 perElementS2.placeholder = phText; 359 } 360 } 361 362 // Select2's clear (Ã) only works when a placeholder exists â the 363 // cleared state IS the placeholder. With allowClear:true but no 364 // placeholder (e.g. salary type / currency / position, yes-no 365 // toggles â selects that always carry a value), the à renders but 366 // clicking it blanks the field into a broken state. So only keep 367 // allowClear on when a placeholder actually resolved for this 368 // element; otherwise drop it. (An explicit allowClear:false from 369 // the caller is already respected above.) 370 if (typeof perElementS2.placeholder === 'undefined' || perElementS2.placeholder === '') { 371 perElementS2.allowClear = false;
372 } 373 374 // Wrap templateResult so (a) the empty placeholder row never 375 // appears in the dropdown list, and (b) hierarchical taxonomy 376 // options render with depth-based padding (preserving the 377 // parent/child visual that Selectize was showing). 378 perElementS2.templateResult = withHiddenPlaceholderRow(perElementS2.templateResult); 379 380 // Selection chip: strip leading NBSP indent so the chosen 381 // value displays as "Child Term", not the indented version. 382 if (typeof perElementS2.templateSelection !== 'function') { 383 perElementS2.templateSelection = defaultTemplateSelection; 384 } 385 386 $el.select2(perElementS2); 387 388 // Selectize-compat handle on the DOM node. 389 this.selectize = buildInstance($el, opts || {}); 390 391 if (opts) { 392 if (typeof opts.onChange === 'function') { 393 $el.off('change.selectizeShim').on('change.selectizeShim', function () { 394 opts.onChange.call(this.selectize || null, $el.val()); 395 }); 396 } 397 if (typeof opts.onItemAdd === 'function') { 398 $el.off('select2:select.selectizeShim').on('select2:select.selectizeShim', function (e) { 399 opts.onItemAdd.call(this.selectize || null, e.params.data.id, $(e.params.data.element)); 400 }); 401 } 402 if (typeof opts.onItemRemove === 'function') { 403 $el.off('select2:unselect.selectizeShim').on('select2:unselect.selectizeShim', function (e) { 404 opts.onItemRemove.call(this.selectize || null, e.params.data.id, $(e.params.data.element)); 405 }); 406 } 407 if (typeof opts.onInitialize === 'function') { 408 opts.onInitialize.call(this.selectize); 409 } 410 } 411 }); 412 }; 413 $.fn.selectize._jobsearchShim = true; 414})(window.jQuery);
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.