From 651d7e1e809051b5b62ffbe512e07b48b2b30b71 Mon Sep 17 00:00:00 2001
From: Thomas Citharel <tcit@tcit.fr>
Date: Thu, 10 Oct 2019 10:25:33 +0200
Subject: [PATCH] =?UTF-8?q?Fix=20editor=20buttons=20reloading=20page=20?=
 =?UTF-8?q?=F0=9F=98=B0=EF=B8=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
---
 js/src/components/Editor.vue          | 324 ++++++++++++--------------
 js/src/typings/tiptap-extensions.d.ts |  28 +++
 js/src/typings/tiptap.d.ts            |  24 ++
 js/src/views/Event/Edit.vue           |   5 +-
 4 files changed, 209 insertions(+), 172 deletions(-)
 create mode 100644 js/src/typings/tiptap-extensions.d.ts
 create mode 100644 js/src/typings/tiptap.d.ts

diff --git a/js/src/components/Editor.vue b/js/src/components/Editor.vue
index f48b45761..ed5eca9ce 100644
--- a/js/src/components/Editor.vue
+++ b/js/src/components/Editor.vue
@@ -1,135 +1,128 @@
 <template>
-    <div>
-    <div class="editor" id="tiptab-editor" :data-actor-id="currentActor && currentActor.id">
-        <editor-menu-bar :editor="editor" v-slot="{ commands, isActive, focused }">
-            <div class="menubar bar-is-hidden" :class="{ 'is-focused': focused }">
+    <div v-if="editor">
+        <div class="editor" id="tiptab-editor" :data-actor-id="currentActor && currentActor.id">
+            <editor-menu-bar :editor="editor" v-slot="{ commands, isActive, focused }">
+                <div class="menubar bar-is-hidden" :class="{ 'is-focused': focused }">
 
