/* * Riquadri personalizzabili per utente. * * (facoltativo: altrimenti il pulsante viene creato) *
*
…
*
* * In modalità "Personalizza": trascina dalla barra (dito o mouse) oppure usa ↑ ↓, * cambia larghezza, nascondi/mostra. "Fatto" salva sul server per l'utente, "Predefinito" ripristina. * Richiede SortableJS (caricato al volo se manca) e jQuery solo se c'è DataTables. */ (function () { "use strict"; const SORTABLE_URL = "https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"; const SIZES = [null, 12, 8, 6, 4]; // null = larghezza originale const SIZE_TXT = { null: "Auto", 12: "Intera", 8: "2/3", 6: "Metà", 4: "1/3", 7: "7/12", 5: "5/12", }; const LS = "ui_layout_"; function css() { if (document.getElementById("ulCss")) return; const s = document.createElement("style"); s.id = "ulCss"; s.textContent = ` .ul-btn{display:inline-flex;align-items:center;gap:6px;border:1px solid #cbd5e1;background:#fff;color:#334155; border-radius:999px;padding:6px 14px;font-size:.85rem;font-weight:600;cursor:pointer;min-height:38px} .ul-btn:hover{background:#f1f5f9} .ul-btn.on{background:#0f766e;border-color:#0f766e;color:#fff} .ul-btnbar{display:flex;justify-content:flex-end;margin-bottom:8px} [data-layout].ul-edit>[data-widget]{position:relative} [data-layout].ul-edit>[data-widget]>*:not(.ul-bar){pointer-events:none;opacity:.55;max-height:260px;overflow:hidden} [data-layout].ul-edit>[data-widget].ul-hidden>*:not(.ul-bar){opacity:.18} [data-layout]>[data-widget].ul-hidden{display:none} [data-layout].ul-edit>[data-widget].ul-hidden{display:block} .ul-bar{display:none} [data-layout].ul-edit .ul-bar{display:flex;align-items:center;gap:6px;flex-wrap:wrap;background:#0f766e;color:#fff; border-radius:12px;padding:6px 8px;margin-bottom:6px;touch-action:none;cursor:grab;user-select:none;-webkit-user-select:none} .ul-bar .ul-t{flex:1 1 auto;font-weight:700;font-size:.9rem;min-width:90px;display:flex;align-items:center;gap:8px} .ul-bar button{border:0;background:rgba(255,255,255,.18);color:#fff;border-radius:9px;min-width:40px;height:40px; padding:0 10px;font-size:.85rem;font-weight:600;cursor:pointer} .ul-bar button:active{background:rgba(255,255,255,.35)} .ul-hidden .ul-bar{background:#64748b} .ul-ghost .ul-bar{background:#f59e0b} .ul-ghost{opacity:.6} .ul-dock{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);z-index:1080;display:flex;gap:8px;align-items:center; background:#0f172a;color:#fff;border-radius:999px;padding:8px 10px 8px 18px;box-shadow:0 10px 30px rgba(0,0,0,.35); font-size:.9rem;max-width:calc(100vw - 24px);flex-wrap:wrap;justify-content:center} .ul-dock button{border:0;border-radius:999px;padding:0 16px;height:42px;font-weight:700;cursor:pointer} .ul-dock .ok{background:#22c55e;color:#052e16} .ul-dock .rs{background:#334155;color:#fff} .ul-toast{position:fixed;right:18px;bottom:18px;z-index:1090;background:#0f172a;color:#fff;border-radius:12px; padding:10px 16px;font-size:.9rem;box-shadow:0 8px 24px rgba(0,0,0,.3)} `; document.head.appendChild(s); } function loadSortable() { if (window.Sortable) return Promise.resolve(); return new Promise((res) => { const s = document.createElement("script"); s.src = SORTABLE_URL; s.onload = res; s.onerror = res; document.head.appendChild(s); }); } function toast(msg) { const t = document.createElement("div"); t.className = "ul-toast"; t.textContent = msg; document.body.appendChild(t); setTimeout(() => t.remove(), 2200); } // grafici e tabelle si ridisegnano sulla nuova larghezza function relayout() { window.dispatchEvent(new Event("resize")); try { if (window.jQuery && jQuery.fn.dataTable) jQuery.fn.dataTable .tables({ visible: true, api: true }) .columns.adjust(); } catch (e) {} } function Layout(box) { this.box = box; this.page = box.dataset.layout; this.widgets = Array.from(box.children).filter( (el) => el.dataset.widget, ); this.defOrder = this.widgets.map((w) => w.dataset.widget); this.widgets.forEach((w) => { w.dataset.ulClass = w.className; }); let p = {}; try { p = JSON.parse(box.dataset.prefs || "{}") || {}; } catch (e) {} if (!p.order || !p.order.length) { // server vuoto: eventuale copia locale try { p = JSON.parse( localStorage.getItem(LS + this.page) || "null", ) || p; } catch (e) {} } this.state = { order: Array.isArray(p.order) ? p.order : [], hidden: Array.isArray(p.hidden) ? p.hidden : [], sizes: p.sizes && typeof p.sizes === "object" ? p.sizes : {}, }; this.editing = false; this.apply(); this.button(); } Layout.prototype.byKey = function (k) { return this.widgets.find((w) => w.dataset.widget === k); }; Layout.prototype.apply = function () { const st = this.state; // ordine salvato; i riquadri nuovi (non presenti nel salvataggio) restano vicino al loro posto originale const known = st.order.filter((k) => this.byKey(k)); const final = known.slice(); this.defOrder.forEach((k, i) => { if (final.includes(k)) return; const prev = this.defOrder .slice(0, i) .reverse() .find((x) => final.includes(x)); final.splice(prev ? final.indexOf(prev) + 1 : 0, 0, k); }); final.forEach((k) => this.box.appendChild(this.byKey(k))); this.widgets.forEach((w) => { const k = w.dataset.widget; this.setSize(w, st.sizes[k] || null); w.classList.toggle("ul-hidden", st.hidden.includes(k)); }); }; Layout.prototype.setSize = function (w, n) { if (!n) { w.className = w.dataset.ulClass; } else { const keep = w.dataset.ulClass .split(/\s+/) .filter( (c) => c && !/^col(-(sm|md|lg|xl|xxl))?(-\d+)?$/.test(c), ); w.className = keep .concat(n === 12 ? ["col-12"] : ["col-12", "col-xl-" + n]) .join(" "); } if ( w.classList.contains("ul-hidden") !== this.state.hidden.includes(w.dataset.widget) ) w.classList.toggle("ul-hidden"); const b = w.querySelector(':scope > .ul-bar [data-ul="size"]'); if (b) b.textContent = "↔ " + SIZE_TXT[n || null]; }; Layout.prototype.read = function () { this.state.order = Array.from(this.box.children) .filter((el) => el.dataset.widget) .map((el) => el.dataset.widget); }; Layout.prototype.button = function () { let b = document.querySelector(`[data-layout-btn="${this.page}"]`); if (!b) { const bar = document.createElement("div"); bar.className = "ul-btnbar"; b = document.createElement("button"); b.type = "button"; bar.appendChild(b); this.box.parentNode.insertBefore(bar, this.box); } b.classList.add("ul-btn"); if (!b.innerHTML.trim()) b.innerHTML = ' Personalizza'; b.title = "Sposta, allarga o nascondi i riquadri di questa pagina"; this.btn = b; b.addEventListener("click", () => this.editing ? this.done() : this.edit(), ); }; Layout.prototype.edit = function () { if (document.querySelector(".ul-dock")) return; // un solo contenitore alla volta this.editing = true; this.btn.classList.add("on"); this.box.classList.add("ul-edit"); this.widgets.forEach((w) => { const k = w.dataset.widget; const bar = document.createElement("div"); bar.className = "ul-bar"; bar.innerHTML = `${(w.dataset.title || k).replace(/ `; w.insertBefore(bar, w.firstChild); this.setSize(w, this.state.sizes[k] || null); this.eye(w); bar.addEventListener("click", (e) => { const a = e.target.closest("[data-ul]"); if (a) this.act(w, a.dataset.ul); }); }); this.dock = document.createElement("div"); this.dock.className = "ul-dock"; this.dock.innerHTML = `Trascina i riquadri o usa ↑ ↓ `; document.body.appendChild(this.dock); this.dock.querySelector(".ok").onclick = () => this.done(); this.dock.querySelector(".rs").onclick = () => this.reset(); loadSortable().then(() => { if (!window.Sortable || !this.editing) return; this.sortable = Sortable.create(this.box, { draggable: "[data-widget]", handle: ".ul-bar", filter: "button", preventOnFilter: false, forceFallback: true, fallbackTolerance: 4, animation: 160, ghostClass: "ul-ghost", scroll: true, bubbleScroll: true, onEnd: () => this.read(), }); }); relayout(); }; Layout.prototype.eye = function (w) { const b = w.querySelector(':scope > .ul-bar [data-ul="eye"]'); if (b) b.textContent = this.state.hidden.includes(w.dataset.widget) ? "🚫 Nascosto" : "👁 Visibile"; }; Layout.prototype.act = function (w, a) { const k = w.dataset.widget; const st = this.state; if ( a === "up" && w.previousElementSibling && w.previousElementSibling.dataset.widget ) { this.box.insertBefore(w, w.previousElementSibling); } else if (a === "down" && w.nextElementSibling) { this.box.insertBefore(w.nextElementSibling, w); } else if (a === "size") { const cur = st.sizes[k] || null; const n = SIZES[(SIZES.indexOf(cur) + 1) % SIZES.length]; if (n) st.sizes[k] = n; else delete st.sizes[k]; this.setSize(w, n); relayout(); } else if (a === "eye") { st.hidden = st.hidden.includes(k) ? st.hidden.filter((x) => x !== k) : st.hidden.concat(k); w.classList.toggle("ul-hidden", st.hidden.includes(k)); this.eye(w); } this.read(); if (a === "up" || a === "down") w.scrollIntoView({ block: "nearest", behavior: "smooth" }); }; Layout.prototype.stop = function () { this.editing = false; this.btn.classList.remove("on"); this.box.classList.remove("ul-edit"); this.box .querySelectorAll(":scope > [data-widget] > .ul-bar") .forEach((b) => b.remove()); if (this.sortable) { this.sortable.destroy(); this.sortable = null; } if (this.dock) { this.dock.remove(); this.dock = null; } setTimeout(relayout, 50); }; Layout.prototype.post = function (data) { const fd = new FormData(); Object.keys(data).forEach((k) => fd.append(k, data[k])); fd.append("page", this.page); return fetch("ui_prefs.php", { method: "POST", body: fd, credentials: "same-origin", }) .then((r) => r.json()) .catch(() => ({ ok: false })); }; Layout.prototype.done = function () { this.read(); const st = this.state; this.stop(); try { localStorage.setItem(LS + this.page, JSON.stringify(st)); } catch (e) {} this.post({ action: "save", layout: JSON.stringify(st) }).then((r) => { toast( r && r.ok ? "Disposizione salvata" : "Salvata solo su questo dispositivo", ); }); }; Layout.prototype.reset = function () { this.state = { order: [], hidden: [], sizes: {} }; this.apply(); this.stop(); try { localStorage.removeItem(LS + this.page); } catch (e) {} this.post({ action: "reset" }).then(() => toast("Disposizione predefinita ripristinata"), ); }; function init() { css(); document.querySelectorAll("[data-layout]").forEach((box) => { if (!box.__ul) box.__ul = new Layout(box); }); } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); else init(); })();