UNPKG

@doyosi/laravel

Version:

Complete JavaScript plugins collection for Laravel applications - AJAX, forms, UI components, and more

2,076 lines 76.6 kB
/*!
 * @doyosi/laravel-js v1.0.5
 * JavaScript plugins for Laravel applications
 * (c) 2025 Karyazilim
 * Released under MIT License
 */
'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

/**
 * AjaxDivBox - Ajax-based grid/list view with card-like div rendering.
 *
 * @param {Object} options - Configuration options.
 * @see constructor for all available options.
 *
 * @example
 * import AjaxDivBox from './AjaxDivBox.js';
 * const grid = new AjaxDivBox({
 * url: '/api/items',
 * container: '#ajax-list',
 * templateId: 'box-template',
 * filterSelector: '#filters',
 * pagination: '#pagination-container'
 * });
 * grid.init(); // Fetch initial data
 * grid.on('rendered', ({ data }) => console.log('Loaded', data.length, 'items'));
 */
class AjaxDivBox {
    /**
     * @param {Object} options
     * @param {string} options.url - API endpoint for data.
     * @param {string|Element} options.container - The container to render the items into.
     * @param {string} [options.templateId='box-template'] - The ID of the script template for rendering items.
     * @param {string} [options.metaKey='meta'] - The key in the API response for pagination metadata.
     * @param {string} [options.dataKey='data'] - The key in the API response for the array of items.
     * @param {string} [options.fetcher='axios'|'fetch'] - The HTTP client to use. Auto-detects Axios.
     * @param {function|null} [options.onBox=null] - A custom function to render an item, overrides templateId.
     * @param {string|Element|null} [options.pagination=null] - The container for pagination links.
     * @param {string|Element|null} [options.filterSelector=null] - The form or div containing filter inputs.
     * @param {string|Element|null} [options.loadingIndicator='.loading-list'] - Selector for the loading element.
     * @param {string|Element|null} [options.nothingFoundBlock='.nothing-found-list'] - Element to show when no results are found.
     * @param {string|Element|null} [options.errorBlock='.list-render-error'] - Element to show on fetch error.
     * @param {function|null} [options.additionalParams=null] - Function that returns an object of extra query parameters.
     */
    constructor({
                    url,
                    container,
                    templateId = 'box-template',
                    metaKey = 'meta',
                    dataKey = 'data',
                    fetcher = window.axios ? 'axios' : 'fetch',
                    onBox = null,
                    pagination = null,
                    filterSelector = null,
                    loadingIndicator = '.loading-list',
                    nothingFoundBlock = '.nothing-found-list',
                    errorBlock = '.list-render-error',
                    additionalParams = null,
                }) {
        this.url = url;
        this.config = { templateId, metaKey, dataKey, fetcher, onBox, additionalParams };
        this.filters = {};
        this._handlers = {};
        this.debounceTimer = null;

        // --- UPDATE: Centralized DOM element querying ---
        // Query all elements once and store them.
        const getElement = (selector) => {
            if (selector instanceof HTMLElement) return selector;
            if (typeof selector === 'string') return document.querySelector(selector);
            return null;
        };

        this.elements = {
            container: getElement(container),
            pagination: getElement(pagination),
            filters: getElement(filterSelector),
            loader: getElement(loadingIndicator),
            nothingFound: getElement(nothingFoundBlock),
            error: getElement(errorBlock),
        };

        if (!this.elements.container) {
            throw new Error('AjaxDivBox: The main container element is required and was not found.');
        }

        // --- UPDATE: Event listeners are bound once in the constructor ---
        this._bindFilterEvents();
        if (this.config.autoInit !== false) {
            this.init();
        }
    }

    /**
     * Registers an event handler.
     * @param {'start'|'rendered'|'error'|'pageChange'} event - The event name.
     * @param {Function} fn - The callback function.
     * @returns {this}
     */
    on(event, fn) {
        if (!this._handlers[event]) this._handlers[event] = [];
        this._handlers[event].push(fn);
        return this;
    }

    /**
     * Emits an event to all registered handlers.
     * @private
     */
    _emit(event, payload) {
        (this._handlers[event] || []).forEach(fn => fn(payload));
    }

    /**
     * Initializes the component by fetching the first page of data.
     * This should be called after instantiation.
     */
    init() {
        if (this.elements.filters) {
            this._updateFilters();
        }
        return this.fetchData(1);
    }

    /**
     * Refreshes the data using the current filters and page.
     */
    refresh() {
        // UPDATE: More descriptive name than init() for a refresh action.
        const currentPage = this.lastMeta?.current_page || 1;
        return this.fetchData(currentPage);
    }

    /**
     * Binds change/input events to filter elements.
     * @private
     */
    _bindFilterEvents() {
        if (!this.elements.filters) return;

        this.elements.filters.addEventListener('input', e => {
            const target = e.target;
            if (target.matches('input, select')) {
                // UPDATE: Simplified debounce logic.
                clearTimeout(this.debounceTimer);
                this.debounceTimer = setTimeout(() => {
                    this._updateFilters();
                    this.fetchData(1);
                }, 300);
            }
        });
    }

    /**
     * Reads the current values from the filter inputs.
     * @private
     */
    _updateFilters() {
        this.filters = {};
        if (!this.elements.filters) return;

        const formData = new FormData(this.elements.filters.tagName === 'FORM' ? this.elements.filters : undefined);
        if (this.elements.filters.tagName !== 'FORM') {
            this.elements.filters.querySelectorAll('input, select').forEach(input => {
                if (input.name) formData.append(input.name, input.value);
            });
        }

        for (const [key, value] of formData.entries()) {
            if (value) this.filters[key] = value;
        }
    }

    /**
     * Constructs the query string from current filters and additional params.
     * @private
     */
    _buildQueryString(page = 1) {
        let params = { ...this.filters, page };

        if (typeof this.config.additionalParams === 'function') {
            params = { ...params, ...this.config.additionalParams() };
        }

        return new URLSearchParams(Object.entries(params).filter(([, v]) => v !== '' && v != null)).toString();
    }

    /**
     * Manages the visibility of elements based on the current state.
     * @private
     * @param {'loading'|'content'|'error'|'empty'} state - The state to display.
     * @param {string} [errorMessage] - An optional error message.
     */
    _setState(state, errorMessage = 'An error occurred.') {
        const { container, loader, nothingFound, error } = this.elements;
        const all = [container, loader, nothingFound, error];

        all.forEach(el => el?.classList.add('hidden'));

        if (state === 'loading' && loader) loader.classList.remove('hidden');
        else if (state === 'content' && container) container.classList.remove('hidden');
        else if (state === 'empty' && nothingFound) nothingFound.classList.remove('hidden');
        else if (state === 'error' && error) {
            error.classList.remove('hidden');
            const errorTextField = error.querySelector('.list-render-error-text') || error;
            errorTextField.textContent = errorMessage;
        }
    }

    /**
     * Fetches data from the API endpoint.
     * @param {number} [page=1] - The page number to fetch.
     */
    async fetchData(page = 1) {
        this._setState('loading');
        this._emit("start", { page });

        const endpoint = `${this.url.split('?')[0]}?${this._buildQueryString(page)}`;

        try {
            const response = this.config.fetcher === 'axios'
                ? (await window.axios.get(endpoint)).data
                : await (await fetch(endpoint, { headers: { 'Accept': 'application/json' } })).json();

            if (response.ok === false) throw response; // Handle API-level errors

            const meta = response[this.config.metaKey] || {};
            this.lastMeta = meta; // Cache for refresh

            // Check for pre-rendered HTML in the response
            if (response.html !== undefined && response.html !== null) {
                // If API returns pre-rendered HTML, use it directly
                this.elements.container.innerHTML = response.html;
                this._renderPagination(meta);

                const hasContent = response.html.trim().length > 0;
                this._setState(hasContent ? 'content' : 'empty');
                this._emit("rendered", { html: response.html, meta, page });
            } else {
                // Otherwise, use the standard template rendering
                const data = response[this.config.dataKey] || [];
                this._renderBoxes(data);
                this._renderPagination(meta);

                this._setState(data.length > 0 ? 'content' : 'empty');
                this._emit("rendered", { data, meta, page });
            }

        } catch (err) {
            console.error('AjaxDivBox fetch error:', err);
            const message = err?.message || 'Failed to load data.';
            this._setState('error', message);
            this._emit("error", { error: err, message });
        }
    }

