362 lines
14 KiB
JavaScript
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, "<")}</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();
|
|
})();
|