replace Quill.js with tiptap (based on ProseMirror)

This commit is contained in:
2020-12-14 01:04:14 +01:00
parent b76ec685de
commit 62bd86e436
8 changed files with 485 additions and 324 deletions
+109 -184
View File
@@ -1,12 +1,14 @@
<template>
<div class="ui-rte" :disabled="disabled">
<div ref="editor" :id="id" class="pell" v-show="!raw"></div>
<div class="pell-raw" v-show="raw">
<div class="pell-actionbar">
<button class="pell-button" title="Accept" type="button" @click="acceptHtml"><i class="fth-check"></i></button>
<button class="pell-button" title="Cancel" type="button" @click="raw = false"><i class="fth-x"></i></button>
</div>
<pre class="pell-raw-content"></pre>
<div ref="editor" :id="id">
<editor-menu-bubble :editor="editor" :keep-in-bounds="true" v-slot="{ commands, isActive, menu }">
<div class="ui-rte-controls theme-dark" :class="{ 'is-active': menu.isActive }" :style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`">
<button type="button" class="ui-rte-control" :class="{ 'is-active': isActive.bold() }" @click="commands.bold">B</button>
<button type="button" class="ui-rte-control" :class="{ 'is-active': isActive.italic() }" @click="commands.italic">I</button>
<button type="button" class="ui-rte-control" :class="{ 'is-active': isActive.code() }" @click="commands.code">C</button>
</div>
</editor-menu-bubble>
<editor-content class="ui-rte-input" :editor="editor" />
</div>
</div>
</template>
@@ -14,11 +16,20 @@
<script>
import Strings from 'zero/helpers/strings.js';
import Pell from './rte.pell.dependency.js';
import { debounce as _debounce } from 'underscore';
import { Editor, EditorContent, EditorMenuBubble } from 'tiptap';
import
{
Blockquote, BulletList, CodeBlock, HardBreak, Heading, ListItem, OrderedList, TodoItem, TodoList,
Bold, Code, Italic, Link, Strike, Underline, History
}
from 'tiptap-extensions';
export default {
name: 'uiRte',
components: { EditorContent, EditorMenuBubble },
props: {
value: {
type: String,
@@ -31,218 +42,132 @@
},
data: () => ({
raw: false,
editor: null,
id: 'rte-' + Strings.guid(),
id: 'quill-' + Strings.guid(),
blocked: false,
actions: [
'bold',
'underline',
'italic',
'strikethrough',
'olist',
'ulist',
'line'
]
onDebouncedChange: null,
editor: null
}),
watch: {
value()
{
this.setValue();
this.init();
}
},
created()
{
this.actions.push({
name: 'link',
result: this.onLinkCreate
});
this.actions.push({
name: 'raw',
icon: '<i class="fth-edit-2"></i>',
title: 'Edit HTML',
result: () =>
{
this.$el.querySelector('.pell-raw-content').innerText = this.value;
this.raw = true;
}
});
this.onDebouncedChange = _debounce(this.onChange, 350);
},
mounted()
{
this.initialize();
this.editor = new Editor({
extensions: [
new CodeBlock(),
new HardBreak(),
new Link(),
new Bold(),
new Code(),
new Italic(),
new Strike(),
new Underline(),
new History(),
],
onUpdate: ({ getHTML }) =>
{
this.onDebouncedChange(getHTML());
},
});
this.init();
},
methods: {
initialize()
init()
{
this.$el.querySelector('.pell-raw-content').contentEditable = true;
this.editor = Pell.init({
element: this.$refs.editor,
defaultParagraphSeparator: 'p',
onChange: html =>
{
this.blocked = true;
this.$emit('input', html);
this.blocked = false;
//setValue(html);
},
actions: this.actions
});
this.editor.content.innerHTML = this.value;
//this.setValue();
this.editor.setContent(this.value);
},
setValue()
onChange(content)
{
if (this.editor.content.innerHTML !== this.value)
{
this.editor.content.innerHTML = this.value;
}
},
onLinkCreate()
{
},
acceptHtml()
{
var html = this.$el.querySelector('.pell-raw-content').innerText;
this.editor.content.innerHTML = html;
this.raw = false;
// TODO this will automatically set the cursor to the end, not desired ;-)
this.$emit('input', content);
}
},
beforeDestroy()
{
this.editor.destroy();
}
}
</script>
<style lang="scss">
.ui-rte
.ui-rte-input
{
font-size: var(--font-size);
height: auto;
min-height: 42px;
padding-top: 9px;
.pell,
.pell-raw
{
background: var(--color-input);
max-width: 800px;
min-height: 42px;
display: flex;
width: 100%;
flex-direction: column;
font-size: var(--font-size);
display: inline-block;
line-height: 1.5;
color: var(--color-text);
border-radius: var(--radius);
border: 1px solid transparent;
&:focus-within
{
background: var(--color-input-focus-bg) !important;
border: var(--color-input-focus-border);
box-shadow: var(--color-input-focus-shadow);
outline: none;
.pell-button:hover,
.pell-button-selected
{
background: var(--color-box-nested);
color: var(--color-text);
}
}
}
.pell, .pell-content, .pell-raw
{
box-sizing: border-box;
}
.pell-content,
.pell-raw-content
{
min-height: 60px;
outline: none;
overflow-y: auto;
padding: 6px 12px 12px;
max-height: 400px;
}
pre.pell-raw-content
p
{
margin: 0;
background: none;
border: none;
word-wrap: normal;
white-space: normal;
font-family: Monaco,Menlo,Consolas,Courier New,monospace;
font-size: var(--font-size);
}
u-rte.-oneline .pell-content
p + p
{
min-height: 46px;
}
.pell-content
{
p
{
margin: 0;
}
h3, h2
{
margin-bottom: .5em;
margin-top: 1em;
}
}
.pell-content a, .pell-content a:hover, .pell-content a:visited, .pell-content a:focus
{
text-decoration: underline;
color: var(--color-text);
}
.pell-actionbar
{
border-radius: var(--radius) var(--radius) 0 0;
padding: 5px;
}
.pell-button
{
background-color: transparent;
cursor: pointer;
height: 30px;
width: 30px;
text-align: center;
border-radius: 5px;
font-size: 14px;
vertical-align: bottom;
}
.pell-button + .pell-button
{
margin-left: 15px;
}
.pell-button:hover,
.pell-button-selected
{
background: var(--color-box);
color: var(--color-text);
margin-top: 1em;
}
}
.ui-rte-controls
{
position: absolute;
display: flex;
z-index: 20;
background: var(--color-bg);
border-radius: var(--radius);
padding: 5px;
margin-bottom: 8px;
transform: translateX(-50%);
visibility: hidden;
opacity: 0;
transition: opacity 0.2s, visibility 0.2s;
pointer-events: none;
}
.ui-rte-controls.is-active
{
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.ui-rte-control
{
display: inline-flex;
align-items: center;
justify-content: center;
background: transparent;
border: 0;
color: var(--color-text);
height: 32px;
width: 32px;
margin-right: 4px;
border-radius: var(--radius);
cursor: pointer;
}
.ui-rte-control:hover
{
background-color: var(--color-bg-shade-1);
}
.ui-rte-control.is-active
{
background-color: var(--color-bg-shade-1);
color: var(--color-text-dim);
}
</style>
+1 -1
View File
@@ -14,7 +14,7 @@
input[type="text"], input[type="color"], input[type="date"], input[type="datetime"], input[type="email"], input[type="file"], input[type="month"], input[type="number"],
input[type="password"], input[type="range"], input[type="search"], input[type="tel"], input[type="time"], input[type="url"], input[type="week"],
textarea, .ui-native-select
textarea, .ui-native-select, .ui-rte-input
{
@extend %font;
background: var(--color-input);
-2
View File
@@ -14,7 +14,6 @@ import uiForm from './forms/form.vue';
import uiInputList from './forms/input-list.vue';
import uiProperty from './forms/property.vue';
import uiRte from './forms/rte.vue';
import uiQuill from './forms/quill.vue';
import uiSearch from './forms/search.vue';
import uiTags from './forms/tags.vue';
import uiToggle from './forms/toggle.vue';
@@ -78,7 +77,6 @@ export default {
uiInputList,
uiProperty,
uiRte,
uiQuill,
uiSearch,
uiTags,
uiToggle,
+125 -105
View File
@@ -1,19 +1,66 @@
<template>
<div class="ui-quill" :disabled="disabled">
<div ref="editor" :id="id" v-show="!raw"></div>
<div ref="editor" :id="id">
<editor-menu-bubble :editor="editor" :keep-in-bounds="keepInBounds" v-slot="{ commands, isActive, menu }">
<div class="menububble"
:class="{ 'is-active': menu.isActive }"
:style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`">
<button type="button" class="menububble__button"
:class="{ 'is-active': isActive.bold() }"
@click="commands.bold">
B
</button>
<button type="button" class="menububble__button"
:class="{ 'is-active': isActive.italic() }"
@click="commands.italic">
I
</button>
<button type="button" class="menububble__button"
:class="{ 'is-active': isActive.code() }"
@click="commands.code">
C
</button>
</div>
</editor-menu-bubble>
<editor-content class="editor__content" :editor="editor" />
</div>
</div>
</template>
<script>
import Strings from 'zero/helpers/strings.js';
import Pell from './rte.pell.dependency.js';
import Quill from 'quill';
import 'quill/dist/quill.bubble.css';
import { Editor, EditorContent, EditorMenuBubble } from 'tiptap'
import
{
Blockquote,
BulletList,
CodeBlock,
HardBreak,
Heading,
ListItem,
OrderedList,
TodoItem,
TodoList,
Bold,
Code,
Italic,
Link,
Strike,
Underline,
History,
} from 'tiptap-extensions'
export default {
name: 'uiQuill',
components: { EditorContent, EditorMenuBubble },
props: {
value: {
type: String,
@@ -26,19 +73,31 @@
},
data: () => ({
raw: false,
editor: null,
id: 'quill-' + Strings.guid(),
blocked: false
blocked: false,
keepInBounds: true,
editor: new Editor({
extensions: [
new Blockquote(),
new BulletList(),
new CodeBlock(),
new HardBreak(),
new Heading({ levels: [1, 2, 3] }),
new ListItem(),
new OrderedList(),
new TodoItem(),
new TodoList(),
new Link(),
new Bold(),
new Code(),
new Italic(),
new Strike(),
new Underline(),
new History(),
],
})
}),
watch: {
value()
{
this.setValue();
}
},
created()
{
@@ -53,19 +112,7 @@
initialize()
{
this.quill = new Quill(this.$refs.editor, {
theme: 'bubble',
placeholder: 'Write something...',
modules: {
toolbar: [
['bold', 'italic', 'underline', 'strike', 'code', 'link'],
['clean']
]
},
formats: ['bold','italic','underline','strike','code','link']
});
this.$refs.editor.querySelector('.ql-tooltip').classList.add('theme-dark');
},
@@ -86,94 +133,67 @@
}
}
},
beforeDestroy()
{
this.editor.destroy();
},
}
</script>
<style lang="scss">
.ui-quill
{
font-size: var(--font-size);
.menububble {
position: absolute;
display: flex;
z-index: 20;
background: black;
border-radius: 5px;
padding: 0.3rem;
margin-bottom: 0.5rem;
transform: translateX(-50%);
visibility: hidden;
opacity: 0;
transition: opacity 0.2s, visibility 0.2s;
&.is-active {
opacity: 1;
visibility: visible;
}
.ui-quill .ql-editor
{
line-height: 1.5;
padding: 10px 12px 8px;
&__button {
display: inline-flex;
background: transparent;
border: 0;
color: white;
padding: 0.2rem 0.5rem;
margin-right: 0.2rem;
border-radius: 3px;
cursor: pointer;
&:last-child {
margin-right: 0;
}
&:hover {
background-color: rgba(white, 0.1);
}
&.is-active {
background-color: rgba(white, 0.2);
}
}
.ui-quill .ql-editor.ql-blank::before
{
left: 12px;
color: var(--color-input-placeholder);
font-style: normal;
}
.ui-quill .ql-container
{
background: var(--color-input);
max-width: 800px;
min-height: 42px;
&__form {
display: flex;
width: 100%;
flex-direction: column;
font-size: var(--font-size);
display: inline-block;
line-height: 1.5;
color: var(--color-text);
border-radius: var(--radius);
border: 1px solid transparent;
font-family: var(--font);
align-items: center;
}
.ui-quill .ql-container:focus, .ui-quill .ql-container:focus-within
{
background-color: var(--color-input-focus-bg);
border: var(--color-input-focus-border);
box-shadow: var(--color-input-focus-shadow);
outline: none;
}
.ui-quill .ql-tooltip
{
color: var(--color-text);
background-color: var(--color-dropdown);
border-radius: var(--radius);
}
.ui-quill .ql-toolbar
{
padding: 3px 6px;
}
.ui-quill .ql-tooltip-arrow
{
border-bottom-color: var(--color-dropdown) !important;
}
.ui-quill .ql-toolbar button
{
height: 36px;
width: 36px;
padding: 8px 10px;
}
.ui-quill .ql-toolbar button svg
{
height: 85%;
}
.ui-quill .ql-toolbar .ql-formats
{
margin: 0 !important;
}
.ui-quill .ql-bubble .ql-fill
{
fill: var(--color-text);
}
.ui-quill .ql-bubble .ql-stroke
{
stroke: var(--color-text);
&__input {
font: inherit;
border: none;
background: transparent;
color: white;
}
}
</style>
@@ -0,0 +1,248 @@
<template>
<div class="ui-rte" :disabled="disabled">
<div ref="editor" :id="id" class="pell" v-show="!raw"></div>
<div class="pell-raw" v-show="raw">
<div class="pell-actionbar">
<button class="pell-button" title="Accept" type="button" @click="acceptHtml"><i class="fth-check"></i></button>
<button class="pell-button" title="Cancel" type="button" @click="raw = false"><i class="fth-x"></i></button>
</div>
<pre class="pell-raw-content"></pre>
</div>
</div>
</template>
<script>
import Strings from 'zero/helpers/strings.js';
import Pell from './rte.pell.dependency.js';
export default {
name: 'uiRte',
props: {
value: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
},
},
data: () => ({
raw: false,
editor: null,
id: 'rte-' + Strings.guid(),
blocked: false,
actions: [
'bold',
'underline',
'italic',
'strikethrough',
'olist',
'ulist',
'line'
]
}),
watch: {
value()
{
this.setValue();
}
},
created()
{
this.actions.push({
name: 'link',
result: this.onLinkCreate
});
this.actions.push({
name: 'raw',
icon: '<i class="fth-edit-2"></i>',
title: 'Edit HTML',
result: () =>
{
this.$el.querySelector('.pell-raw-content').innerText = this.value;
this.raw = true;
}
});
},
mounted()
{
this.initialize();
},
methods: {
initialize()
{
this.$el.querySelector('.pell-raw-content').contentEditable = true;
this.editor = Pell.init({
element: this.$refs.editor,
defaultParagraphSeparator: 'p',
onChange: html =>
{
this.blocked = true;
this.$emit('input', html);
this.blocked = false;
//setValue(html);
},
actions: this.actions
});
this.editor.content.innerHTML = this.value;
//this.setValue();
},
setValue()
{
if (this.editor.content.innerHTML !== this.value)
{
this.editor.content.innerHTML = this.value;
}
},
onLinkCreate()
{
},
acceptHtml()
{
var html = this.$el.querySelector('.pell-raw-content').innerText;
this.editor.content.innerHTML = html;
this.raw = false;
}
}
}
</script>
<style lang="scss">
.ui-rte
{
font-size: var(--font-size);
.pell,
.pell-raw
{
background: var(--color-input);
max-width: 800px;
min-height: 42px;
display: flex;
width: 100%;
flex-direction: column;
font-size: var(--font-size);
display: inline-block;
line-height: 1.5;
color: var(--color-text);
border-radius: var(--radius);
border: 1px solid transparent;
&:focus-within
{
background: var(--color-input-focus-bg) !important;
border: var(--color-input-focus-border);
box-shadow: var(--color-input-focus-shadow);
outline: none;
.pell-button:hover,
.pell-button-selected
{
background: var(--color-box-nested);
color: var(--color-text);
}
}
}
.pell, .pell-content, .pell-raw
{
box-sizing: border-box;
}
.pell-content,
.pell-raw-content
{
min-height: 60px;
outline: none;
overflow-y: auto;
padding: 6px 12px 12px;
max-height: 400px;
}
pre.pell-raw-content
{
margin: 0;
background: none;
border: none;
word-wrap: normal;
white-space: normal;
font-family: Monaco,Menlo,Consolas,Courier New,monospace;
font-size: var(--font-size);
}
u-rte.-oneline .pell-content
{
min-height: 46px;
}
.pell-content
{
p
{
margin: 0;
}
h3, h2
{
margin-bottom: .5em;
margin-top: 1em;
}
}
.pell-content a, .pell-content a:hover, .pell-content a:visited, .pell-content a:focus
{
text-decoration: underline;
color: var(--color-text);
}
.pell-actionbar
{
border-radius: var(--radius) var(--radius) 0 0;
padding: 5px;
}
.pell-button
{
background-color: transparent;
cursor: pointer;
height: 30px;
width: 30px;
text-align: center;
border-radius: 5px;
font-size: 14px;
vertical-align: bottom;
}
.pell-button + .pell-button
{
margin-left: 15px;
}
.pell-button:hover,
.pell-button-selected
{
background: var(--color-box);
color: var(--color-text);
}
}
</style>
-11
View File
@@ -3,7 +3,6 @@ import Text from '../editor/fields/text.vue';
import Currency from '../editor/fields/currency.vue';
import Number from '../editor/fields/number.vue';
import Rte from '../editor/fields/rte.vue';
import QuillRte from '../editor/fields/quill.vue';
import Select from '../editor/fields/select.vue';
import Textarea from '../editor/fields/textarea.vue';
import Toggle from '../editor/fields/toggle.vue';
@@ -201,16 +200,6 @@ class EditorField
}
/**
* Render a rich-text editor (powered by Quill) field
* @returns {EditorField}
*/
quill()
{
return this._setComponent(QuillRte);
}
/**
* @typedef {object} EditorSelectItem
* @param {object} key - Key/Id of the item
-20
View File
@@ -1,20 +0,0 @@
<template>
<ui-quill :value="value" @input="$emit('input', $event)" :disabled="disabled" />
</template>
<script>
export default {
props: {
value: {
type: String,
default: null
},
disabled: {
type: Boolean,
default: false
},
config: Object
}
}
</script>
+2 -1
View File
@@ -11,8 +11,9 @@
"dayjs": "^1.8.26",
"flatpickr": "^4.6.3",
"qs": "^6.9.4",
"quill": "^1.3.7",
"sortablejs": "^1.10.2",
"tiptap": "^1.30.0",
"tiptap-extensions": "^1.33.2",
"underscore": "^1.10.2",
"vue": "^2.6.12",
"vue-router": "^3.4.8"