@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 NHN Cloud Corp.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+10
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,816 @@
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker.register("/static/sw.js");
|
||||
});
|
||||
}
|
||||
|
||||
const viewPreferenceKey = `gardomatic.view-preferences.${document.body.dataset.userId || "anonymous"}`;
|
||||
|
||||
function readViewPreferences() {
|
||||
try { return { default: "grid", ...JSON.parse(localStorage.getItem(viewPreferenceKey) || "{}") }; }
|
||||
catch (_) { return { default: "grid" }; }
|
||||
}
|
||||
|
||||
function initializeCollections(root = document) {
|
||||
const preferences = readViewPreferences();
|
||||
const selectedMode = (key) => {
|
||||
const selected = preferences[key];
|
||||
return selected && selected !== "inherit" ? selected : preferences.default;
|
||||
};
|
||||
root.querySelectorAll("[data-background-image]").forEach((card) => {
|
||||
card.style.backgroundImage = `url("${card.dataset.backgroundImage}")`;
|
||||
});
|
||||
root.querySelectorAll("[data-view-key]").forEach((collection) => {
|
||||
collection.dataset.viewMode = selectedMode(collection.dataset.viewKey);
|
||||
});
|
||||
root.querySelectorAll("[data-view-variants]").forEach((container) => {
|
||||
const mode = selectedMode(container.dataset.viewKey);
|
||||
container.querySelectorAll(":scope > [data-view-variant]").forEach((variant) => {
|
||||
variant.hidden = variant.dataset.viewVariant !== mode;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initializeViewToggles(root = document) {
|
||||
root.querySelectorAll("[data-view-toggle]").forEach((button) => {
|
||||
const key = button.dataset.viewToggle;
|
||||
const update = () => {
|
||||
const preferences = readViewPreferences();
|
||||
const mode = preferences[key] && preferences[key] !== "inherit" ? preferences[key] : preferences.default;
|
||||
button.dataset.viewMode = mode;
|
||||
button.setAttribute("aria-label", mode === "grid" ? "Als Liste anzeigen" : "Als Kacheln anzeigen");
|
||||
button.title = button.getAttribute("aria-label");
|
||||
};
|
||||
button.addEventListener("click", () => {
|
||||
const preferences = readViewPreferences();
|
||||
const current = preferences[key] && preferences[key] !== "inherit" ? preferences[key] : preferences.default;
|
||||
const next = current === "grid" ? "list" : "grid";
|
||||
preferences[key] = next;
|
||||
localStorage.setItem(viewPreferenceKey, JSON.stringify(preferences));
|
||||
root.querySelectorAll(`[data-view-key="${key}"]`).forEach((collection) => {
|
||||
collection.dataset.viewMode = next;
|
||||
if (collection.hasAttribute("data-view-variants")) collection.querySelectorAll(":scope > [data-view-variant]").forEach((variant) => { variant.hidden = variant.dataset.viewVariant !== next; });
|
||||
});
|
||||
update();
|
||||
});
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
function initializeViewSettings() {
|
||||
const form = document.querySelector("[data-view-settings]");
|
||||
if (!form) return;
|
||||
const preferences = readViewPreferences();
|
||||
Array.from(form.elements).forEach((field) => {
|
||||
if (!(field instanceof HTMLSelectElement)) return;
|
||||
if (field.name === "entriesPerPage") field.value = preferences.entriesPerPage || preferences.tasksPerPage || "20";
|
||||
else field.value = preferences[field.name] || (field.name === "default" ? "grid" : "inherit");
|
||||
});
|
||||
form.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
const next = {};
|
||||
new FormData(form).forEach((value, key) => { next[key] = value; });
|
||||
localStorage.setItem(viewPreferenceKey, JSON.stringify(next));
|
||||
const userSuffix = document.body.dataset.userId ? `.${document.body.dataset.userId}` : "";
|
||||
document.cookie = `gardomatic.entries-per-page${userSuffix}=${encodeURIComponent(next.entriesPerPage || "20")}; Path=/; Max-Age=31536000; SameSite=Lax`;
|
||||
form.querySelector("[data-settings-message]").textContent = "Einstellungen gespeichert.";
|
||||
});
|
||||
}
|
||||
|
||||
function initializeSearchTabs() {
|
||||
const tabs = Array.from(document.querySelectorAll("[data-search-tab]"));
|
||||
if (!tabs.length) return;
|
||||
const sections = tabs.map((tab) => document.getElementById(tab.dataset.searchTab)).filter(Boolean);
|
||||
const select = (id) => {
|
||||
tabs.forEach((tab) => {
|
||||
const selected = tab.dataset.searchTab === id;
|
||||
tab.setAttribute("aria-current", selected ? "page" : "false");
|
||||
});
|
||||
sections.forEach((section) => { section.hidden = section.id !== id; });
|
||||
};
|
||||
tabs.forEach((tab) => tab.addEventListener("click", () => select(tab.dataset.searchTab)));
|
||||
const initial = window.location.hash.slice(1);
|
||||
select(sections.some((section) => section.id === initial) ? initial : "search-all");
|
||||
}
|
||||
|
||||
initializeCollections();
|
||||
initializeViewToggles();
|
||||
initializeViewSettings();
|
||||
initializeSearchTabs();
|
||||
|
||||
document.querySelector("[data-history-back]")?.addEventListener("click", () => window.history.back());
|
||||
|
||||
function initializePlantCompatibility(root = document) {
|
||||
const species = root.querySelector("[data-plant-species]");
|
||||
const warning = root.querySelector("[data-compatibility-warning]");
|
||||
if (!species || !warning) return;
|
||||
const check = () => {
|
||||
const expected = species.selectedOptions[0]?.dataset || {};
|
||||
const problems = [];
|
||||
root.querySelectorAll("[data-assignment-row] select[name='location_id']").forEach((select) => {
|
||||
const actual = root.querySelector(`[data-location-id="${select.value}"]`)?.dataset || {};
|
||||
const name = select.selectedOptions[0]?.textContent?.trim();
|
||||
const fields=[];
|
||||
if(expected.sun && actual.sun && expected.sun!==actual.sun) fields.push("Lichtverhältnis");
|
||||
if(expected.soil && actual.soil && expected.soil!==actual.soil) fields.push("Bodenbeschaffenheit");
|
||||
if(expected.reaction && actual.reaction && expected.reaction!==actual.reaction) fields.push("Bodenreaktion");
|
||||
if(fields.length) problems.push(`${name}: ${fields.join(" und ")}`);
|
||||
});
|
||||
warning.hidden=problems.length===0;
|
||||
warning.textContent=problems.length?`Hinweis: Standort und Art unterscheiden sich bei ${problems.join("; ")}.`:"";
|
||||
};
|
||||
root.addEventListener("change",(event)=>{if(event.target===species||event.target.matches("select[name='location_id']"))check()});
|
||||
check();
|
||||
}
|
||||
initializePlantCompatibility();
|
||||
|
||||
function initializeJournalEditor() {
|
||||
const form = document.querySelector("[data-journal-editor]");
|
||||
const textarea = form?.querySelector("[data-journal-body]");
|
||||
const mount = document.querySelector("#journal-editor");
|
||||
if (!form || !textarea || !mount) return;
|
||||
|
||||
const embeddedInput = form.querySelector("[data-embedded-images]");
|
||||
const embeddedFiles = new DataTransfer();
|
||||
const richTextEnabled = readViewPreferences().journalEditor !== "plain" && window.toastui?.Editor;
|
||||
if (richTextEnabled) {
|
||||
const editor = new toastui.Editor({
|
||||
el: mount,
|
||||
height: "480px",
|
||||
minHeight: "300px",
|
||||
initialEditType: "wysiwyg",
|
||||
previewStyle: "vertical",
|
||||
initialValue: textarea.value,
|
||||
language: "de-DE",
|
||||
usageStatistics: false,
|
||||
placeholder: form.dataset.entryKind === "pinboard" ? "Idee oder Notiz festhalten …" : "Was ist heute im Garten passiert?",
|
||||
hooks: {
|
||||
addImageBlobHook(blob, callback) {
|
||||
const extension = (blob.type.split("/")[1] || "jpg").replace("jpeg", "jpg");
|
||||
const file = blob instanceof File ? blob : new File([blob], `tagebuchbild-${Date.now()}.${extension}`, { type: blob.type || "image/jpeg" });
|
||||
const index = embeddedFiles.files.length;
|
||||
embeddedFiles.items.add(file);
|
||||
embeddedInput.files = embeddedFiles.files;
|
||||
callback(`/pending-journal-image/${index}`, file.name);
|
||||
},
|
||||
},
|
||||
});
|
||||
textarea.hidden = true;
|
||||
form.addEventListener("submit", () => { textarea.value = editor.getMarkdown(); });
|
||||
} else {
|
||||
mount.hidden = true;
|
||||
textarea.hidden = false;
|
||||
textarea.placeholder = form.dataset.entryKind === "pinboard" ? "Idee oder Notiz festhalten …" : "Was ist heute im Garten passiert?";
|
||||
}
|
||||
|
||||
const fileInputs = Array.from(form.querySelectorAll("[data-journal-files]"));
|
||||
const generatedInput = form.querySelector("[data-journal-generated-files]");
|
||||
const preview = form.querySelector("[data-attachment-preview]");
|
||||
const libraryImages = Array.from(form.querySelectorAll("[data-journal-library-image]"));
|
||||
let chosenFiles = new DataTransfer();
|
||||
const chosenEntries = [];
|
||||
const rebuildChosenFiles = () => {
|
||||
chosenFiles = new DataTransfer();
|
||||
chosenEntries.forEach((entry) => chosenFiles.items.add(entry.file));
|
||||
generatedInput.files = chosenFiles.files;
|
||||
};
|
||||
const addChosenFile = (file, photoId) => { chosenEntries.push({ file, photoId }); rebuildChosenFiles(); renderFiles(); };
|
||||
const replaceChosenFile = (file, photoId) => {
|
||||
const entry = chosenEntries.find((value) => value.photoId === photoId);
|
||||
if (!entry) return addChosenFile(file, photoId);
|
||||
entry.file = file;
|
||||
rebuildChosenFiles();
|
||||
renderFiles();
|
||||
};
|
||||
const renderFiles = () => {
|
||||
const nativeFiles = fileInputs.flatMap((input) => Array.from(input.files || []));
|
||||
const uploaded = [...nativeFiles, ...Array.from(chosenFiles.files)].map((file) => {
|
||||
const item = document.createElement("li");
|
||||
item.textContent = `${file.name} · ${(file.size / 1024 / 1024).toFixed(1)} MB`;
|
||||
return item;
|
||||
});
|
||||
const fromLibrary = libraryImages.filter((input) => input.checked).map((input) => {
|
||||
const item = document.createElement("li");
|
||||
const size = Number(input.dataset.imageSize || 0);
|
||||
item.textContent = `${input.dataset.imageName || "Bild"} · ${(size / 1024 / 1024).toFixed(1)} MB · Bilderdatenbank`;
|
||||
return item;
|
||||
});
|
||||
preview.replaceChildren(...uploaded,...fromLibrary);
|
||||
};
|
||||
fileInputs.forEach((input) => input.addEventListener("change", renderFiles));
|
||||
libraryImages.forEach((input) => input.addEventListener("change", renderFiles));
|
||||
renderFiles();
|
||||
form.addEventListener("journal-photo-ready", (event) => {
|
||||
const data = event.detail?.data;
|
||||
if (!data?.startsWith("data:")) return;
|
||||
const [header, encoded] = data.split(",", 2);
|
||||
const mime = header.match(/data:([^;]+)/)?.[1] || "image/jpeg";
|
||||
const binary = atob(encoded || "");
|
||||
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
||||
const file = new File([bytes], `tagebuchfoto-${Date.now()}.jpg`, { type: mime });
|
||||
if (event.detail?.photoId) replaceChosenFile(file, event.detail.photoId);
|
||||
else addChosenFile(file);
|
||||
});
|
||||
|
||||
const recordButton = form.querySelector("[data-audio-record]");
|
||||
const audioDialog = document.querySelector("[data-audio-dialog]");
|
||||
const audioToggle = document.querySelector("[data-audio-toggle]");
|
||||
const audioPause = document.querySelector("[data-audio-pause]");
|
||||
const audioApply = document.querySelector("[data-audio-apply]");
|
||||
const audioPreview = document.querySelector("[data-audio-preview]");
|
||||
const audioCancel = document.querySelector("[data-audio-cancel]");
|
||||
const audioStatus = document.querySelector("[data-audio-status]");
|
||||
const audioError = document.querySelector("[data-audio-error]");
|
||||
const recordingIndicator = document.querySelector("[data-audio-indicator]");
|
||||
let recorder;
|
||||
let recordingStream;
|
||||
let chunks = [];
|
||||
let cancelAudio = false;
|
||||
let pendingAudio;
|
||||
let audioPreviewURL;
|
||||
const stopAudioStream = () => { recordingStream?.getTracks().forEach((track) => track.stop()); recordingStream = undefined; };
|
||||
const clearAudioPreview = () => {
|
||||
if (audioPreviewURL) URL.revokeObjectURL(audioPreviewURL);
|
||||
audioPreviewURL = undefined;
|
||||
pendingAudio = undefined;
|
||||
audioPreview.removeAttribute("src");
|
||||
audioPreview.load();
|
||||
audioPreview.hidden = true;
|
||||
audioApply.hidden = true;
|
||||
audioPause.textContent = "Pause";
|
||||
audioPause.hidden = true;
|
||||
recordingIndicator.hidden = true;
|
||||
};
|
||||
recordButton?.addEventListener("click", () => {
|
||||
clearAudioPreview();
|
||||
audioError.textContent = "";
|
||||
audioStatus.textContent = "Bereit zur Aufnahme.";
|
||||
audioToggle.textContent = "Aufnahme starten";
|
||||
audioToggle.hidden = false;
|
||||
audioPause.hidden = true;
|
||||
audioDialog?.showModal();
|
||||
});
|
||||
audioToggle?.addEventListener("click", async () => {
|
||||
if (recorder?.state === "recording" || recorder?.state === "paused") { recorder.stop(); return; }
|
||||
if (!navigator.mediaDevices?.getUserMedia || !window.MediaRecorder) {
|
||||
audioError.textContent = "Audioaufnahme ist in diesem Browser oder ohne HTTPS nicht verfügbar.";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
recordingStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
recorder = new MediaRecorder(recordingStream);
|
||||
chunks = [];
|
||||
recorder.addEventListener("dataavailable", (event) => { if (event.data.size) chunks.push(event.data); });
|
||||
recorder.addEventListener("stop", () => {
|
||||
if (cancelAudio) { cancelAudio = false; chunks = []; stopAudioStream(); return; }
|
||||
const type = recorder.mimeType || "audio/webm";
|
||||
pendingAudio = new File(chunks, `audio-${new Date().toISOString().replace(/[:.]/g, "-")}.webm`, { type });
|
||||
audioPreviewURL = URL.createObjectURL(pendingAudio);
|
||||
audioPreview.src = audioPreviewURL;
|
||||
audioPreview.hidden = false;
|
||||
stopAudioStream();
|
||||
audioToggle.hidden = true;
|
||||
audioPause.hidden = true;
|
||||
recordingIndicator.hidden = true;
|
||||
audioApply.hidden = false;
|
||||
audioStatus.textContent = "Aufnahme bereit. Bitte prüfen und übernehmen.";
|
||||
});
|
||||
recorder.start();
|
||||
audioToggle.textContent = "Aufnahme stoppen";
|
||||
audioPause.hidden = false;
|
||||
recordingIndicator.hidden = false;
|
||||
audioStatus.textContent = "Aufnahme läuft …";
|
||||
} catch (_) {
|
||||
audioError.textContent = "Das Mikrofon konnte nicht geöffnet werden. Bitte prüfe die Freigabe.";
|
||||
}
|
||||
});
|
||||
audioPause?.addEventListener("click", () => {
|
||||
if (!recorder) return;
|
||||
if (recorder.state === "recording") { recorder.pause(); audioPause.textContent = "Fortsetzen"; recordingIndicator.hidden = true; audioStatus.textContent = "Aufnahme pausiert."; }
|
||||
else if (recorder.state === "paused") { recorder.resume(); audioPause.textContent = "Pause"; recordingIndicator.hidden = false; audioStatus.textContent = "Aufnahme läuft …"; }
|
||||
});
|
||||
audioApply?.addEventListener("click", () => {
|
||||
if (!pendingAudio) return;
|
||||
addChosenFile(pendingAudio);
|
||||
clearAudioPreview();
|
||||
audioDialog?.close();
|
||||
});
|
||||
audioCancel?.addEventListener("click", () => { if (recorder?.state === "recording" || recorder?.state === "paused") { cancelAudio = true; recorder.stop(); } else stopAudioStream(); clearAudioPreview(); audioDialog?.close(); });
|
||||
audioDialog?.addEventListener("close", () => { if (recorder?.state === "recording" || recorder?.state === "paused") { cancelAudio = true; recorder.stop(); } stopAudioStream(); clearAudioPreview(); });
|
||||
|
||||
const videoButton = form.querySelector("[data-video-record]");
|
||||
const videoDialog = document.querySelector("[data-video-dialog]");
|
||||
const videoPreview = document.querySelector("[data-video-preview]");
|
||||
const videoToggle = document.querySelector("[data-video-toggle]");
|
||||
const videoPause = document.querySelector("[data-video-pause]");
|
||||
const videoApply = document.querySelector("[data-video-apply]");
|
||||
const videoCancel = document.querySelector("[data-video-cancel]");
|
||||
const videoSwitchCamera = document.querySelector("[data-video-switch-camera]");
|
||||
const videoError = document.querySelector("[data-video-error]");
|
||||
let videoStream;
|
||||
let videoRecorder;
|
||||
let videoChunks = [];
|
||||
let cancelVideo = false;
|
||||
let pendingVideo;
|
||||
let videoPreviewURL;
|
||||
let videoFacingMode = "environment";
|
||||
const stopVideoStream = () => { videoStream?.getTracks().forEach((track) => track.stop()); videoStream = undefined; videoPreview.srcObject = null; };
|
||||
const startVideoStream = async () => {
|
||||
stopVideoStream();
|
||||
videoStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: videoFacingMode } }, audio: true });
|
||||
videoPreview.srcObject = videoStream;
|
||||
await videoPreview.play();
|
||||
const devices = await navigator.mediaDevices.enumerateDevices?.();
|
||||
if (videoSwitchCamera) videoSwitchCamera.hidden = devices ? devices.filter((device) => device.kind === "videoinput").length < 2 : false;
|
||||
};
|
||||
const clearVideoPreview = () => {
|
||||
videoPreview.pause();
|
||||
if (videoPreviewURL) URL.revokeObjectURL(videoPreviewURL);
|
||||
videoPreviewURL = undefined;
|
||||
pendingVideo = undefined;
|
||||
videoPreview.removeAttribute("src");
|
||||
videoPreview.load();
|
||||
videoPreview.controls = false;
|
||||
videoPreview.muted = true;
|
||||
videoApply.hidden = true;
|
||||
videoPause.textContent = "Pause";
|
||||
videoPause.hidden = true;
|
||||
};
|
||||
videoButton?.addEventListener("click", async () => {
|
||||
clearVideoPreview();
|
||||
videoToggle.hidden = false;
|
||||
videoPause.hidden = true;
|
||||
if (!navigator.mediaDevices?.getUserMedia || !window.MediaRecorder) { videoError.textContent = "Videoaufnahme ist in diesem Browser oder ohne HTTPS nicht verfügbar."; videoDialog.showModal(); return; }
|
||||
try {
|
||||
await startVideoStream();
|
||||
videoError.textContent = "";
|
||||
videoToggle.textContent = "Aufnahme starten";
|
||||
videoDialog.showModal();
|
||||
} catch (_) { videoError.textContent = "Kamera und Mikrofon konnten nicht geöffnet werden. Bitte prüfe die Freigaben."; videoDialog.showModal(); }
|
||||
});
|
||||
videoSwitchCamera?.addEventListener("click", async () => {
|
||||
videoFacingMode = videoFacingMode === "environment" ? "user" : "environment";
|
||||
videoError.textContent = "Kamera wird gewechselt …";
|
||||
try {
|
||||
await startVideoStream();
|
||||
videoError.textContent = "";
|
||||
} catch (_) {
|
||||
videoError.textContent = "Die andere Kamera konnte nicht geöffnet werden.";
|
||||
}
|
||||
});
|
||||
videoToggle?.addEventListener("click", () => {
|
||||
if (videoRecorder?.state === "recording" || videoRecorder?.state === "paused") { videoRecorder.stop(); return; }
|
||||
if (!videoStream) return;
|
||||
videoChunks = [];
|
||||
videoRecorder = new MediaRecorder(videoStream);
|
||||
if (videoSwitchCamera) videoSwitchCamera.hidden = true;
|
||||
videoRecorder.addEventListener("dataavailable", (event) => { if (event.data.size) videoChunks.push(event.data); });
|
||||
videoRecorder.addEventListener("stop", () => {
|
||||
if (cancelVideo) { cancelVideo = false; videoChunks = []; stopVideoStream(); return; }
|
||||
const type = videoRecorder.mimeType || "video/webm";
|
||||
pendingVideo = new File(videoChunks, `video-${new Date().toISOString().replace(/[:.]/g, "-")}.webm`, { type });
|
||||
stopVideoStream();
|
||||
videoPreviewURL = URL.createObjectURL(pendingVideo);
|
||||
videoPreview.src = videoPreviewURL;
|
||||
videoPreview.controls = true;
|
||||
videoPreview.muted = false;
|
||||
videoToggle.hidden = true;
|
||||
videoPause.hidden = true;
|
||||
videoApply.hidden = false;
|
||||
videoError.textContent = "Aufnahme bereit. Bitte prüfen und übernehmen.";
|
||||
});
|
||||
videoRecorder.start();
|
||||
videoToggle.textContent = "Aufnahme stoppen";
|
||||
videoPause.hidden = false;
|
||||
});
|
||||
videoPause?.addEventListener("click", () => {
|
||||
if (!videoRecorder) return;
|
||||
if (videoRecorder.state === "recording") { videoRecorder.pause(); videoPause.textContent = "Fortsetzen"; videoError.textContent = "Aufnahme pausiert."; }
|
||||
else if (videoRecorder.state === "paused") { videoRecorder.resume(); videoPause.textContent = "Pause"; videoError.textContent = "Aufnahme läuft …"; }
|
||||
});
|
||||
videoApply?.addEventListener("click", () => {
|
||||
if (!pendingVideo) return;
|
||||
addChosenFile(pendingVideo);
|
||||
clearVideoPreview();
|
||||
videoDialog.close();
|
||||
});
|
||||
videoCancel?.addEventListener("click", () => { if (videoRecorder?.state === "recording" || videoRecorder?.state === "paused") { cancelVideo = true; videoRecorder.stop(); } else stopVideoStream(); clearVideoPreview(); videoDialog.close(); });
|
||||
videoDialog?.addEventListener("close", () => { if (videoRecorder?.state === "recording" || videoRecorder?.state === "paused") { cancelVideo = true; videoRecorder.stop(); } stopVideoStream(); clearVideoPreview(); });
|
||||
}
|
||||
|
||||
initializeJournalEditor();
|
||||
|
||||
function initializeTagEditors(root = document) {
|
||||
root.querySelectorAll("[data-tag-editor]").forEach((editor) => {
|
||||
if (editor.dataset.tagInitialized) return;
|
||||
const input = editor.querySelector("[data-tag-input]");
|
||||
const bubbles = editor.querySelector("[data-tag-bubbles]");
|
||||
const suggestions = editor.querySelector("[data-tag-suggestions]");
|
||||
if (!input || !bubbles || !suggestions) return;
|
||||
let tags = input.value.split(",").map((tag) => tag.trim().toLowerCase()).filter(Boolean);
|
||||
tags = [...new Set(tags)];
|
||||
const render = () => {
|
||||
bubbles.replaceChildren(...tags.map((tag) => {
|
||||
const bubble = document.createElement("span");
|
||||
bubble.className = "tag-bubble";
|
||||
bubble.textContent = tag;
|
||||
const remove = document.createElement("button");
|
||||
remove.type = "button";
|
||||
remove.className = "tag-bubble-remove";
|
||||
remove.textContent = "×";
|
||||
remove.setAttribute("aria-label", `${tag} entfernen`);
|
||||
remove.addEventListener("click", () => { tags = tags.filter((value) => value !== tag); render(); });
|
||||
bubble.append(remove);
|
||||
return bubble;
|
||||
}));
|
||||
updateSuggestions();
|
||||
};
|
||||
const updateSuggestions = () => {
|
||||
const prefix = input.value.trim().toLowerCase();
|
||||
let visible = 0;
|
||||
suggestions.querySelectorAll("[data-tag-suggestion]").forEach((button) => {
|
||||
const value = button.dataset.tagSuggestion.toLowerCase();
|
||||
const show = prefix.length > 0 && value.startsWith(prefix) && !tags.includes(value);
|
||||
button.hidden = !show;
|
||||
visible += show ? 1 : 0;
|
||||
});
|
||||
suggestions.hidden = visible === 0;
|
||||
};
|
||||
const add = (value) => {
|
||||
value = value.trim().toLowerCase();
|
||||
if (value && !tags.includes(value)) tags.push(value);
|
||||
input.value = "";
|
||||
render();
|
||||
};
|
||||
input.addEventListener("input", () => {
|
||||
if (input.value.includes(",")) {
|
||||
const values = input.value.split(",");
|
||||
values.slice(0, -1).forEach(add);
|
||||
input.value = values.at(-1).trim();
|
||||
}
|
||||
updateSuggestions();
|
||||
});
|
||||
input.addEventListener("keydown", (event) => {
|
||||
if ((event.key === "Enter" || event.key === ",") && input.value.trim()) { event.preventDefault(); add(input.value); }
|
||||
if (event.key === "Backspace" && !input.value && tags.length) { tags.pop(); render(); }
|
||||
});
|
||||
suggestions.querySelectorAll("[data-tag-suggestion]").forEach((button) => button.addEventListener("click", () => add(button.dataset.tagSuggestion)));
|
||||
editor.closest("form")?.addEventListener("submit", () => { if (input.value.trim()) add(input.value); input.value = tags.join(", "); });
|
||||
render();
|
||||
editor.dataset.tagInitialized = "true";
|
||||
});
|
||||
}
|
||||
|
||||
initializeTagEditors();
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
document.querySelectorAll(".user-menu[open]").forEach((menu) => {
|
||||
if (!menu.contains(event.target)) {
|
||||
menu.removeAttribute("open");
|
||||
}
|
||||
});
|
||||
|
||||
if (event.target.closest("[data-close-dialog]")) {
|
||||
event.target.closest("dialog")?.remove();
|
||||
}
|
||||
|
||||
const dialogOpener = event.target.closest("[data-open-dialog]");
|
||||
if (dialogOpener) {
|
||||
const dialog = document.getElementById(dialogOpener.dataset.openDialog);
|
||||
if (dialog instanceof HTMLDialogElement && !dialog.open) dialog.showModal();
|
||||
}
|
||||
|
||||
if (event.target.closest("[data-cancel-dialog]")) {
|
||||
event.target.closest("dialog")?.close();
|
||||
}
|
||||
|
||||
const card = event.target.closest("[data-card-href]");
|
||||
if (card && !event.target.closest("a, button, input, select, textarea, form, summary, details, video, audio, img")) {
|
||||
window.location.assign(card.dataset.cardHref);
|
||||
}
|
||||
|
||||
const addAssignment = event.target.closest("[data-add-assignment]");
|
||||
if (addAssignment) {
|
||||
const template = document.querySelector("#assignment-row-template");
|
||||
const list = document.querySelector("[data-assignment-list]");
|
||||
if (template && list) {
|
||||
list.append(template.content.cloneNode(true));
|
||||
refreshAssignmentFields(list);
|
||||
list.lastElementChild?.querySelector("select")?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
const removeAssignment = event.target.closest("[data-remove-assignment]");
|
||||
if (removeAssignment) {
|
||||
const row = removeAssignment.closest("[data-assignment-row]");
|
||||
const assignmentID = row?.querySelector("[name='assignment_id']");
|
||||
if (row && assignmentID?.value !== "0") {
|
||||
row.querySelector("[name='location_id']").value = "0";
|
||||
row.querySelector("[name='quantity']").value = "1";
|
||||
row.hidden = true;
|
||||
} else {
|
||||
row?.remove();
|
||||
}
|
||||
refreshAssignmentFields(document.querySelector("[data-assignment-list]"));
|
||||
}
|
||||
|
||||
const removePlantTask = event.target.closest("[data-remove-plant-task]");
|
||||
if (removePlantTask) {
|
||||
const row = removePlantTask.closest("[data-plant-task-row]");
|
||||
const taskID = row?.querySelector("[name='plant_task_id']");
|
||||
if (row && taskID?.value !== "0") {
|
||||
row.querySelector("[data-plant-task-delete]").value = "true";
|
||||
row.hidden = true;
|
||||
row.querySelectorAll("[required]").forEach((field) => field.required = false);
|
||||
} else {
|
||||
row?.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function initializePlantName() {
|
||||
const name = document.querySelector("[data-plant-name]");
|
||||
const species = document.querySelector("[data-plant-species]");
|
||||
if (!name || !species || name.dataset.nameInitialized) return;
|
||||
const selectedName = species.selectedOptions[0]?.dataset.plantNameValue || "";
|
||||
let manuallyEdited = name.value !== "" && name.value !== selectedName;
|
||||
if (!name.value && selectedName) name.value = selectedName;
|
||||
name.addEventListener("input", () => { manuallyEdited = true; });
|
||||
species.addEventListener("change", () => {
|
||||
if (manuallyEdited) return;
|
||||
name.value = species.selectedOptions[0]?.dataset.plantNameValue || "";
|
||||
});
|
||||
name.dataset.nameInitialized = "true";
|
||||
}
|
||||
|
||||
initializePlantName();
|
||||
|
||||
function initializeImageEditors(root = document) {
|
||||
root.querySelectorAll("[data-image-editor]").forEach((editor) => {
|
||||
if (editor.dataset.imageInitialized) return;
|
||||
const value = editor.querySelector("[data-image-value]");
|
||||
const imageID = editor.querySelector("[data-image-id]");
|
||||
const preview = editor.querySelector("[data-image-preview]");
|
||||
const placeholder = preview?.querySelector("span");
|
||||
const dialog = editor.querySelector("[data-image-dialog]");
|
||||
const image = editor.querySelector("[data-cropper-image]");
|
||||
const remove = editor.querySelector("[data-image-remove]");
|
||||
const apply = editor.querySelector("[data-image-apply]");
|
||||
const error = editor.querySelector("[data-image-error]");
|
||||
const cameraDialog = editor.querySelector("[data-camera-dialog]");
|
||||
const cameraVideo = editor.querySelector("[data-camera-video]");
|
||||
const cameraCapture = editor.querySelector("[data-camera-capture]");
|
||||
const cameraSwitch = editor.querySelector("[data-camera-switch]");
|
||||
const cameraError = editor.querySelector("[data-camera-error]");
|
||||
let cropper;
|
||||
let cameraStream;
|
||||
let cameraFacingMode = "environment";
|
||||
|
||||
if (value.value) {
|
||||
if (preview) preview.style.backgroundImage = `url("${value.value}")`;
|
||||
if (placeholder) placeholder.hidden = true;
|
||||
if (remove) remove.hidden = false;
|
||||
}
|
||||
|
||||
const openCropper = (source) => {
|
||||
cropper?.destroy();
|
||||
cropper = undefined;
|
||||
apply.disabled = true;
|
||||
error.textContent = "Bild wird geladen …";
|
||||
image.onload = () => {
|
||||
cropper = new Cropper(image, {
|
||||
aspectRatio: 3 / 2,
|
||||
viewMode: 1,
|
||||
autoCropArea: 1,
|
||||
background: false,
|
||||
ready() {
|
||||
apply.disabled = false;
|
||||
error.textContent = "";
|
||||
},
|
||||
});
|
||||
};
|
||||
image.onerror = () => { error.textContent = "Das Bild konnte nicht geladen werden."; };
|
||||
image.src = String(source);
|
||||
dialog.showModal();
|
||||
};
|
||||
|
||||
const choose = (event) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file || !file.type.startsWith("image/")) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => openCropper(reader.result);
|
||||
reader.readAsDataURL(file);
|
||||
event.target.value = "";
|
||||
};
|
||||
editor.querySelector("[data-image-file]")?.addEventListener("change", choose);
|
||||
|
||||
const stopCamera = () => {
|
||||
cameraStream?.getTracks().forEach((track) => track.stop());
|
||||
cameraStream = undefined;
|
||||
cameraVideo.srcObject = null;
|
||||
cameraCapture.disabled = true;
|
||||
};
|
||||
const startCamera = async () => {
|
||||
stopCamera();
|
||||
cameraStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: cameraFacingMode } }, audio: false });
|
||||
cameraVideo.srcObject = cameraStream;
|
||||
await cameraVideo.play();
|
||||
cameraCapture.disabled = cameraVideo.videoWidth === 0;
|
||||
cameraVideo.onloadedmetadata = () => { cameraCapture.disabled = false; };
|
||||
const devices = await navigator.mediaDevices.enumerateDevices?.();
|
||||
if (cameraSwitch) cameraSwitch.hidden = devices ? devices.filter((device) => device.kind === "videoinput").length < 2 : false;
|
||||
};
|
||||
editor.querySelector("[data-image-camera]")?.addEventListener("click", async () => {
|
||||
cameraError.textContent = "Webcam wird gestartet …";
|
||||
cameraCapture.disabled = true;
|
||||
cameraDialog.showModal();
|
||||
if (!navigator.mediaDevices?.getUserMedia) {
|
||||
cameraError.textContent = "Die Webcam ist in diesem Browser oder ohne HTTPS nicht verfügbar.";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await startCamera();
|
||||
cameraError.textContent = "";
|
||||
} catch (_) {
|
||||
cameraError.textContent = "Die Webcam konnte nicht geöffnet werden. Bitte prüfe die Kamerafreigabe des Browsers.";
|
||||
stopCamera();
|
||||
}
|
||||
});
|
||||
cameraSwitch?.addEventListener("click", async () => {
|
||||
cameraFacingMode = cameraFacingMode === "environment" ? "user" : "environment";
|
||||
cameraError.textContent = "Kamera wird gewechselt …";
|
||||
cameraCapture.disabled = true;
|
||||
try {
|
||||
await startCamera();
|
||||
cameraError.textContent = "";
|
||||
} catch (_) {
|
||||
cameraError.textContent = "Die andere Kamera konnte nicht geöffnet werden.";
|
||||
stopCamera();
|
||||
}
|
||||
});
|
||||
editor.querySelector("[data-camera-cancel]")?.addEventListener("click", () => {
|
||||
stopCamera();
|
||||
cameraDialog.close();
|
||||
});
|
||||
cameraDialog?.addEventListener("close", stopCamera);
|
||||
cameraCapture?.addEventListener("click", () => {
|
||||
if (!cameraVideo.videoWidth || !cameraVideo.videoHeight) return;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = cameraVideo.videoWidth;
|
||||
canvas.height = cameraVideo.videoHeight;
|
||||
canvas.getContext("2d").drawImage(cameraVideo, 0, 0);
|
||||
const source = canvas.toDataURL("image/jpeg", 0.9);
|
||||
stopCamera();
|
||||
cameraDialog.close();
|
||||
if (editor.hasAttribute("data-journal-photo-editor")) editor.dataset.photoId = `photo-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
openCropper(source);
|
||||
});
|
||||
editor.querySelector("[data-image-rotate-left]")?.addEventListener("click", () => cropper?.rotate(-90));
|
||||
editor.querySelector("[data-image-rotate-right]")?.addEventListener("click", () => cropper?.rotate(90));
|
||||
editor.querySelector("[data-image-cancel]")?.addEventListener("click", () => { dialog.close(); cropper?.destroy(); cropper = undefined; image.onload = null; });
|
||||
apply?.addEventListener("click", () => {
|
||||
if (!cropper) return;
|
||||
const canvas = cropper.getCroppedCanvas({ width: 960, height: 640, imageSmoothingQuality: "high" });
|
||||
if (!canvas) {
|
||||
error.textContent = "Das Bild ist noch nicht bereit. Bitte versuche es gleich noch einmal.";
|
||||
return;
|
||||
}
|
||||
const data = canvas.toDataURL("image/jpeg", 0.82);
|
||||
value.value = data;
|
||||
if (imageID) imageID.value = "";
|
||||
if (editor.hasAttribute("data-journal-photo-editor")) editor.closest("form")?.dispatchEvent(new CustomEvent("journal-photo-ready", { detail: { data, photoId: editor.dataset.photoId } }));
|
||||
if (preview) preview.style.backgroundImage = `url("${data}")`;
|
||||
if (placeholder) placeholder.hidden = true;
|
||||
if (remove) remove.hidden = false;
|
||||
dialog.close();
|
||||
cropper.destroy();
|
||||
cropper = undefined;
|
||||
});
|
||||
remove?.addEventListener("click", () => {
|
||||
value.value = "";
|
||||
preview.style.backgroundImage = "";
|
||||
placeholder.hidden = false;
|
||||
remove.hidden = true;
|
||||
if (imageID) imageID.value = "";
|
||||
});
|
||||
const libraryDialog = editor.querySelector("[data-image-library-dialog]");
|
||||
editor.querySelector("[data-image-library-open]")?.addEventListener("click", () => libraryDialog?.showModal());
|
||||
editor.querySelector("[data-image-library-cancel]")?.addEventListener("click", () => libraryDialog?.close());
|
||||
editor.querySelectorAll("[data-image-library-select]").forEach((button) => button.addEventListener("click", () => {
|
||||
value.value = "";
|
||||
if (imageID) imageID.value = button.dataset.imageId || "";
|
||||
if (preview) preview.style.backgroundImage = `url("${button.dataset.imageUrl}")`;
|
||||
if (placeholder) placeholder.hidden = true;
|
||||
if (remove) remove.hidden = false;
|
||||
libraryDialog?.close();
|
||||
}));
|
||||
editor.dataset.imageInitialized = "true";
|
||||
});
|
||||
}
|
||||
|
||||
initializeImageEditors();
|
||||
|
||||
document.querySelector("[data-species-template]")?.addEventListener("change", (event) => {
|
||||
const url = new URL(window.location.href);
|
||||
if (event.target.value === "0") url.searchParams.delete("template_id");
|
||||
else url.searchParams.set("template_id", event.target.value);
|
||||
window.location.assign(url);
|
||||
});
|
||||
|
||||
function initializeTaskTemplateTriggers(root = document) {
|
||||
root.querySelectorAll("[data-template-trigger]").forEach((trigger) => {
|
||||
if (trigger.dataset.triggerInitialized) return;
|
||||
const update = () => {
|
||||
const form = trigger.closest("form");
|
||||
form?.querySelectorAll("[data-trigger-fields]").forEach((group) => {
|
||||
const visible = group.dataset.triggerFields === trigger.value || (group.dataset.triggerFields === "relative" && trigger.value !== "month_of_year");
|
||||
group.hidden = !visible;
|
||||
group.disabled = !visible;
|
||||
});
|
||||
};
|
||||
trigger.addEventListener("change", update);
|
||||
trigger.dataset.triggerInitialized = "true";
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
initializeTaskTemplateTriggers();
|
||||
|
||||
function initializeRoleEditors(root = document) {
|
||||
root.querySelectorAll("[data-role-editor]").forEach((editor) => {
|
||||
if (editor.dataset.roleEditorInitialized) return;
|
||||
const select = editor.querySelector("[data-role-select]");
|
||||
const update = () => editor.querySelectorAll("[data-role-pane]").forEach((pane) => {
|
||||
pane.hidden = pane.dataset.rolePane !== select.value;
|
||||
});
|
||||
select?.addEventListener("change", update);
|
||||
editor.dataset.roleEditorInitialized = "true";
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
initializeRoleEditors();
|
||||
|
||||
document.addEventListener("change", (event) => {
|
||||
const control = event.target.closest("[data-toggle-control]");
|
||||
if (!control) return;
|
||||
const row = control.closest(".plant-task-row, .plant-template-task-row");
|
||||
const value = row?.querySelector("[data-toggle-value]");
|
||||
if (value) value.value = control.checked ? "true" : "false";
|
||||
});
|
||||
|
||||
function refreshAssignmentFields(list) {
|
||||
list?.querySelectorAll("[data-assignment-row]").forEach((row, index) => {
|
||||
const select = row.querySelector("[name='location_id']");
|
||||
const quantity = row.querySelector("[name='quantity']");
|
||||
const locationLabel = select?.previousElementSibling;
|
||||
const quantityLabel = quantity?.previousElementSibling;
|
||||
if (select) select.id = index === 0 ? "location_id" : `location_id_${index}`;
|
||||
if (quantity) quantity.id = `quantity_${index}`;
|
||||
if (locationLabel) locationLabel.htmlFor = select.id;
|
||||
if (quantityLabel) quantityLabel.htmlFor = quantity.id;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
document.querySelectorAll(".user-menu[open]").forEach((menu) => {
|
||||
menu.removeAttribute("open");
|
||||
menu.querySelector("summary")?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
const card = event.target.closest("[data-card-href]");
|
||||
if (card && !event.target.closest("a, button, input, select, textarea, form, summary, details") && (event.key === "Enter" || event.key === " ")) {
|
||||
event.preventDefault();
|
||||
window.location.assign(card.dataset.cardHref);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("locationCreated", () => {
|
||||
document.querySelector("#location-dialog-host dialog")?.remove();
|
||||
});
|
||||
|
||||
document.addEventListener("plantTaskSaved", () => {
|
||||
document.querySelector("#plant-task-dialog-host dialog")?.remove();
|
||||
});
|
||||
|
||||
document.addEventListener("htmx:afterSwap", (event) => {
|
||||
initializeTaskTemplateTriggers(event.detail.target || document);
|
||||
initializeImageEditors(event.detail.target || document);
|
||||
initializeCollections(event.detail.target || document);
|
||||
initializeTagEditors(event.detail.target || document);
|
||||
initializeViewToggles(event.detail.target || document);
|
||||
initializeRoleEditors(event.detail.target || document);
|
||||
const dialog = event.detail.target?.querySelector(".location-dialog, .task-template-dialog, .plant-task-dialog");
|
||||
if (dialog && !dialog.open) {
|
||||
dialog.showModal();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("htmx:afterRequest", (event) => {
|
||||
if (!event.detail.successful) return;
|
||||
const trigger = event.detail.elt;
|
||||
const addSection = trigger?.closest(".care-instruction-add");
|
||||
if (addSection) addSection.querySelector("form")?.reset();
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
||||
/*!
|
||||
* TOAST UI Editor : i18n
|
||||
* @version 3.2.2
|
||||
* @author NHN Cloud FE Development Lab <dl_javascript@nhn.com>
|
||||
* @license MIT
|
||||
*/
|
||||
!function(e,n){if("object"==typeof exports&&"object"==typeof module)module.exports=n(require("@toast-ui/editor"));else if("function"==typeof define&&define.amd)define(["@toast-ui/editor"],n);else{var t="object"==typeof exports?n(require("@toast-ui/editor")):n(e.toastui.Editor);for(var o in t)("object"==typeof exports?exports:e)[o]=t[o]}}(self,(function(e){return function(){"use strict";var n={213:function(n){n.exports=e}},t={};function o(e){var r=t[e];if(void 0!==r)return r.exports;var i=t[e]={exports:{}};return n[e](i,i.exports,o),i.exports}o.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(n,{a:n}),n},o.d=function(e,n){for(var t in n)o.o(n,t)&&!o.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:n[t]})},o.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var r={};return function(){o.r(r);var e=o(213);o.n(e)().setLanguage(["de","de-DE"],{Markdown:"Markdown",WYSIWYG:"WYSIWYG",Write:"Verfassen",Preview:"Vorschau",Headings:"Überschriften",Paragraph:"Text",Bold:"Fett",Italic:"Kursiv",Strike:"Durchgestrichen",Code:"Code",Line:"Trennlinie",Blockquote:"Blocktext","Unordered list":"Aufzählung","Ordered list":"Nummerierte Aufzählung",Task:"Aufgabe",Indent:"Einrücken",Outdent:"Ausrücken","Insert link":"Link einfügen","Insert CodeBlock":"Codeblock einfügen","Insert table":"Tabelle einfügen","Insert image":"Grafik einfügen",Heading:"Titel","Image URL":"Bild URL","Select image file":"Grafik auswählen","Choose a file":"Wähle eine Datei","No file":"Keine Datei",Description:"Beschreibung",OK:"OK",More:"Mehr",Cancel:"Abbrechen",File:"Datei",URL:"URL","Link text":"Anzuzeigender Text","Add row to up":"Zeile nach oben hinzufügen","Add row to down":"Zeile nach unten hinzufügen","Add column to left":"Spalte links hinzufügen","Add column to right":"Spalte rechts hinzufügen","Remove row":"Zeile entfernen","Remove column":"Spalte entfernen","Align column to left":"Links ausrichten","Align column to center":"Zentrieren","Align column to right":"Rechts ausrichten","Remove table":"Tabelle entfernen","Would you like to paste as table?":"Möchten Sie eine Tabelle einfügen?","Text color":"Textfarbe","Auto scroll enabled":"Autoscrollen aktiviert","Auto scroll disabled":"Autoscrollen deaktiviert","Choose language":"Sprache auswählen"})}(),r}()}));
|
||||
Reference in New Issue
Block a user