    /**
     * Renders the items into the container.
     * @private
     */
    _renderBoxes(data) {
        if (!this.elements.container) return;
        this.elements.container.innerHTML = ''; // Clear previous content

        const fragment = document.createDocumentFragment();
        data.forEach(item => {
            const itemHtml = this._renderTemplate(item);
            const tempDiv = document.createElement('div');
            tempDiv.innerHTML = itemHtml;
            // Append all children from the temp div to the fragment
            while (tempDiv.firstChild) {
                fragment.appendChild(tempDiv.firstChild);
            }
        });
        this.elements.container.appendChild(fragment);
    }

    /**
     * Renders a single item using either the custom onBox function or the template.
     * @private
     */
    _renderTemplate(item) {
        // UPDATE: Prioritize per-item HTML if it exists.
        if (item.html !== undefined && item.html !== null) {
            return item.html;
        }

        if (typeof this.config.onBox === 'function') {
            return this.config.onBox(item);
        }

        const tpl = document.getElementById(this.config.templateId);
        if (!tpl) {
            console.error(`Template with id "${this.config.templateId}" not found.`);
            return '';
        }


        let html = tpl.innerHTML;

        // Replace nested object properties (e.g., data.user.name)
        html = html.replace(/data\.([a-zA-Z0-9_]+\.[a-zA-Z0-9_]+)/g, (match, path) => {
            const keys = path.split('.');
            let value = item;
            for (const key of keys) {
                value = value?.[key];
                if (value === undefined) break;
            }
            return value ?? '';
        });

        // Replace simple properties (e.g., data.name)
        html = html.replace(/data\.([a-zA-Z0-9_]+)/g, (_, key) => item[key] ?? '');

        return html;
    }

    /**
     * Renders pagination links based on metadata.
     * @private
     */
    _renderPagination(meta) {
        const pagEl = this.elements.pagination;
        if (!pagEl) return;

        pagEl.innerHTML = '';
        if (!meta?.links || meta.last_page <= 1) {
            pagEl.classList.add('hidden');
            return;
        }

        pagEl.classList.remove('hidden');

        const fragment = document.createDocumentFragment();
        meta.links.forEach(link => {
            const pageNum = new URL(link.url || '', window.location.origin).searchParams.get('page');

            if (link.label.includes('...')) {
                const span = document.createElement('span');
                span.className = 'btn btn-disabled join-item';
                span.textContent = '...';
                fragment.appendChild(span);
                return;
            }

            const btn = document.createElement('button');
            btn.className = `join-item btn ${link.active ? 'btn-active' : ''}`;
            btn.disabled = !link.url || link.active;
            btn.innerHTML = link.label.replace(/&laquo;|&raquo;/g, '');

            if (link.url) {
                btn.onclick = (e) => {
                    e.preventDefault();
                    this._emit("pageChange", { page: parseInt(pageNum), label: link.label });
                    this.fetchData(pageNum);
                };
            }
            fragment.appendChild(btn);
        });

        const joinDiv = document.createElement('div');
        joinDiv.className = "join";
        joinDiv.appendChild(fragment);
        pagEl.appendChild(joinDiv);
    }
}

class AjaxTable {
    constructor({
        url,
        container,
        templateId = 'row-template',
        metaKey = 'meta',
        dataKey = 'data',
        fetcher = window.axios ? 'axios' : 'fetch',
        onRow = null,
        pagination = null,
        filterSelector = null,
        loadingIndicator = '.loading-table',
        nothingFoundBlock = '.nothing-found-table',
        errorBlock = '.table-render-error',
        additionalParams = null,
        autoInit = true,
        debounceTime = 300
    }) {
        this.url = url;
        this.config = { templateId, metaKey, dataKey, fetcher, onRow, additionalParams, debounceTime };
        this.filters = {};
        this._handlers = {};
        this.debounceTimer = null;

        const getEl = (sel) => typeof sel === 'string' ? document.querySelector(sel) : sel;

        this.elements = {
            container: getEl(container),
            table: getEl(container)?.querySelector('table'),
            tbody: getEl(container)?.querySelector('tbody'),
            pagination: getEl(pagination),
            filters: getEl(filterSelector),
            loader: getEl(loadingIndicator),
            nothingFound: getEl(nothingFoundBlock),
            error: getEl(errorBlock),
        };

        if (!this.elements.table || !this.elements.tbody) {
            throw new Error('AjaxTable: Table or tbody element not found.');
        }

        this._bindFilterEvents();

        if (autoInit) this.init();
    }

    on(event, fn) {
        if (!this._handlers[event]) this._handlers[event] = [];
        this._handlers[event].push(fn);
        return this;
    }

    _emit(event, payload) {
        (this._handlers[event] || []).forEach(fn => fn(payload));
    }

    init() {
        if (this.elements.filters) this._updateFilters();
        return this.fetchData(1);
    }

    refresh() {
        const currentPage = this.lastMeta?.current_page || 1;
        return this.fetchData(currentPage);
    }

    _bindFilterEvents() {
        if (!this.elements.filters) return;

        const handler = () => {
            clearTimeout(this.debounceTimer);
            this.debounceTimer = setTimeout(() => {
                this._updateFilters();
                this.fetchData(1);
            }, this.config.debounceTime);
        };

        this.elements.filters.querySelectorAll('input, select').forEach(input => {
            const type = input.tagName.toLowerCase();
            if (type === 'input') {
                input.addEventListener('input', handler);
            } else {
                input.addEventListener('change', handler);
            }
        });
    }

    _updateFilters() {
        this.filters = {};
        const formData = new FormData(this.elements.filters.tagName === 'FORM' ? this.elements.filters : undefined);
        if (this.elements.filters.tagName !== 'FORM') {
            this.elements.filters.querySelectorAll('input, select').forEach(input => {
                if (input.name) formData.append(input.name, input.value);
            });
        }
        for (const [key, value] of formData.entries()) {
            if (value) this.filters[key] = value;
        }
    }

    _buildQueryString(page = 1) {
        let params = { ...this.filters, page };
        if (typeof this.config.additionalParams === 'function') {
            params = { ...params, ...this.config.additionalParams() };
        }
        return new URLSearchParams(Object.entries(params).filter(([, v]) => v != null && v !== '')).toString();
    }

    _setState(state, errorMessage = 'An error occurred.') {
        const { container, loader, nothingFound, error } = this.elements;
        const all = [container, loader, nothingFound, error];
        all.forEach(el => el?.classList.add('hidden'));

        if (state === 'loading' && loader) loader.classList.remove('hidden');
        else if (state === 'content' && container) container.classList.remove('hidden');
        else if (state === 'empty' && nothingFound) nothingFound.classList.remove('hidden');
        else if (state === 'error' && error) {
            error.classList.remove('hidden');
            const errorTextField = error.querySelector('.table-render-error-text') || error;
            errorTextField.textContent = errorMessage;
        }
    }

    async fetchData(page = 1) {
        this._setState('loading');
        this._emit('start', { page });
        const pagEl = this.elements.pagination;
        if (pagEl) {
            pagEl.innerHTML = '';
            pagEl.classList.add('hidden');
        }
        const endpoint = `${this.url.split('?')[0]}?${this._buildQueryString(page)}`;

        try {
            const response = this.config.fetcher === 'axios'
                ? (await window.axios.get(endpoint)).data
                : await (await fetch(endpoint)).json();

            const meta = response[this.config.metaKey] || {};
            this.lastMeta = meta;

            const data = response[this.config.dataKey] || [];

            this._renderRows(data);
            this._renderPagination(meta);

            this._setState(data.length > 0 ? 'content' : 'empty');
            this._emit('rendered', { data, meta, page });

        } catch (err) {
            console.error('AjaxTable fetch error:', err);
            const msg = err?.message || 'Failed to load data.';
            this._setState('error', msg);
            this._emit('error', { error: err, message: msg });
        }
    }

    _renderRows(data) {
        if (!this.elements.tbody) return;
        this.elements.tbody.innerHTML = '';

        const fragment = document.createDocumentFragment();
        data.forEach(row => {
            const tr = document.createElement('tr');
            tr.innerHTML = this._renderTemplate(row);
            fragment.appendChild(tr.children.length === 1 && tr.children[0].tagName === 'TR'
                ? tr.children[0]
                : tr);
        });

        this.elements.tbody.appendChild(fragment);
    }