-                <button
-                        class="menubar__button"
-                        :class="{ 'is-active': isActive.bold() }"
-                        @click="commands.bold"
-                >
-                    <b-icon icon="format-bold" />
-                </button>
-
-                <button
-                        class="menubar__button"
-                        :class="{ 'is-active': isActive.italic() }"
-                        @click="commands.italic"
-                >
-                    <b-icon icon="format-italic" />
-                </button>
-
-                <button
-                        class="menubar__button"
-                        :class="{ 'is-active': isActive.underline() }"
-                        @click="commands.underline"
-                >
-                    <b-icon icon="format-underline" />
-                </button>
-
-                <button
-                        class="menubar__button"
-                        :class="{ 'is-active': isActive.heading({ level: 1 }) }"
-                        @click="commands.heading({ level: 1 })"
-                >
-                    <b-icon icon="format-header-1" />
-                </button>
-
-                <button
-                        class="menubar__button"
-                        :class="{ 'is-active': isActive.heading({ level: 2 }) }"
-                        @click="commands.heading({ level: 2 })"
-                >
-                    <b-icon icon="format-header-2" />
-                </button>
-
-                <button
-                        class="menubar__button"
-                        :class="{ 'is-active': isActive.heading({ level: 3 }) }"
-                        @click="commands.heading({ level: 3 })"
-                >
-                    <b-icon icon="format-header-3" />
-                </button>
-
-                <button
-                        class="menubar__button"
-                        @click="showImagePrompt(commands.image)"
-                >
-                    <b-icon icon="image" />
-                </button>
-
-                <button
-                        class="menubar__button"
-                        :class="{ 'is-active': isActive.bullet_list() }"
-                        @click="commands.bullet_list"
-                >
-                    <b-icon icon="format-list-bulleted" />
-                </button>
-
-                <button
-                        class="menubar__button"
-                        :class="{ 'is-active': isActive.ordered_list() }"
-                        @click="commands.ordered_list"
-                >
-                    <b-icon icon="format-list-numbered" />
-                </button>
-
-                <button
-                        class="menubar__button"
-                        :class="{ 'is-active': isActive.blockquote() }"
-                        @click="commands.blockquote"
-                >
-                    <b-icon icon="format-quote-close" />
-                </button>
-
-                <button
-                        class="menubar__button"
-                        @click="commands.undo"
-                >
-                    <b-icon icon="undo" />
-                </button>
-
-                <button
-                        class="menubar__button"
-                        @click="commands.redo"
-                >
-                    <b-icon icon="redo" />
-                </button>
-
-            </div>
-        </editor-menu-bar>
-
-        <editor-menu-bubble class="menububble" :editor="editor" @hide="hideLinkMenu" v-slot="{ commands, isActive, getMarkAttrs, menu }">
-            <div
-                    class="menububble"
-                    :class="{ 'is-active': menu.isActive }"
-                    :style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`"
-            >
-
-                <form class="menububble__form" v-if="linkMenuIsActive" @submit.prevent="setLinkUrl(commands.link, linkUrl)">
-                    <input class="menububble__input" type="text" v-model="linkUrl" placeholder="https://" ref="linkInput" @keydown.esc="hideLinkMenu"/>
-                    <button class="menububble__button" @click="setLinkUrl(commands.link, null)" type="button">
-                        <b-icon icon="delete" />
-                    </button>
-                </form>
-
-                <template v-else>
                     <button
-                            class="menububble__button"
-                            @click="showLinkMenu(getMarkAttrs('link'))"
-                            :class="{ 'is-active': isActive.link() }"
+                            class="menubar__button"
+                            :class="{ 'is-active': isActive.bold() }"
+                            @click="commands.bold"
+                            type="button"
+                    >
+                        <b-icon icon="format-bold" />
+                    </button>
+
+                    <button
+                            class="menubar__button"
+                            :class="{ 'is-active': isActive.italic() }"
+                            @click="commands.italic"
+                            type="button"
+                    >
+                        <b-icon icon="format-italic" />
+                    </button>
+
+                    <button
+                            class="menubar__button"
+                            :class="{ 'is-active': isActive.underline() }"
+                            @click="commands.underline"
+                            type="button"
+                    >
+                        <b-icon icon="format-underline" />
+                    </button>
+
+                    <button
+                            class="menubar__button"
+                            :class="{ 'is-active': isActive.heading({ level: 1 }) }"
+                            @click="commands.heading({ level: 1 })"
+                            type="button"
+                    >
+                        <b-icon icon="format-header-1" />
+                    </button>
+
+                    <button
+                            class="menubar__button"
+                            :class="{ 'is-active': isActive.heading({ level: 2 }) }"
+                            @click="commands.heading({ level: 2 })"
+                            type="button"
+                    >
+                        <b-icon icon="format-header-2" />
+                    </button>
+
+                    <button
+                            class="menubar__button"
+                            :class="{ 'is-active': isActive.heading({ level: 3 }) }"
+                            @click="commands.heading({ level: 3 })"
+                            type="button"
+                    >
+                        <b-icon icon="format-header-3" />
+                    </button>
+
+                    <button
+                            class="menubar__button"
+                            @click="showLinkMenu(commands.link, isActive.link())"
+                            :class="{ 'is-active': isActive.link() }"
+                            type="button"
                     >
-                        <span>{{ isActive.link() ? 'Update Link' : 'Add Link'}}</span>
                         <b-icon icon="link" />
                     </button>
-                </template>
 
-            </div>
-        </editor-menu-bubble>
+                    <button
+                            class="menubar__button"
+                            @click="showImagePrompt(commands.image)"
+                            type="button"
+                    >
+                        <b-icon icon="image" />
+                    </button>
 
-        <editor-content class="editor__content" :editor="editor" />
-    </div>
+                    <button
+                            class="menubar__button"
+                            :class="{ 'is-active': isActive.bullet_list() }"
+                            @click="commands.bullet_list"
+                            type="button"
+                    >
+                        <b-icon icon="format-list-bulleted" />
+                    </button>
+
+                    <button
+                            class="menubar__button"
+                            :class="{ 'is-active': isActive.ordered_list() }"
+                            @click="commands.ordered_list"
+                            type="button"
+                    >
+                        <b-icon icon="format-list-numbered" />
+                    </button>
+
+                    <button
+                            class="menubar__button"
+                            :class="{ 'is-active': isActive.blockquote() }"
+                            @click="commands.blockquote"
+                            type="button"
+                    >
+                        <b-icon icon="format-quote-close" />
+                    </button>
+
+                    <button
+                            class="menubar__button"
+                            @click="commands.undo"
+                            type="button"
+                    >
+                        <b-icon icon="undo" />
+                    </button>
+
+                    <button
+                            class="menubar__button"
+                            @click="commands.redo"
+                            type="button"
+                    >
+                        <b-icon icon="redo" />
+                    </button>
+
+                </div>
+            </editor-menu-bar>
+
+            <editor-content class="editor__content" :editor="editor" />
+        </div>
         <div class="suggestion-list" v-show="showSuggestions" ref="suggestions">
             <template v-if="hasResults">
                 <div
@@ -186,18 +179,12 @@ import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
     },
   },
 })
-export default class CreateEvent extends Vue {
-  @Prop({ required: true }) value!: String;
+export default class EditorComponent extends Vue {
+  @Prop({ required: true }) value!: string;
 
   currentActor!: IPerson;
 
-  editor: Editor = null;
-
-    /**
-     * Editor Link
-     */
-  linkUrl!: string|null;
-  linkMenuIsActive: boolean = false;
+  editor: Editor|null = null;
 
     /**
      * Editor Suggestions
@@ -233,14 +220,14 @@ export default class CreateEvent extends Vue {
             this.filteredActors = items;
             this.suggestionRange = range;
             this.renderPopup(virtualNode);
-            // we save the command for inserting a selected mention
-            // this allows us to call it inside of our custom popup
-            // via keyboard navigation and on click
+                      // we save the command for inserting a selected mention
+                      // this allows us to call it inside of our custom popup
+                      // via keyboard navigation and on click
             this.insertMention = command;
           },
-          /**
-           * is called when a suggestion has changed
-           */
+                  /**
+                   * is called when a suggestion has changed
+                   */
           onChange: ({ items, query, range, virtualNode }) => {
             this.query = query;
             this.filteredActors = items;
@@ -249,11 +236,11 @@ export default class CreateEvent extends Vue {
             this.renderPopup(virtualNode);
           },
 
-          /**
-           * is called when a suggestion is cancelled
-           */
+                  /**
+                   * is called when a suggestion is cancelled
+                   */
           onExit: () => {
-            // reset all saved values
+                      // reset all saved values
             this.query = null;
             this.filteredActors = [];
             this.suggestionRange = null;
@@ -261,21 +248,21 @@ export default class CreateEvent extends Vue {
             this.destroyPopup();
           },
 
-          /**
-           * is called on every keyDown event while a suggestion is active
-           */
+                  /**
+                   * is called on every keyDown event while a suggestion is active
+                   */
           onKeyDown: ({ event }) => {
-            // pressing up arrow
+                      // pressing up arrow
             if (event.keyCode === 38) {
               this.upHandler();
               return true;
             }
-            // pressing down arrow
+                      // pressing down arrow
             if (event.keyCode === 40) {
               this.downHandler();
               return true;
             }
-            // pressing enter
+                      // pressing enter
             if (event.keyCode === 13) {
               this.enterHandler();
               return true;
@@ -292,7 +279,7 @@ export default class CreateEvent extends Vue {
                 searchText: query,
               },
             });
-            // TODO: TipTap doesn't handle async for onFilter, hence the following line.
+                      // TODO: TipTap doesn't handle async for onFilter, hence the following line.
             this.filteredActors = result.data.searchPersons.elements;
             return this.filteredActors;
           },
@@ -323,28 +310,29 @@ export default class CreateEvent extends Vue {
 
   @Watch('value')
   onValueChanged(val: string) {
+    if (!this.editor) return;
     if (val !== this.editor.getHTML()) {
       this.editor.setContent(val);
     }
   }
 
-  showLinkMenu(attrs: any) {
-    this.linkUrl = attrs.href;
-    this.linkMenuIsActive = true;
-    this.$nextTick(() => {
-      const linkInput = this.$refs.linkInput as HTMLElement;
-      linkInput.focus();
+  showLinkMenu(command, active: boolean) {
+    if (!this.editor) return;
+    if (active) return command({ href: null });
+    this.$buefy.dialog.prompt({
+      message: this.$t('Enter the link URL') as string,
+      inputAttrs: {
+        type: 'url',
+      },
+      // @ts-ignore https://github.com/buefy/buefy/commit/62539ac4026c8610509850a3a973fc283bac50ef#diff-02b38ee0a78d8316f075e520b3a442ae
+      trapFocus: true,
+      onConfirm: (value) => {
+        command({ href: value });
+        if (!this.editor) return;
+        this.editor.focus();
+      },
     });
   }
-  hideLinkMenu() {
-    this.linkUrl = '';
-    this.linkMenuIsActive = false;
-  }
-  setLinkUrl(command, url: string) {
-    command({ href: url });
-    this.hideLinkMenu();
-    this.editor.focus();
-  }
 
   upHandler() {
     this.navigatedUserIndex = ((this.navigatedUserIndex + this.filteredActors.length) - 1) % this.filteredActors.length;
@@ -378,6 +366,7 @@ export default class CreateEvent extends Vue {
         label: actor.name,
       },
     });
+    if (!this.editor) return;
     this.editor.focus();
   }
 
@@ -447,6 +436,7 @@ export default class CreateEvent extends Vue {
   }
 
   beforeDestroy() {
+    if (!this.editor) return;
     this.editor.destroy();
   }
 }
@@ -539,10 +529,6 @@ export default class CreateEvent extends Vue {
                 margin: 0;
             }
 
-            a {
-                color: inherit;
-            }
-
             blockquote {
                 border-left: 3px solid rgba($color-black, 0.1);
                 color: rgba($color-black, 0.8);
diff --git a/js/src/typings/tiptap-extensions.d.ts b/js/src/typings/tiptap-extensions.d.ts
new file mode 100644
index 000000000..e9fe8e613
--- /dev/null
+++ b/js/src/typings/tiptap-extensions.d.ts
@@ -0,0 +1,28 @@
+declare module 'tiptap-extensions' {
+    import Vue from 'vue';
+
+    export class Blockquote {}
+    export class CodeBlock {}
+    export class HardBreak {}
+    export class Heading {
+      constructor(object: object)
+    }
+    export class OrderedList {}
+    export class BulletList {}
+    export class ListItem {}
+    export class TodoItem {}
+    export class TodoList {}
+    export class Bold {}
+    export class Code {}
+    export class Italic {}
+    export class Link {}
+    export class Strike {}
+    export class Underline {}
+    export class History {}
+    export class Placeholder {
+      constructor(object: object)
+    }
+    export class Mention {
+      constructor(object: object)
+    }
+}
diff --git a/js/src/typings/tiptap.d.ts b/js/src/typings/tiptap.d.ts
new file mode 100644
index 000000000..789eb3a24
--- /dev/null
+++ b/js/src/typings/tiptap.d.ts
@@ -0,0 +1,24 @@
+declare module 'tiptap' {
+    import Vue from 'vue';
+    export class Editor {
+      public constructor({});
+
+      public setOptions({}): void;
+      public setContent(content: string): void;
+      public focus(): void;
+      public getHTML(): string;
+      public destroy(): void;
+    }
+
+    export class Node {}
+
+    export class Plugin {
+      public constructor({});
+    }
+
+    export class EditorMenuBar extends Vue {}
+
+    export class EditorContent extends Vue {}
+
+    export class EditorMenuBubble extends Vue {}
+}
diff --git a/js/src/views/Event/Edit.vue b/js/src/views/Event/Edit.vue
index e6f09e42d..7c05101f8 100644
--- a/js/src/views/Event/Edit.vue
+++ b/js/src/views/Event/Edit.vue
@@ -1,4 +1,3 @@
-import {ParticipantRole} from "@/types/event.model";
 <template>
   <section>
     <div class="container">
@@ -246,7 +245,7 @@ import {
 import { CURRENT_ACTOR_CLIENT, IDENTITIES, LOGGED_USER_DRAFTS, LOGGED_USER_PARTICIPATIONS } from '@/graphql/actor';
 import { IPerson, Person } from '@/types/actor';
 import PictureUpload from '@/components/PictureUpload.vue';
-import Editor from '@/components/Editor.vue';
+import EditorComponent from '@/components/Editor.vue';
 import DateTimePicker from '@/components/Event/DateTimePicker.vue';
 import TagInput from '@/components/Event/TagInput.vue';
 import { TAGS } from '@/graphql/tags';
@@ -257,7 +256,7 @@ import IdentityPickerWrapper from '@/views/Account/IdentityPickerWrapper.vue';
 import { RouteName } from '@/router';
 
 @Component({
-  components: { IdentityPickerWrapper, AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor },
+  components: { IdentityPickerWrapper, AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor: EditorComponent },
   apollo: {
     currentActor: {
       query: CURRENT_ACTOR_CLIENT,