Add web UI for WebDAV files and CardDAV contacts
Files browser (web/files): - Alphabetical listing grouped folders-then-files - Upload via button or drag-and-drop, including whole folders - Folder creation - Fixed layout for long filenames without spaces (table-fixed + break-all) Contacts (web/contacts): - Full CRUD for CardDAV contacts (create/edit/delete) - VCF import (multi-card files) and export (single/all) - Photo upload with preview, birthday field - TYPE labels (private/business) for phone, email, address - Repeatable multi-input rows for phones/emails/addresses instead of textareas - Sanitizes a known malformed TYPE parameter pattern from some vCard exporters (e.g. Nextcloud Contacts) that otherwise caused phone numbers to be silently dropped on import Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
// Contact form behavior: adding/removing repeatable phone, email, and
|
||||
// address rows, plus a live preview when choosing a new photo.
|
||||
(function initContactForm(): void {
|
||||
document.querySelectorAll<HTMLButtonElement>(".add-row-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const containerId = btn.dataset.addTarget;
|
||||
if (!containerId) {
|
||||
return;
|
||||
}
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
const rows = container.querySelectorAll<HTMLElement>(".form-row");
|
||||
const lastRow = rows[rows.length - 1];
|
||||
if (!lastRow) {
|
||||
return;
|
||||
}
|
||||
// Clone the last row rather than keeping a separate <template>,
|
||||
// so the JS-added row always matches whatever markup the server
|
||||
// rendered (including the TYPE select's options).
|
||||
const newRow = lastRow.cloneNode(true) as HTMLElement;
|
||||
newRow.querySelectorAll("input").forEach((el) => {
|
||||
(el as HTMLInputElement).value = "";
|
||||
});
|
||||
newRow.querySelectorAll("select").forEach((el) => {
|
||||
(el as HTMLSelectElement).selectedIndex = 0;
|
||||
});
|
||||
container.appendChild(newRow);
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.classList.contains("remove-row-btn")) {
|
||||
return;
|
||||
}
|
||||
const row = target.closest<HTMLElement>(".form-row");
|
||||
const container = row?.parentElement;
|
||||
if (!row || !container) {
|
||||
return;
|
||||
}
|
||||
const rows = container.querySelectorAll(".form-row");
|
||||
if (rows.length > 1) {
|
||||
row.remove();
|
||||
return;
|
||||
}
|
||||
// Keep at least one row per section — just clear it instead of
|
||||
// removing it entirely.
|
||||
row.querySelectorAll("input").forEach((el) => {
|
||||
(el as HTMLInputElement).value = "";
|
||||
});
|
||||
row.querySelectorAll("select").forEach((el) => {
|
||||
(el as HTMLSelectElement).selectedIndex = 0;
|
||||
});
|
||||
});
|
||||
|
||||
const photoInput = document.getElementById("photo-input") as HTMLInputElement | null;
|
||||
const photoPreview = document.getElementById("photo-preview") as HTMLImageElement | null;
|
||||
const currentAvatar = document.getElementById("current-avatar") as HTMLElement | null;
|
||||
const removePhotoCheckbox = document.getElementById("remove-photo") as HTMLInputElement | null;
|
||||
|
||||
photoInput?.addEventListener("change", () => {
|
||||
const file = photoInput.files?.[0];
|
||||
if (!file || !photoPreview) {
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
photoPreview.src = reader.result as string;
|
||||
photoPreview.classList.remove("hidden");
|
||||
currentAvatar?.classList.add("hidden");
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
if (removePhotoCheckbox) {
|
||||
removePhotoCheckbox.checked = false;
|
||||
}
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user