From 3afc7c7febb071697e1b773a30716730701fd33c Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Sun, 2 May 2021 19:27:23 +0200 Subject: [PATCH] Fix mentions Signed-off-by: Thomas Citharel --- js/package.json | 1 + js/src/components/Editor.vue | 532 ++++++++--------------- js/src/components/Editor/Mention.ts | 36 +- js/src/components/Editor/MentionList.vue | 23 +- js/src/components/Editor/style.scss | 58 +++ js/src/types/actor/actor.model.ts | 10 +- js/yarn.lock | 5 + 7 files changed, 291 insertions(+), 374 deletions(-) create mode 100644 js/src/components/Editor/style.scss diff --git a/js/package.json b/js/package.json index 58f6790dd..ceb5ab3c4 100644 --- a/js/package.json +++ b/js/package.json @@ -48,6 +48,7 @@ "leaflet.locatecontrol": "^0.73.0", "lodash": "^4.17.11", "ngeohash": "^0.6.3", + "p-debounce": "^4.0.0", "phoenix": "^1.4.11", "register-service-worker": "^1.7.1", "tippy.js": "^6.2.3", diff --git a/js/src/components/Editor.vue b/js/src/components/Editor.vue index a338f9a18..1ede50769 100644 --- a/js/src/components/Editor.vue +++ b/js/src/components/Editor.vue @@ -6,206 +6,173 @@ id="tiptab-editor" :data-actor-id="currentActor && currentActor.id" > -
- - +
-
- -
- {{ $t("No profiles found") }} -
-
@@ -216,8 +183,6 @@ import { defaultExtensions } from "@tiptap/starter-kit"; import Document from "@tiptap/extension-document"; import Paragraph from "@tiptap/extension-paragraph"; import Text from "@tiptap/extension-text"; -import tippy, { Instance, sticky } from "tippy.js"; -// import { SEARCH_PERSONS } from "../graphql/search"; import { Actor, IActor, IPerson } from "../types/actor"; import CustomImage from "./Editor/Image"; import { UPLOAD_MEDIA } from "../graphql/upload"; @@ -251,19 +216,6 @@ export default class EditorComponent extends Vue { editor: Editor | null = null; - /** - * Editor Suggestions - */ - query!: string | null; - - filteredActors: IActor[] = []; - - suggestionRange!: Record | null; - - navigatedActorIndex = 0; - - popup!: Instance[] | null; - get isDescriptionMode(): boolean { return this.mode === "description" || this.isBasicMode; } @@ -276,14 +228,6 @@ export default class EditorComponent extends Vue { return this.isBasicMode; } - get hasResults(): boolean { - return this.filteredActors.length > 0; - } - - get showSuggestions(): boolean { - return (this.query || this.hasResults) as boolean; - } - get isBasicMode(): boolean { return this.mode === "basic"; } @@ -312,11 +256,11 @@ export default class EditorComponent extends Vue { }), ...defaultExtensions(), ], - onUpdate: ({ editor }) => { - this.$emit("input", editor.getHTML()); + content: this.value, + onUpdate: () => { + this.$emit("input", this.editor?.getHTML()); }, }); - this.editor.commands.setContent(this.value); } @Watch("value") @@ -327,8 +271,10 @@ export default class EditorComponent extends Vue { } } - // eslint-disable-next-line @typescript-eslint/ban-types - showLinkMenu(): Function | undefined { + /** + * Show a popup to get the link from the URL + */ + showLinkMenu(): void { this.$buefy.dialog.prompt({ message: this.$t("Enter the link URL") as string, inputAttrs: { @@ -340,106 +286,11 @@ export default class EditorComponent extends Vue { this.editor.chain().focus().setLink({ href: value }).run(); }, }); - return undefined; - } - - upHandler(): void { - this.navigatedActorIndex = - (this.navigatedActorIndex + this.filteredActors.length - 1) % - this.filteredActors.length; - } - - /** - * navigate to the next item - * if it's the last item, navigate to the first one - */ - downHandler(): void { - this.navigatedActorIndex = - (this.navigatedActorIndex + 1) % this.filteredActors.length; - } - - enterHandler(): void { - const actor = this.filteredActors[this.navigatedActorIndex]; - if (actor) { - this.selectActor(actor); - } - } - - /** - * we have to replace our suggestion text with a mention - * so it's important to pass also the position of your suggestion text - * @param actor IActor - */ - selectActor(actor: IActor): void { - const actorModel = new Actor(actor); - this.insertMention({ - range: this.suggestionRange, - attrs: { - id: actorModel.id, - // usernameWithDomain returns with a @ prefix and tiptap adds one itself - label: actorModel.usernameWithDomain().substring(1), - }, - }); - if (!this.editor) return; - this.editor.commands.focus(); - } - - /** We use this to programatically insert an actor mention when creating a reply to comment */ - replyToComment(comment: IComment): void { - if (!comment.actor) return; - // const actorModel = new Actor(comment.actor); - if (!this.editor) return; - // this.editor.commands.mention({ - // id: actorModel.id, - // label: actorModel.usernameWithDomain().substring(1), - // }); - this.editor.commands.focus(); - } - - /** - * renders a popup with suggestions - * tiptap provides a virtualNode object for using popper.js (or tippy.js) for popups - * @param node - */ - renderPopup(node: Element): void { - if (this.popup) { - return; - } - this.popup = tippy("#mobilizon", { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - getReferenceClientRect: node.getBoundingClientRect, - appendTo: () => document.body, - content: this.$refs.suggestions as HTMLElement, - trigger: "mouseenter", - interactive: true, - sticky: true, // make sure position of tippy is updated when content changes - plugins: [sticky], - showOnCreate: true, - theme: "dark", - placement: "top-start", - inertia: true, - duration: [400, 200], - }) as Instance[]; - } - - destroyPopup(): void { - if (this.popup) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this.popup[0].destroy(); - this.popup = null; - } - if (this.observer) { - this.observer.disconnect(); - } } /** * Show a file prompt, upload picture and insert it into editor - * @param command */ - // eslint-disable-next-line @typescript-eslint/ban-types async showImagePrompt(): Promise { const image = await listenFileUpload(); try { @@ -470,14 +321,28 @@ export default class EditorComponent extends Vue { } } - beforeDestroy(): void { + /** + * We use this to programatically insert an actor mention when creating a reply to comment + */ + replyToComment(comment: IComment): void { + if (!comment.actor) return; + // const actorModel = new Actor(comment.actor); if (!this.editor) return; - this.destroyPopup(); - this.editor.destroy(); + // this.editor.commands.mention({ + // id: actorModel.id, + // label: actorModel.usernameWithDomain().substring(1), + // }); + this.editor.commands.focus(); + } + + beforeDestroy(): void { + this.editor?.destroy(); } } diff --git a/js/src/components/Editor/style.scss b/js/src/components/Editor/style.scss new file mode 100644 index 000000000..cced5a400 --- /dev/null +++ b/js/src/components/Editor/style.scss @@ -0,0 +1,58 @@ +/** + * From https://www.tiptap.dev/api/editor/#inject-css + * https://github.com/ueberdosis/tiptap/blob/main/packages/core/src/style.ts + */ + +.ProseMirror { + position: relative; + word-wrap: break-word; + white-space: pre-wrap; + -webkit-font-variant-ligatures: none; + font-variant-ligatures: none; + + & [contenteditable="false"] { + white-space: normal; + } + & [contenteditable="false"] [contenteditable="true"] { + white-space: pre-wrap; + } + pre { + white-space: pre-wrap; + } +} +.ProseMirror-gapcursor { + display: none; + pointer-events: none; + position: absolute; + + &:after { + content: ""; + display: block; + position: absolute; + top: -2px; + width: 20px; + border-top: 1px solid black; + animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite; + } +} +@keyframes ProseMirror-cursor-blink { + to { + visibility: hidden; + } +} +.ProseMirror-hideselection * { + &::selection { + background: transparent; + } + &::-moz-selection { + background: transparent; + } + caret-color: transparent; +} + +.ProseMirror-focused .ProseMirror-gapcursor { + display: block; +} +.tippy-box[data-animation="fade"][data-state="hidden"] { + opacity: 0; +} diff --git a/js/src/types/actor/actor.model.ts b/js/src/types/actor/actor.model.ts index 42b2680af..df1ed3cae 100644 --- a/js/src/types/actor/actor.model.ts +++ b/js/src/types/actor/actor.model.ts @@ -52,9 +52,7 @@ export class Actor implements IActor { } public displayName(): string { - return this.name != null && this.name !== "" - ? this.name - : this.usernameWithDomain(); + return displayName(this); } } @@ -68,6 +66,12 @@ export function usernameWithDomain(actor: IActor, force = false): string { return actor.preferredUsername; } +export function displayName(actor: IActor): string { + return actor.name != null && actor.name !== "" + ? actor.name + : usernameWithDomain(actor); +} + export function displayNameAndUsername(actor: IActor): string { if (actor.name) { return `${actor.name} (@${usernameWithDomain(actor)})`; diff --git a/js/yarn.lock b/js/yarn.lock index 6bf4aae24..8b3ebc4ad 100644 --- a/js/yarn.lock +++ b/js/yarn.lock @@ -9445,6 +9445,11 @@ os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= +p-debounce@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-debounce/-/p-debounce-4.0.0.tgz#348e3f44489baa9435cc7d807f17b3bb2fb16b24" + integrity sha512-4Ispi9I9qYGO4lueiLDhe4q4iK5ERK8reLsuzH6BPaXn53EGaua8H66PXIFGrW897hwjXp+pVLrm/DLxN0RF0A== + p-each-series@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71"