    _renderTemplate(row) {
        if (typeof this.config.onRow === 'function') return this.config.onRow(row);
        if (row.html) return row.html;

        const tpl = document.getElementById(this.config.templateId);
        if (!tpl) return '';
        let html = tpl.innerHTML;

        html = html.replace(/data\.([a-zA-Z0-9_]+\.[a-zA-Z0-9_]+)/g, (_, path) => {
            const keys = path.split('.');
            let value = row;
            for (const key of keys) {
                value = value?.[key];
                if (value === undefined) break;
            }
            return value ?? '';
        });

        html = html.replace(/data\.([a-zA-Z0-9_]+)/g, (_, key) => row[key] ?? '');
        return html;
    }

    _renderPagination(meta) {
        const pagEl = this.elements.pagination;
        if (!pagEl || !meta?.links || meta.last_page <= 1) {
            pagEl?.classList.add('hidden');
            return;
        }

        pagEl.classList.remove('hidden');
        pagEl.innerHTML = '';

        const fragment = document.createDocumentFragment();
        meta.links.forEach(link => {
            const pageNum = new URL(link.url || '', window.location.origin).searchParams.get('page');
            const btn = document.createElement('button');

            btn.className = `join-item btn ${link.active ? 'btn-active' : ''}`;
            btn.disabled = !link.url || link.active;
            btn.innerHTML = link.label.replace(/&laquo;|&raquo;/g, '');

            if (link.url) {
                btn.onclick = e => {
                    e.preventDefault();
                    this._emit('pageChange', { page: parseInt(pageNum), label: link.label });
                    this.fetchData(pageNum);
                };
            }

            fragment.appendChild(link.label.includes('...') ? Object.assign(document.createElement('span'), {
                className: 'btn btn-disabled join-item',
                textContent: '...'
            }) : btn);
        });

        const joinDiv = document.createElement('div');
        joinDiv.className = 'join';
        joinDiv.appendChild(fragment);
        pagEl.appendChild(joinDiv);
    }
}

class CodeInput {
    constructor(selector, hiddenName) {
        this.inputs = Array.from(document.querySelectorAll(`${selector}[data-id="${hiddenName}"]`));
        this.hidden = document.querySelector(`input[type=hidden][name="${hiddenName}"]`);
        this._bindEvents();
        this._updateHidden();
    }

    _bindEvents() {
        this.inputs.forEach((inpt, idx) => {
            inpt.setAttribute('maxlength', 1);
            inpt.addEventListener('input', e => this._onInput(e, idx));
            inpt.addEventListener('keydown', e => this._onKeyDown(e, idx));
            inpt.addEventListener('paste', e => this._onPaste(e, idx));
        });
    }

    _onInput(e, idx) {
        const val = e.target.value.replace(/[^0-9]/g, '').charAt(0);
        e.target.value = val;
        this._updateHidden();
        if (val && this.inputs[idx + 1]) {
            this.inputs[idx + 1].focus();
        }
    }

    _onKeyDown(e, idx) {
        if (e.key === 'Backspace' && !e.target.value && this.inputs[idx - 1]) {
            this.inputs[idx - 1].focus();
        }
    }

    _onPaste(e, idx) {
        e.preventDefault();
        const paste = e.clipboardData.getData('text').trim().replace(/\s+/g, '');
        const vals = paste.split('').slice(0, this.inputs.length - idx);
        vals.forEach((ch, i) => {
            this.inputs[idx + i].value = ch;
        });
        this.inputs[Math.min(this.inputs.length - 1, idx + vals.length - 1)].focus();
        this._updateHidden();
    }

    _updateHidden() {
        const code = this.inputs.map(i => i.value || '').join('');
        if (this.hidden) this.hidden.value = code;
    }
}

// DeleteContent.js
class DeleteContent {
    /**
     * @param {string|NodeList|HTMLElement[]} selector   Buttons selector, e.g. ".delete-btn" or a NodeList
     * @param {Object} options
     * @param {string} [options.confirmText] Modal header text
     * @param {string} [options.message] Confirmation message
     * @param {string} [options.successText] Success modal header
     * @param {string} [options.successMessage] Success description
     * @param {string} [options.errorText] Error modal header
     * @param {string} [options.errorMessage] Error description
     * @param {Function} [options.onDelete] Callback after delete: (success, data, button) => {}
     */
    constructor(selector, options = {}) {
        this.options = Object.assign({
            confirmText: "Are you sure?",
            message: "This action cannot be undone!",
            successText: "Deleted!",
            successMessage: "Your item has been successfully deleted.",
            errorText: "Error!",
            errorMessage: "Delete failed.",
            successTimeout: null,
            errorTimeout: null,
        }, options);


        this.buttons = typeof selector === 'string'
            ? document.querySelectorAll(selector)
            : selector;

        this._initModals();
        this._bindEvents();
    }

    // Initializes modals (creates them if they don't exist in the DOM)
    _initModals() {
        // Confirm Modal
        if (!document.getElementById('delete_modal')) {
            document.body.insertAdjacentHTML('beforeend', `
            <dialog id="delete_modal" class="modal">
              <div class="modal-box bg-red-800/90 text-white">
                <h3 class="font-bold text-lg text-white flex items-center gap-2 justify-center">
                  <svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
                  <span id="delete_modal_title">${this.options.confirmText}</span>
                </h3>
                <p class="py-1 text-sm text-center" id="delete_modal_message">${this.options.message}</p>
                <div class="modal-action flex gap-2 justify-center">
                  <button class="btn btn-sm shadow-none" type="button" id="cancelDeleteBtn">Cancel</button>
                  <button class="btn btn-sm btn-error shadow-none" type="button" id="confirmDeleteBtn">Delete</button>
                </div>
              </div>
            </dialog>`);
        }
        // Success Modal
        if (!document.getElementById('success_modal')) {
            document.body.insertAdjacentHTML('beforeend', `
            <dialog id="success_modal" class="modal">
              <div class="modal-box bg-green-800/90 text-white">
               <div class="flex items-center justify-between gap-1 border-b border-gray-200 py-2">
                    <h3 class="text-lg font-bold">
                        <span id="success_modal_title">${this.options.successText}</span>
                    </h3>
                    <form method="dialog">
                        <button class="btn btn-xs btn-ghost rounded-full btn-circle" type="submit"> <svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg></button>
                    </form>
                </div>
                <p class="py-4" id="success_modal_message">${this.options.successMessage}</p>
              </div>
            </dialog>`);
        }
        // Error Modal
        if (!document.getElementById('error_modal')) {
            document.body.insertAdjacentHTML('beforeend', `
            <dialog id="error_modal" class="modal">
              <div class="modal-box bg-red-800/90 text-white">
              <div class="flex items-center justify-between gap-1 border-b border-gray-200 py-2">
                    <h3 class="text-lg font-bold">
                        <span id="error_modal_title">${this.options.errorText}</span>
                    </h3>
                    <form method="dialog">
                        <button class="btn btn-xs btn-ghost rounded-full btn-circle" type="submit"> <svg class="size-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg></button>
                    </form>
                </div>
                <p class="py-4" id="error_modal_message">${this.options.errorMessage}</p>
              </div>
            </dialog>`);
        }

        // Cache modal elements for later use
        this.deleteModal = document.getElementById('delete_modal');
        this.successModal = document.getElementById('success_modal');
        this.errorModal = document.getElementById('error_modal');
        this.confirmBtn = document.getElementById('confirmDeleteBtn');
        this.cancelBtn = document.getElementById('cancelDeleteBtn');
    }

