Files
arnefandCopilot 3fa0297715 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>
2026-08-20 15:06:39 +02:00

78 lines
2.8 KiB
JavaScript

"use strict";
// Contact form behavior: adding/removing repeatable phone, email, and
// address rows, plus a live preview when choosing a new photo.
(function initContactForm() {
document.querySelectorAll(".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(".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);
newRow.querySelectorAll("input").forEach((el) => {
el.value = "";
});
newRow.querySelectorAll("select").forEach((el) => {
el.selectedIndex = 0;
});
container.appendChild(newRow);
});
});
document.addEventListener("click", (e) => {
const target = e.target;
if (!target.classList.contains("remove-row-btn")) {
return;
}
const row = target.closest(".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.value = "";
});
row.querySelectorAll("select").forEach((el) => {
el.selectedIndex = 0;
});
});
const photoInput = document.getElementById("photo-input");
const photoPreview = document.getElementById("photo-preview");
const currentAvatar = document.getElementById("current-avatar");
const removePhotoCheckbox = document.getElementById("remove-photo");
photoInput?.addEventListener("change", () => {
const file = photoInput.files?.[0];
if (!file || !photoPreview) {
return;
}
const reader = new FileReader();
reader.onload = () => {
photoPreview.src = reader.result;
photoPreview.classList.remove("hidden");
currentAvatar?.classList.add("hidden");
};
reader.readAsDataURL(file);
if (removePhotoCheckbox) {
removePhotoCheckbox.checked = false;
}
});
})();