@@ -434,7 +536,9 @@ export default function mount(el, ctx) {
const emails = [...el.querySelectorAll("[data-emailrow]")].map((r) => ({ value: r.querySelector("[data-val]").value.trim(), type: r.querySelector("[data-type]").value })).filter((e) => e.value);
const phones = [...el.querySelectorAll("[data-phonerow]")].map((r) => ({ value: r.querySelector("[data-val]").value.trim(), type: r.querySelector("[data-type]").value })).filter((p) => p.value);
const given = v("[data-f=given]"), family = v("[data-f=family]"), prefix = v("[data-f=prefix]");
- const display = [prefix, given, family].filter(Boolean).join(" ") || (emails[0] ? emails[0].value : "");
+ const org = v("[data-f=org]");
+ // FN = personal name, else company. Never the email (display falls back to it).
+ const display = [prefix, given, family].filter(Boolean).join(" ") || org || "";
const month = v("[data-f=bmonth]"), day = v("[data-f=bday]"), year = v("[data-f=byear]");
let birthday = null;
if (month && day && year) birthday = `${year.padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
@@ -445,7 +549,7 @@ export default function mount(el, ctx) {
display_name: display,
name_parts: { prefix, given, family, middle: "", suffix: "" },
nickname: v("[data-f=nickname]"),
- title: v("[data-f=title]"), department: v("[data-f=department]"), org: v("[data-f=org]"),
+ title: v("[data-f=title]"), department: v("[data-f=department]"), org,
birthday,
emails, default_email: emails[0] ? emails[0].value : "",
phones,
@@ -472,18 +576,28 @@ export default function mount(el, ctx) {
// ---- render + wire -------------------------------------------------------
function render() {
- el.innerHTML = `
+ // If the search box currently has focus, keep it (and the caret) across the
+ // re-render — covers the debounced reload and the async mail-load re-render.
+ const prev = el.querySelector(".ct-search");
+ const keepFocus = prev && document.activeElement === prev;
+ const caret = keepFocus ? prev.selectionStart : null;
+ el.innerHTML = `
${state.leftCollapsed ? railPanel() : `
${leftPanel()}
`}
${listPanel()}
${detailBar()}${detailPanel()}
+ ${state.confirmDelete ? confirmModal() : ""}
`;
wire();
+ if (keepFocus) {
+ const si = el.querySelector(".ct-search");
+ if (si) { si.focus(); const p = caret != null ? caret : si.value.length; si.setSelectionRange(p, p); }
+ }
}
function wire() {
const on = (sel, ev, fn) => el.querySelectorAll(sel).forEach((n) => n.addEventListener(ev, fn));
const act = (name, fn) => on(`[data-act="${name}"]`, "click", fn);
- const setFilter = (f) => { state.filter = f; state.group = ""; state.menuOpen = false; reloadList(false); };
+ const setFilter = (f) => { state.filter = f; state.group = ""; state.menuOpen = false; state.checked.clear(); reloadList(false); };
on("[data-app]", "click", (e) => { const a = state.apps[Number(e.currentTarget.getAttribute("data-app"))]; state.menuOpen = false; if (a.to) ctx.navigate(a.to); else render(); });
act("menu", () => { state.menuOpen = !state.menuOpen; render(); });
@@ -499,10 +613,31 @@ export default function mount(el, ctx) {
on("[data-group]", "click", (e) => {
state.group = e.currentTarget.getAttribute("data-group");
state.groupLabel = e.currentTarget.getAttribute("data-groupname");
- state.filter = "group"; state.menuOpen = false; reloadList(false);
+ state.filter = "group"; state.menuOpen = false; state.checked.clear(); reloadList(false);
});
on("[data-contact]", "click", (e) => selectContact(e.currentTarget.getAttribute("data-contact")));
+ // Selection checkboxes (revealed on avatar-hover / in select mode) — Mail-style.
+ on("[data-check]", "click", (e) => {
+ e.stopPropagation();
+ const id = e.currentTarget.getAttribute("data-check");
+ if (state.checked.has(id)) state.checked.delete(id); else state.checked.add(id);
+ render();
+ });
+ act("select-all", () => {
+ const ids = state.contacts.map((c) => c.id);
+ const all = ids.length > 0 && ids.every((id) => state.checked.has(id));
+ if (all) state.checked.clear(); else ids.forEach((id) => state.checked.add(id));
+ render();
+ });
+ act("clear-sel", () => { state.checked.clear(); render(); });
+ on(".ct-search", "input", (e) => {
+ state.q = e.currentTarget.value;
+ if (searchTimer) clearTimeout(searchTimer);
+ searchTimer = setTimeout(runSearch, 250);
+ });
+ on(".ct-search", "keydown", (e) => { if (e.key === "Escape") { state.q = ""; runSearch(); } });
+ act("search-clear", () => { state.q = ""; runSearch(); });
act("sort", () => { state.sortAsc = !state.sortAsc; render(); });
act("new", () => { state.mode = "new"; state.selectedId = null; state.detail = null; render(); });
@@ -514,7 +649,10 @@ export default function mount(el, ctx) {
act("call", () => { const p = state.detail.phones && state.detail.phones[0]; if (p) window.open(`tel:${p.value}`); });
act("message", () => { if (state.detail.default_email) window.location.href = `mailto:${state.detail.default_email}`; });
act("compose", () => { if (state.detail.default_email) window.location.href = `mailto:${state.detail.default_email}`; });
- act("invite", () => ctx.navigate("/p/calendar"));
+ act("invite", () => {
+ const email = state.detail && state.detail.default_email;
+ ctx.navigate(email ? `/p/calendar?guest=${encodeURIComponent(email)}` : "/p/calendar");
+ });
act("find-mail", () => ctx.navigate(`/mail/search/${encodeURIComponent('"' + state.detail.default_email + '"')}`));
// menus
@@ -529,8 +667,10 @@ export default function mount(el, ctx) {
await api.pluginFetch(`/contacts/${state.selectedId}`, { method: "PUT", body: JSON.stringify(body) });
await loadGroups(); await loadDetail(state.selectedId); render();
});
- act("delete", async () => {
- state.menu = null;
+ act("delete", () => { state.menu = null; state.confirmDelete = true; render(); });
+ act("confirm-cancel", () => { state.confirmDelete = false; render(); });
+ act("confirm-delete", async () => {
+ state.confirmDelete = false;
await api.pluginFetch(`/contacts/${state.selectedId}`, { method: "DELETE" });
state.selectedId = null;
await reloadList(false);