Files

362 lines
14 KiB
JavaScript

/*
* Riquadri personalizzabili per utente.
*
* <button data-layout-btn="pagina">…</button> (facoltativo: altrimenti il pulsante viene creato)
* <div class="row g-3" data-layout="pagina" data-prefs='{…}'>
* <div class="col-xl-6" data-widget="chiave" data-title="Nome leggibile">…</div>
* </div>
*
* 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 =
'<i class="fa-solid fa-table-cells-large"></i> 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 = `<span class="ul-t"><i class="fa-solid fa-grip-vertical"></i>${(w.dataset.title || k).replace(/</g, "&lt;")}</span>
<button type="button" data-ul="up" title="Sposta su">↑</button>
<button type="button" data-ul="down" title="Sposta giù">↓</button>
<button type="button" data-ul="size" title="Larghezza">↔</button>
<button type="button" data-ul="eye" title="Mostra / nascondi"></button>`;
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 = `<span>Trascina i riquadri o usa ↑ ↓</span>
<button type="button" class="rs">Predefinito</button><button type="button" class="ok">✓ Fatto</button>`;
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();
})();