form update and typing animation

This commit is contained in:
Julian Krauser 2025-03-03 17:06:10 +01:00
parent be473c7e75
commit 05220efd00
14 changed files with 274 additions and 115 deletions

88
package-lock.json generated
View file

@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@headlessui/vue": "^1.7.13", "@headlessui/vue": "^1.7.13",
"@heroicons/vue": "^2.1.5", "@heroicons/vue": "^2.1.5",
"@lottiefiles/lottie-player": "^2.0.12",
"@vueup/vue-quill": "^1.2.0", "@vueup/vue-quill": "^1.2.0",
"axios": "^1.7.9", "axios": "^1.7.9",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
@ -28,7 +29,6 @@
"uuid": "^9.0.0", "uuid": "^9.0.0",
"vue": "^3.4.29", "vue": "^3.4.29",
"vue-router": "^4.3.3", "vue-router": "^4.3.3",
"y-protocols": "^1.0.6",
"y-quill": "0.1.3", "y-quill": "0.1.3",
"yjs": "^13.6.23" "yjs": "^13.6.23"
}, },
@ -2587,6 +2587,34 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@lit-labs/ssr-dom-shim": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz",
"integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==",
"license": "BSD-3-Clause"
},
"node_modules/@lit/reactive-element": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz",
"integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.0.0"
}
},
"node_modules/@lottiefiles/lottie-player": {
"version": "2.0.12",
"resolved": "https://registry.npmjs.org/@lottiefiles/lottie-player/-/lottie-player-2.0.12.tgz",
"integrity": "sha512-VuQnB+IFaY4ijrUTByth7jOLz9p7xK6goeYr/MtyGOVIggSl/TDCcSp6qztltdflFhyZrFpfbHEZNxeK5AiVgg==",
"license": "MIT",
"dependencies": {
"@types/pako": "^1.0.1",
"lit": "^2.1.2",
"lottie-web": "^5.12.2",
"pako": "^2.0.4",
"resize-observer-polyfill": "^1.5.1"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -3108,6 +3136,12 @@
"integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==", "integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==",
"dev": true "dev": true
}, },
"node_modules/@types/pako": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-1.0.7.tgz",
"integrity": "sha512-YBtzT2ztNF6R/9+UXj2wTGFnC9NklAnASt3sC0h2m1bbH7G6FyBIkt4AN8ThZpNfxUo1b2iMVO0UawiJymEt8A==",
"license": "MIT"
},
"node_modules/@types/qrcode": { "node_modules/@types/qrcode": {
"version": "1.5.5", "version": "1.5.5",
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz",
@ -3139,8 +3173,7 @@
"node_modules/@types/trusted-types": { "node_modules/@types/trusted-types": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
"dev": true
}, },
"node_modules/@types/uuid": { "node_modules/@types/uuid": {
"version": "9.0.8", "version": "9.0.8",
@ -6885,6 +6918,37 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true "dev": true
}, },
"node_modules/lit": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz",
"integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit/reactive-element": "^1.6.0",
"lit-element": "^3.3.0",
"lit-html": "^2.8.0"
}
},
"node_modules/lit-element": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz",
"integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==",
"license": "BSD-3-Clause",
"dependencies": {
"@lit-labs/ssr-dom-shim": "^1.1.0",
"@lit/reactive-element": "^1.3.0",
"lit-html": "^2.8.0"
}
},
"node_modules/lit-html": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz",
"integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==",
"license": "BSD-3-Clause",
"dependencies": {
"@types/trusted-types": "^2.0.2"
}
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -6948,6 +7012,12 @@
"integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==",
"dev": true "dev": true
}, },
"node_modules/lottie-web": {
"version": "5.12.2",
"resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.12.2.tgz",
"integrity": "sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==",
"license": "MIT"
},
"node_modules/lru-cache": { "node_modules/lru-cache": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -7440,6 +7510,12 @@
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==",
"dev": true "dev": true
}, },
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/parchment": { "node_modules/parchment": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
@ -8274,6 +8350,12 @@
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
}, },
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
"license": "MIT"
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.8", "version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",

View file

