PageSourceSearch

https://khushiipropertiies.com/wp-content/plugins/hash-form/js/frontend.js?ver=1.4.4

js khushiipropertiies.com collected 2026-10-09 23:36:11 UTC 36,904 bytes, 899 lines download raw bytes

1jQuery(function ($) {
2
3    'use strict';
4
5    /* -----------------------------------------------------------------------
6     * Range sliders
7     * -------------------------------------------------------------------- */
8
9    function initRangeSliders(inputs) {
10        inputs.each(function () {
11            const input = $(this);
12            input.prev('.hf-range-slider').slider({
13                value: input.val(),
14                min: parseFloat(input.attr('min')),
15                max: parseFloat(input.attr('max')),
16                step: parseFloat(input.attr('step')),
17                range: 'min',
18                slide: function (e, ui) {
19                    $(this).next().val(ui.value);
20                }
21            });
22        });
23    }
24
25    initRangeSliders($('.hf-range-input-selector'));
26
27    // Update the slider when the input loses focus, as it has most likely changed.
28    $('.hf-range-input-selector').on('blur', function () {
29        const input = $(this);
30        let value = isNaN(input.val()) ? '' : input.val();
31
32        // Keep a manually typed value within the min/max bounds.
33        if (value) {
34            const min = parseFloat(input.attr('min'));
35            const max = parseFloat(input.attr('max'));
36            if (value < min) {
37                value = min;
38            }
39            if (value > max) {
40                value = max;
41            }
42        }
43
44        input.val(value);
45        input.prev('.hf-range-slider').slider('value', value);
46    });
47
48    /* -----------------------------------------------------------------------
49     * Form submission
50     * -------------------------------------------------------------------- */
51
52    // Scroll to the field holding the first validation error.
53    function firstErrorField(errors) {
54        const firstError = Object.keys(errors)[0];
55        const separator = firstError.indexOf('-');
56
57        if (separator > 0) {
58            const fieldId = firstError.slice(0, separator).replace('field', '');
59            const subField = firstError.slice(separator + 1);
60            return $('#hf-subfield-container-' + subField + '-' + fieldId);
61        }
62
63        return $('#hf-field-container-' + firstError.replace('field', ''));
64    }
65
66    /**
67     * Put a captcha back to its unanswered state.
68     *
69     * Each service's script is loaded whenever a field of its kind is on the
70     * form, whether or not a site key was set, so the script can be present
71     * with nothing rendered — grecaptcha.reset() throws "No reCAPTCHA clients
72     * exist" in that case. That used to abandon the rest of the failure
73     * handler, which is why an hCaptcha widget kept its spent token and the
74     * visitor's second attempt failed on a token already used up.
75     */
76    function resetCaptcha(name) {
77        const api = window[name];
78
79        if (!api || typeof api.reset !== 'function') {
80            return;
81        }
82
83        try {
84            api.reset();
85        } catch (e) {
86            // Nothing rendered to reset.
87        }
88    }
89
90    function resetRecaptcha() {
91        resetCaptcha('grecaptcha');
92        resetCaptcha('hcaptcha');
93        resetCaptcha('turnstile');
94    }
95
96    /**
97     * Never render an empty failure notice.
98     *
99     * A form whose Error Message setting is blank was appending an empty span,
100     * which reads to the visitor exactly like the submission having been ignored.
101     */
102    function failureText(message) {
103        return (typeof message === 'string' && message.trim())
104            ? message
105            : hashform_vars.generic_error;
106    }
107
108    /**
109     * Tie an error message to the control it belongs to.
110     *
111     * The message used to be appended as raw html with no relationship to the
112     * input at all, so a screen reader never announced it: the field stayed
113     * "valid" as far as assistive technology was concerned and the only sign
114     * anything had gone wrong was a red line someone had to see. The message
115     * now gets an id, the control points at it and is marked invalid, and the
116     * message is announced when it appears.
117     */
118    function attachError(container, key, message) {
119        const errorId = 'hf-error-' + key;
120        const msg = $('<span/>', {
121            'class': 'hf-error-msg',
122            id: errorId,
123            role: 'alert'
124        }).text(message);
125
126        container.addClass('hashform-error-container').append(msg);
127
128        // Every control the field owns - a composite field has several.
129        const inputs = container.find('input, select, textarea')
130            .not('[type="hidden"], [type="submit"], [type="button"]');
131
132        inputs.attr('aria-invalid', 'true');
133
134        // Keep any describedby the field already had; the help text is still
135        // relevant when the answer is wrong.
136        inputs.each(function () {
137            const input = $(this);
138            const described = (input.attr('aria-describedby') || '').split(/\s+/).filter(
138Boolean);
139
140            if (described.indexOf(errorId) === -1) {
141                described.push(errorId);
142                input.attr('aria-describedby', described.join(' '));
143            }
144        });
145
146        return inputs.first();
147    }
148
149    function showValidationErrors(errors) {
150        let firstInput = $();
151
152        $.each(errors, function (key, message) {
153            const container = $('#hf-field-container-' + key.replace('field', ''));
154
155            if (!container.length) {
156                return;
157            }
158
159            const input = attachError(container, key, message);
160
161            if (!firstInput.length) {
162                firstInput = input;
163            }
164        });
165
166        resetRecaptcha();
167
168        const target = firstErrorField(errors);
169
170        if (!target || !target.length) {
171            return;
172        }
173
174        $('html, body').animate({
175            scrollTop: target.offset().top - 300
176        }, 300, function () {
177            // Put the caret where the problem is, so a keyboard user is not
178            // dropped back at the top of the form to hunt for it.
179            if (firstInput.length && firstInput.is(':visible')) {
180                firstInput.trigger('focus');
181            }
182        });
183    }
184
185    /**
186     * Clear the previous attempt's error state, markers included.
187     */
188    function clearErrors(form) {
189        const scope = form && form.length ? form : $(document);
190
191        scope.find('.hf-error-msg, .hf-success-msg, .hf-failed-msg').remove();
192        scope.find('.hashform-error-container').removeClass('hashform-error-container');
193        scope.find('[aria-invalid]').removeAttr('aria-invalid');
194
195        // Drop only the ids this script added, leaving the field's own help
196        // text still linked.
197        scope.find('[aria-describedby]').each(function () {
198            const input = $(this);
199            const kept = (input.attr('aria-describedby') || '')
200                .split(/\s+/)
201                .filter(function (id) {
202                    return id && id.indexOf('hf-error-') !== 0;
203                });
204
205            if (kept.length) {
206                input.attr('aria-describedby', kept.join(' '));
207            } else {
208                input.removeAttr('aria-describedby');
209            }
210        });
211    }
212
213    /**
214     * A whole-form notice, announced rather than merely drawn.
215     */
216    function appendNotice(form, cls, message, assertive) {
217        $('<span/>', {
218            'class': cls,
219            role: assertive ? 'alert' : 'status',
220            'aria-live': assertive ? 'assertive' : 'polite'
221        }).text(message).appendTo(form);
222    }
223
224    function resetForm(form) {
225        form.trigger('reset');
226        form.find('.hf-star-rating').removeClass('hf-star-checked');
227        initRangeSliders(form.find('.hf-range-input-selector'));
228        $('body').find('.hf-preview-remove').trigger('click');
229    }
230
231    /**
232     * A reCAPTCHA v3 token for this form, or '' when there is nothing to fetch.
233     *
234     * Scoped to the form being submitted. The widget used to be looked up as
235     * $('.g-recaptcha') — the first one in the document — so with two forms on
236     * a page one of them read the other's settings and took a path meant for a
237     * captcha it did not have.
238     */
239    function captchaToken(form) {
240        const captcha = form.find('.g-recaptcha');
241        const siteKey = captcha.attr('data-sitekey');
242        const isV3 = captcha.attr('data-size') === 'invisible';
243
244        if (!isV3 || !siteKey || typeof grecaptcha === 'undefined') {
245            return $.Deferred().resolve('').promise();
246        }
247
248        const pending = $.Deferred();
249
250        grecaptcha.ready(function () {
251            grecaptcha.execute(siteKey, { action: 'hashform' }).then(
252                (token) => pending.resolve(token),
253                // Let the server say what went wrong rather than stalling here.
254                () => pending.resolve('')
255            );
256        });
257
258        return pending.promise();
259    }
260
261    $(document).on('submit.hashform-form', '.hashform-form', function (e) {
262        e.preventDefault();
263
264        const form = $(this);
265        const submitButton = form.find('button.hf-submit-button');
266
267        // Ignore repeat submits while a request is in flight.
268        if (submitButton.hasClass('hf-button-loading')) {
269            return;
270        }
271        submitButton.addClass('hf-button-loading');
272
273        clearErrors(form);
274
275        /*
276         * Wait for the token itself rather than for a second on the clock.
277         *
278         * A fixed one-second setTimeout used to wrap this whole block, so
279         * every submission of every form waited a second whether a captcha was
280         * on the page or not, and a token that took longer than that was
281         * dropped: the form posted an empty response and the visitor was told
282         * the reCAPTCHA was not entered correctly, with no way to get past it.
283         */
284        captchaToken(form).then((token) => {
285            const data = form.serializeArray();
286
287            if (token) {
288                const existing = data.find((item) => item.name === 'g-recaptcha-response');
289
290                if (existing) {
291                    existing.value = existing.value || token;
292                } else {
293                    data.push({ name: 'g-recaptcha-response', value: token });
294                }
295            }
296
297            $.ajax({
298                type: 'POST',
299                url: hashform_vars.ajaxurl,
300                dataType: 'json',
301                data: {
302                    action: 'hashform_process_entry',
303                    data: $.param(data),
304                    location: window.location.href
305                },
306                success: function (response) {
307                    submitButton.removeClass('hf-button-loading');
308
309                    if (!response || typeof response !== 'object') {
310                        resetRecaptcha();
311                        form.trigger('hashform:failed');
312                        appendNotice(form, 'hf-failed-msg', failureText(''), true);
313                        return;
314                    }
315
316                    /*
317                     * Announce the outcome on the form itself. An add-on -
318                     * the payment script is the one that needs this - can
319                     * then follow a submission it does not own, instead of
320                     * having to wrap or replace this handler.
321                     */
322                    form.trigger('hashform:' + response.status, [response]);
323
324                    if (response.status === 'redirect') {
325                        window.location.replace(response.message);
326                    } else if (response.status === 'success') {
327                        resetForm(form);
328
329                        // An empty confirmation means the server already said
330                        // its piece; do not draw an empty box for it.
331                        if (typeof response.message === 'string' && response.message.trim()) {
332                            appendNotice(form, 'hf-success-msg', response.message, false);
333                        }
334                    } else if (response.status === 'failed') {
335                        resetRecaptcha();
336                        appendNotice(form, 'hf-failed-msg', failureText(response.message), true);
337                    } else if (response.message && typeof response.message === 'object') {
338                        /*
339                         * A captcha that was not passed comes back as a field
340                         * error like any other, and this branch never reset the
341                         * widget — so the token stayed in the form, already
342                         * spent, and the visitor's next attempt failed on the
343                         * same token however carefully they answered it.
344                         */
345                        resetRecaptcha();
346                        showValidationErrors(response.message);
347                    } else {
348                        // status:'error' normally carries an object of per-field
349                        // errors, but the spam checks reject a whole submission
350                        // with a plain string. $.each on a string throws, which
351                        // left the visitor looking at a form that appeared to do
352                        // nothing at all.
353                        resetRecaptcha();
354                        appendNotice(form, 'hf-failed-msg', failureText(response.message), true);
355                    }
356                },
357                /*
358                 * There was no error handler at all, so a request that never
359                 * came back - a 500, a dropped connection, a security plugin
360                 * eating the post - left the button spinning for good with no
361                 * explanation and no way to try again.
362                 */
363                error: function () {
364                    submitButton.removeClass('hf-button-loading');
365                    resetRecaptcha();
366                    form.trigger('hashform:failed');
367                    appendNotice(form, 'hf-failed-msg', failureText(''), true);
368                }
369            });
370        });
371    });
372
373    /* -----------------------------------------------------------------------
374     * Spinner field
375     * -------------------------------------------------------------------- */
376
377    $('.hashform-field-type-spinner .hf-quantity .mdi-plus').on('click', function () {
378        const input = $(this).closest('.hashform-field-type-spinner').find('input');
379        const max = input.attr('max');
380        const value = Number(input.val());
381        input.val(value < max ? value + 1 : max);
382    });
383
384    $('.hashform-field-type-spinner .hf-quantity .mdi-minus').on('click', function () {
385        const input = $(this).closest('.hashform-field-type-spinner').find('input');
386        const min = input.attr('min');
387        const value = Number(input.val());
388        input.val(value > min ? value - 1 : min);
389    });
390
391    /* -----------------------------------------------------------------------
392     * Star rating field
393     * -------------------------------------------------------------------- */
394
395    $(document).on('click', '.hashform-star-group input', function () {
396        $(this).closest('.hashform-star-group').find('.hf-star-rating').removeClass('hf-star-checked');
397        $(this).parent('.hf-star-rating').prevAll('.hf-star-rating').addBack().addClass('hf-star-checked');
398    });
399
400    $(document).on('mouseenter', '.hashform-star-group .hf-star-rating:not(.hf-star-rating-readonly)', function () {
401        $(this).prevAll('.hf-star-rating').addBack().addClass('hf-star-hovered');
402        $(this).nextAll('.hf-star-rating').addClass('hf-star-non-hovered');
403    });
404
405    $(document).on('mouseleave', '.hashform-star-group .hf-star-rating:not(.hf-star-rating-readonly)', function () {
406        $(this).closest('.hashform-star-group').find('.hf-star-rating').removeClass('hf-star-hovered hf-star-non-hovered');
407    });
408
409    /* -----------------------------------------------------------------------
410     * Date and time fields
411     * -------------------------------------------------------------------- */
412
413    /**
414     * Carry a form's style tokens onto a picker panel.
415     *
416     * The per form --hf-* properties are declared on #hf-container-{id}, but
417     * both pickers move their panel to <body> as they open, so neither is a
418     * descendant of the form and neither inherits any of them. Copying the few
419     * that matter across on open is what lets a picker match the form that was
420     * clicked, which also keeps two differently styled forms on one page from
421     * sharing a palette. Anything the form leaves unset is skipped so the
422     * stylesheet fallback stays in play.
423     */
424    function bridgePickerStyles(input, panel) {
425        const container = input.closest('[id^="hf-container-"]');
426        if (!container.length || !panel || !panel.length) {
427            return;
428        }
429
430        const from = window.getComputedStyle(container[0]);
431        const accent = from.getPropertyValue('--hf-field-border-color-focus').trim()
432            || from.getPropertyValue('--hf-button-bg-color-normal').trim();
433
434        const tokens = {
435            '--hf-pick-accent': accent,
436            '--hf-pick-text': from.getPropertyValue('--hf-field-color-normal').trim(),
437            '--hf-pick-font': from.getPropertyValue('--hf-field-typo-font-family').trim()
438        };
439
440        Object.keys(tokens).forEach(function (name) {
441            if (tokens[name]) {
442                panel[0].style.setProperty(name, tokens[name]);
443            } else {
444                panel[0].style.removeProperty(name);
445            }
446        });
447    }
448
449    $('.hashform-field-type-date input').each(function () {
450        const input = $(this);
451        const format = input.attr('data-format');
452        const value = input.val();
453
454        if (value) {
455            const date = new Date(value);
456            const momentFormat = format.replace('dd', 'DD').replace('MM', 'MMMM').replace('mm', 'MM');
457            input.val(Number.isNaN(date.getTime()) ? '' : moment(date).format(momentFormat));
458        }
459
460        input.datepicker({
461            changeMonth: true,
462            dateFormat: format,
463            beforeShow: function () {
464                bridgePickerStyles(input, $('#ui-datepicker-div'));
465            }
466        });
467    });
468
469    $('.hashform-field-type-time .hf-timepicker').each(function () {
470        const input = $(this);
471
472        // Step, Min Time and Max Time are set per field in the builder and
473        // printed as data attributes. The library does not read those itself,
474        // so they have to be passed in or the field silently lists every hour
475        // of the day whatever the user configured.
476        const step = parseInt(input.attr('data-step'), 10);
477        const minTime = input.attr('data-min-time');
478        const maxTime = input.attr('data-max-time');
479
480        const options = {
481            showDuration: false,
482            timeFormat: 'g:ia'
483        };
484
485        // The library reads step as minutes, which is the unit the stored
486        // default of 60 already assumes: one slot per hour.
487        if (step > 0) {
488            options.step = step;
489        }
490        if (minTime) {
491            options.minTime = minTime;
492        }
493        if (maxTime) {
494            options.maxTime = maxTime;
495        }
496
497        input.timepicker(options);
498
499        // The library has no beforeShow hook, but it does fire showTimepicker
500        // on the input, and only one list is ever open at a time.
501        input.on('showTimepicker', function () {
502            bridgePickerStyles(input, $('.ui-timepicker-wrapper:visible').first());
503        });
504    });
505
506    /* -----------------------------------------------------------------------
507     * Conditional logic
508     * -------------------------------------------------------------------- */
509
510    // Compare a single configured value against the set of checked values.
511    function arrayValsCompare(compareValue, arrayVals, condition) {
512        switch (condition) {
513            case 'equal':
514                return arrayVals.includes(compareValue);
515            case 'less_than':
516                return arrayVals.length > 0 && arrayVals.every((val) => compareValue > val);
517            case 'less_than_or_equal':
518                return arrayVals.length > 0 && arrayVals.every((val) => compareValue >= val);
519            case 'greater_than':
520                return arrayVals.length > 0 && arrayVals.every((val) => compareValue < val);
521            case 'greater_than_or_equal':
522                return arrayVals.length > 0 && arrayVals.every((val) => compareValue <= val);
523            case 'is_like':
524                return arrayVals.some((val) => val.indexOf(compareValue) >= 0);
525            default:
526                return false;
527        }
528    }
529
530    // Returns whether the condition holds, or null when the condition is unknown.
531    function conditionMatches(condition, value, compareValue, arrayVals, isArrayVals) {
532        switch (condition) {
533            case 'equal':
534            case 'not_equal': {
535                const equal = isArrayVals
536                    ? arrayValsCompare(compareValue, arrayVals, 'equal')
537                    : arrayValsCompare(value, compareValue.split(/\s*,\s*/), 'equal');
538                return condition === 'equal' ? equal : !equal;
539            }
540
541            case 'less_than':
542            case 'less_than_or_equal':
543            case 'greater_than':
544            case 'greater_than_or_equal': {
545                if (isArrayVals) {
546                    return arrayValsCompare(compareValue, arrayVals, condition);
547                }
548                const number = (value == '') ? 0 : parseInt(value, 10);
549                if (condition === 'less_than') {
550                    return number < compareValue;
551                }
552                if (condition === 'less_than_or_equal') {
553                    return number <= compareValue;
554                }
555                if (condition === 'greater_than') {
556                    return number > compareValue;
557                }
558                return number >= compareValue;
559            }
560
561            case 'is_like':
562            case 'is_not_like': {
563                const like = isArrayVals
564                    ? arrayValsCompare(compareValue, arrayVals, 'is_like')
565                    : value.indexOf(compareValue) >= 0;
566                return condition === 'is_like' ? like : !like;
567            }
568
569            default:
570                return null;
571        }
572    }
573
574    $('.hashform-form-conditions').each(function () {
575        const parentForm = $(this).closest('form');
576
577        $.each(JSON.parse($(this).val()), function (index, val) {
578            const actionField = parentForm.find('#hf-field-container-' + val.compare_from);
579            const showOnMatch = val.condition_action === 'show';
580
581            // Fields that accept several values post as item_meta[id][].
582            let conditionTrigger = parentForm.find('[name="item_meta[' + val.compare_to + ']');
583            let isArrayVals = false;
584            if (!(conditionTrigger.length > 0)) {
585                conditionTrigger = parentForm.find('[name="item_meta[' + val.compare_to + '][]');
586                isArrayVals = true;
587            }
588
589            conditionTrigger.on('change', function () {
590                const trigger = $(this);
591                let value = trigger.val();
592                let arrayVals = [];
593
594                if (isArrayVals) {
595                    arrayVals = conditionTrigger.map(function () {
596                        return $(this).is(':checked') ? $(this).val() : null;
597                    }).toArray();
598                }
599
600                // An unchecked checkbox posts nothing, so treat it as empty.
601                if (trigger.attr('type') === 'checkbox' && !trigger.is(':checked')) {
602                    value = '';
603                }
604
605                const matched = conditionMatches(val.compare_condition, value, val.compare_value, arrayVals, isArrayVals);
606
607                if (matched !== null && actionField.length) {
608                    actionField.toggle(matched === showOnMatch);
609                }
610            }).trigger('change');
611        });
612    });
613
614    /* -----------------------------------------------------------------------
615     * Field focus styling
616     * -------------------------------------------------------------------- */
617
618    $('.hf-field-content input, .hf-field-content select, .hf-field-content textarea').on('focus', function () {
619        $(this).parent().addClass('hf-field-focussed');
620    }).on('focusout', function () {
621        $(this).parent().removeClass('hf-field-focussed');
622    });
623
624    /* -----------------------------------------------------------------------
625     * File uploads
626     * -------------------------------------------------------------------- */
627
628    // Which uploads get a thumbnail preview rather than just a filename. Kept
629    // in step with the formats the server accepts.
630    const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'bmp'];
631
632    // Drawn with currentColor so it follows whatever the dropzone text is.
633    const UPLOAD_ICON = '<svg class="hf-upload-dropzone-icon" width="28" height="28" viewBox="0 0 24 24" fill="none"'
634        + ' stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"'
635        + ' aria-hidden="true" focusable="false">'
636        + '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>'
637        + '<polyline points="17 8 12 3 7 8"/>'
638        + '<line x1="12" y1="3" x2="12" y2="15"/>'
639        + '</svg>';
640
641    // The label and extension list are author supplied, and they are being
642    // concatenated into a template string rather than set as text.
643    function escapeHtml(value) {
644        return String(value).replace(/[&<>"']/g, function (char) {
645            return {
646                '&': '&amp;',
647                '<': '&lt;',
648                '>': '&gt;',
649                '"': '&quot;',
650                "'": '&#39;'
651            }[char];
652        });
653    }
654
655    // Bytes to something a person reads. The library has its own _formatSize,
656    // but it is a private method on the instance and this runs before one
657    // exists.
658    function formatUploadSize(bytes) {
659        const size = Number(bytes);
660
661        if (!size || size < 0) {
662            return '';
663        }
664        if (size >= 1024 * 1024 * 1024) {
665            return Math.round(size / (1024 * 1024 * 1024) * 10) / 10 + ' GB';
666        }
667        if (size >= 1024 * 1024) {
668            return Math.round(size / (1024 * 1024)) + ' MB';
669        }
670        return Math.max(1, Math.round(size / 1024)) + ' KB';
671    }
672
673    // Holds each uploader instance for the lifetime of the page.
674    const uploaders = [];
675
676    $('.hf-file-uploader').each(function () {
677        const element = $(this);
678        const elementId = element.attr('id');
679
680        /*
681         * The builder canvas and the style preview draw a static copy of the
682         * uploader to show what it looks like, without an id or any of the
683         * settings. There is nothing to start up on one of those — and reading
684         * its extensions threw, which stopped every uploader after it on the
685         * page from being set up at all.
686         */
687        if (!elementId) {
688            return;
689        }
690
691        const sizeLimit = element.attr('data-max-upload-size');
692        const minSizeLimit = Number(element.attr('data-min-upload-size')) || 0;
693        const uploaderLabel = element.attr('data-upload-label');
694        const multipleUpload = element.attr('data-multiple-uploads') === 'true';
695        const extensions = (element.attr('data-extensions') || '').split(',').filter(
695Boolean);
696        const extensionErrorMessage = element.attr('data-extensions-error-message');
697
698        let uploadLimit = element.attr('data-multiple-uploads-limit');
699        let uploadLimitMessage = element.attr('data-multiple-uploads-error-message');
700        uploadLimit = uploadLimit < 1 ? 1 : uploadLimit;
701
702        const wrapper = () => $('#' + elementId).closest('.hf-file-uploader-wrapper');
703
704        // The constraints are already enforced, but nowhere on the page said
705        // what they were, so the first a visitor heard about a limit was an
706        // alert() after picking a file. Spelling them out on the dropzone is
707        // the whole reason it is worth having one.
708        const constraints = [];
709
710        if (extensions.length && extensions[0] !== '') {
711            constraints.push(extensions.map((ext) => ext.trim().toUpperCase()).join(', '));
712        }
713        if (minSizeLimit > 0) {
714            constraints.push(formatUploadSize(minSizeLimit) + ' to ' + formatUploadSize(sizeLimit));
715        } else if (sizeLimit > 0) {
716            constraints.push('up to ' + formatUploadSize(sizeLimit));
717        }
718        if (multipleUpload && uploadLimit > 0) {
719            constraints.push(uploadLimit + ' files max');
720        }
721
722        // Filters the operating system's own file dialog. Without it a JPG only
723        // field still offers every file on the machine and the rule is only
724        // discovered after picking one.
725        const acceptFiles = extensions
726            .map((ext) => ext.trim())
727            .filter(Boolean)
728            .map((ext) => '.' + ext.toLowerCase())
729            .join(',');
730
731        // One place for every rejection, so a failure reads on the form instead
732        // of in a browser dialog the visitor has to dismiss before continuing.
733        const showUploadError = (message) => {
734            const box = wrapper().find('.hf-upload-error');
735
736            if (!box.length) {
737                window.alert(message);
738                return;
739            }
740
741            box.text(message).addClass('hf-upload-error-visible');
742        };
743
744        const clearUploadError = () => {
745            wrapper().find('.hf-upload-error').text('').removeClass('hf-upload-error-visible');
746        };
747
748        const dropTitle = multipleUpload
749            ? 'Drag and drop your files here'
750            : 'Drag and drop your file here';
751
752        uploaders.push(new qq.FileUploader({
753            element: document.getElementById(elementId),
754            action: hashform_vars.ajaxurl,
755            params: {
756                action: 'hashform_file_upload_action',
757                file_uploader_nonce: hashform_vars.ajax_nounce,
758                allowedExtensions: extensions,
759                sizeLimit: sizeLimit
760            },
761            allowedExtensions: extensions,
762            sizeLimit: sizeLimit,
763            // 50 bytes is the old hardcoded floor, kept as the default so a
764            // field with no minimum set behaves exactly as it did.
765            minSizeLimit: minSizeLimit > 0 ? minSizeLimit : 50,
766            acceptFiles: acceptFiles,
767            uploadButtonText: uploaderLabel,
768            multiple: multipleUpload,
769
770            // The stock template was a bare grey button with a 300px drop area
771            // that only existed mid-drag. .qq-upload-button is kept as a real
772            // button inside the card rather than becoming the card itself, so
773            // the --hf-upload-* settings a site has already configured in the
774            // styler keep applying to exactly what they were configured for.
775            template: '<div class="qq-uploader">' +
776                '<div class="hf-upload-dropzone">' +
777                    UPLOAD_ICON +
778                    '<span class="hf-upload-dropzone-title">' + escapeHtml(dropTitle) + '</span>' +
779                    '<span class="hf-upload-dropzone-or">or</span>' +
780                    '<div class="qq-upload-button">{uploadButtonText}</div>' +
781                    (constraints.length
782                        ? '<span class="hf-upload-dropzone-hint">' + escapeHtml(constraints.join('  ·  ')) + '</span>'
783                        : '') +
784                    '<div class="qq-upload-drop-area"><span>{dragText}</span></div>' +
785                '</div>' +
786                '<div class="hf-upload-error" role="alert" aria-live="polite"></div>' +
787                '<ul class="qq-upload-list"></ul>' +
788                '</div>',
789
790            // The progress bar is wrapped in a track so it has something to run
791            // against. _find() resolves by class at any depth, so nesting is
792            // safe and the library still drives the width.
793            fileTemplate: '<li>' +
794                '<span class="hf-file-row">' +
795                    '<span class="qq-upload-file"></span>' +
796                    '<span class="qq-upload-spinner"></span>' +
797                    '<span class="qq-upload-size"></span>' +
798                    '<span class="qq-upload-failed-text">{failUploadtext}</span>' +
799                    '<a class="qq-upload-cancel" href="#">{cancelButtonText}</a>' +
800                '</span>' +
801                '<span class="hf-progress-track"><span class="qq-progress-bar"></span></span>' +
802                '</li>',
803
804            onSubmit: function (id, fileName) {
805                clearUploadError();
806
807                if (!multipleUpload || uploadLimit == -1) {
808                    return;
809                }
810
811                const counter = element.parent().find('.hf-multiple-upload-limit');
812                const used = Number(counter.val()) + 1;
813                counter.val(used);
814
815                if (used > uploadLimit) {
816                    uploadLimitMessage = uploadLimitMessage !== ''
817                        ? uploadLimitMessage
818                        : 'Maximum number of files allowed is ' + uploadLimit;
819                    showUploadError(uploadLimitMessage);
820                    counter.val(uploadLimit);
821                    return false;
822                }
823            },
824
825            onComplete: function (id, fileName, responseJSON) {
826                if (!responseJSON.success) {
827                    // The server rejected it. Its own message is surfaced by
828                    // showMessage; this only guards against a silent failure.
829                    if (!responseJSON.error) {
830                        showUploadError('That file could not be uploaded. Please try again.');
831                    }
832                    return;
833                }
834
835                clearUploadError();
836
837                const extension = fileName.split('.').pop();
838                const previewImage = IMAGE_EXTENSIONS.includes(extension.toLowerCase()) ? responseJSON.url : '';
839
840                let previewHtml = '<div class="hf-prev-holder" id="hf-uploaded-' + id + '">';
841                if (previewImage) {
842                    previewHtml += '<img src="' + previewImage + '" />';
843                }
844                previewHtml += '<span class="hf-prev-name">' + fileName + '</span></div>';
845
846                const uploadedFiles = wrapper().find('.hf-uploaded-files');
847
848                if (multipleUpload) {
849                    const existing = uploadedFiles.val();
850                    uploadedFiles.val(existing === '' ? responseJSON.url : existing + ',' + responseJSON.url);
851                    wrapper().find('.hf-file-preview').append(previewHtml);
852                } else {
853                    uploadedFiles.val(responseJSON.url);
854                    wrapper().find('.hf-file-preview').html(previewHtml);
855                }
856            },
857
858            messages: {
859                typeError: extensionErrorMessage,
860                sizeError: '{file} is too large, maximum file size is {sizeLimit}.',
861                minSizeError: '{file} is too small, minimum file size is {minSizeLimit}.',
862                emptyError: '{file} is empty, please select files again without it.',
863                onLeave: 'The files are being uploaded, if you leave now the upload will be cancelled.'
864            },
865
866            showMessage: function (message) {
867                showUploadError(message);
868            }
869        }));
870    });
871
872    $('body').on('click', '.hf-preview-remove', function () {
873        const button = $(this);
874
875        $.ajax({
876            url: hashform_vars.ajaxurl,
877            type: 'post',
878            data: 'action=hashform_file_delete_action&path=' + button.data('path') + '&_wpnonce=' + hashform_vars.ajax_nounce,
879            success: function (res) {
880                if (res !== 'success') {
881                    return;
882                }
883
884                const wrapper = button.closest('.hf-file-uploader-wrapper');
885                const uploadedFiles = wrapper.find('.hf-uploaded-files');
886                uploadedFiles.val(uploadedFiles.val().replace(button.data('url'), '').replace(',,', ','));
887
888                const counter = wrapper.find('.hf-multiple-upload-limit');
889                counter.val(Math.max(0, Number(counter.val()) - 1));
890
891                button.parent().fadeOut('1500', function () {
892                    button.parent().remove();
893                    wrapper.find('#' + button.attr('data-remove-id')).remove();
894                });
895            }
896        });
897    });
898
899});

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.