    _bindEvents() {
        // For keeping track of which button was clicked
        this.currentBtn = null;

        // Open the confirmation modal on delete button click
        this.buttons.forEach(btn => {
            btn.addEventListener('click', (e) => {
                e.preventDefault();
                this.currentBtn = btn;
                document.getElementById('delete_modal_title').textContent = this.options.confirmText;
                document.getElementById('delete_modal_message').textContent = this.options.message;
                this.confirmBtn.disabled = false;
                this.cancelBtn.disabled = false;
                this.deleteModal.showModal();
            });
        });

        // Cancel delete
        this.cancelBtn.addEventListener('click', () => {
            this.deleteModal.close();
            this.currentBtn = null;
        });

        // Confirm delete
        this.confirmBtn.addEventListener('click', async () => {
            if (!this.currentBtn) return;
            const url = this.currentBtn.dataset.href;
            // Disable both buttons to prevent multiple clicks
            this.confirmBtn.disabled = true;
            this.cancelBtn.disabled = true;

            try {
                let resp;
                if (window.axios) {
                    resp = await axios.delete(url, {
                        headers: {'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json'}
                    });
                    if (resp.status === 200 || resp.status === 204) {
                        this._showSuccess();
                        this._afterDelete(true, resp.data);
                    } else throw resp;
                } else {
                    const res = await fetch(url, { method: 'DELETE', headers: {'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json'} });
                    if (res.ok) {
                        this._showSuccess();
                        let data = await res.json().catch(() => ({}));
                        this._afterDelete(true, data);
                    } else {
                        throw res;
                    }
                }
            } catch (err) {
                this._showError(err);
                this._afterDelete(false, err);
            } finally {
                this.deleteModal.close();
                this.currentBtn = null;
                // Always re-enable buttons when modal re-opens
                setTimeout(() => {
                    this.confirmBtn.disabled = false;
                    this.cancelBtn.disabled = false;
                }, 300);
            }
        });
    }


    // Shows the success modal
    _showSuccess() {
        document.getElementById('success_modal_title').textContent = this.options.successText;
        document.getElementById('success_modal_message').textContent = this.options.successMessage;
        this.successModal.showModal();
        // Only auto-close if timeout is set and > 0
        if (this.options.successTimeout && this.options.successTimeout > 0) {
            setTimeout(() => {
                this.successModal.close();
            }, this.options.successTimeout);
        }
    }

    _showError(err) {
        document.getElementById('error_modal_title').textContent = this.options.errorText;
        let msg = this.options.errorMessage;
        if (err && err.response && err.response.data && err.response.data.message)
            msg = err.response.data.message;
        document.getElementById('error_modal_message').textContent = msg;
        this.errorModal.showModal();
        // Only auto-close if timeout is set and > 0
        if (this.options.errorTimeout && this.options.errorTimeout > 0) {
            setTimeout(() => {
                this.errorModal.close();
            }, this.options.errorTimeout);
        }
    }


    /**
     * Hook that fires after delete request completes
     * @param {boolean} success Was the delete successful?
     * @param {object} data Response data or error
     * @param {HTMLElement} button The button that triggered the delete
     */
    _afterDelete(success, data) {
        if (typeof this.options.onDelete === 'function') {
            this.options.onDelete(success, data, this.currentBtn);
        }
    }
}

/**
 * EditContent.js
 *
 * Easily manage "edit" form workflows for CRUD apps.
 * - Fills form via AJAX (fetch, axios supported)
 * - Switches form to edit mode, changes button/title, reveals cancel button
 * - Handles cancel back to add mode
 * - Emits events/hooks for integration
 *
 * Example usage:
 *   import EditContent from './EditContent';
 *   const edit = new EditContent({
 *     form: '#lineForm',
 *     editButtonSelector: '.btn-edit-line',
 *     cancelButtonSelector: '.edit-cancel',
 *     submitButtonSelector: '#lineButton',
 *     addTitle: 'Add New Line',
 *     editTitle: 'Edit Line',
 *     addButtonText: 'Save',
 *     editButtonText: 'Edit',
 *     onEditStart: (data) => {},
 *     onEditEnd: () => {},
 *   });
 */
class EditContent {
    /**
     * @param {Object} opts
     * @param {string|HTMLElement} opts.form - Form selector or element
     * @param {string} opts.editButtonSelector - Selector for edit buttons
     * @param {string} opts.cancelButtonSelector - Selector for cancel button
     * @param {string} [opts.submitButtonSelector] - Selector for submit button (default: first [type=submit] in form)
     * @param {string} [opts.addTitle] - Default title in add mode
     * @param {string} [opts.editTitle] - Title in edit mode
     * @param {string} [opts.addButtonText] - Default button text in add mode
     * @param {string} [opts.editButtonText] - Button text in edit mode
     * @param {Function} [opts.onEditStart] - Hook after edit mode, receives loaded data
     * @param {Function} [opts.onEditEnd] - Hook after returning to add mode
     */
    constructor(opts = {}) {
        // Config/DOM
        this.form = typeof opts.form === 'string' ? document.querySelector(opts.form) : opts.form;
        this.editButtonSelector = opts.editButtonSelector;
        this.cancelButton = document.querySelector(opts.cancelButtonSelector);
        this.submitButton = opts.submitButtonSelector
            ? document.querySelector(opts.submitButtonSelector)
            : this.form.querySelector('[type="submit"]');
        this.buttonText = this.submitButton?.querySelector('.button-text') || this.submitButton;
        this.header = this.form.closest('.pcard')?.querySelector('.pcard-title');
        this.addTitle = opts.addTitle || this.header?.dataset.addTitle || this.header?.textContent || 'Add';
        this.editTitle = opts.editTitle || this.header?.dataset.editTitle || 'Edit';
        this.addButtonText = opts.addButtonText || this.buttonText?.dataset.addTitle || this.buttonText?.textContent || 'Save';
        this.editButtonText = opts.editButtonText || this.buttonText?.dataset.editTitle || 'Edit';
        this.onEditStart = opts.onEditStart;
        this.onEditEnd = opts.onEditEnd;
        this.editMode = false;

        this._bindEditButtons();
        this._bindCancelButton();
    }

    /** Listen to all edit buttons for click */
    _bindEditButtons() {
        document.body.addEventListener('click', async (e) => {
            const btn = e.target.closest(this.editButtonSelector);
            if (!btn) return;

            e.preventDefault();
            const url = btn.dataset.href;
            if (!url) return;

            try {
                let res, data;
                if (window.axios) {
                    res = await axios.get(url, { headers: { 'Accept': 'application/json' } });
                    data = res.data.data || res.data;
                } else {
                    res = await fetch(url, { headers: { 'Accept': 'application/json' } });
                    data = (await res.json()).data || {};
                }
                this._fillForm(data, {
                    action: url,
                    method: 'POST',
                    title: this.editTitle,
                    buttonText: this.editButtonText,
                });
                if (this.cancelButton) this.cancelButton.classList.remove('hidden');
                this.editMode = true;
                if (typeof this.onEditStart === 'function') this.onEditStart(data);
            } catch (err) {
                console.error(err);
                window.Toast
                    ? new Toast({ message: "Cannot fetch item.", type: "error" })
                    : alert('Fetch error');
            }
        });
    }

    /** Cancel edit, reset form to add mode */
    _bindCancelButton() {
        if (!this.cancelButton) return;
        this.cancelButton.addEventListener('click', () => this.resetToAddMode());
    }

    /**
     * Fill the form with data, and update action/title/button text
     */
    _fillForm(data = {}, config = {}) {
        ['input', 'textarea', 'select'].forEach(tag => {
            this.form.querySelectorAll(tag).forEach(input => {
                if (!input.name) return;
                if (input.type === "file") return;

                // Match title[en], description[tr] etc.
                let match = input.name.match(/^(\w+)\[(\w+)\]$/);
                if (match) {
                    let [_, parent, locale] = match;
                    // e.g. parent='title', locale='en' -> look for data.titles?.en
                    let pluralKey = parent + 's'; // title -> titles
                    if (data[pluralKey] && data[pluralKey][locale] !== undefined) {
                        input.value = data[pluralKey][locale];
                    }
                    // Fallback to e.g. data.title?.en
                    else if (data[parent] && typeof data[parent] === 'object' && data[parent][locale] !== undefined) {
                        input.value = data[parent][locale];
                    }
                } else if (data[input.name] !== undefined) {
                    console.log(input.name, data[input.name]);
                    input.value = data[input.name];
                }
            });
        });

        if (config.action) this.form.action = config.action;
        if (config.method) this.form.setAttribute('data-method', config.method);

        if (this.header && config.title) this.header.textContent = config.title;
        if (this.buttonText && config.buttonText) this.buttonText.textContent = config.buttonText;

        this.form.setAttribute('data-edit-mode', this.editMode ? 'true' : 'false');
    }