@ -26,6 +26,7 @@
"dependencies": { "dependencies": {
"@headlessui/vue": "^1.7.13", "@headlessui/vue": "^1.7.13",
"@heroicons/vue": "^2.1.5", "@heroicons/vue": "^2.1.5",
"@lottiefiles/lottie-player": "^2.0.12",
"@vueup/vue-quill": "^1.2.0", "@vueup/vue-quill": "^1.2.0",
"axios": "^1.7.9", "axios": "^1.7.9",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
@ -43,7 +44,6 @@
"uuid": "^9.0.0", "uuid": "^9.0.0",
"vue": "^3.4.29", "vue": "^3.4.29",
"vue-router": "^4.3.3", "vue-router": "^4.3.3",
"y-protocols": "^1.0.6",
"y-quill": "0.1.3", "y-quill": "0.1.3",
"yjs": "^13.6.23" "yjs": "^13.6.23"
}, },

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,65 @@
<template>
<div class="flex flex-col">
<div class="flex flex-row gap-2 items-center">
<label :for="title">{{ title }}</label>
<lottie-player src="/typing_animation.json" class="w-fit h-5" loop autoplay />
</div>
<QuillEditor
:id="title"
theme="snow"
style="height: 250px; max-height: 250px; min-height: 250px"
contentType="html"
:options="{ modules: moduleOptions }"
@ready="initEditor"
/>
<!--
:enable="can('create', 'operation', 'mission')"
:style="!can('create', 'operation', 'mission') ? 'opacity: 75%; background: rgb(243 244 246)' : ''"
-->
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { Quill, QuillEditor } from "@vueup/vue-quill";
import type QuillCursors from "quill-cursors";
import "@vueup/vue-quill/dist/vue-quill.snow.css";
import { QuillBinding } from "y-quill";
import { moduleOptions } from "@/helpers/quillConfig";
import * as Y from "yjs";
import "@lottiefiles/lottie-player";
</script>
<script lang="ts">
export default defineComponent({
props: {
text: {
type: Object as PropType<Y.Text>,
required: true,
},
title: {
type: String,
default: "",
},
},
data() {
return {
binding: undefined as undefined | QuillBinding,
cursors: undefined as undefined | QuillCursors,
};
},
beforeUnmount() {
if (this.binding) {
this.binding.destroy();
this.binding = undefined;
}
},
methods: {
initEditor(quill: Quill) {
quill.history.clear();
this.binding = new QuillBinding(this.text, quill); // TODO: awareness
this.cursors = quill.getModule("cursors") as QuillCursors;
},
},
});
</script>

View file

@ -0,0 +1,53 @@
<template>
<div :class="growing ? 'grow' : 'w-full'">
<div class="flex flex-row gap-2 items-center">
<label :for="title">{{ title }}</label>
<lottie-player src="/typing_animation.json" class="w-fit h-5" loop autoplay />
</div>
<input :type="type" :id="title" v-model="value" :min="min" />
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapActions, mapState, mapWritableState } from "pinia";
import "@lottiefiles/lottie-player";
</script>
<script lang="ts">
export default defineComponent({
props: {
modelValue: {
type: String,
default: "",
},
title: {
type: String,
default: "",
},
growing: {
type: Boolean,
default: false,
},
type: {
type: String,
default: "text",
},
min: {
type: String,
default: "",
},
},
emits: ["update:model-value"],
computed: {
value: {
get() {
return this.modelValue;
},
set(val: string) {
this.$emit("update:model-value", val);
},
},
},
});
</script>

View file

