mirror of
https://github.com/versia-pub/frontend.git
synced 2026-06-14 15:39:15 +02:00
chore: ⬆️ Upgrade to Nuxt 4
Some checks failed
Some checks failed
This commit is contained in:
parent
8debe97f63
commit
7f7cf20311
386 changed files with 2376 additions and 2332 deletions
189
app/components/editor/suggestion.ts
Normal file
189
app/components/editor/suggestion.ts
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
import { computePosition, flip, shift } from "@floating-ui/dom";
|
||||
import type { Editor } from "@tiptap/core";
|
||||
import type { MentionNodeAttrs } from "@tiptap/extension-mention";
|
||||
import type { SuggestionOptions } from "@tiptap/suggestion";
|
||||
import { posToDOMRect, VueRenderer } from "@tiptap/vue-3";
|
||||
import type { Account, CustomEmoji } from "@versia/client/schemas";
|
||||
import { go } from "fuzzysort";
|
||||
import type { z } from "zod";
|
||||
import EmojiList from "./emojis-list.vue";
|
||||
import MentionList from "./mentions-list.vue";
|
||||
|
||||
export type UserData = {
|
||||
key: string;
|
||||
value: z.infer<typeof Account>;
|
||||
};
|
||||
|
||||
const updatePosition = (editor: Editor, element: HTMLElement): void => {
|
||||
const virtualElement = {
|
||||
getBoundingClientRect: () =>
|
||||
posToDOMRect(
|
||||
editor.view,
|
||||
editor.state.selection.from,
|
||||
editor.state.selection.to,
|
||||
),
|
||||
};
|
||||
|
||||
computePosition(virtualElement, element, {
|
||||
placement: "bottom-start",
|
||||
strategy: "absolute",
|
||||
middleware: [shift(), flip()],
|
||||
}).then(({ x, y, strategy }) => {
|
||||
element.style.width = "max-content";
|
||||
element.style.position = strategy;
|
||||
element.style.left = `${x}px`;
|
||||
element.style.top = `${y}px`;
|
||||
});
|
||||
};
|
||||
|
||||
export const mentionSuggestion = {
|
||||
items: async ({ query }) => {
|
||||
if (query.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const users = await client.value.searchAccount(query, { limit: 20 });
|
||||
|
||||
return go(
|
||||
query,
|
||||
users.data
|
||||
// Deduplicate users
|
||||
.filter(
|
||||
(user, index, self) =>
|
||||
self.findIndex((u) => u.acct === user.acct) === index,
|
||||
)
|
||||
.map((user) => ({
|
||||
key: user.acct,
|
||||
value: user,
|
||||
})),
|
||||
{ key: "key" },
|
||||
)
|
||||
.map((result) => ({
|
||||
key: result.obj.key,
|
||||
value: result.obj.value,
|
||||
}))
|
||||
.slice(0, 20);
|
||||
},
|
||||
|
||||
render: () => {
|
||||
let component: VueRenderer;
|
||||
|
||||
return {
|
||||
onStart: (props) => {
|
||||
component = new VueRenderer(MentionList, {
|
||||
props,
|
||||
editor: props.editor,
|
||||
});
|
||||
|
||||
if (!props.clientRect || !component.element) {
|
||||
return;
|
||||
}
|
||||
|
||||
(component.element as HTMLElement).style.position = "absolute";
|
||||
|
||||
props.editor.view.dom.parentElement?.appendChild(
|
||||
component.element,
|
||||
);
|
||||
|
||||
updatePosition(props.editor, component.element as HTMLElement);
|
||||
},
|
||||
|
||||
onUpdate(props) {
|
||||
component.updateProps(props);
|
||||
|
||||
if (!props.clientRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
updatePosition(props.editor, component.element as HTMLElement);
|
||||
},
|
||||
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === "Escape") {
|
||||
component.destroy();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return component.ref?.onKeyDown(props);
|
||||
},
|
||||
|
||||
onExit() {
|
||||
component.element?.remove();
|
||||
component.destroy();
|
||||
},
|
||||
};
|
||||
},
|
||||
} as Omit<SuggestionOptions<UserData, MentionNodeAttrs>, "editor">;
|
||||
|
||||
export const emojiSuggestion = {
|
||||
items: ({ query }) => {
|
||||
if (query.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const emojis = (identity.value as Identity).emojis;
|
||||
|
||||
return go(
|
||||
query,
|
||||
emojis
|
||||
.filter((emoji) => emoji.shortcode.includes(query))
|
||||
.map((emoji) => ({
|
||||
key: emoji.shortcode,
|
||||
value: emoji,
|
||||
})),
|
||||
{ key: "key" },
|
||||
)
|
||||
.map((result) => result.obj.key)
|
||||
.slice(0, 20);
|
||||
},
|
||||
render: () => {
|
||||
let component: VueRenderer;
|
||||
|
||||
return {
|
||||
onStart: (props) => {
|
||||
component = new VueRenderer(EmojiList, {
|
||||
props,
|
||||
editor: props.editor,
|
||||
});
|
||||
|
||||
if (!props.clientRect || !component.element) {
|
||||
return;
|
||||
}
|
||||
|
||||
(component.element as HTMLElement).style.position = "absolute";
|
||||
|
||||
props.editor.view.dom.parentElement?.appendChild(
|
||||
component.element,
|
||||
);
|
||||
|
||||
updatePosition(props.editor, component.element as HTMLElement);
|
||||
},
|
||||
|
||||
onUpdate(props) {
|
||||
component.updateProps(props);
|
||||
|
||||
if (!props.clientRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
updatePosition(props.editor, component.element as HTMLElement);
|
||||
},
|
||||
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === "Escape") {
|
||||
component.destroy();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return component.ref?.onKeyDown(props);
|
||||
},
|
||||
|
||||
onExit() {
|
||||
component.element?.remove();
|
||||
component.destroy();
|
||||
},
|
||||
};
|
||||
},
|
||||
} as Omit<SuggestionOptions<string>, "editor">;
|
||||
Loading…
Add table
Add a link
Reference in a new issue