Add TipTap table extensions for improved rich text editing
This commit is contained in:
parent
a99d90b105
commit
69494a9834
@ -65,6 +65,7 @@ import {
|
|||||||
faEllipsisV,
|
faEllipsisV,
|
||||||
faLocationArrow,
|
faLocationArrow,
|
||||||
faTools,
|
faTools,
|
||||||
|
faTable,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -178,4 +179,5 @@ library.add(
|
|||||||
faLocationArrow,
|
faLocationArrow,
|
||||||
faTools,
|
faTools,
|
||||||
faExclamationCircle,
|
faExclamationCircle,
|
||||||
|
faTable,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -43,3 +43,70 @@
|
|||||||
padding-bottom: 0px !important;
|
padding-bottom: 0px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Basic editor styles */
|
||||||
|
.editor__content :first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table-specific styling */
|
||||||
|
.editor__content table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor__content td,
|
||||||
|
.editor__content th {
|
||||||
|
border: 1px solid var(--gray);
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-width: 1em;
|
||||||
|
padding: 6px 8px;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor__content td > *,
|
||||||
|
.editor__content th > * {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor__content th {
|
||||||
|
background-color: var(--gray-dark);
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor__content .selectedCell::after {
|
||||||
|
background: var(--gray-dark);
|
||||||
|
content: "";
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor__content .column-resize-handle {
|
||||||
|
background-color: var(--purple);
|
||||||
|
bottom: -2px;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
right: -2px;
|
||||||
|
top: 0;
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor__content .tableWrapper {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor__content .resize-cursor {
|
||||||
|
cursor: ew-resize;
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
||||||
|
|||||||
@ -75,6 +75,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return rtr
|
return rtr
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<b-dropdown
|
<b-dropdown
|
||||||
menu-class="text-center"
|
menu-class="text-center"
|
||||||
variant="link">
|
variant="link"
|
||||||
|
>
|
||||||
<template slot="button-content">
|
<template slot="button-content">
|
||||||
<span class="text-dark font-weight-bold">
|
<span class="text-dark font-weight-bold">
|
||||||
<span :class="rootActiveClasses()">
|
<span :class="rootActiveClasses()">
|
||||||
@ -28,7 +28,8 @@
|
|||||||
<span :class="activeClasses(v.attrs)">
|
<span :class="activeClasses(v.attrs)">
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
v-if="format.icon"
|
v-if="format.icon"
|
||||||
:icon="v.icon" />
|
:icon="v.icon"
|
||||||
|
/>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ v.label }}
|
{{ v.label }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<b-dropdown
|
||||||
|
menu-class="text-center"
|
||||||
|
variant="link">
|
||||||
|
<template slot="button-content">
|
||||||
|
<span class="text-dark font-weight-bold">
|
||||||
|
<span :class="rootActiveClasses()">
|
||||||
|
<font-awesome-icon
|
||||||
|
v-if="format.icon"
|
||||||
|
:icon="format.icon" />
|
||||||
|
<span v-else>
|
||||||
|
{{ format.label }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<b-dropdown-item
|
||||||
|
v-for="v of format.variants"
|
||||||
|
:key="v.variant"
|
||||||
|
@click="emitClick(v)"
|
||||||
|
>
|
||||||
|
{{ v.label }}
|
||||||
|
|
||||||
|
</b-dropdown-item>
|
||||||
|
</b-dropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import base from '../TNode/base.vue'
|
||||||
|
import { nodeTypes } from '../../lib/formats'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component is used to display node alignment formatting
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: 't-nattr-table',
|
||||||
|
extends: base,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
isActive: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
activeClasses (attrs) {
|
||||||
|
const an = this.activeNode(nodeTypes, attrs)
|
||||||
|
if (!an || !an.node) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const ac = (type, attrs) => {
|
||||||
|
const b = (this.isActive[type])
|
||||||
|
return b && (b(attrs))
|
||||||
|
}
|
||||||
|
if (ac(an.node.type.name, { ...an.node.attrs, ...attrs })) {
|
||||||
|
return ['text-success']
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* dispatches node attr update for all affected nodes
|
||||||
|
* use a single transaction, so ctrl + z works as intended
|
||||||
|
*/
|
||||||
|
dispatchTransaction (v) {
|
||||||
|
const ann = this.activeNodes(nodeTypes)
|
||||||
|
const tr = this.$attrs.editor.state.tr
|
||||||
|
for (const an of ann) {
|
||||||
|
tr.setNodeMarkup(an.position, an.node.type, { ...an.node.attrs, ...v.attrs })
|
||||||
|
}
|
||||||
|
this.$attrs.editor.dispatchTransaction(tr)
|
||||||
|
},
|
||||||
|
|
||||||
|
emitClick (v) {
|
||||||
|
this.$emit('click', { type: v.type, attrs: { ...v.attrs } })
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to determine if the root formater should be shown as active
|
||||||
|
* @returns {Array|undefined}
|
||||||
|
*/
|
||||||
|
rootActiveClasses (v) {
|
||||||
|
if (this.format.variants.find(({ type, attrs }) => this.activeClasses(attrs))) {
|
||||||
|
return ['text-success']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import Alignment from './Alignment.vue'
|
import Alignment from './Alignment.vue'
|
||||||
|
import Table from './Table.vue'
|
||||||
export default {
|
export default {
|
||||||
Alignment,
|
Alignment,
|
||||||
|
Table,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,13 +2,13 @@
|
|||||||
<div class="d-flex flex-wrap">
|
<div class="d-flex flex-wrap">
|
||||||
<component
|
<component
|
||||||
v-for="(f, i) of formats"
|
v-for="(f, i) of formats"
|
||||||
:key="`${f.name}${i}}`"
|
:key="`${f.name}${i}`"
|
||||||
:is="getItem(f)"
|
:is="getItem(f)"
|
||||||
:format="f"
|
:format="f"
|
||||||
v-bind="$props"
|
v-bind="$props"
|
||||||
:labels="labels"
|
:labels="labels"
|
||||||
:current-value="currentValue"
|
:current-value="currentValue"
|
||||||
@click="(commands[$event.type])($event.attrs)" />
|
@click="triggerCommand" />
|
||||||
|
|
||||||
<!-- Extra button to remove formatting -->
|
<!-- Extra button to remove formatting -->
|
||||||
<b-button
|
<b-button
|
||||||
@ -103,6 +103,10 @@ export default {
|
|||||||
removeMarks () {
|
removeMarks () {
|
||||||
removeMark(null)(this.editor.view.state, this.editor.view.dispatch)
|
removeMark(null)(this.editor.view.state, this.editor.view.dispatch)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
triggerCommand (v) {
|
||||||
|
this.commands[v.type](v.attrs)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -51,6 +51,7 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
|
||||||
labels: {
|
labels: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
@ -62,7 +63,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
formats,
|
formats,
|
||||||
toolbar: getToolbar(),
|
toolbar: getToolbar(),
|
||||||
// Helper to determine if current content differes from prop's content
|
// Helper to determine if current content differs from prop's content
|
||||||
emittedContent: false,
|
emittedContent: false,
|
||||||
editor: undefined,
|
editor: undefined,
|
||||||
currentValue: '',
|
currentValue: '',
|
||||||
|
|||||||
@ -23,6 +23,10 @@ import {
|
|||||||
ListItem,
|
ListItem,
|
||||||
TodoItem,
|
TodoItem,
|
||||||
History,
|
History,
|
||||||
|
Table,
|
||||||
|
TableRow,
|
||||||
|
TableHeader,
|
||||||
|
TableCell,
|
||||||
} from 'tiptap-extensions'
|
} from 'tiptap-extensions'
|
||||||
|
|
||||||
// Defines a set of formats that our document supports
|
// Defines a set of formats that our document supports
|
||||||
@ -46,6 +50,12 @@ export const getFormats = () => [
|
|||||||
new History(),
|
new History(),
|
||||||
new TextBackground(),
|
new TextBackground(),
|
||||||
new TextColor(),
|
new TextColor(),
|
||||||
|
new Table({
|
||||||
|
resizable: true,
|
||||||
|
}),
|
||||||
|
new TableHeader(),
|
||||||
|
new TableCell(),
|
||||||
|
new TableRow(),
|
||||||
]
|
]
|
||||||
|
|
||||||
// Defines the structure of our editor toolbar
|
// Defines the structure of our editor toolbar
|
||||||
@ -80,6 +90,26 @@ export const getToolbar = () => [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'table',
|
||||||
|
icon: 'table',
|
||||||
|
nodeAttr: true,
|
||||||
|
component: 'Table',
|
||||||
|
variants: [
|
||||||
|
{ variant: 'insert', icon: '', label: 'Insert Table', type: 'createTable', attrs: {rowsCount: 3, colsCount: 3, withHeaderRow: true } },
|
||||||
|
{ variant: 'splitCell', icon: '', label: 'Split Cell', type: 'splitCell', },
|
||||||
|
{ variant: 'mergeCells', icon: '', label: 'Merge Cells', type: 'mergeCells', },
|
||||||
|
{ variant: 'deleteColumn', icon: '', label: 'Delete Column', type: 'deleteColumn', },
|
||||||
|
{ variant: 'insertRowBefore', icon: '', label: 'Add Row Before', type: 'addRowBefore', },
|
||||||
|
{ variant: 'insertColumnAfter', icon: '', label: 'Insert Column After', type: 'addColumnAfter', },
|
||||||
|
{ variant: 'insertHeaderRow', icon: '', label: 'Insert Header Row', type: 'toggleHeaderRow', },
|
||||||
|
{ variant: 'insertHeaderCell', icon: '', label: 'Insert Header Cell', type: 'toggleHeaderCell', },
|
||||||
|
{ variant: 'insertColumnBefore', icon: '', label: 'Insert Column Before', type: 'addColumnBefore', },
|
||||||
|
{ variant: 'insertHeaderColumn', icon: '', label: 'Insert Header Column', type: 'toggleHeaderColumn', },
|
||||||
|
{ variant: 'deleteTable', icon: '', label: 'Delete Table', type: 'deleteTable', },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
{ type: 'link', mark: true, component: 'Link', icon: 'link', attrs: { href: null } },
|
{ type: 'link', mark: true, component: 'Link', icon: 'link', attrs: { href: null } },
|
||||||
|
|
||||||
// @note There is no free FA icon for this
|
// @note There is no free FA icon for this
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user