PageSourceSearch

https://equipoadip.com.ar/wp-content/plugins/wp-jobsearch/js/jobsearch-select2-shim.js?ver=3.3.6

js equipoadip.com.ar collected 2026-10-05 22:05:06 UTC 18,274 bytes, 414 lines download raw bytes

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 "&nbsp;&nbsp;" or "&nbsp; &nbsp;" 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 "&nbsp;&nbsp;&nbsp;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.