    /** Public: reset form and UI back to add mode */
    resetToAddMode(defaults = {}) {
        this.form.reset();
        this._fillForm(defaults, {
            action: this.form.dataset.action,
            method: 'POST',
            title: this.addTitle,
            buttonText: this.addButtonText,
        });
        if (this.cancelButton) this.cancelButton.classList.add('hidden');
        this.editMode = false;
        if (typeof this.onEditEnd === 'function') this.onEditEnd();
    }
}

class FormSubmit {
    constructor(config = {}) {
        this.config = Object.assign({
            method: 'axios',
            httpMethod: 'POST',
            formSelector: null,
            submitButtonSelector: null,
            action: null,
            successMessage: 'Operation completed successfully!',
            errorMessage: 'An error occurred.',
            useToast: typeof Toast !== 'undefined',
            disableOnSuccess: false,
            redirectUrl: null,
            beforeSubmit: null,
            afterSubmit: null,
            getUrl: null,
            buttonOnly: false,
        }, config);

        this.handlers = { success: [], error: [], beforeSubmit: [], afterSubmit: [] };

        this.form = this._getFormElement(this.config.formSelector);
        this.submitButton = this._getSubmitButton();
        this.isButtonOnly = !this.form || this.config.buttonOnly;

        if (!this.isButtonOnly && !this.form) throw new Error('Form element not found.');
        if (!this.submitButton) throw new Error('Submit button not found.');

        this.originalButtonText = this.submitButton.querySelector('.button-text')?.textContent || this.submitButton.textContent;
        this._setupEventListeners();
    }

    on(event, fn) {
        if (this.handlers[event]) this.handlers[event].push(fn);
        return this;
    }

    _emit(event, payload) {
        (this.handlers[event] || []).forEach(fn => fn(payload));
    }

    _getFormElement(sel) {
        if (!sel) return null;
        if (typeof sel === 'string') return document.querySelector(sel);
        if (sel instanceof HTMLFormElement) return sel;
        return null;
    }

    _getSubmitButton() {
        const sel = this.config.submitButtonSelector;
        if (!sel && this.form) return this.form.querySelector('[type="submit"]');
        if (sel instanceof HTMLElement) return sel;
        if (typeof sel === 'string') return document.querySelector(sel);
        return null;
    }

    _setupEventListeners() {
        this.submitButton.addEventListener('click', e => {
            e.preventDefault();
            this._clearErrors();
            if (this.isButtonOnly) this._handleSubmit();
            else this.form.dispatchEvent(new Event('submit', { cancelable: true }));
        });

        if (this.form && !this.isButtonOnly) {
            this.form.addEventListener('submit', e => {
                e.preventDefault();
                this._handleSubmit();
            });
        }
    }

    async _handleSubmit() {
        let formData = new FormData(this.isButtonOnly ? undefined : this.form);

        if (this.isButtonOnly && this.submitButton.dataset.data) {
            try {
                const buttonData = JSON.parse(this.submitButton.dataset.data);
                Object.entries(buttonData).forEach(([k, v]) => formData.append(k, v));
            } catch (e) {
                console.warn('Invalid JSON in button data attribute:', e);
            }
        }

        const method = this.config.httpMethod.toUpperCase();
        if (method !== 'POST' && !formData.has('_method')) formData.append('_method', method);

        const action = typeof this.config.getUrl === 'function'
            ? this.config.getUrl(this.submitButton)
            : (this.config.action || this.form?.action || window.location.href);

        try {
            if (typeof this.config.beforeSubmit === 'function') {
                const result = await this.config.beforeSubmit(formData, this.form, this.submitButton);
                if (result instanceof FormData) formData = result;
                else if (result === false) return;
            }
            this._emit('beforeSubmit', { formData, form: this.form, button: this.submitButton });
        } catch (error) {
            console.error('Error in beforeSubmit hook:', error);
            return;
        }

        this._setLoading(true);
        let response, success = false;

        try {
            if (this.config.method === 'axios' && window.axios) {
                const resp = await axios({ url: action, method: method.toLowerCase(), data: formData });
                response = resp.data ?? resp;
            } else if (this.config.method === 'xhr') {
                response = await this._sendWithXHR(action, formData, method);
            } else {
                response = await this._sendWithFetch(action, formData, method);
            }

            success = true;
            await this._handleSuccess(response);
            if (!this.config.disableOnSuccess) this._setLoading(false);
        } catch (error) {
            response = error;
            await this._handleError(error);
            this._setLoading(false);
        }

        try {
            if (typeof this.config.afterSubmit === 'function') {
                await this.config.afterSubmit(response, success, this.form, this.submitButton);
            }
            this._emit('afterSubmit', { response, success, form: this.form, button: this.submitButton });
        } catch (error) {
            console.error('Error in afterSubmit hook:', error);
        }
    }

    async _sendWithFetch(url, fd, method) {
        const res = await fetch(url, { method, body: fd, headers: { 'Accept': 'application/json' } });
        const data = await res.json().catch(() => ({}));
        if (!res.ok) throw { response: { data, status: res.status } };
        return data;
    }

    _sendWithXHR(url, fd, method) {
        return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            xhr.open(method, url);
            xhr.setRequestHeader('Accept', 'application/json');
            xhr.onload = () => {
                try {
                    const json = JSON.parse(xhr.responseText);
                    (xhr.status >= 200 && xhr.status < 300)
                        ? resolve(json)
                        : reject({ response: { data: json, status: xhr.status } });
                } catch (e) {
                    reject({ message: 'Invalid JSON', error: e });
                }
            };
            xhr.onerror = () => reject(new Error('Network error'));
            xhr.send(fd);
        });
    }

    _setLoading(loading) {
        if (!this.submitButton) return;
        const btnText = this.submitButton.querySelector('.button-text');
        this.submitButton.disabled = loading;
        const loadingText = this.submitButton.dataset.loadingText || 'Loading…';

        if (loading) {
            if (btnText) btnText.textContent = loadingText;
            else this.submitButton.textContent = loadingText;
        } else {
            const defaultText = btnText?.dataset.default || this.originalButtonText;
            if (btnText) btnText.textContent = defaultText;
            else this.submitButton.textContent = this.originalButtonText;
        }
    }

    async _handleSuccess(res) {
        const msg = res?.message || res?.data?.message || this.config.successMessage;
        const redirect = res?.redirect || res?.data?.redirect || this.config.redirectUrl;
        this._showMessage(msg, 'success');
        this._emit('success', res);
        if (redirect) setTimeout(() => window.location.href = redirect, 1000);
    }

    async _handleError(err) {
        console.error(err);
        const d = err.response?.data || err;
        const msg = d?.message || this.config.errorMessage;

        if (d?.errors && this.form) {
            Object.entries(d.errors).forEach(([name, messages]) => {
                const inputName = name.includes('.') ? name.replace('.', '[') + ']' : name;
                const el = this.form.querySelector(`.form-error[data-input="${inputName}"]`);
                if (el) {
                    el.textContent = Array.isArray(messages) ? messages.join(' ') : messages;
                    el.classList.remove('hidden');
                }
            });
        }

        this._showMessage(msg, 'error');
        this._emit('error', err);
    }

    _clearErrors() {
        if (!this.form) return;
        this.form.querySelectorAll('.form-error[data-input]').forEach(span => {
            span.classList.add('hidden');
            span.textContent = '';
        });
    }

    _showMessage(msg, type) {
        if (this.config.useToast && window.Toast) {
            new Toast({ message: msg, type, duration: 4000, position: 'top-center-full' });
        } else {
            alert(msg);
        }
    }

    /** PUBLIC API **/

    reset() {
        if (!this.form) {
            console.warn('FormSubmit: Cannot reset without form.');
            return;
        }
        this.form.reset();
        this._clearErrors();
        this._setLoading(false);
    }

    submit() {
        this._handleSubmit();
    }

    getFormData() {
        if (!this.form) {
            console.warn('FormSubmit: No form available.');
            return new FormData();
        }
        return new FormData(this.form);
    }

    setFieldValue(name, value) {
        const el = this.form?.querySelector(`[name="${name}"]`);
        if (el) el.value = value;
    }

    getFieldValue(name) {
        const el = this.form?.querySelector(`[name="${name}"]`);
        return el ? el.value : null;
    }
}

