• File: themestrap.plugin.select-20261026181605.js
  • Full Path: /home/daemondevin/public_html/blog/assets/admin/themestrap/themestrap.plugin.select-20261026181605.js
  • Date Modified: 10/17/2026 7:00 PM
  • File size: 27.19 KB
  • MIME-type: text/plain
  • Charset: utf-8
// Select
(((themestrap = {}, $) => {

    const STYLE_ID   = 'ts-select-styles';
    const instanceName = '__pluginSelect';
    let _instanceCount = 0;
    let _uid = 0;

    const SVG_CHEVRON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>`;
    const SVG_CHECK   = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>`;

    const CSS = `
        .ts-select {
            --ts-select-h:                2.25rem;
            --ts-select-trigger-bg:       var(--bs-body-bg, #fff);
            --ts-select-trigger-border:   var(--bs-border-color, #dee2e6);
            --ts-select-trigger-radius:   0.375rem;
            --ts-select-trigger-color:    var(--bs-body-color, #212529);
            --ts-select-trigger-muted:    var(--bs-secondary-color, #6c757d);
            --ts-select-trigger-dim-bg:   var(--bs-secondary-bg, #e9ecef);
            --ts-select-focus-ring:       0 0 0 0.25rem rgba(13,110,253,.25);
            --ts-select-focus-border:     #86b7fe;
            --ts-select-content-bg:       var(--bs-body-bg, #fff);
            --ts-select-content-border:   var(--bs-border-color, #dee2e6);
            --ts-select-content-radius:   0.5rem;
            --ts-select-content-shadow:   0 4px 16px -2px rgba(0,0,0,.1), 0 2px 4px -2px rgba(0,0,0,.06);
            --ts-select-content-z:        1050;
            --ts-select-item-h:           2rem;
            --ts-select-item-radius:      0.25rem;
            --ts-select-item-color:       var(--bs-body-color, #212529);
            --ts-select-item-hover-bg:    var(--bs-tertiary-bg, #f8f9fa);
            --ts-select-separator-bg:     var(--bs-border-color, #dee2e6);
            position: relative;
            display: block;
        }

        @media (prefers-color-scheme: dark) {
            .ts-select {
                --ts-select-trigger-bg:      var(--bs-body-bg, #212529);
                --ts-select-trigger-border:  var(--bs-border-color, #495057);
                --ts-select-trigger-color:   var(--bs-body-color, #dee2e6);
                --ts-select-trigger-muted:   var(--bs-secondary-color, #adb5bd);
                --ts-select-trigger-dim-bg:  var(--bs-secondary-bg, #343a40);
                --ts-select-content-bg:      var(--bs-body-bg, #1a1d20);
                --ts-select-content-border:  var(--bs-border-color, #495057);
                --ts-select-content-shadow:  0 4px 16px -2px rgba(0,0,0,.4), 0 2px 4px -2px rgba(0,0,0,.3);
                --ts-select-item-hover-bg:   var(--bs-tertiary-bg, #2b2f33);
            }
        }

        html.dark .ts-select,
        [data-bs-theme="dark"] .ts-select {
            --ts-select-trigger-bg:      var(--bs-body-bg, #212529);
            --ts-select-trigger-border:  var(--bs-border-color, #495057);
            --ts-select-trigger-color:   var(--bs-body-color, #dee2e6);
            --ts-select-trigger-muted:   var(--bs-secondary-color, #adb5bd);
            --ts-select-trigger-dim-bg:  var(--bs-secondary-bg, #343a40);
            --ts-select-content-bg:      var(--bs-body-bg, #1a1d20);
            --ts-select-content-border:  var(--bs-border-color, #495057);
            --ts-select-content-shadow:  0 4px 16px -2px rgba(0,0,0,.4), 0 2px 4px -2px rgba(0,0,0,.3);
            --ts-select-item-hover-bg:   var(--bs-tertiary-bg, #2b2f33);
        }

        [data-bs-theme="light"] .ts-select {
            --ts-select-trigger-bg:      var(--bs-body-bg, #fff);
            --ts-select-trigger-border:  var(--bs-border-color, #dee2e6);
            --ts-select-trigger-color:   var(--bs-body-color, #212529);
            --ts-select-trigger-muted:   var(--bs-secondary-color, #6c757d);
            --ts-select-trigger-dim-bg:  var(--bs-secondary-bg, #e9ecef);
            --ts-select-content-bg:      var(--bs-body-bg, #fff);
            --ts-select-content-border:  var(--bs-border-color, #dee2e6);
            --ts-select-content-shadow:  0 4px 16px -2px rgba(0,0,0,.1), 0 2px 4px -2px rgba(0,0,0,.06);
            --ts-select-item-hover-bg:   var(--bs-tertiary-bg, #f8f9fa);
        }

        /* hide the native select — stays in DOM for form submission */
        .ts-select select { display: none !important; }

        /* trigger */
        .ts-select-trigger {
            display: inline-flex;
            align-items: center;
            justify-content: space-between;
            gap: 0.5rem;
            width: 100%;
            height: var(--ts-select-h);
            padding: 0 0.75rem;
            background: var(--ts-select-trigger-bg);
            border: 1px solid var(--ts-select-trigger-border);
            border-radius: var(--ts-select-trigger-radius);
            color: var(--ts-select-trigger-color);
            font-family: inherit;
            font-size: 0.875rem;
            line-height: 1;
            cursor: default;
            text-align: left;
            white-space: nowrap;
            user-select: none;
            transition: border-color .15s ease, box-shadow .15s ease;
            outline: none;
        }
        .ts-select-trigger:focus-visible {
            border-color: var(--ts-select-focus-border);
            box-shadow: var(--ts-select-focus-ring);
        }
        .ts-select-trigger:disabled,
        .ts-select--disabled .ts-select-trigger {
            background: var(--ts-select-trigger-dim-bg);
            color: var(--ts-select-trigger-muted);
            cursor: not-allowed;
            opacity: 0.65;
        }

        .ts-select-value {
            flex: 1;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .ts-select-value.ts-select-placeholder {
            color: var(--ts-select-trigger-muted);
        }

        .ts-select-icon {
            flex-shrink: 0;
            display: flex;
            align-items: center;
            color: var(--ts-select-trigger-muted);
            transition: transform .2s ease;
        }
        .ts-select--open .ts-select-icon {
            transform: rotate(180deg);
        }

        /* dropdown */
        .ts-select-content {
            position: absolute;
            top: calc(100% + 4px);
            left: 0;
            right: 0;
            z-index: var(--ts-select-content-z);
            background: var(--ts-select-content-bg);
            border: 1px solid var(--ts-select-content-border);
            border-radius: var(--ts-select-content-radius);
            box-shadow: var(--ts-select-content-shadow);
            padding: 0.25rem;
            overflow: hidden;
            opacity: 0;
            visibility: hidden;
            pointer-events: none;
            transform: translateY(-4px) scale(.98);
            transform-origin: top center;
            transition: opacity .15s ease, transform .15s ease, visibility 0ms .15s;
            outline: none;
        }
        .ts-select-content--open {
            opacity: 1;
            visibility: visible;
            pointer-events: auto;
            transform: none;
            transition-delay: 0ms;
        }

        /* flip upward when there is more space above */
        .ts-select--up .ts-select-content {
            top: auto;
            bottom: calc(100% + 4px);
            transform-origin: bottom center;
        }
        .ts-select--up .ts-select-content:not(.ts-select-content--open) {
            transform: translateY(4px) scale(.98);
        }

        /* scrollable option list */
        .ts-select-viewport {
            max-height: 280px;
            overflow-y: auto;
            scrollbar-width: thin;
            scrollbar-color: var(--ts-select-separator-bg) transparent;
        }
        .ts-select-viewport::-webkit-scrollbar        { width: 4px; }
        .ts-select-viewport::-webkit-scrollbar-track  { background: transparent; }
        .ts-select-viewport::-webkit-scrollbar-thumb  {
            background: var(--ts-select-separator-bg);
            border-radius: 2px;
        }

        /* option items */
        .ts-select-item {
            position: relative;
            display: flex;
            align-items: center;
            padding: 0 0.5rem 0 2rem;
            min-height: var(--ts-select-item-h);
            border-radius: var(--ts-select-item-radius);
            color: var(--ts-select-item-color);
            font-size: 0.875rem;
            cursor: default;
            user-select: none;
        }
        .ts-select-item:not(.ts-select-item--disabled):hover,
        .ts-select-item:not(.ts-select-item--disabled).ts-select-item--focus {
            background: var(--ts-select-item-hover-bg);
        }
        .ts-select-item--disabled {
            color: var(--ts-select-trigger-muted);
            opacity: 0.5;
            pointer-events: none;
        }

        /* checkmark indicator */
        .ts-select-item-indicator {
            position: absolute;
            left: 0.5rem;
            display: flex;
            align-items: center;
            width: 1rem;
            height: 1rem;
            opacity: 0;
        }
        .ts-select-item--selected .ts-select-item-indicator {
            opacity: 1;
        }

        .ts-select-item-text {
            flex: 1;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        /* option groups */
        .ts-select-group:not(:first-child) {
            border-top: 1px solid var(--ts-select-separator-bg);
            margin-top: 0.25rem;
            padding-top: 0.25rem;
        }
        .ts-select-group-label {
            padding: 0.375rem 0.5rem 0.25rem;
            font-size: 0.75rem;
            font-weight: 600;
            line-height: 1.5;
            color: var(--ts-select-trigger-muted);
            user-select: none;
            pointer-events: none;
        }

        /* explicit separator element */
        .ts-select-separator {
            height: 1px;
            background: var(--ts-select-separator-bg);
            margin: 0.25rem 0;
        }
    `;

    class PluginSelect {

        constructor($el, opts) {
            return this.initialize($el, opts);
        }

        initialize($el, opts) {
            if ($el.data(instanceName)) { return this; }
            this.$el             = $el;
            this.initialHTML     = $el.html();
            this.isOpen          = false;
            this._uid            = ++_uid;
            this._typeaheadTimer  = null;
            this._typeaheadBuffer = '';
            this.setData().setOptions(opts).build().events();
            return this;
        }

        setData() {
            this.$el.data(instanceName, this);
            return this;
        }

        setOptions(opts) {
            this.options = $.extend(true, {}, PluginSelect.defaults, opts, { wrapper: this.$el });
            return this;
        }

        build() {
            const self = this;
            const opts = self.options;

            if (!document.getElementById(STYLE_ID)) {
                $('<style>').attr('id', STYLE_ID).text(CSS).appendTo('head');
            }
            _instanceCount++;

            self.$native = self.$el.find('select');
            if (!self.$native.length) { return this; }

            if (self.$native.is(':disabled')) { opts.disabled = true; }

            self._items = self._parseSelect();

            /* read placeholder text from the first blank <option> */
            const $blank = self.$native.find('option').filter(function() {
                return $(this).val() === '';
            }).first();
            if ($blank.length) {
                opts.placeholder = $blank.text().trim() || opts.placeholder;
            }

            const id        = `ts-select-${self._uid}`;
            const listboxId = `${id}-listbox`;
            const rawVal    = self.$native.val() || '';
            const current   = self._findItem(rawVal);
            self.currentValue = current ? rawVal : '';
            self.currentText  = current ? current.text : opts.placeholder;

            self.$el.addClass('ts-select');
            if (opts.disabled) { self.$el.addClass('ts-select--disabled'); }

            /* trigger button */
            const $trigger = $('<button>', {
                type:            'button',
                class:           'ts-select-trigger',
                role:            'combobox',
                'aria-expanded': 'false',
                'aria-haspopup': 'listbox',
                'aria-controls': listboxId,
                id:              `${id}-trigger`
            });
            if (opts.disabled) {
                $trigger.prop('disabled', true).attr('aria-disabled', 'true');
            }

            self.$value = $('<span>', {
                class: 'ts-select-value' + (current ? '' : ' ts-select-placeholder')
            }).text(self.currentText);

            const $icon = $('<span class="ts-select-icon">').html(SVG_CHEVRON);
            $trigger.append(self.$value, $icon);

            /* dropdown */
            self.$viewport = $('<div class="ts-select-viewport">').html(self._buildItems());
            self.$content  = $('<div>', {
                class:             'ts-select-content',
                role:              'listbox',
                id:                listboxId,
                'aria-labelledby': `${id}-trigger`,
                tabindex:          '-1'
            }).append(self.$viewport);

            self.$trigger = $trigger;
            self.$el.append(self.$trigger, self.$content);
            return this;
        }

        /* parse native <select> into a plain items array */
        _parseSelect() {
            const items = [];
            this.$native.children().each(function() {
                const $c = $(this);
                if ($c.is('optgroup')) {
                    const group = { type: 'group', label: $c.attr('label') || '', children: [] };
                    $c.find('option').each(function() {
                        const $o = $(this);
                        group.children.push({ type: 'option', value: $o.val(), text: $o.text().trim(), disabled: $o.is(':disabled') });
                    });
                    if (group.children.length) { items.push(group); }
                } else if ($c.is('option') && $c.val() !== '') {
                    items.push({ type: 'option', value: $c.val(), text: $c.text().trim(), disabled: $c.is(':disabled') });
                }
            });
            return items;
        }

        /* find a non-disabled item by value */
        _findItem(value) {
            if (!value) { return null; }
            for (const item of this._items) {
                if (item.type === 'option' && item.value === value && !item.disabled) { return item; }
                if (item.type === 'group') {
                    for (const c of item.children) {
                        if (c.value === value && !c.disabled) { return c; }
                    }
                }
            }
            return null;
        }

        /* build inner HTML for the dropdown viewport */
        _buildItems() {
            const self = this;
            let html = '';
            let gi   = 0;
            self._items.forEach(item => {
                if (item.type === 'option') {
                    html += self._buildOption(item);
                } else if (item.type === 'group') {
                    const lid = `ts-select-${self._uid}-gl-${++gi}`;
                    html += `<div class="ts-select-group" role="group" aria-labelledby="${lid}">`;
                    html += `<div class="ts-select-group-label" id="${lid}">${self._esc(item.label)}</div>`;
                    item.children.forEach(c => { html += self._buildOption(c); });
                    html += '</div>';
                }
            });
            return html;
        }

        _buildOption(item) {
            const sel = item.value === this.currentValue;
            const cls = ['ts-select-item', sel ? 'ts-select-item--selected' : '', item.disabled ? 'ts-select-item--disabled' : ''].filter(Boolean).join(' ');
            return `<div class="${cls}" role="option" data-value="${this._esc(item.value)}" aria-selected="${sel}"${item.disabled ? ' aria-disabled="true"' : ''}>`
                + `<span class="ts-select-item-indicator">${SVG_CHECK}</span>`
                + `<span class="ts-select-item-text">${this._esc(item.text)}</span>`
                + '</div>';
        }

        _esc(str) {
            return String(str)
                .replace(/&/g, '&amp;')
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;')
                .replace(/"/g, '&quot;');
        }

        events() {
            const self = this;

            /* open / close on trigger click */
            self.$trigger.on('click.select', function(e) {
                e.stopPropagation();
                if (self.$trigger.prop('disabled')) { return; }
                self.isOpen ? self.close() : self.open();
            });

            /* select on item click */
            self.$content.on('click.select', '.ts-select-item:not(.ts-select-item--disabled)', function(e) {
                e.stopPropagation();
                self.select($(this).data('value'));
                self.close();
            });

            /* keyboard on the trigger */
            self.$trigger.on('keydown.select', function(e) { self._onTriggerKey(e); });

            /* keyboard on the open dropdown */
            self.$content.on('keydown.select', function(e) { self._onContentKey(e); });

            /* close on outside click */
            $(document).on(`click.select.${self._uid}`, function(e) {
                if (self.isOpen && !self.$el.is(e.target) && !$.contains(self.$el[0], e.target)) {
                    self.close();
                }
            });

            return this;
        }

        _onTriggerKey(e) {
            const self = this;
            if (['ArrowDown', 'ArrowUp', 'Enter', ' '].indexOf(e.key) !== -1) {
                e.preventDefault();
                if (!self.isOpen) {
                    self.open();
                    const $sel = self.$content.find('[aria-selected="true"]');
                    if ($sel.length) { self._focusItem($sel); } else { self._moveFocus(0); }
                }
            } else if (e.key === 'Escape') {
                self.close();
            }
        }

        _onContentKey(e) {
            const self   = this;
            const $items = self.$content.find('.ts-select-item:not(.ts-select-item--disabled)');
            const idx    = $items.index(self.$content.find('.ts-select-item--focus'));

            switch (e.key) {
                case 'ArrowDown':
                    e.preventDefault();
                    self._moveFocus(Math.min(idx + 1, $items.length - 1), $items);
                    break;
                case 'ArrowUp':
                    e.preventDefault();
                    self._moveFocus(idx <= 0 ? 0 : idx - 1, $items);
                    break;
                case 'Home':
                    e.preventDefault();
                    self._moveFocus(0, $items);
                    break;
                case 'End':
                    e.preventDefault();
                    self._moveFocus($items.length - 1, $items);
                    break;
                case 'Enter':
                case ' ': {
                    e.preventDefault();
                    const $f = self.$content.find('.ts-select-item--focus');
                    if ($f.length) { self.select($f.data('value')); self.close(); }
                    break;
                }
                case 'Escape':
                case 'Tab':
                    e.preventDefault();
                    self.close();
                    self.$trigger[0].focus();
                    break;
                default:
                    if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
                        self._typeahead(e.key, $items);
                    }
            }
        }

        _focusItem($item) {
            const self = this;
            self.$content.find('.ts-select-item--focus').removeClass('ts-select-item--focus');
            $item.addClass('ts-select-item--focus');
            /* scroll into view within the viewport */
            const el = $item[0];
            const vp = self.$viewport[0];
            if (el && vp) {
                const t = el.offsetTop;
                const b = t + el.offsetHeight;
                if (t < vp.scrollTop)                        { vp.scrollTop = t; }
                else if (b > vp.scrollTop + vp.clientHeight) { vp.scrollTop = b - vp.clientHeight; }
            }
        }

        _moveFocus(idx, $items) {
            const $all = $items || this.$content.find('.ts-select-item:not(.ts-select-item--disabled)');
            if ($all.length && idx >= 0) { this._focusItem($all.eq(idx)); }
        }

        /* type-ahead character search */
        _typeahead(char, $items) {
            const self = this;
            clearTimeout(self._typeaheadTimer);
            self._typeaheadBuffer += char.toLowerCase();
            const buf = self._typeaheadBuffer;
            const $m  = $items.filter(function() {
                return $(this).find('.ts-select-item-text').text().trim().toLowerCase().startsWith(buf);
            }).first();
            if ($m.length) { self._focusItem($m); }
            self._typeaheadTimer = setTimeout(() => { self._typeaheadBuffer = ''; }, 500);
        }

        /* detect whether the dropdown should open above or below */
        _position() {
            const r     = this.$trigger[0].getBoundingClientRect();
            const below = window.innerHeight - r.bottom;
            const above = r.top;
            if (below < 300 && above > below) {
                this.$el.addClass('ts-select--up');
            } else {
                this.$el.removeClass('ts-select--up');
            }
        }

        open() {
            const self = this;
            if (self.isOpen || self.$trigger.prop('disabled')) { return this; }
            self._position();
            self.isOpen = true;
            self.$el.addClass('ts-select--open');
            self.$content.addClass('ts-select-content--open');
            self.$trigger.attr('aria-expanded', 'true');
            // Long lists open at the selected item instead of at the top
            const $sel = self.$viewport.find('.ts-select-item--selected').first();
            if ($sel.length) {
                const v  = self.$viewport[0];
                const vr = v.getBoundingClientRect();
                // The dropdown opens with a small scale transform, which shortens every measured distance
                // (by a hundred pixels or more in a long list), so divide it out
                const k  = (vr.height / (v.offsetHeight || 1)) || 1;
                const y  = ($sel[0].getBoundingClientRect().top - vr.top) / k + v.scrollTop;
                v.scrollTop = Math.max(0, y - (v.clientHeight - $sel[0].offsetHeight) / 2);
            }
            self.$content[0].focus();
            self.$el[0].dispatchEvent(new CustomEvent('ts.select.open', {
                bubbles: true,
                detail:  { value: self.currentValue }
            }));
            return this;
        }

        close() {
            const self = this;
            if (!self.isOpen) { return this; }
            self.isOpen = false;
            self.$el.removeClass('ts-select--open');
            self.$content.removeClass('ts-select-content--open');
            self.$trigger.attr('aria-expanded', 'false');
            self.$content.find('.ts-select-item--focus').removeClass('ts-select-item--focus');
            self.$el[0].dispatchEvent(new CustomEvent('ts.select.close', {
                bubbles: true,
                detail:  { value: self.currentValue }
            }));
            return this;
        }

        select(value) {
            const self = this;
            const prev = self.currentValue;
            const item = self._findItem(value);
            if (!item) { return this; }
            self.currentValue = value;
            self.currentText  = item.text;
            self.$native.val(value);
            self.$value.text(item.text).removeClass('ts-select-placeholder');
            self.$content.find('[role="option"]').each(function() {
                const $o = $(this);
                const s  = $o.data('value') === value;
                $o.attr('aria-selected', s ? 'true' : 'false').toggleClass('ts-select-item--selected', s);
            });
            self.$el[0].dispatchEvent(new CustomEvent('ts.select.change', {
                bubbles: true,
                detail:  { value, text: item.text, previousValue: prev }
            }));
            // The wrapped <select> behaves like a native one: forms, validators and listeners see input and change
            if (value !== prev) {
                self.$native[0].dispatchEvent(new Event('input',  { bubbles: true }));
                self.$native[0].dispatchEvent(new Event('change', { bubbles: true }));
            }
            return this;
        }

        getValue() { return this.currentValue; }

        setValue(value) { return this.select(value); }

        reset() {
            const self = this;
            self.currentValue = '';
            self.currentText  = self.options.placeholder;
            self.$native.val('');
            self.$value.text(self.options.placeholder).addClass('ts-select-placeholder');
            self.$content.find('[role="option"]').attr('aria-selected', 'false').removeClass('ts-select-item--selected');
            return this;
        }

        enable() {
            this.options.disabled = false;
            this.$trigger.prop('disabled', false).removeAttr('aria-disabled');
            this.$el.removeClass('ts-select--disabled');
            return this;
        }

        disable() {
            this.options.disabled = true;
            this.$trigger.prop('disabled', true).attr('aria-disabled', 'true');
            this.$el.addClass('ts-select--disabled');
            return this;
        }

        destroy() {
            const self = this;
            clearTimeout(self._typeaheadTimer);
            $(document).off(`.select.${self._uid}`);
            self.$el.html(self.initialHTML);
            self.$el.removeClass('ts-select ts-select--open ts-select--disabled ts-select--up');
            self.$el.removeData(instanceName);
            _instanceCount--;
            if (_instanceCount <= 0) {
                const s = document.getElementById(STYLE_ID);
                if (s) { s.remove(); }
                _instanceCount = 0;
            }
            return this;
        }
    }

    PluginSelect.defaults = {
        placeholder: 'Select an option…',
        disabled:    false
    };

    $.extend(themestrap, { PluginSelect });

    $.fn.themestrapPluginSelect = function(opts) {
        const args = Array.prototype.slice.call(arguments, 1);
        return this.map(function() {
            const $this = $(this);
            const inst  = $this.data(instanceName);
            if (inst) {
                if (typeof opts === 'string' && typeof inst[opts] === 'function') {
                    return inst[opts].apply(inst, args);
                }
                return inst;
            }
            return new PluginSelect($this, typeof opts !== 'string' ? opts : undefined);
        });
    };

})).apply(this, [window.themestrap, jQuery]);