/** * WPSubsEditList — editable ordered list of text items. * * A reusable admin component: renders items in a reorderable list with per-row * remove/move controls, plus an inline input + add button to append new items. * The ordered list is serialized as JSON ([{ key, label }]) into a hidden input * so it submits with the surrounding form. `key` is a slug derived from the label. * * Usage: * PHP: SettingsHelper::render_editlist() / any markup with the classes below * JS: WPSubsEditList.init() — auto-inits all .wpsubs-editlist elements * * Expected structure inside .wpsubs-editlist: * input[type=hidden] — JSON store * .wpsubs-editlist__items > .wpsubs-editlist__item[data-key] * .wpsubs-editlist__label * [data-editlist-up] [data-editlist-down] [data-editlist-remove] * .wpsubs-editlist__empty — shown only when empty * .wpsubs-editlist__input — inline text input * [data-editlist-add] — add/confirm button * * Events fired on the root element (bubbles): * wpsubs:change — { items } after any add/remove/reorder */ (function () { "use strict"; /** * Derive a slug key from a label. * * @param {string} label * @return {string} */ function slugify(label) { return String(label) .toLowerCase() .replace(/[^a-z0-9]+/g, "_") .replace(/^_+|_+$/g, ""); } var SVG_UP = ''; var SVG_DOWN = ''; var SVG_TRASH = ''; /** * @param {HTMLElement} el Root .wpsubs-editlist element. */ function WPSubsEditList(el) { this.el = el; this.hidden = el.querySelector('input[type="hidden"]'); this.list = el.querySelector(".wpsubs-editlist__items"); this.emptyEl = el.querySelector(".wpsubs-editlist__empty"); this.input = el.querySelector(".wpsubs-editlist__input"); // Optional live count badge (e.g. when the list lives inside a modal trigger). this.countEl = el.querySelector(".wpsubs-editlist__count"); this._bind(); this._serialize(); } WPSubsEditList.prototype._rows = function () { return this.list ? this.list.querySelectorAll(".wpsubs-editlist__item") : []; }; /** Serialize the current rows into the hidden input as JSON. */ WPSubsEditList.prototype._serialize = function () { var items = []; this._rows().forEach(function (row) { var labelEl = row.querySelector(".wpsubs-editlist__label"); items.push({ key: row.getAttribute("data-key") || "", label: labelEl ? labelEl.textContent : "", }); }); if (this.hidden) this.hidden.value = JSON.stringify(items); if (this.emptyEl) this.emptyEl.hidden = items.length > 0; if (this.countEl) this.countEl.textContent = String(items.length); this.el.dispatchEvent(new CustomEvent("wpsubs:change", { bubbles: true, detail: { items: items } })); }; /** * Append a new item row. * * @param {string} label */ WPSubsEditList.prototype._addItem = function (label) { var text = String(label).trim(); if (!text || !this.list) return; var row = document.createElement("li"); row.className = "wpsubs-editlist__item"; row.setAttribute("data-key", slugify(text)); var handle = document.createElement("span"); handle.className = "wpsubs-editlist__handle"; handle.setAttribute("aria-hidden", "true"); handle.innerHTML = "⋮⋮"; var lab = document.createElement("span"); lab.className = "wpsubs-editlist__label"; lab.textContent = text; var actions = document.createElement("span"); actions.className = "wpsubs-editlist__actions"; actions.innerHTML = '" + '" + '"; row.appendChild(handle); row.appendChild(lab); row.appendChild(actions); this.list.appendChild(row); this._serialize(); }; /** Add the item currently typed in the inline input, then reset it. */ WPSubsEditList.prototype._commitInput = function () { if (!this.input) return; var val = this.input.value.trim(); if (!val) return; this._addItem(val); this.input.value = ""; this.input.focus(); }; WPSubsEditList.prototype._bind = function () { var self = this; this.el.addEventListener("click", function (e) { if (e.target.closest("[data-editlist-add]")) { self._commitInput(); return; } var row = e.target.closest(".wpsubs-editlist__item"); if (!row) return; if (e.target.closest("[data-editlist-remove]")) { row.parentNode.removeChild(row); self._serialize(); } else if (e.target.closest("[data-editlist-up]")) { if (row.previousElementSibling) { row.parentNode.insertBefore(row, row.previousElementSibling); self._serialize(); } } else if (e.target.closest("[data-editlist-down]")) { if (row.nextElementSibling) { row.parentNode.insertBefore(row.nextElementSibling, row); self._serialize(); } } }); if (this.input) { this.input.addEventListener("keydown", function (e) { if (e.key === "Enter") { e.preventDefault(); self._commitInput(); } }); } // Drag to reorder — only starts from the handle. this._dragRow = null; this.el.addEventListener("mousedown", function (e) { var handle = e.target.closest(".wpsubs-editlist__handle"); if (!handle) return; var row = handle.closest(".wpsubs-editlist__item"); if (row) row.setAttribute("draggable", "true"); }); this.el.addEventListener("dragstart", function (e) { var row = e.target.closest(".wpsubs-editlist__item"); if (!row || row.getAttribute("draggable") !== "true") return; self._dragRow = row; row.classList.add("wpsubs-editlist__item--dragging"); if (e.dataTransfer) { e.dataTransfer.effectAllowed = "move"; try { e.dataTransfer.setData("text/plain", ""); } catch (err) { /* IE guard */ } } }); this.el.addEventListener("dragover", function (e) { if (!self._dragRow || !self.list) return; e.preventDefault(); var over = e.target.closest(".wpsubs-editlist__item"); if (!over || over === self._dragRow) return; var rect = over.getBoundingClientRect(); var after = (e.clientY - rect.top) / rect.height > 0.5; self.list.insertBefore(self._dragRow, after ? over.nextSibling : over); }); this.el.addEventListener("drop", function (e) { if (self._dragRow) e.preventDefault(); }); this.el.addEventListener("dragend", function () { if (!self._dragRow) return; self._dragRow.classList.remove("wpsubs-editlist__item--dragging"); self._dragRow.removeAttribute("draggable"); self._dragRow = null; self._serialize(); }); }; /** * Initialise all un-initialised .wpsubs-editlist elements under root. * * @param {Document|HTMLElement} [root] */ function init(root) { (root || document).querySelectorAll(".wpsubs-editlist:not([data-editlist-init])").forEach(function (el) { el.setAttribute("data-editlist-init", "1"); new WPSubsEditList(el); }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", function () { init(); }); } else { init(); } // Public API window.WPSubsEditList = { init: init }; })();