// ImageInput.js
// Usage: new ImageInput('#myFileInput') OR new ImageInput(document.querySelector('input[type="file"]'));

/**
 * ImageInput - auto updates preview image on file select, restores on ESC/cancel.
 * @param {string|HTMLElement} selector - file input selector or element
 */
class ImageInput {
    /**
     * @param {string|HTMLElement|NodeList} input - Selector, Element, or NodeList of inputs
     */
    constructor(input) {
        // Allow single or multiple
        if (typeof input === "string") {
            this.inputs = document.querySelectorAll(input);
        } else if (input instanceof HTMLElement) {
            this.inputs = [input];
        } else if (input instanceof NodeList) {
            this.inputs = Array.from(input);
        } else {
            throw new Error('ImageInput: Invalid input selector/element.');
        }
        this.inputs.forEach(fileInput => this.init(fileInput));

    }

    init(fileInput) {
        const imgSel = fileInput.closest('.form-group')?.querySelector('[data-img-preview]') ||
            (fileInput.dataset.imgPreview && document.querySelector(fileInput.dataset.imgPreview));
        if (!imgSel) return;

        const img = imgSel;
        const originalSrc = img.getAttribute('data-src') || img.src;

        // File change: show preview
        fileInput.addEventListener('change', (e) => {
            if (fileInput.files && fileInput.files[0]) {
                const reader = new FileReader();
                reader.onload = ev => img.src = ev.target.result;
                reader.readAsDataURL(fileInput.files[0]);
            }
        });

        // Cancel with ESC: restore image & clear input
        fileInput.addEventListener('keydown', (e) => {
            if (e.key === 'Escape') {
                this.reset(fileInput, img, originalSrc);
            }
        });

        // Optional: double-click preview image resets it
        img.addEventListener('dblclick', () => this.reset(fileInput, img, originalSrc));
    }

    reset(fileInput, img, originalSrc) {
        img.src = originalSrc;
        fileInput.value = '';
    }
}

/**
 * LanguageSwitch
 * Usage:
 *   new LanguageSwitch({
 *     root: document, // Or any parent node
 *     localeDropdownSelector: '.dropdown',
 *     buttonSelector: '.btn-locale',
 *     defaultLocale: 'en'
 *   });
 */
class LanguageSwitch {
    /**
     * @param {Object} params
     * @param {HTMLElement|Document} [params.root]
     * @param {string} params.localeDropdownSelector
     * @param {string} params.buttonSelector
     * @param {string} [params.defaultLocale]
     */
    constructor({
                    root = document,
                    localeDropdownSelector = '.dropdown',
                    buttonSelector = '.btn-locale',
                    defaultLocale = null
                } = {}) {
        this.root = root;
        this.dropdown = this.root.querySelector(localeDropdownSelector);
        this.buttonSelector = buttonSelector;

        // Use summary's data-default-locale or fallback
        this.defaultLocale =
            defaultLocale ||
            (this.dropdown?.querySelector('summary[data-default-locale]')
                ?.dataset.defaultLocale ?? 'en');
        this.currentLocale = this.defaultLocale;

        this.init();
    }

    init() {
        // Click listeners for dropdown
        this.dropdown?.querySelectorAll(this.buttonSelector).forEach(btn => {
            btn.addEventListener('click', e => {
                e.preventDefault();
                const newLocale = btn.dataset.locale;
                if (!newLocale) return;
                this.switchLocale(newLocale);
            });
        });
        this.switchLocale(this.currentLocale);
    }

    switchLocale(locale) {
        this.currentLocale = locale;
        // Optionally update dropdown summary
        const summary = this.dropdown?.querySelector('summary');
        if (summary) {
            const clickedBtn = this.dropdown.querySelector(
                `${this.buttonSelector}[data-locale="${locale}"]`
            );
            if (clickedBtn) {
                summary.innerHTML = clickedBtn.innerHTML;
                summary.setAttribute('data-default-locale', locale);
                this.dropdown.removeAttribute('open');
            }
        }

        // 1. Toggle input labels (by data-locale)
        document.querySelectorAll('[data-locale-form]').forEach(el => {
            el.classList.toggle('hidden', el.dataset.localeForm !== locale);
            el.style.display = el.classList.contains('hidden') ? 'none' : '';
        });
    }
}

// SelectDropdown.js
class SelectDropdown {
    /**
     * @param {string|HTMLElement} containerSelector  #id or DOM node of <details>
     * @param {Object} config
     * @param {Function} [config.onSelect]  callback({label, value}, element)
     * @param {boolean} [config.closeOnSelect=true]
     */
    constructor(containerSelector, config = {}) {
        this.config = Object.assign({ closeOnSelect: true }, config);
        this.root = typeof containerSelector === 'string' ?
            document.querySelector(containerSelector) : containerSelector;
         //this.root.id remove _dropdown
        const inputId =  this.root.id.replace(/_dropdown$/, '');
        this.summary = this.root.querySelector('summary');
        this.optionsBox = this.root.querySelector('.options');
        this.searchInput = this.root.querySelector('.search-input');
        this.hiddenInput = document.querySelector(`input#${inputId}`);
        this.options = Array.from(this.optionsBox.querySelectorAll('.option'));
        this.selectedIndex = -1; // None selected at start

        // After collecting this.options in the constructor, add:
        const selectedValue = this.hiddenInput?.value;
        if (selectedValue) {
            const initialOpt = this.options.find(o => o.dataset.value == selectedValue);
            if (initialOpt) {
                initialOpt.setAttribute('data-selected', 'true');
                initialOpt.classList.add('bg-base-300');
                this.summary.textContent = initialOpt.dataset.label || initialOpt.textContent.trim();
            }
        }
        this.resetBtn = this.root.closest('.form-control').querySelector('.select-reset');
        // After setting initial selection
        if (this.resetBtn) {
            if (selectedValue) {
                this.resetBtn.classList.remove('hidden');
            } else {
                this.resetBtn.classList.add('hidden');
            }
        }


        if (this.resetBtn) {
            this.resetBtn.addEventListener('click', (e) => {
                e.preventDefault();
                this._reset();
            });
        }


        this._bindEvents();
    }

    _bindEvents() {
        // Open/close with summary click
        this.summary.addEventListener('click', e => {
            // toggle handled by <details>
            setTimeout(() => this._focusSearch(), 120); // delay to open dropdown
        });

        // Option click
        this.optionsBox.addEventListener('click', e => {
            const opt = e.target.closest('.option');
            if (!opt) return;
            this._select(opt);
        });

        // Highlight on hover
        this.options.forEach((opt, idx) => {
            opt.addEventListener('mouseenter', () => this._highlight(idx));
        });

        // Keyboard navigation and selection
        this.root.addEventListener('keydown', e => {
            const open = this.root.hasAttribute('open');
            if (!open) return;
            if (e.key === "ArrowDown") {
                this._moveHighlight(1);
                e.preventDefault();
            } else if (e.key === "ArrowUp") {
                this._moveHighlight(-1);
                e.preventDefault();
            } else if (e.key === "Enter") {
                if (this.selectedIndex >= 0) {
                    this._select(this.options[this.selectedIndex]);
                }
                e.preventDefault();
            } else if (e.key === "Escape") {
                this.root.removeAttribute('open');
                this.selectedIndex = -1;
                e.preventDefault();
            }
        });

        // Filter options on search
        this.searchInput.addEventListener('input', () => this._filterOptions());

        // On blur: close dropdown if clicked outside
        document.addEventListener('mousedown', (e) => {
            if (!this.root.contains(e.target)) {
                this.root.removeAttribute('open');
            }
        });
    }

    _focusSearch() {
        if (this.searchInput) this.searchInput.focus();
    }

    _highlight(idx) {
        this.options.forEach((o, i) => o.classList.toggle('bg-base-200', i === idx));
        this.selectedIndex = idx;
    }

    _moveHighlight(step) {
        let visibleOpts = this.options.filter(o => !o.classList.contains('hidden'));
        if (!visibleOpts.length) return;
        let idx = visibleOpts.findIndex(o => o.classList.contains('bg-base-200'));
        idx = idx === -1 ? 0 : idx + step;
        if (idx < 0) idx = visibleOpts.length - 1;
        if (idx >= visibleOpts.length) idx = 0;
        visibleOpts.forEach(o => o.classList.remove('bg-base-200'));
        visibleOpts[idx].classList.add('bg-base-200');
        this.selectedIndex = this.options.indexOf(visibleOpts[idx]);
        // Scroll into view if necessary
        visibleOpts[idx].scrollIntoView({ block: 'nearest' });
    }

