use awareness info inside form inputs

This commit is contained in:
Julian Krauser 2025-03-04 15:03:34 +01:00
parent 12772bfcfa
commit 27e3ed525b
9 changed files with 135 additions and 23 deletions

32
package-lock.json generated
View file

@ -30,6 +30,7 @@
"uuid": "^9.0.0",
"vue": "^3.4.29",
"vue-router": "^4.3.3",
"vue-tippy": "^6.6.0",
"y-quill": "0.1.3",
"yjs": "^13.6.23"
},
@ -2680,6 +2681,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "15.2.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
@ -9333,6 +9344,15 @@
"node": ">=0.8"
}
},
"node_modules/tippy.js": {
"version": "6.3.7",
"resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz",
"integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==",
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.9.0"
}
},
"node_modules/to-data-view": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/to-data-view/-/to-data-view-1.1.0.tgz",
@ -9953,6 +9973,18 @@
"vue": "^3.2.0"
}
},
"node_modules/vue-tippy": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/vue-tippy/-/vue-tippy-6.6.0.tgz",
"integrity": "sha512-ISRIUQDlcEP05K1nCbvlVcd8yuWS6S3dI91qD0A2slgtwwWjih8Fn9Aymq4SNaHQsdiP5+MLRPZVDxFjKMPgKA==",
"license": "MIT",
"dependencies": {
"tippy.js": "^6.3.7"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/vue-tsc": {
"version": "2.0.29",
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.29.tgz",

View file

@ -45,6 +45,7 @@
"uuid": "^9.0.0",
"vue": "^3.4.29",
"vue-router": "^4.3.3",
"vue-tippy": "^6.6.0",
"y-quill": "0.1.3",
"yjs": "^13.6.23"
},

View file

