You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wiki/ux/src/components/EditorWysiwyg.vue

911 lines
23 KiB

<template lang="pug">
.wysiwyg-container
.wysiwyg-toolbar(v-if='editor')
template(v-for='menuItem of menuBar')
q-separator.q-mx-xs(
v-if='menuItem.type === `divider`'
vertical
)
q-btn(
v-else-if='menuItem.type === `dropdown`'
:key='`ddn-` + menuItem.key'
flat
:icon='menuItem.icon'
padding='xs'
:class='{ "is-active": menuItem.isActive && menuItem.isActive() }'
:color='menuItem.isActive && menuItem.isActive() ? `primary` : `grey-10`'
:aria-label='menuItem.title'
split
:disabled='menuItem.disabled && menuItem.disabled()'
)
q-menu
q-list(
dense
padding
)
template(v-for='child of menuItem.children')
q-separator.q-my-sm(v-if='child.type === `divider`')
q-item(
v-else
:key='child.key'
clickable
@click='child.action'
:active='child.isActive && child.isActive()'
active-class='text-primary'
:disabled='child.disabled && child.disabled()'
)
q-item-section(side)
q-icon(
:name='child.icon'
:color='child.color'
)
q-item-section
q-item-label {{child.title}}
q-btn-group(
v-else-if='menuItem.type === `btngroup`'
:key='`btngrp-` + menuItem.key'
flat
)
q-btn(
v-for='child of menuItem.children'
:key='child.key'
flat
:icon='child.icon'
padding='xs'
:class='{ "is-active": child.isActive && child.isActive() }'
:color='child.isActive && child.isActive() ? `primary` : `grey-10`'
@click='child.action'
:aria-label='child.title'
:disabled='menuItem.disabled && menuItem.disabled()'
)
q-btn(
v-else
:key='`btn-` + menuItem.key'
flat
:icon='menuItem.icon'
padding='xs'
:class='{ "is-active": menuItem.isActive && menuItem.isActive() }'
:color='menuItem.isActive && menuItem.isActive() ? `primary` : `grey-10`'
@click='menuItem.action'
:aria-label='menuItem.title'
:disabled='menuItem.disabled && menuItem.disabled()'
)
//- q-space
//- q-btn(
//- size='sm'
//- unelevated
//- color='red'
//- label='Test'
//- @click='snapshot'
//- )
//- q-scroll-area(
//- :thumb-style='thumbStyle'
//- :bar-style='barStyle'
//- style='height: 100%;'
//- )
editor-content(:editor='editor')
</template>
<script setup>
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
// import Collaboration from '@tiptap/extension-collaboration'
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
import { Color } from '@tiptap/extension-color'
import FontFamily from '@tiptap/extension-font-family'
import Highlight from '@tiptap/extension-highlight'
import Image from '@tiptap/extension-image'
import Mention from '@tiptap/extension-mention'
import Placeholder from '@tiptap/extension-placeholder'
import Table from '@tiptap/extension-table'
import TableRow from '@tiptap/extension-table-row'
import TableCell from '@tiptap/extension-table-cell'
import TableHeader from '@tiptap/extension-table-header'
import TaskList from '@tiptap/extension-task-list'
import TaskItem from '@tiptap/extension-task-item'
import TextAlign from '@tiptap/extension-text-align'
import TextStyle from '@tiptap/extension-text-style'
import Typography from '@tiptap/extension-typography'
import { common, createLowlight } from 'lowlight'
import { onBeforeUnmount, onMounted, reactive, shallowRef } from 'vue'
// import * as Y from 'yjs'
// import { IndexeddbPersistence } from 'y-indexeddb'
// import { WebsocketProvider } from 'y-websocket'
import { useMeta, useQuasar, setCssVar } from 'quasar'
import { useI18n } from 'vue-i18n'
import { DateTime } from 'luxon'
import { useEditorStore } from 'src/stores/editor'
import { usePageStore } from 'src/stores/page'
import { useSiteStore } from 'src/stores/site'
const lowlight = createLowlight(common)
// QUASAR
const $q = useQuasar()
// STORES
const editorStore = useEditorStore()
const pageStore = usePageStore()
const siteStore = useSiteStore()
// I18N
const { t } = useI18n()
// STATE
const state = reactive({
// editor: null,
ydoc: null
})
let editor = null
const thumbStyle = {
right: '2px',
borderRadius: '5px',
backgroundColor: '#000',
width: '5px',
opacity: 0.15
}
const barStyle = {
backgroundColor: '#FAFAFA',
width: '9px',
opacity: 1
}
const menuBar = [
{
key: 'bold',
icon: 'mdi-format-bold',
title: 'Bold',
action: () => editor.value.chain().focus().toggleBold().run(),
isActive: () => editor.value.isActive('bold')
},
{
key: 'italic',
icon: 'mdi-format-italic',
title: 'Italic',
action: () => editor.value.chain().focus().toggleItalic().run(),
isActive: () => editor.value.isActive('italic')
},
{
key: 'strikethrough',
icon: 'mdi-format-strikethrough',
title: 'Strike',
action: () => editor.value.chain().focus().toggleStrike().run(),
isActive: () => editor.value.isActive('strike')
},
{
key: 'code',
icon: 'mdi-code-tags',
title: 'Code',
action: () => editor.value.chain().focus().toggleCode().run(),
isActive: () => editor.value.isActive('code')
},
{
key: 'fontfamily',
icon: 'mdi-format-font',
title: 'Font Family',
type: 'dropdown',
isActive: () => editor.value.isActive('fontFamily'),
children: [
{
key: 'fontunset',
icon: 'mdi-format-font',
title: 'Sans-Serif',
action: () => editor.value.chain().focus().unsetFontFamily().run()
},
{
key: 'monospace',
icon: 'mdi-format-font',
title: 'Monospace',
action: () => editor.value.chain().focus().setFontFamily('monospace').run()
}
]
},
{
key: 'color',
icon: 'mdi-palette',
title: 'Text Color',
type: 'dropdown',
isActive: () => editor.value.isActive('color'),
children: [
{
key: 'color-blue',
icon: 'mdi-palette',
title: 'Blue',
color: 'blue',
action: () => editor.value.chain().focus().toggleHighlight().run()
},
{
key: 'color-brown',
icon: 'mdi-palette',
title: 'Brown',
color: 'brown',
action: () => editor.value.chain().focus().toggleHighlight().run()
},
{
key: 'color-green',
icon: 'mdi-palette',
title: 'Green',
color: 'green',
action: () => editor.value.chain().focus().toggleHighlight().run()
},
{
key: 'color-orange',
icon: 'mdi-palette',
title: 'Orange',
color: 'orange',
action: () => editor.value.chain().focus().toggleHighlight().run()
},
{
key: 'color-pink',
icon: 'mdi-palette',
title: 'Pink',
color: 'pink',
action: () => editor.value.chain().focus().toggleHighlight().run()
},
{
key: 'color-purple',
icon: 'mdi-palette',
title: 'Purple',
color: 'purple',
action: () => editor.value.chain().focus().toggleHighlight().run()
},
{
key: 'color-red',
icon: 'mdi-palette',
title: 'Red',
color: 'red',
action: () => editor.value.chain().focus().toggleHighlight().run()
},
{
key: 'color-teal',
icon: 'mdi-palette',
title: 'Teal',
color: 'teal',
action: () => editor.value.chain().focus().toggleHighlight().run()
},
{
key: 'color-yellow',
icon: 'mdi-palette',
title: 'Yellow',
color: 'yellow',
action: () => editor.value.chain().focus().toggleHighlight().run()
},
{
type: 'divider'
},
{
key: 'color-remove',
icon: 'mdi-palette',
title: 'Default',
color: 'grey',
action: () => editor.value.chain().focus().unsetHighlight().run()
}
]
},
{
key: 'highlight',
icon: 'mdi-marker',
title: 'Highlight',
type: 'dropdown',
isActive: () => editor.value.isActive('highlight'),
children: [
{
key: 'highlight-yellow',
icon: 'mdi-marker',
title: 'Yellow',
color: 'yellow',
action: () => editor.value.chain().focus().toggleHighlight().run()
},
{
key: 'highlight-blue',
icon: 'mdi-marker',
title: 'Blue',
color: 'blue',
action: () => editor.value.chain().focus().toggleHighlight().run()
},
{
key: 'highlight-pink',
icon: 'mdi-marker',
title: 'Pink',
color: 'pink',
action: () => editor.value.chain().focus().toggleHighlight().run()
},
{
key: 'highlight-green',
icon: 'mdi-marker',
title: 'Green',
color: 'green',
action: () => editor.value.chain().focus().toggleHighlight().run()
},
{
key: 'highlight-orange',
icon: 'mdi-marker',
title: 'Orange',
color: 'orange',
action: () => editor.value.chain().focus().toggleHighlight().run()
},
{
type: 'divider'
},
{
key: 'highlight-remove',
icon: 'mdi-marker-cancel',
title: 'Remove',
color: 'grey',
action: () => editor.value.chain().focus().unsetHighlight().run()
}
]
},
{
type: 'divider'
},
{
key: 'header',
icon: 'mdi-format-header-pound',
title: 'Header',
type: 'dropdown',
isActive: () => editor.value.isActive('heading'),
children: [
{
key: 'h1',
icon: 'mdi-format-header-1',
title: 'Header 1',
action: () => editor.value.chain().focus().toggleHeading({ level: 1 }).run(),
isActive: () => editor.value.isActive('heading', { level: 1 })
},
{
key: 'h2',
icon: 'mdi-format-header-2',
title: 'Header 2',
action: () => editor.value.chain().focus().toggleHeading({ level: 2 }).run(),
isActive: () => editor.value.isActive('heading', { level: 2 })
},
{
key: 'h3',
icon: 'mdi-format-header-3',
title: 'Header 3',
action: () => editor.value.chain().focus().toggleHeading({ level: 3 }).run(),
isActive: () => editor.value.isActive('heading', { level: 3 })
},
{
key: 'h4',
icon: 'mdi-format-header-4',
title: 'Header 4',
action: () => editor.value.chain().focus().toggleHeading({ level: 4 }).run(),
isActive: () => editor.value.isActive('heading', { level: 4 })
},
{
key: 'h5',
icon: 'mdi-format-header-5',
title: 'Header 5',
action: () => editor.value.chain().focus().toggleHeading({ level: 5 }).run(),
isActive: () => editor.value.isActive('heading', { level: 5 })
},
{
key: 'h6',
icon: 'mdi-format-header-6',
title: 'Header 6',
action: () => editor.value.chain().focus().toggleHeading({ level: 6 }).run(),
isActive: () => editor.value.isActive('heading', { level: 6 })
}
]
},
{
key: 'paragraph',
icon: 'mdi-format-paragraph',
title: 'Paragraph',
action: () => editor.value.chain().focus().setParagraph().run(),
isActive: () => editor.value.isActive('paragraph')
},
{
type: 'divider'
},
{
key: 'align',
type: 'btngroup',
children: [
{
key: 'align-left',
icon: 'mdi-format-align-left',
title: 'Left Align',
action: () => editor.value.chain().focus().setTextAlign('left').run(),
isActive: () => editor.value.isActive({ textAlign: 'left' })
},
{
key: 'align-center',
icon: 'mdi-format-align-center',
title: 'Center Align',
action: () => editor.value.chain().focus().setTextAlign('center').run(),
isActive: () => editor.value.isActive({ textAlign: 'center' })
},
{
key: 'align-right',
icon: 'mdi-format-align-right',
title: 'Right Align',
action: () => editor.value.chain().focus().setTextAlign('right').run(),
isActive: () => editor.value.isActive({ textAlign: 'right' })
},
{
key: 'align-justify',
icon: 'mdi-format-align-justify',
title: 'Justify Align',
action: () => editor.value.chain().focus().setTextAlign('justify').run(),
isActive: () => editor.value.isActive({ textAlign: 'justify' })
}
]
},
{
type: 'divider'
},
{
key: 'bulletlist',
icon: 'mdi-format-list-bulleted',
title: 'Bullet List',
action: () => editor.value.chain().focus().toggleBulletList().run(),
isActive: () => editor.value.isActive('bulletList')
},
{
key: 'orderedlist',
icon: 'mdi-format-list-numbered',
title: 'Ordered List',
action: () => editor.value.chain().focus().toggleOrderedList().run(),
isActive: () => editor.value.isActive('orderedList')
},
{
key: 'tasklist',
icon: 'mdi-format-list-checks',
title: 'Task List',
action: () => editor.value.chain().focus().toggleTaskList().run(),
isActive: () => editor.value.isActive('taskList')
},
{
type: 'divider'
},
{
key: 'codeblock',
icon: 'mdi-code-json',
title: 'Code Block',
action: () => editor.value.chain().focus().toggleCodeBlock().run(),
isActive: () => editor.value.isActive('codeBlock')
},
{
key: 'blockquote',
icon: 'mdi-format-quote-open',
title: 'Blockquote',
action: () => editor.value.chain().focus().toggleBlockquote().run(),
isActive: () => editor.value.isActive('blockquote')
},
{
key: 'rule',
icon: 'mdi-minus',
title: 'Horizontal Rule',
action: () => editor.value.chain().focus().setHorizontalRule().run()
},
{
key: 'link',
icon: 'mdi-link-variant',
title: 'Link',
action: () => {
// TODO: insert link
}
},
{
key: 'image',
icon: 'mdi-image-plus',
title: 'Image',
action: () => {
siteStore.openFileManager({ insertMode: true })
}
},
{
key: 'table',
icon: 'mdi-table',
title: 'Table',
type: 'dropdown',
isActive: () => editor.value.isActive('table'),
children: [
{
key: 'table-insert',
icon: 'mdi-table-large-plus',
title: 'Insert Table',
action: () => editor.value.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()
},
{
type: 'divider'
},
{
key: 'table-addcolumnbefore',
icon: 'mdi-table-column-plus-before',
title: 'Add Column Before',
action: () => editor.value.chain().focus().addColumnBefore().run(),
disabled: () => !editor.value.can().addColumnBefore()
},
{
key: 'table-addcolumnafter',
icon: 'mdi-table-column-plus-after',
title: 'Add Column After',
action: () => editor.value.chain().focus().addColumnAfter().run(),
disabled: () => !editor.value.can().addColumnAfter()
},
{
key: 'table-deletecolumn',
icon: 'mdi-table-column-remove',
title: 'Remove Column',
action: () => editor.value.chain().focus().deleteColumn().run(),
disabled: () => !editor.value.can().deleteColumn()
},
{
type: 'divider'
},
{
key: 'table-addrowbefore',
icon: 'mdi-table-row-plus-before',
title: 'Add Row Before',
action: () => editor.value.chain().focus().addRowBefore().run(),
disabled: () => !editor.value.can().addRowBefore()
},
{
key: 'table-addrowafter',
icon: 'mdi-table-row-plus-after',
title: 'Add Row After',
action: () => editor.value.chain().focus().addRowAfter().run(),
disabled: () => !editor.value.can().addRowAfter()
},
{
key: 'table-deleterow',
icon: 'mdi-table-row-remove',
title: 'Remove Row',
action: () => editor.value.chain().focus().deleteRow().run(),
disabled: () => !editor.value.can().deleteRow()
},
{
type: 'divider'
},
{
key: 'table-merge',
icon: 'mdi-table-merge-cells',
title: 'Merge Cells',
action: () => editor.value.chain().focus().mergeCells().run(),
disabled: () => !editor.value.can().mergeCells()
},
{
key: 'table-split',
icon: 'mdi-table-split-cell',
title: 'Split Cell',
action: () => editor.value.chain().focus().splitCell().run(),
disabled: () => !editor.value.can().splitCell()
},
{
type: 'divider'
},
{
key: 'table-toggleHeaderColumn',
icon: 'mdi-table-column',
title: 'Toggle Header Column',
action: () => editor.value.chain().focus().toggleHeaderColumn().run(),
disabled: () => !editor.value.can().toggleHeaderColumn()
},
{
key: 'table-toggleHeaderRow',
icon: 'mdi-table-row',
title: 'Toggle Header Row',
action: () => editor.value.chain().focus().toggleHeaderRow().run(),
disabled: () => !editor.value.can().toggleHeaderRow()
},
{
key: 'table-toggleHeaderCell',
icon: 'mdi-crop-square',
title: 'Toggle Header Cell',
action: () => editor.value.chain().focus().toggleHeaderCell().run(),
disabled: () => !editor.value.can().toggleHeaderCell()
},
{
type: 'divider'
},
{
key: 'table-fix',
icon: 'mdi-table-heart',
title: 'Fix Table',
action: () => editor.value.chain().focus().fixTables().run(),
disabled: () => !editor.value.can().fixTables()
},
{
key: 'table-remove',
icon: 'mdi-table-large-remove',
title: 'Delete Table',
action: () => editor.value.chain().focus().deleteTable().run(),
disabled: () => !editor.value.can().deleteTable()
}
]
},
{
type: 'divider'
},
{
key: 'pagebreak',
icon: 'mdi-format-page-break',
title: 'Hard Break',
action: () => editor.value.chain().focus().setHardBreak().run()
},
{
key: 'clearformat',
icon: 'mdi-format-clear',
title: 'Clear Format',
action: () => editor.value.chain()
.focus()
.clearNodes()
.unsetAllMarks()
.run()
},
{
type: 'divider'
},
{
key: 'undo',
icon: 'mdi-undo-variant',
title: 'Undo',
action: () => editor.value.chain().focus().undo().run(),
disabled: () => !editor.value.can().undo()
},
{
key: 'redo',
icon: 'mdi-redo-variant',
title: 'Redo',
action: () => editor.value.chain().focus().redo().run(),
disabled: () => !editor.value.can().redo()
}
]
// METHODS
function init () {
// -> Setup Editor View
editorStore.$patch({
hideSideNav: false
})
// -> Init Live Collab
// this.ydoc = new Y.Doc()
/* eslint-disable no-unused-vars */
// const dbProvider = new IndexeddbPersistence('example-document', this.ydoc)
// const wsProvider = new WebsocketProvider('ws://127.0.0.1:1234', 'example-document', this.ydoc)
/* eslint-enable no-unused-vars */
// -> Initialize TipTap
editor = useEditor({
content: pageStore.content && pageStore.content.startsWith('{') ? JSON.parse(pageStore.content) : `<p>${pageStore.content}</p>`,
extensions: [
StarterKit.configure({
codeBlock: false,
history: {
depth: 500
}
}),
CodeBlockLowlight.configure({
lowlight
}),
Color,
// Collaboration.configure({
// document: this.ydoc
// }),
FontFamily,
Highlight.configure({
multicolor: true
}),
Image,
Mention.configure({
// TODO: suggestions
}),
Placeholder.configure({
placeholder: 'Enter some content here...'
}),
Table.configure({
resizable: true
}),
TableRow,
TableHeader,
TableCell,
TaskList,
TaskItem,
TextAlign,
TextStyle,
Typography
],
onUpdate: ({ editor }) => {
editorStore.$patch({
lastChangeTimestamp: DateTime.utc()
})
pageStore.$patch({
content: JSON.stringify(editor.getJSON()),
render: editor.getHTML()
})
}
})
}
function insertTable () {
// this.ql.getModule('table').insertTable(3, 3)
}
function snapshot () {
// console.info(Y.encodeStateVector(this.ydoc))
}
// MOUNTED
onMounted(() => {
// init()
})
onBeforeUnmount(() => {
editor.value.destroy()
})
init()
</script>
<style lang="scss">
.wysiwyg-container {
height: calc(100% - 41px);
.wysiwyg-toolbar {
border: none;
border-bottom: 1px solid $grey-4;
display: flex;
align-items: center;
padding: 4px;
background: linear-gradient(to top, $grey-1 0%, #FFF 100%);
}
.ProseMirror {
padding: 16px;
min-height: 75vh;
&-focused {
border: none;
outline: none;
}
> * + * {
margin-top: 0.75em;
}
ul,
ol {
padding: 0 1rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
}
img {
max-width: 100%;
height: auto;
}
blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
}
hr {
border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1);
margin: 2rem 0;
}
table {
border-collapse: collapse;
table-layout: fixed;
width: 100%;
margin: 0;
overflow: hidden;
td,
th {
min-width: 1em;
border: 2px solid #ced4da;
padding: 3px 5px;
vertical-align: top;
box-sizing: border-box;
position: relative;
> * {
margin-bottom: 0;
}
}
th {
font-weight: bold;
text-align: left;
background-color: #f1f3f5;
}
.selectedCell:after {
z-index: 2;
position: absolute;
content: "";
left: 0; right: 0; top: 0; bottom: 0;
background: rgba(200, 200, 255, 0.4);
pointer-events: none;
}
.column-resize-handle {
position: absolute;
right: -2px;
top: 0;
bottom: -2px;
width: 4px;
background-color: #adf;
pointer-events: none;
}
}
.tableWrapper {
overflow-x: auto;
}
.resize-cursor {
cursor: ew-resize;
cursor: col-resize;
}
ul[data-type="taskList"] {
list-style: none;
padding: 0;
li {
display: flex;
align-items: center;
> label {
flex: 0 0 auto;
margin-right: 0.5rem;
}
}
}
p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: #ced4da;
pointer-events: none;
height: 0;
}
}
}
</style>