    _filterOptions() {
        const term = this.searchInput.value.trim().toLowerCase();
        this.options.forEach(opt => {
            const label = opt.dataset.label.toLowerCase();
            opt.classList.toggle('hidden', !label.includes(term));
        });
        // Reset highlight
        this.selectedIndex = -1;
    }

    _select(opt) {
        const value = opt.dataset.value;
        const label = opt.dataset.label || opt.textContent.trim();
        this.summary.textContent = label;
        if (this.hiddenInput) this.hiddenInput.value = value;
        this.options.forEach(o => {
            o.setAttribute('data-selected', '');
            o.classList.remove('bg-base-300');
        });
        opt.setAttribute('data-selected', 'true');
        opt.classList.add('bg-base-300');
        if (this.config.closeOnSelect) this.root.removeAttribute('open');
        if (typeof this.config.onSelect === 'function') {
            this.config.onSelect({ label, value }, opt);
        }
        // Show reset button if available
        if (this.resetBtn) this.resetBtn.classList.remove('hidden');
    }

    _reset() {
        if (this.hiddenInput) this.hiddenInput.value = '';
        this.summary.textContent =  this.summary.dataset.label || this.summary.textContent.trim();
        this.options.forEach(o => {
            o.setAttribute('data-selected', '');
            o.classList.remove('bg-base-300');
        });
        if (this.searchInput) this.searchInput.value = '';
        this.options.forEach(opt => opt.classList.remove('hidden'));
        if (typeof this.config.onSelect === 'function') {
            this.config.onSelect({ label: null, value: null }, null);
        }
        // Hide reset button if available
        if (this.resetBtn) this.resetBtn.classList.add('hidden');
    }

    selectValue(value) {
        // Find the option with the given value
        const opt = this.options.find(o => o.dataset.value == value);
        if (opt) {
            this._select(opt);
        }
    }


}

// SelectMultipleDropdown.js
class SelectMultipleDropdown {
    /**
     * @param {string|HTMLElement} containerSelector  #id or DOM node of <details>
     * @param {Object} config
     * @param {Function} [config.onSelect]  callback({selections}, element)
     * @param {Function} [config.onRemove]  callback({removedValue, selections}, element)
     * @param {number} [config.maxSelections]  maximum number of selections allowed
     * @param {boolean} [config.closeOnSelect=false]
     */
    constructor(containerSelector, config = {}) {
        this.config = Object.assign({
            closeOnSelect: false,
            maxSelections: null
        }, config);

        this.root = typeof containerSelector === 'string' ?
            document.querySelector(containerSelector) : containerSelector;

        this.root.id.replace(/_dropdown$/, '');
        this.summary = this.root.querySelector('summary');
        this.optionsBox = this.root.querySelector('.options');
        this.searchInput = this.root.querySelector('.search-input');
        this.hiddenInputsContainer = this.root.closest('.form-control').querySelector('.hidden-inputs');
        this.selectedDisplay = this.root.querySelector('.selected-display');
        this.placeholderText = this.root.querySelector('.placeholder-text');
        this.selectionCountSpan = this.root.querySelector('.selection-count');
        this.options = Array.from(this.optionsBox.querySelectorAll('.option'));
        this.checkboxes = Array.from(this.optionsBox.querySelectorAll('.option-checkbox'));
        this.resetBtn = this.root.closest('.form-control').querySelector('.select-reset');

        this.selections = new Set();
        this.disabledOptions = new Set(); // Track disabled options

        // Initialize with existing selections
        this._initializeSelections();
        this._updateDisplay();
        this._updateResetButton();

        this._bindEvents();
    }

    _initializeSelections() {
        this.checkboxes.forEach(checkbox => {
            if (checkbox.checked) {
                this.selections.add(checkbox.value);
            }
        });
    }

    _bindEvents() {
        // Open/close with summary click
        this.summary.addEventListener('click', e => {
            // Prevent checkbox clicks from bubbling up
            if (e.target.closest('.remove-selection')) {
                e.preventDefault();
                e.stopPropagation();
                return;
            }
            setTimeout(() => this._focusSearch(), 120);
        });

        // Checkbox change
        this.optionsBox.addEventListener('change', e => {
            if (e.target.classList.contains('option-checkbox')) {
                this._toggleSelection(e.target);
            }
        });

        // Remove selection from badge
        this.selectedDisplay.addEventListener('click', e => {
            const removeBtn = e.target.closest('.remove-selection');
            if (removeBtn) {
                e.preventDefault();
                e.stopPropagation();
                const value = removeBtn.dataset.value;
                this._removeSelection(value);
            }
        });

        // Reset button
        if (this.resetBtn) {
            this.resetBtn.addEventListener('click', (e) => {
                e.preventDefault();
                this._reset();
            });
        }

        // Filter options on search
        if (this.searchInput) {
            this.searchInput.addEventListener('input', () => this._filterOptions());
        }

        // Keyboard navigation
        this.root.addEventListener('keydown', e => {
            const open = this.root.hasAttribute('open');
            if (!open) return;

            if (e.key === "Escape") {
                this.root.removeAttribute('open');
                e.preventDefault();
            }
        });

        // Close dropdown when clicking outside
        document.addEventListener('mousedown', (e) => {
            if (!this.root.contains(e.target)) {
                this.root.removeAttribute('open');
            }
        });
    }

    _focusSearch() {
        if (this.searchInput) this.searchInput.focus();
    }

    _toggleSelection(checkbox) {
        const value = checkbox.value;
        const isChecked = checkbox.checked;

        // Check if option is disabled
        if (this.disabledOptions.has(value)) {
            checkbox.checked = false;
            return;
        }

        if (isChecked) {
            // Check max selections limit
            if (this.config.maxSelections && this.selections.size >= this.config.maxSelections) {
                checkbox.checked = false;
                this._showMaxSelectionWarning();
                return;
            }
            this._addSelection(value);
        } else {
            this._removeSelection(value);
        }
    }

    _addSelection(value) {
        this.selections.add(value);
        const checkbox = this.checkboxes.find(cb => cb.value === value);
        const option = checkbox?.closest('.option');

        if (checkbox) checkbox.checked = true;
        if (option) option.classList.add('bg-base-300');

        this._updateDisplay();
        this._updateHiddenInputs();
        this._updateResetButton();
        this._updateSelectionCount();

        if (typeof this.config.onSelect === 'function') {
            this.config.onSelect({
                selections: Array.from(this.selections),
                added: value
            }, option);
        }
    }

    _removeSelection(value) {
        this.selections.delete(value);
        const checkbox = this.checkboxes.find(cb => cb.value === value);
        const option = checkbox?.closest('.option');

        if (checkbox) checkbox.checked = false;
        if (option) option.classList.remove('bg-base-300');

        this._updateDisplay();
        this._updateHiddenInputs();
        this._updateResetButton();
        this._updateSelectionCount();

        if (typeof this.config.onRemove === 'function') {
            this.config.onRemove({
                selections: Array.from(this.selections),
                removed: value
            }, option);
        }
    }

    _updateDisplay() {
        if (this.selections.size === 0) {
            this.selectedDisplay.innerHTML = `<span class="placeholder-text text-gray-500">${this.summary.dataset.label || 'Select items'}</span>`;
        } else {
            const badges = Array.from(this.selections).map(value => {
                const option = this.options.find(opt => opt.dataset.value === value);
                const label = option?.dataset.label || value;
                return `<span class="badge badge-primary badge-sm">
                    ${label}
                    <button type="button" class="ml-1 remove-selection" data-value="${value}">
                        <svg class="size-3" viewBox="0 0 24 24"><path fill="currentColor" d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"/></svg>
                    </button>
                </span>`;
            }).join('');

            this.selectedDisplay.innerHTML = `<div class="flex flex-wrap gap-1">${badges}</div>`;
        }
    }

