Files
mixtape/zero.Web.UI/App/components/forms/rte.vue
T
2021-01-22 14:45:48 +01:00

362 lines
8.0 KiB
Vue

<template>
<div class="ui-rte" :disabled="disabled">
<div ref="editor" :id="id">
<editor-menu-bubble :editor="editor" :keep-in-bounds="true" v-slot="{ commands, isActive, menu }">
<div class="ui-rte-overlay-controls theme-dark" :class="{ 'is-active': menu.isActive }" :style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`">
<button type="button" class="ui-rte-overlay-control" :class="{ 'is-active': isActive.bold() }" @click="commands.bold"><ui-icon symbol="fth-bold" /></button>
<button type="button" class="ui-rte-overlay-control" :class="{ 'is-active': isActive.italic() }" @click="commands.italic"><ui-icon symbol="fth-italic" /></button>
<button type="button" class="ui-rte-overlay-control" :class="{ 'is-active': isActive.underline() }" @click="commands.underline"><ui-icon symbol="fth-underline" /></button>
<button type="button" class="ui-rte-overlay-control" :class="{ 'is-active': isActive.link() }" @click="commands.link"><ui-icon symbol="fth-link" /></button>
<button type="button" class="ui-rte-overlay-control" :class="{ 'is-active': isActive.code() }" @click="commands.code"><ui-icon symbol="fth-code" /></button>
</div>
</editor-menu-bubble>
<editor-menu-bar :editor="editor" v-slot="{ commands, isActive }"> <!--// TODO this triggers a recursive loop error when used in multiple properties per editor-->
<div class="ui-rte-controls">
<span class="ui-rte-controls-label">Editor</span>
<button type="button" title="Undo" class="ui-rte-control" @click="commands.undo" :disabled="commands.undoDepth() < 1"><ui-icon symbol="fth-chevron-left" /></button>
<button type="button" title="Redo" class="ui-rte-control" @click="commands.redo" :disabled="commands.redoDepth() < 1"><ui-icon symbol="fth-chevron-right" /></button>
</div>
</editor-menu-bar>
<editor-content class="ui-rte-input" :editor="editor" />
</div>
</div>
</template>
<script>
import Strings from 'zero/helpers/strings.js';
import { debounce as _debounce } from 'underscore';
import { Editor, EditorContent, EditorMenuBubble, EditorMenuBar } from 'tiptap';
import { HardBreak } from './rte.extensions.js';
import { Bold, Code, Italic, Link, Strike, Underline, History, Placeholder, HorizontalRule, ListItem, BulletList, OrderedList, Heading } from 'tiptap-extensions';
export default {
name: 'uiRte',
components: { EditorContent, EditorMenuBubble, EditorMenuBar },
props: {
value: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: null
},
extensions: {
type: Array,
default: () => []
},
disabledExtensions: {
type: Array,
default: () => []
}
},
data: () => ({
id: 'rte-' + Strings.guid(),
blocked: false,
onDebouncedChange: null,
editor: null
}),
watch: {
value()
{
if (!this.blocked)
{
this.init();
}
},
disabled(value)
{
this.editor.setOptions({
editable: !value
})
},
},
created()
{
this.onDebouncedChange = _debounce(this.onChange, 350);
},
mounted()
{
let extensions = [
...this.extensions,
new HardBreak(),
new Link(),
new Bold(),
new Code(),
new Italic(),
new Strike(),
new Underline(),
new History(),
new ListItem(),
new BulletList(),
new OrderedList(),
new HorizontalRule(),
new Heading({
levels: [2, 3, 4],
})
];
if (this.placeholder)
{
extensions.push(new Placeholder({
emptyEditorClass: 'is-editor-empty',
emptyNodeClass: 'is-empty',
emptyNodeText: this.placeholder,
showOnlyWhenEditable: true,
showOnlyCurrent: true
}));
}
if (this.disabledExtensions.length)
{
extensions = extensions.filter(x => this.disabledExtensions.indexOf(x.name) < 0);
}
this.editor = new Editor({
editable: !this.disabled,
extensions: extensions,
onUpdate: opts => this.onDebouncedChange(opts.getHTML())
});
this.init();
},
methods: {
init()
{
this.editor.setContent(this.value);
},
onChange(content)
{
this.blocked = true;
this.$emit('input', content);
this.$nextTick(() => this.blocked = false);
}
},
beforeDestroy()
{
this.editor.destroy();
}
}
</script>
<style lang="scss">
.ui-rte-input
{
height: auto;
min-height: 48px;
padding-top: 9px;
p
{
margin: 0;
}
p + p
{
margin-top: 1em;
}
p.is-editor-empty:first-child:before
{
content: attr(data-empty-text);
float: left;
color: var(--color-input-placeholder);
opacity: .7;
pointer-events: none;
height: 0;
}
a
{
color: var(--color-primary);
text-decoration: underline;
cursor: pointer;
}
}
.ui-rte-overlay-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;
&:after
{
content: '';
position: absolute;
left: 50%;
top: 100%;
width: 0;
height: 0;
border: 8px solid transparent;
margin-left: -7px;
border-top-color: var(--color-bg);
}
}
.ui-rte-overlay-controls.is-active
{
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.ui-rte-overlay-control
{
display: inline-flex;
align-items: center;
justify-content: center;
background: transparent;
border: 0;
color: var(--color-text);
height: 32px;
width: 32px;
border-radius: var(--radius);
cursor: pointer;
}
.ui-rte-overlay-control + .ui-rte-overlay-control
{
margin-left: 4px;
}
.ui-rte-overlay-control:hover
{
background-color: var(--color-bg-shade-1);
}
.ui-rte-overlay-control.is-active
{
background-color: var(--color-bg-shade-1);
color: var(--color-text-dim);
}
.ui-rte-controls
{
background: var(--color-input);
border-radius: var(--radius) var(--radius) 0 0;
padding: 5px 5px 0;
display: flex;
align-items: center;
justify-content: flex-start;
}
.ui-rte-controls + .ui-rte-input
{
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.ui-rte-controls-label
{
color: var(--color-text-dim);
margin: 0 14px 0 8px;
font-size: 10px;
text-transform: uppercase;
pointer-events: none;
user-select: none;
}
.ui-rte-control
{
background-color: transparent;
cursor: pointer;
height: 30px;
width: 30px;
text-align: center;
border-radius: 5px;
font-size: 14px;
vertical-align: bottom;
}
.ui-rte-control[disabled]
{
opacity: .5;
}
.ui-rte-control + .ui-rte-control
{
margin-left: 5px;
}
.ui-rte-control:hover, .ui-rte-control.is-active
{
background: var(--color-box);
color: var(--color-text);
}
.ui-rte-input .ProseMirror
{
> *
{
margin: 1rem 0;
}
> :first-child
{
margin-top: 0;
}
> :last-child
{
margin-bottom: 0;
}
h1,h2,h3,h4,h5,h6
{
margin-bottom: .5rem;
font-weight: bold;
}
h1
{
font-size: 1.4em;
}
h2
{
font-size: 1.2em;
}
h3
{
font-size: 1.1em;
}
h4, h5, h6
{
font-size: 1em;
}
hr
{
border-bottom-style: dashed;
border-bottom-color: var(--color-line-dashed);
}
}
</style>