awareness on quill editor

This commit is contained in:
Julian Krauser 2025-03-05 09:52:21 +01:00
parent 27e3ed525b
commit e9e38db606
5 changed files with 79 additions and 10 deletions

View file

@ -35,6 +35,14 @@ export default defineComponent({
localStorage.removeItem("refresh_token"); localStorage.removeItem("refresh_token");
}); });
} }
window.addEventListener("online", function (e) {
e.preventDefault();
});
},
beforeUnmount() {
window.removeEventListener("online", function (e) {
e.preventDefault();
});
}, },
}); });
</script> </script>

View file

@ -2,7 +2,14 @@
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-row gap-2 items-center"> <div class="flex flex-row gap-2 items-center">
<label :for="title">{{ title }}</label> <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> </div>
<QuillEditor <QuillEditor
:id="title" :id="title"
@ -11,6 +18,8 @@
contentType="html" contentType="html"
:options="{ modules: moduleOptions }" :options="{ modules: moduleOptions }"
@ready="initEditor" @ready="initEditor"
@selectionChange="selectionChange"
@blur="blured"
/> />
<!-- <!--
:enable="can('create', 'operation', 'mission')" :enable="can('create', 'operation', 'mission')"
@ -28,7 +37,7 @@ import { QuillBinding } from "y-quill";
import { moduleOptions } from "@/helpers/quillConfig"; import { moduleOptions } from "@/helpers/quillConfig";
import * as Y from "yjs"; import * as Y from "yjs";
import "@lottiefiles/lottie-player"; import "@lottiefiles/lottie-player";
import { Awareness } from "@/helpers/awareness"; import { Awareness, type AwarenessActions, type EditorRange, type EditorState } from "@/helpers/awareness";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -39,7 +48,7 @@ export default defineComponent({
required: true, required: true,
}, },
awareness: { awareness: {
type: Object as PropType<Partial<Awareness>>, type: Object as PropType<Awareness>,
required: true, required: true,
}, },
title: { title: {
@ -53,18 +62,64 @@ export default defineComponent({
cursors: undefined as undefined | QuillCursors, cursors: undefined as undefined | QuillCursors,
}; };
}, },
computed: {
currentEditors() {
return this.awareness.getEditorObjsByField(this.title);
},
},
mounted() {
this.awareness.emitter.on("change", this.handleChange);
this.awareness.emitter.on("clear", this.clear);
},
beforeUnmount() { beforeUnmount() {
if (this.binding) { if (this.binding) {
this.binding.destroy(); this.binding.destroy();
this.binding = undefined; this.binding = undefined;
} }
this.awareness.emitter.off("change", this.handleChange);
this.awareness.emitter.off("clear", this.clear);
}, },
methods: { methods: {
initEditor(quill: Quill) { initEditor(quill: Quill) {
quill.history.clear(); quill.history.clear();
this.binding = new QuillBinding(this.text, quill); // TODO: awareness this.binding = new QuillBinding(this.text, quill);
this.cursors = quill.getModule("cursors") as QuillCursors; this.cursors = quill.getModule("cursors") as QuillCursors;
}, },
selectionChange({ range, oldRange, source }: { range: EditorRange; oldRange: any; source: string }) {
if (range != null) {
this.awareness.publishMyState({
field: this.title,
range: range,
});
}
},
blured() {
this.awareness.publishMyState({
field: "blured user",
});
},
handleChange(
d: {
socketId: string;
action: AwarenessActions;
} & EditorState
) {
if (d.action == "remove") {
this.cursors?.removeCursor(d.socketId);
}
if (d.field == this.title) {
let user = this.awareness.getEditor(d.socketId);
if (!user || !d.range) return;
this.cursors?.createCursor(d.socketId, user.username, user.color);
this.cursors?.moveCursor(d.socketId, d.range);
this.cursors?.toggleFlag(d.socketId, true);
} else if (d.field == "blured user") {
this.cursors?.removeCursor(d.socketId);
}
},
clear() {
this.cursors?.clearCursors();
},
}, },
}); });
</script> </script>

View file

@ -75,15 +75,11 @@ export default defineComponent({
focused() { focused() {
this.awareness.publishMyState({ this.awareness.publishMyState({
field: this.title, field: this.title,
cursor: undefined,
range: undefined,
}); });
}, },
blured() { blured() {
this.awareness.publishMyState({ this.awareness.publishMyState({
field: "blured user", field: "blured user",
cursor: undefined,
range: undefined,
}); });
}, },
}, },

View file

@ -9,8 +9,12 @@ export interface Editor {
export interface EditorState { export interface EditorState {
field: string; field: string;
cursor: any; cursor?: number;
range: any; range?: EditorRange;
}
export interface EditorRange {
index: number;
length: number;
} }
export type AwarenessActions = "update" | "remove"; export type AwarenessActions = "update" | "remove";
@ -18,6 +22,7 @@ export type AwarenessActions = "update" | "remove";
export type AwarenessEvents = { export type AwarenessEvents = {
update: { data: EditorState }; update: { data: EditorState };
change: { socketId: string; action: AwarenessActions } & EditorState; change: { socketId: string; action: AwarenessActions } & EditorState;
clear: void;
}; };
export class Awareness { export class Awareness {
@ -73,6 +78,7 @@ export class Awareness {
public reset() { public reset() {
this.editors.clear(); this.editors.clear();
this.editorStates.clear(); this.editorStates.clear();
this.emitter.emit("clear");
} }
public destroy() { public destroy() {

View file

@ -149,3 +149,7 @@ summary::-webkit-details-marker {
@apply !order-1; @apply !order-1;
} }
} }
.ql-cursor-caret-container {
z-index: 1 !important;
}