    _updateHiddenInputs() {
        this.hiddenInputsContainer.innerHTML = '';
        this.selections.forEach(value => {
            const input = document.createElement('input');
            input.type = 'hidden';
            input.name = `${this.root.id.replace(/_dropdown$/, '')}[]`;
            input.value = value;
            this.hiddenInputsContainer.appendChild(input);
        });
    }

    _updateResetButton() {
        if (this.resetBtn) {
            this.resetBtn.classList.toggle('hidden', this.selections.size === 0);
        }
    }

    _updateSelectionCount() {
        if (this.selectionCountSpan) {
            this.selectionCountSpan.textContent = this.selections.size;
        }
    }

    _filterOptions() {
        const term = this.searchInput.value.trim().toLowerCase();
        this.options.forEach(opt => {
            const label = opt.dataset.label.toLowerCase();
            const isDisabled = this.disabledOptions.has(opt.dataset.value);
            // Hide if doesn't match search OR if disabled
            opt.classList.toggle('hidden', !label.includes(term) || isDisabled);
        });
    }

    _updateOptionVisualState(value) {
        const checkbox = this.checkboxes.find(cb => cb.value === value);
        const option = checkbox?.closest('.option');

        if (checkbox && option) {
            const isDisabled = this.disabledOptions.has(value);

            // Update checkbox state
            checkbox.disabled = isDisabled;

            // Update visual styling
            if (isDisabled) {
                option.classList.add('opacity-50', 'cursor-not-allowed');
                option.classList.remove('hover:bg-base-200', 'cursor-pointer');
                // If currently selected and being disabled, remove selection
                if (this.selections.has(value)) {
                    this._removeSelection(value);
                }
            } else {
                option.classList.remove('opacity-50', 'cursor-not-allowed');
                option.classList.add('hover:bg-base-200', 'cursor-pointer');
            }
        }
    }

    _reset() {
        this.selections.clear();
        this.checkboxes.forEach(checkbox => {
            checkbox.checked = false;
            checkbox.closest('.option').classList.remove('bg-base-300');
        });

        this._updateDisplay();
        this._updateHiddenInputs();
        this._updateResetButton();
        this._updateSelectionCount();

        if (this.searchInput) this.searchInput.value = '';
        this.options.forEach(opt => opt.classList.remove('hidden'));

        if (typeof this.config.onSelect === 'function') {
            this.config.onSelect({
                selections: [],
                cleared: true
            }, null);
        }
    }

    _showMaxSelectionWarning() {
        // You can customize this notification
        console.warn(`Maximum ${this.config.maxSelections} selections allowed`);
        // Or show a toast notification if available
        // new Toast({ message: `Maximum ${this.config.maxSelections} selections allowed`, type: 'warning' });
    }

    // Public methods
    selectValues(values) {
        values.forEach(value => {
            if (!this.selections.has(value) && !this.disabledOptions.has(value)) {
                this._addSelection(value);
            }
        });
    }

    getSelections() {
        return Array.from(this.selections);
    }

    clearSelections() {
        this._reset();
    }

    /**
     * Disable specific option(s)
     * @param {string|string[]} values - Single value or array of values to disable
     */
    disableOption(values) {
        const valueArray = Array.isArray(values) ? values : [values];

        valueArray.forEach(value => {
            this.disabledOptions.add(value);
            this._updateOptionVisualState(value);
        });

        // Re-apply filter to hide disabled options if search is active
        if (this.searchInput && this.searchInput.value.trim()) {
            this._filterOptions();
        }
    }

    /**
     * Enable specific option(s)
     * @param {string|string[]} values - Single value or array of values to enable
     */
    enableOption(values) {
        const valueArray = Array.isArray(values) ? values : [values];

        valueArray.forEach(value => {
            this.disabledOptions.delete(value);
            this._updateOptionVisualState(value);
        });

        // Re-apply filter to show enabled options if search is active
        if (this.searchInput && this.searchInput.value.trim()) {
            this._filterOptions();
        }
    }

    /**
     * Check if option is disabled
     * @param {string} value - Value to check
     * @returns {boolean}
     */
    isOptionDisabled(value) {
        return this.disabledOptions.has(value);
    }

    /**
     * Get all disabled options
     * @returns {string[]}
     */
    getDisabledOptions() {
        return Array.from(this.disabledOptions);
    }

    /**
     * Clear all disabled options
     */
    clearDisabledOptions() {
        const allDisabled = Array.from(this.disabledOptions);
        this.disabledOptions.clear();

        allDisabled.forEach(value => {
            this._updateOptionVisualState(value);
        });

        // Re-apply filter if search is active
        if (this.searchInput && this.searchInput.value.trim()) {
            this._filterOptions();
        }
    }
}

let Toast$1 = class Toast {
    constructor({ message = '', title = '', type = 'info', duration = 4000, position = 'top-center', icon = null } = {}) {
        Object.assign(this, { message, title, type, duration, position, icon });
        this._show();
    }

    _show() {
        const toast = document.createElement('div');
        toast.className = [
            'fixed', 'z-50', 'transform', '-translate-y-full', 'pointer-events-none',
            ...this._posClass().split(' '),
            'w-full', 'mx-auto',
        ].filter(Boolean).join(' ');

        const bgMap = {
            success: 'bg-green-600',
            error: 'bg-red-600',
            warning: 'bg-yellow-500',
            info: 'bg-blue-600',
        };
        const bg = bgMap[this.type];

        toast.innerHTML = `
<div class="flex flex-col items-center justify-center w-full ${bg} bg-opacity-80 ">
      <div class="text-white p-4 flex items-center space-x-2 pointer-events-auto justify-center">
        ${this.icon ? `<img src="${this.icon}" class="size-[30px] flex-shrink-0"/>` : ''}
        <div class="flex-1 text-left">
          ${this.title ? `<h4 class="font-semibold">${this.title}</h4>` : ''}
          <p class="text-sm">${this.message}</p>
        </div>
      </div>
      <div class="${bg} h-1 overflow-hidden w-full">
        <div class="toast-bar h-full bg-white w-full"></div>
      </div>
      </div>
    `;

        document.body.appendChild(toast);

        // Açılış animasyonu
        requestAnimationFrame(() => {
            toast.classList.add('animate-toast-in');
            toast.classList.remove('-translate-y-full');
        });

        // Bar animasyonu
        const bar = toast.querySelector('.toast-bar');
        const startTime = performance.now();

        const tick = now => {
            const elapsed = now - startTime;
            const pct = Math.max(0, 1 - elapsed / this.duration);
            bar.style.width = pct * 100 + '%';
            if (elapsed < this.duration) requestAnimationFrame(tick);
            else {
                toast.classList.replace('animate-toast-in', 'animate-toast-out');
                toast.addEventListener('animationend', () => toast.remove());
            }
        };
        requestAnimationFrame(tick);
    }

    _posClass() {
        const map = {
            'top-left': 'top-4 left-4',
            'top-center': 'top-4 left-1/2 transform -translate-x-1/2',
            'top-center-full': 'top-0 left-1/2 transform -translate-x-1/2',
            'bottom-center-full': 'bottom-0 left-1/2 transform -translate-x-1/2',
            'top-right': 'top-4 right-4',
            'bottom-left': 'bottom-4 left-4',
            'bottom-center': 'bottom-4 left-1/2 transform -translate-x-1/2',
            'bottom-right': 'bottom-4 right-4',
        };
        return map[this.position] || map['top-center'];
    }
};

// Export all 11 plugins

// Default export object with all 11 plugins
var index = {
    AjaxDivBox,
    AjaxTable,
    CodeInput,
    DeleteContent,
    EditContent,
    FormSubmit,
    ImageInput,
    LanguageSwitch,
    SelectDropdown,
    SelectMultipleDropdown,
    Toast: Toast$1
};

const version = '1.0.0';

exports.AjaxDivBox = AjaxDivBox;
exports.AjaxTable = AjaxTable;
exports.CodeInput = CodeInput;
exports.DeleteContent = DeleteContent;
exports.EditContent = EditContent;
exports.FormSubmit = FormSubmit;
exports.ImageInput = ImageInput;
exports.LanguageSwitch = LanguageSwitch;
exports.SelectDropdown = SelectDropdown;
exports.SelectMultipleDropdown = SelectMultipleDropdown;
exports.Toast = Toast$1;
exports.default = index;
exports.version = version;
//# sourceMappingURL=index.js.map