@ -8,10 +8,12 @@ export const useConnectionStore = defineStore("connection", {
return { return {
connection: undefined as undefined | Socket, connection: undefined as undefined | Socket,
connected: false as boolean, connected: false as boolean,
performingManualReconnect: false as boolean,
}; };
}, },
getters: { getters: {
connectionStatus: (state) => state.connected, connectionStatus: (state) => state.connected,
socketId: (state) => state.connection?.id,
}, },
actions: { actions: {
connectClient(): void { connectClient(): void {
@ -19,7 +21,7 @@ export const useConnectionStore = defineStore("connection", {
const notificationStore = useNotificationStore(); const notificationStore = useNotificationStore();
this.connection?.disconnect(); this.connection?.disconnect();
this.connection = io(url, { this.connection = io(url, {
reconnection: true, reconnection: false,
reconnectionDelayMax: 1000, reconnectionDelayMax: 1000,
reconnectionAttempts: 1, reconnectionAttempts: 1,
auth: (cb) => { auth: (cb) => {
@ -42,7 +44,9 @@ export const useConnectionStore = defineStore("connection", {
this.connection.on("disconnect", () => { this.connection.on("disconnect", () => {
this.connected = false; this.connected = false;
this.$reset(); this.$reset();
this.connectClient(); if (!this.performingManualReconnect) {
this.connectClient();
}
}); });
this.connection.on("warning", (msg: string) => { this.connection.on("warning", (msg: string) => {
notificationStore.push("Socket-Warnung", msg, "warning"); notificationStore.push("Socket-Warnung", msg, "warning");
@ -62,16 +66,20 @@ export const useConnectionStore = defineStore("connection", {
if (err.message == "xhr poll error") { if (err.message == "xhr poll error") {
notificationStore.push("Socket-Netzwerk-Fehler", "Reconnect Versuch in 10s", "error"); notificationStore.push("Socket-Netzwerk-Fehler", "Reconnect Versuch in 10s", "error");
this.performingManualReconnect = true;
this.disconnectClient(); this.disconnectClient();
setTimeout(() => { setTimeout(() => {
this.connectClient(); this.connectClient();
this.performingManualReconnect = false;
}, 10000); }, 10000);
} else if (err.message == "Token expired") { } else if (err.message == "Token expired") {
notificationStore.push("Session", "Session wird verlängert", "info"); notificationStore.push("Session", "Session wird verlängert", "info");
refreshToken() refreshToken()
.then(() => { .then(() => {
notificationStore.push("Session", "Session erfolgreich verlängert", "success"); notificationStore.push("Session", "Session erfolgreich verlängert", "success");
this.performingManualReconnect = true;
this.connection?.disconnect().connect(); this.connection?.disconnect().connect();
this.performingManualReconnect = false;
}) })
.catch(() => { .catch(() => {
notificationStore.push("Session-Fehler", "Anmeldung wurde nicht verlängert", "error"); notificationStore.push("Session-Fehler", "Anmeldung wurde nicht verlängert", "error");

View file

@ -1,13 +1,12 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { useConnectionStore } from "./connection"; import { useConnectionStore } from "./connection";
import * as Y from "yjs"; import * as Y from "yjs";
import * as AwarenessProtocol from "y-protocols/awareness.js";
export const useMissionDetailStore = defineStore("missionDetail", { export const useMissionDetailStore = defineStore("missionDetail", {
state: () => { state: () => {
return { return {
yDoc: new Y.Doc(), yDoc: new Y.Doc(),
awareness: undefined as undefined | AwarenessProtocol.Awareness, awareness: undefined as undefined,
docId: null as null | string, docId: null as null | string,
lastUpdateTimestamp: 0 as number, lastUpdateTimestamp: 0 as number,
connectionStatus: "disconnected", // 'disconnected', 'connecting', 'connected', 'syncing', 'synced' connectionStatus: "disconnected", // 'disconnected', 'connecting', 'connected', 'syncing', 'synced'
@ -18,8 +17,8 @@ export const useMissionDetailStore = defineStore("missionDetail", {
this.docId = docId; this.docId = docId;
this.lastUpdateTimestamp = this.loadLastUpdateFromLocalStorage(); this.lastUpdateTimestamp = this.loadLastUpdateFromLocalStorage();
this.awareness = new AwarenessProtocol.Awareness(this.yDoc); // this.awareness = new AwarenessProtocol.Awareness(this.yDoc);
this.awareness.setLocalStateField("user", { name: "hi", color: "#123456" }); // this.awareness.setLocalStateField("user", { name: "hi", color: "#123456" });
this.setupSocketHandlers(); this.setupSocketHandlers();
this.setupYjsObservers(); this.setupYjsObservers();
@ -80,16 +79,16 @@ export const useMissionDetailStore = defineStore("missionDetail", {
setupAwarenessObservers() { setupAwarenessObservers() {
if (!this.awareness) return; if (!this.awareness) return;
this.awareness.on("update", (update: { added: number[]; updated: number[]; removed: number[] }) => { // this.awareness.on("update", (update: { added: number[]; updated: number[]; removed: number[] }) => {
if (this.awareness != undefined) { // if (this.awareness != undefined) {
const changedClients = update.added.concat(update.updated).concat(update.removed); // const changedClients = update.added.concat(update.updated).concat(update.removed);
const connectionStore = useConnectionStore(); // const connectionStore = useConnectionStore();
connectionStore.connection?.emit( // connectionStore.connection?.emit(
"mission:sync-client-awareness", // "mission:sync-client-awareness",
Uint8Array.from(AwarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients)) // Uint8Array.from(AwarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients))
); // );
} // }
}); // });
}, },
joinDocument() { joinDocument() {
@ -120,10 +119,10 @@ export const useMissionDetailStore = defineStore("missionDetail", {
}, },
cleanup() { cleanup() {
if (this.awareness) { // if (this.awareness) {
AwarenessProtocol.removeAwarenessStates(this.awareness, [this.yDoc.clientID], "window unload"); // AwarenessProtocol.removeAwarenessStates(this.awareness, [this.yDoc.clientID], "window unload");
this.awareness.destroy(); // this.awareness.destroy();
} // }
this.yDoc.destroy(); this.yDoc.destroy();
this.yDoc = new Y.Doc(); this.yDoc = new Y.Doc();

View file

@ -1,22 +1,13 @@
<template> <template>
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto"> <div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
<div> <DetailFormInput title="Einsatztitel" v-model="title" />
<label for="title">Einsatztitel</label>
<input type="text" id="title" v-model="title" />
</div>
<div class="flex flex-col sm:flex-row gap-2"> <div class="flex flex-col sm:flex-row gap-2">
<ForceSelect title="Einsatzleiter" :available-forces="availableForces" v-model="command" /> <ForceSelect title="Einsatzleiter" :available-forces="availableForces" v-model="command" />
<ForceSelect title="Bericht Ersteller" :available-forces="availableForces" v-model="secretary" /> <ForceSelect title="Bericht Ersteller" :available-forces="availableForces" v-model="secretary" />
</div> </div>
<div class="flex flex-col sm:flex-row gap-2"> <div class="flex flex-col sm:flex-row gap-2">
<div class="grow"> <DetailFormInput title="Einsatzbeginn" v-model="start" type="datetime-local" growing />
<label for="start">Einsatzbeginn</label> <DetailFormInput title="Einsatzende" v-model="end" type="datetime-local" :min="start" growing />
<input type="datetime-local" id="start" v-model="start" />
</div>
<div class="grow">
<label for="end">Einsatzende</label>
<input type="datetime-local" id="end" v-model="end" :min="start" />
</div>
<div class="w-full sm:w-fit min-w-fit"> <div class="w-full sm:w-fit min-w-fit">
<p>Dauer</p> <p>Dauer</p>
<p <p
@ -26,41 +17,14 @@
</p> </p>
</div> </div>
</div> </div>
<div> <DetailFormInput title="Stichwort" v-model="mission_short" />
<label for="mission_short">Stichwort</label> <DetailFormInput title="Einsatzort" v-model="location" />
<input type="text" id="mission_short" v-model="mission_short" /> <DetailFormInput title="Weitere Anwesende (andere Wehren, Polizei, Rettungsdienst)" v-model="others" />
</div>
<div>
<label for="location">Einsatzort</label>
<input type="text" id="location" v-model="location" />
</div>
<div>
<label for="others">Weitere Anwesende (andere Wehren, Polizei, Rettungsdienst)</label>
<input type="text" id="others" v-model="others" />
</div>
<div class="flex flex-col sm:flex-row gap-2"> <div class="flex flex-col sm:flex-row gap-2">
<div class="w-full"> <DetailFormInput title="Anzahl getretteter Personen" type="number" v-model="rescued" min="0" />
<label for="rescued">Anzahl getretteter Personen</label> <DetailFormInput title="Anzahl geborgener Personen" type="number" v-model="recovered" min="0" />
<input type="number" id="rescued" min="0" v-model="rescued" />
</div>
<div class="w-full">
<label for="recovered">Anzahl geborgener Personen</label>
<input type="number" id="recovered" min="0" v-model="recovered" />
</div>
</div>
<div class="flex flex-col">
<label for="summary">Einsatzbeschreibung</label>
<QuillEditor
id="summary"
theme="snow"
style="height: 250px; max-height: 250px; min-height: 250px"
contentType="html"
:options="{ modules: moduleOptions }"
:enable="can('create', 'operation', 'mission')"
:style="!can('create', 'operation', 'mission') ? 'opacity: 75%; background: rgb(243 244 246)' : ''"
@ready="initEditor"
/>
</div> </div>
<DetailFormEditor title="Einsatzbeschreibung" :text="editor" />
<div class="flex flex-col"> <div class="flex flex-col">
<p>Eingesetzte Fahrzeuge</p> <p>Eingesetzte Fahrzeuge</p>
</div> </div>
@ -75,7 +39,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, type PropType } from "vue"; import { defineComponent, type PropType } from "vue";
import { mapActions, mapState, mapWritableState } from "pinia"; import { mapState } from "pinia";
import { useAbilityStore } from "@/stores/ability"; import { useAbilityStore } from "@/stores/ability";
import { Quill, QuillEditor } from "@vueup/vue-quill"; import { Quill, QuillEditor } from "@vueup/vue-quill";
import type QuillCursors from "quill-cursors"; import type QuillCursors from "quill-cursors";
@ -85,7 +49,9 @@ import { moduleOptions } from "@/helpers/quillConfig";
import ForceSelect from "@/components/admin/ForceSelect.vue"; import ForceSelect from "@/components/admin/ForceSelect.vue";
import { useForceStore } from "@/stores/admin/configuration/force"; import { useForceStore } from "@/stores/admin/configuration/force";
import * as Y from "yjs"; import * as Y from "yjs";
import type { Awareness } from "y-protocols/awareness.js"; import "@lottiefiles/lottie-player";
import DetailFormInput from "@/components/admin/operation/mission/DetailFormInput.vue";
import DetailFormEditor from "@/components/admin/operation/mission/DetailFormEditor.vue";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -96,16 +62,10 @@ export default defineComponent({
required: true, required: true,
}, },
awareness: { awareness: {
type: Object as PropType<Awareness | undefined>, type: Object as PropType<undefined>,
default: undefined, default: undefined,
}, },
}, },
data() {
return {
binding: undefined as undefined | QuillBinding,
cursors: undefined as undefined | QuillCursors,
};
},
computed: { computed: {
...mapState(useAbilityStore, ["can"]), ...mapState(useAbilityStore, ["can"]),
...mapState(useForceStore, ["availableForces"]), ...mapState(useForceStore, ["availableForces"]),
@ -135,10 +95,7 @@ export default defineComponent({
}, },
start: { start: {
get() { get() {
return ( return this.document.getMap("form").get("start") as string;
(this.document.getMap("form").get("start") as string) ||
new Date(new Date().setHours(new Date().getHours() + 1)).toISOString().slice(0, -8)
);
}, },
set(val: string) { set(val: string) {
this.document.getMap("form").set("start", val); this.document.getMap("form").set("start", val);
@ -165,7 +122,7 @@ export default defineComponent({
}, },
mission_short: { mission_short: {
get() { get() {
return this.document.getMap("form").get("mission_short"); return this.document.getMap("form").get("mission_short") as string;
}, },
set(val: string) { set(val: string) {
this.document.getMap("form").set("mission_short", val); this.document.getMap("form").set("mission_short", val);
@ -173,7 +130,7 @@ export default defineComponent({
}, },
location: { location: {
get() { get() {
return this.document.getMap("form").get("location"); return this.document.getMap("form").get("location") as string;
}, },
set(val: string) { set(val: string) {
this.document.getMap("form").set("location", val); this.document.getMap("form").set("location", val);
@ -181,7 +138,7 @@ export default defineComponent({
}, },
others: { others: {
get() { get() {
return this.document.getMap("form").get("others"); return this.document.getMap("form").get("others") as string;
}, },
set(val: string) { set(val: string) {
this.document.getMap("form").set("others", val); this.document.getMap("form").set("others", val);
@ -189,7 +146,7 @@ export default defineComponent({
}, },
rescued: { rescued: {
get() { get() {
return this.document.getMap("form").get("rescued") || 0; return (this.document.getMap("form").get("rescued") || "0") as string;
}, },
set(val: number) { set(val: number) {
this.document.getMap("form").set("rescued", val); this.document.getMap("form").set("rescued", val);
@ -197,7 +154,7 @@ export default defineComponent({
}, },
recovered: { recovered: {
get() { get() {
return this.document.getMap("form").get("recovered") || 0; return (this.document.getMap("form").get("recovered") || "0") as string;
}, },
set(val: number) { set(val: number) {
this.document.getMap("form").set("recovered", val); this.document.getMap("form").set("recovered", val);
@ -207,18 +164,5 @@ export default defineComponent({
return this.document.getText("editor"); return this.document.getText("editor");
}, },
}, },
beforeUnmount() {
if (this.binding) {
this.binding.destroy();
this.binding = undefined;
}
},
methods: {
initEditor(quill: Quill) {
quill.history.clear();
this.binding = new QuillBinding(this.document.getText("editor"), quill); //this.awareness
this.cursors = quill.getModule("cursors") as QuillCursors;
},
},
}); });
</script> </script>

View file

@ -27,7 +27,7 @@
</RouterLink> </RouterLink>
</div> </div>
<MissionDetail v-show="routeHash == '#edit'" :document="yDoc" :awareness="awareness" /> <MissionDetail v-show="routeHash == '#edit'" :document="yDoc" />
<MissionPresence v-show="routeHash == '#presence'" :document="yDoc" /> <MissionPresence v-show="routeHash == '#presence'" :document="yDoc" />
</div> </div>
</div> </div>
@ -82,7 +82,7 @@ export default defineComponent({
return this.$route.hash; return this.$route.hash;
}, },
editors() { editors() {
return this.awareness?.getStates() ?? []; return this.awareness ?? [];
}, },
}, },
mounted() { mounted() {
@ -90,6 +90,9 @@ export default defineComponent({
this.connectClient(); this.connectClient();
this.initialize(this.id); this.initialize(this.id);
this.getAvailableForces(); this.getAvailableForces();
window.addEventListener("beforeunload", () => {
localStorage.removeItem("yjsDoc_timestamp");
});
}, },
beforeUnmount() { beforeUnmount() {
this.cleanup(); this.cleanup();

View file

@ -87,22 +87,18 @@ export default defineComponent({
...mapState(useForceStore, ["availableForces"]), ...mapState(useForceStore, ["availableForces"]),
presence: { presence: {
get() { get() {
return this.document.getArray<string>("presence").toArray(); return Array.from(this.document.getMap<boolean>("presence").keys());
}, },
set(val: Array<string>) { set(val: Array<string>) {
let added = val.filter((e) => !this.document.getArray<string>("presence").toArray().includes(e)); let added = val.filter((e) => !this.document.getMap<boolean>("presence").has(e));
let removed = this.document let removed = Array.from(this.document.getMap<boolean>("presence").keys()).filter((e) => !val.includes(e));
.getArray<string>("presence")
.toArray() added.forEach((a) => {
.filter((e) => !val.includes(e)); this.document.getMap<boolean>("presence").set(a, true);
console.log(added, removed); });
this.document.getArray("presence").push(added);
removed.forEach((r) => { removed.forEach((r) => {
let index = this.document this.document.getMap<boolean>("presence").delete(r);
.getArray<string>("presence")
.toArray()
.findIndex((a) => a == r);
this.document.getArray("presence").delete(index, 1);
}); });
}, },
}, },

View file

@ -10,6 +10,11 @@ export default defineConfig({
plugins: [ plugins: [
vue({ vue({
include: [/\.vue$/, /\.md$/], include: [/\.vue$/, /\.md$/],
template: {
compilerOptions: {
isCustomElement: (tag) => ["lottie-player"].includes(tag),
},
},
}), }),
vueDevTools(), vueDevTools(),
VitePWA({ VitePWA({