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": {
"@headlessui/vue": "^1.7.13",
"@heroicons/vue": "^2.1.5",
"@lottiefiles/lottie-player": "^2.0.12",
"@vueup/vue-quill": "^1.2.0",
"axios": "^1.7.9",
"jwt-decode": "^4.0.0",
@ -28,7 +29,6 @@
"uuid": "^9.0.0",
"vue": "^3.4.29",
"vue-router": "^4.3.3",
"y-protocols": "^1.0.6",
"y-quill": "0.1.3",
"yjs": "^13.6.23"
},
@ -2587,6 +2587,34 @@
"@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": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -3108,6 +3136,12 @@
"integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==",
"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": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz",
@ -3139,8 +3173,7 @@
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
},
"node_modules/@types/uuid": {
"version": "9.0.8",
@ -6885,6 +6918,37 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"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": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -6948,6 +7012,12 @@
"integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==",
"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": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -7440,6 +7510,12 @@
"integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==",
"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": {
"version": "1.1.4",
"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",
"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": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",

View file

@ -26,6 +26,7 @@
"dependencies": {
"@headlessui/vue": "^1.7.13",
"@heroicons/vue": "^2.1.5",
"@lottiefiles/lottie-player": "^2.0.12",
"@vueup/vue-quill": "^1.2.0",
"axios": "^1.7.9",
"jwt-decode": "^4.0.0",
@ -43,7 +44,6 @@
"uuid": "^9.0.0",
"vue": "^3.4.29",
"vue-router": "^4.3.3",
"y-protocols": "^1.0.6",
"y-quill": "0.1.3",
"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 {
connection: undefined as undefined | Socket,
connected: false as boolean,
performingManualReconnect: false as boolean,
};
},
getters: {
connectionStatus: (state) => state.connected,
socketId: (state) => state.connection?.id,
},
actions: {
connectClient(): void {
@ -19,7 +21,7 @@ export const useConnectionStore = defineStore("connection", {
const notificationStore = useNotificationStore();
this.connection?.disconnect();
this.connection = io(url, {
reconnection: true,
reconnection: false,
reconnectionDelayMax: 1000,
reconnectionAttempts: 1,
auth: (cb) => {
@ -42,7 +44,9 @@ export const useConnectionStore = defineStore("connection", {
this.connection.on("disconnect", () => {
this.connected = false;
this.$reset();
this.connectClient();
if (!this.performingManualReconnect) {
this.connectClient();
}
});
this.connection.on("warning", (msg: string) => {
notificationStore.push("Socket-Warnung", msg, "warning");
@ -62,16 +66,20 @@ export const useConnectionStore = defineStore("connection", {
if (err.message == "xhr poll error") {
notificationStore.push("Socket-Netzwerk-Fehler", "Reconnect Versuch in 10s", "error");
this.performingManualReconnect = true;
this.disconnectClient();
setTimeout(() => {
this.connectClient();
this.performingManualReconnect = false;
}, 10000);
} else if (err.message == "Token expired") {
notificationStore.push("Session", "Session wird verlängert", "info");
refreshToken()
.then(() => {
notificationStore.push("Session", "Session erfolgreich verlängert", "success");
this.performingManualReconnect = true;
this.connection?.disconnect().connect();
this.performingManualReconnect = false;
})
.catch(() => {
notificationStore.push("Session-Fehler", "Anmeldung wurde nicht verlängert", "error");

View file

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

View file

@ -1,22 +1,13 @@
<template>
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
<div>
<label for="title">Einsatztitel</label>
<input type="text" id="title" v-model="title" />
</div>
<DetailFormInput title="Einsatztitel" v-model="title" />
<div class="flex flex-col sm:flex-row gap-2">
<ForceSelect title="Einsatzleiter" :available-forces="availableForces" v-model="command" />
<ForceSelect title="Bericht Ersteller" :available-forces="availableForces" v-model="secretary" />
</div>
<div class="flex flex-col sm:flex-row gap-2">
<div class="grow">
<label for="start">Einsatzbeginn</label>
<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>
<DetailFormInput title="Einsatzbeginn" v-model="start" type="datetime-local" growing />
<DetailFormInput title="Einsatzende" v-model="end" type="datetime-local" :min="start" growing />
<div class="w-full sm:w-fit min-w-fit">
<p>Dauer</p>
<p
@ -26,41 +17,14 @@
</p>
</div>
</div>
<div>
<label for="mission_short">Stichwort</label>
<input type="text" id="mission_short" v-model="mission_short" />
</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>
<DetailFormInput title="Stichwort" v-model="mission_short" />
<DetailFormInput title="Einsatzort" v-model="location" />
<DetailFormInput title="Weitere Anwesende (andere Wehren, Polizei, Rettungsdienst)" v-model="others" />
<div class="flex flex-col sm:flex-row gap-2">
<div class="w-full">
<label for="rescued">Anzahl getretteter Personen</label>
<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"
/>
<DetailFormInput title="Anzahl getretteter Personen" type="number" v-model="rescued" min="0" />
<DetailFormInput title="Anzahl geborgener Personen" type="number" v-model="recovered" min="0" />
</div>
<DetailFormEditor title="Einsatzbeschreibung" :text="editor" />
<div class="flex flex-col">
<p>Eingesetzte Fahrzeuge</p>
</div>
@ -75,7 +39,7 @@
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { mapActions, mapState, mapWritableState } from "pinia";
import { mapState } from "pinia";
import { useAbilityStore } from "@/stores/ability";
import { Quill, QuillEditor } from "@vueup/vue-quill";
import type QuillCursors from "quill-cursors";
@ -85,7 +49,9 @@ import { moduleOptions } from "@/helpers/quillConfig";
import ForceSelect from "@/components/admin/ForceSelect.vue";
import { useForceStore } from "@/stores/admin/configuration/force";
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 lang="ts">
@ -96,16 +62,10 @@ export default defineComponent({
required: true,
},
awareness: {
type: Object as PropType<Awareness | undefined>,
type: Object as PropType<undefined>,
default: undefined,
},
},
data() {
return {
binding: undefined as undefined | QuillBinding,
cursors: undefined as undefined | QuillCursors,
};
},
computed: {
...mapState(useAbilityStore, ["can"]),
...mapState(useForceStore, ["availableForces"]),
@ -135,10 +95,7 @@ export default defineComponent({
},
start: {
get() {
return (
(this.document.getMap("form").get("start") as string) ||
new Date(new Date().setHours(new Date().getHours() + 1)).toISOString().slice(0, -8)
);
return this.document.getMap("form").get("start") as string;
},
set(val: string) {
this.document.getMap("form").set("start", val);
@ -165,7 +122,7 @@ export default defineComponent({
},
mission_short: {
get() {
return this.document.getMap("form").get("mission_short");
return this.document.getMap("form").get("mission_short") as string;
},
set(val: string) {
this.document.getMap("form").set("mission_short", val);
@ -173,7 +130,7 @@ export default defineComponent({
},
location: {
get() {
return this.document.getMap("form").get("location");
return this.document.getMap("form").get("location") as string;
},
set(val: string) {
this.document.getMap("form").set("location", val);
@ -181,7 +138,7 @@ export default defineComponent({
},
others: {
get() {
return this.document.getMap("form").get("others");
return this.document.getMap("form").get("others") as string;
},
set(val: string) {
this.document.getMap("form").set("others", val);
@ -189,7 +146,7 @@ export default defineComponent({
},
rescued: {
get() {
return this.document.getMap("form").get("rescued") || 0;
return (this.document.getMap("form").get("rescued") || "0") as string;
},
set(val: number) {
this.document.getMap("form").set("rescued", val);
@ -197,7 +154,7 @@ export default defineComponent({
},
recovered: {
get() {
return this.document.getMap("form").get("recovered") || 0;
return (this.document.getMap("form").get("recovered") || "0") as string;
},
set(val: number) {
this.document.getMap("form").set("recovered", val);
@ -207,18 +164,5 @@ export default defineComponent({
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>

View file

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

View file

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

View file

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