Fix editor buttons reloading page 😰️
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
84f8e16cd0
commit
651d7e1e80
|
@ -1,135 +1,128 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div v-if="editor">
|
||||||
<div class="editor" id="tiptab-editor" :data-actor-id="currentActor && currentActor.id">
|
<div class="editor" id="tiptab-editor" :data-actor-id="currentActor && currentActor.id">
|
||||||
<editor-menu-bar :editor="editor" v-slot="{ commands, isActive, focused }">
|
<editor-menu-bar :editor="editor" v-slot="{ commands, isActive, focused }">
|
||||||
<div class="menubar bar-is-hidden" :class="{ 'is-focused': 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
|
<button
|
||||||
class="menububble__button"
|
class="menubar__button"
|
||||||
@click="showLinkMenu(getMarkAttrs('link'))"
|
:class="{ 'is-active': isActive.bold() }"
|
||||||
:class="{ 'is-active': isActive.link() }"
|
@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" />
|
<b-icon icon="link" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
|
||||||
|
|
||||||
</div>
|
<button
|
||||||
</editor-menu-bubble>
|
class="menubar__button"
|
||||||
|
@click="showImagePrompt(commands.image)"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<b-icon icon="image" />
|
||||||
|
</button>
|
||||||
|
|
||||||
<editor-content class="editor__content" :editor="editor" />
|
<button
|
||||||
</div>
|
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">
|
<div class="suggestion-list" v-show="showSuggestions" ref="suggestions">
|
||||||
<template v-if="hasResults">
|
<template v-if="hasResults">
|
||||||
<div
|
<div
|
||||||
|
@ -186,18 +179,12 @@ import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class CreateEvent extends Vue {
|
export default class EditorComponent extends Vue {
|
||||||
@Prop({ required: true }) value!: String;
|
@Prop({ required: true }) value!: string;
|
||||||
|
|
||||||
currentActor!: IPerson;
|
currentActor!: IPerson;
|
||||||
|
|
||||||
editor: Editor = null;
|
editor: Editor|null = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Editor Link
|
|
||||||
*/
|
|
||||||
linkUrl!: string|null;
|
|
||||||
linkMenuIsActive: boolean = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Editor Suggestions
|
* Editor Suggestions
|
||||||
|
@ -233,14 +220,14 @@ export default class CreateEvent extends Vue {
|
||||||
this.filteredActors = items;
|
this.filteredActors = items;
|
||||||
this.suggestionRange = range;
|
this.suggestionRange = range;
|
||||||
this.renderPopup(virtualNode);
|
this.renderPopup(virtualNode);
|
||||||
// we save the command for inserting a selected mention
|
// we save the command for inserting a selected mention
|
||||||
// this allows us to call it inside of our custom popup
|
// this allows us to call it inside of our custom popup
|
||||||
// via keyboard navigation and on click
|
// via keyboard navigation and on click
|
||||||
this.insertMention = command;
|
this.insertMention = command;
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* is called when a suggestion has changed
|
* is called when a suggestion has changed
|
||||||
*/
|
*/
|
||||||
onChange: ({ items, query, range, virtualNode }) => {
|
onChange: ({ items, query, range, virtualNode }) => {
|
||||||
this.query = query;
|
this.query = query;
|
||||||
this.filteredActors = items;
|
this.filteredActors = items;
|
||||||
|
@ -249,11 +236,11 @@ export default class CreateEvent extends Vue {
|
||||||
this.renderPopup(virtualNode);
|
this.renderPopup(virtualNode);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* is called when a suggestion is cancelled
|
* is called when a suggestion is cancelled
|
||||||
*/
|
*/
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
// reset all saved values
|
// reset all saved values
|
||||||
this.query = null;
|
this.query = null;
|
||||||
this.filteredActors = [];
|
this.filteredActors = [];
|
||||||
this.suggestionRange = null;
|
this.suggestionRange = null;
|
||||||
|
@ -261,21 +248,21 @@ export default class CreateEvent extends Vue {
|
||||||
this.destroyPopup();
|
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 }) => {
|
onKeyDown: ({ event }) => {
|
||||||
// pressing up arrow
|
// pressing up arrow
|
||||||
if (event.keyCode === 38) {
|
if (event.keyCode === 38) {
|
||||||
this.upHandler();
|
this.upHandler();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// pressing down arrow
|
// pressing down arrow
|
||||||
if (event.keyCode === 40) {
|
if (event.keyCode === 40) {
|
||||||
this.downHandler();
|
this.downHandler();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// pressing enter
|
// pressing enter
|
||||||
if (event.keyCode === 13) {
|
if (event.keyCode === 13) {
|
||||||
this.enterHandler();
|
this.enterHandler();
|
||||||
return true;
|
return true;
|
||||||
|
@ -292,7 +279,7 @@ export default class CreateEvent extends Vue {
|
||||||
searchText: query,
|
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;
|
this.filteredActors = result.data.searchPersons.elements;
|
||||||
return this.filteredActors;
|
return this.filteredActors;
|
||||||
},
|
},
|
||||||
|
@ -323,28 +310,29 @@ export default class CreateEvent extends Vue {
|
||||||
|
|
||||||
@Watch('value')
|
@Watch('value')
|
||||||
onValueChanged(val: string) {
|
onValueChanged(val: string) {
|
||||||
|
if (!this.editor) return;
|
||||||
if (val !== this.editor.getHTML()) {
|
if (val !== this.editor.getHTML()) {
|
||||||
this.editor.setContent(val);
|
this.editor.setContent(val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showLinkMenu(attrs: any) {
|
showLinkMenu(command, active: boolean) {
|
||||||
this.linkUrl = attrs.href;
|
if (!this.editor) return;
|
||||||
this.linkMenuIsActive = true;
|
if (active) return command({ href: null });
|
||||||
this.$nextTick(() => {
|
this.$buefy.dialog.prompt({
|
||||||
const linkInput = this.$refs.linkInput as HTMLElement;
|
message: this.$t('Enter the link URL') as string,
|
||||||
linkInput.focus();
|
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() {
|
upHandler() {
|
||||||
this.navigatedUserIndex = ((this.navigatedUserIndex + this.filteredActors.length) - 1) % this.filteredActors.length;
|
this.navigatedUserIndex = ((this.navigatedUserIndex + this.filteredActors.length) - 1) % this.filteredActors.length;
|
||||||
|
@ -378,6 +366,7 @@ export default class CreateEvent extends Vue {
|
||||||
label: actor.name,
|
label: actor.name,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (!this.editor) return;
|
||||||
this.editor.focus();
|
this.editor.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -447,6 +436,7 @@ export default class CreateEvent extends Vue {
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
if (!this.editor) return;
|
||||||
this.editor.destroy();
|
this.editor.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -539,10 +529,6 @@ export default class CreateEvent extends Vue {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
border-left: 3px solid rgba($color-black, 0.1);
|
border-left: 3px solid rgba($color-black, 0.1);
|
||||||
color: rgba($color-black, 0.8);
|
color: rgba($color-black, 0.8);
|
||||||
|
|
28
js/src/typings/tiptap-extensions.d.ts
vendored
Normal file
28
js/src/typings/tiptap-extensions.d.ts
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
24
js/src/typings/tiptap.d.ts
vendored
Normal file
24
js/src/typings/tiptap.d.ts
vendored
Normal file
|
@ -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 {}
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
import {ParticipantRole} from "@/types/event.model";
|
|
||||||
<template>
|
<template>
|
||||||
<section>
|
<section>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
@ -246,7 +245,7 @@ import {
|
||||||
import { CURRENT_ACTOR_CLIENT, IDENTITIES, LOGGED_USER_DRAFTS, LOGGED_USER_PARTICIPATIONS } from '@/graphql/actor';
|
import { CURRENT_ACTOR_CLIENT, IDENTITIES, LOGGED_USER_DRAFTS, LOGGED_USER_PARTICIPATIONS } from '@/graphql/actor';
|
||||||
import { IPerson, Person } from '@/types/actor';
|
import { IPerson, Person } from '@/types/actor';
|
||||||
import PictureUpload from '@/components/PictureUpload.vue';
|
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 DateTimePicker from '@/components/Event/DateTimePicker.vue';
|
||||||
import TagInput from '@/components/Event/TagInput.vue';
|
import TagInput from '@/components/Event/TagInput.vue';
|
||||||
import { TAGS } from '@/graphql/tags';
|
import { TAGS } from '@/graphql/tags';
|
||||||
|
@ -257,7 +256,7 @@ import IdentityPickerWrapper from '@/views/Account/IdentityPickerWrapper.vue';
|
||||||
import { RouteName } from '@/router';
|
import { RouteName } from '@/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { IdentityPickerWrapper, AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor },
|
components: { IdentityPickerWrapper, AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor: EditorComponent },
|
||||||
apollo: {
|
apollo: {
|
||||||
currentActor: {
|
currentActor: {
|
||||||
query: CURRENT_ACTOR_CLIENT,
|
query: CURRENT_ACTOR_CLIENT,
|
||||||
|
|
Loading…
Reference in a new issue