@ -28,6 +28,7 @@ import { QuillBinding } from "y-quill";
import { moduleOptions } from "@/helpers/quillConfig";
import * as Y from "yjs";
import "@lottiefiles/lottie-player";
import { Awareness } from "@/helpers/awareness";
</script>
<script lang="ts">
@ -37,6 +38,10 @@ export default defineComponent({
type: Object as PropType<Y.Text>,
required: true,
},
awareness: {
type: Object as PropType<Partial<Awareness>>,
required: true,
},
title: {
type: String,
default: "",

View file

@ -2,16 +2,23 @@
<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 />
<lottie-player
v-if="currentEditors.length != 0"
src="/typing_animation.json"
class="w-fit h-5"
loop
autoplay
v-tippy="currentEditors.map((c) => c.username).join(', ')"
/>
</div>
<input :type="type" :id="title" v-model="value" :min="min" />
<input :type="type" :id="title" v-model="value" :min="min" @focus="focused" @blur="blured" />
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapActions, mapState, mapWritableState } from "pinia";
import { defineComponent, type PropType } from "vue";
import "@lottiefiles/lottie-player";
import type { Awareness } from "@/helpers/awareness";
</script>
<script lang="ts">
@ -37,8 +44,17 @@ export default defineComponent({
type: String,
default: "",
},
awareness: {
type: Object as PropType<Awareness>,
required: true,
},
},
emits: ["update:model-value"],
data() {
return {
typing: false,
};
},
computed: {
value: {
get() {
@ -48,6 +64,28 @@ export default defineComponent({
this.$emit("update:model-value", val);
},
},
currentEditors() {
return this.awareness.getEditorObjsByField(this.title);
},
},
mounted() {
this.awareness.emitter.on("change", (d) => {});
},
methods: {
focused() {
this.awareness.publishMyState({
field: this.title,
cursor: undefined,
range: undefined,
});
},
blured() {
this.awareness.publishMyState({
field: "blured user",
cursor: undefined,
range: undefined,
});
},
},
});
</script>

View file

@ -17,11 +17,12 @@ export type AwarenessActions = "update" | "remove";
export type AwarenessEvents = {
update: { data: EditorState };
change: { socketId: string; action: AwarenessActions } & EditorState;
};
export class Awareness {
private editors = new Map<string, Editor>();
private editorStates = new Map<string, EditorState>();
public readonly editors = new Map<string, Editor>();
public readonly editorStates = new Map<string, EditorState>();
public readonly emitter = mitt<AwarenessEvents>();
public getEditors() {
@ -38,6 +39,12 @@ export class Awareness {
.map(([key, val]) => key);
}
public getEditorObjsByField(field: string) {
return Array.from(this.editors.entries())
.filter(([key, val]) => this.getEditorsByField(field).includes(key))
.map(([key, val]) => val);
}
public getEditorStates() {
return this.editorStates;
}
@ -56,6 +63,7 @@ export class Awareness {
} else if (action == "remove") {
this.editorStates.delete(socketId);
}
this.emitter.emit("change", { socketId, action, ...data });
}
public publishMyState(data: EditorState) {

View file

@ -7,6 +7,8 @@ import NProgress from "nprogress";
import "../node_modules/nprogress/nprogress.css";
import { Quill } from "@vueup/vue-quill";
import QuillCursors from "quill-cursors";
import VueTippy from "vue-tippy";
import "tippy.js/dist/tippy.css";
import { http } from "./serverCom";
import "./main.css";
@ -19,6 +21,7 @@ const app = createApp(App);
app.use(createPinia());
app.use(router);
app.use(VueTippy, { theme: "light", defaultProps: { placement: "right" } });
app.config.globalProperties.$http = http;
app.config.globalProperties.$progress = NProgress;

View file

@ -1,13 +1,20 @@
<template>
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
<DetailFormInput title="Einsatztitel" v-model="title" />
<DetailFormInput title="Einsatztitel" v-model="title" :awareness="awareness" />
<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">
<DetailFormInput title="Einsatzbeginn" v-model="start" type="datetime-local" growing />
<DetailFormInput title="Einsatzende" v-model="end" type="datetime-local" :min="start" growing />
<DetailFormInput title="Einsatzbeginn" v-model="start" type="datetime-local" growing :awareness="awareness" />
<DetailFormInput
title="Einsatzende"
v-model="end"
type="datetime-local"
:min="start"
growing
:awareness="awareness"
/>
<div class="w-full sm:w-fit min-w-fit">
<p>Dauer</p>
<p
@ -17,14 +24,30 @@
</p>
</div>
</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" />
<DetailFormInput title="Stichwort" v-model="mission_short" :awareness="awareness" />
<DetailFormInput title="Einsatzort" v-model="location" :awareness="awareness" />
<DetailFormInput
title="Weitere Anwesende (andere Wehren, Polizei, Rettungsdienst)"
v-model="others"
:awareness="awareness"
/>
<div class="flex flex-col sm:flex-row gap-2">
<DetailFormInput title="Anzahl getretteter Personen" type="number" v-model="rescued" min="0" />
<DetailFormInput title="Anzahl geborgener Personen" type="number" v-model="recovered" min="0" />
<DetailFormInput
title="Anzahl getretteter Personen"
type="number"
v-model="rescued"
min="0"
:awareness="awareness"
/>
<DetailFormInput
title="Anzahl geborgener Personen"
type="number"
v-model="recovered"
min="0"
:awareness="awareness"
/>
</div>
<DetailFormEditor title="Einsatzbeschreibung" :text="editor" />
<DetailFormEditor title="Einsatzbeschreibung" :text="editor" :awareness="awareness" />
<div class="flex flex-col">
<p>Eingesetzte Fahrzeuge</p>
</div>
@ -41,17 +64,14 @@
import { defineComponent, type PropType } from "vue";
import { mapState } from "pinia";
import { useAbilityStore } from "@/stores/ability";
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 ForceSelect from "@/components/admin/ForceSelect.vue";
import { useForceStore } from "@/stores/admin/configuration/force";
import * as Y from "yjs";
import "@lottiefiles/lottie-player";
import DetailFormInput from "@/components/admin/operation/mission/DetailFormInput.vue";
import DetailFormEditor from "@/components/admin/operation/mission/DetailFormEditor.vue";
import { Awareness } from "@/helpers/awareness";
</script>
<script lang="ts">
@ -62,8 +82,8 @@ export default defineComponent({
required: true,
},
awareness: {
type: Object as PropType<undefined>,
default: undefined,
type: Object as PropType<Awareness>,
required: true,
},
},
computed: {

View file

@ -27,8 +27,8 @@
</RouterLink>
</div>
<MissionDetail v-show="routeHash == '#edit'" :document="yDoc" />
<MissionPresence v-show="routeHash == '#presence'" :document="yDoc" />
<MissionDetail v-show="routeHash == '#edit'" :document="yDoc" :awareness="awareness" />
<MissionPresence v-show="routeHash == '#presence'" :document="yDoc" :awareness="awareness" />
</div>
</div>
</template>

View file

@ -68,6 +68,7 @@ import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { useForceStore } from "@/stores/admin/configuration/force";
import { mapState } from "pinia";
import * as Y from "yjs";
import { Awareness } from "@/helpers/awareness";
</script>
<script lang="ts">
@ -77,6 +78,10 @@ export default defineComponent({
type: Object as PropType<Y.Doc>,
required: true,
},
awareness: {
type: Object as PropType<Partial<Awareness>>,
required: true,
},
},
data() {
return {