Add TipTap table extensions for improved rich text editing
This commit is contained in:
parent
a99d90b105
commit
69494a9834
@ -65,6 +65,7 @@ import {
|
||||
faEllipsisV,
|
||||
faLocationArrow,
|
||||
faTools,
|
||||
faTable,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import {
|
||||
@ -178,4 +179,5 @@ library.add(
|
||||
faLocationArrow,
|
||||
faTools,
|
||||
faExclamationCircle,
|
||||
faTable,
|
||||
)
|
||||
|
||||
@ -43,3 +43,70 @@
|
||||
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
|
||||
},
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<b-dropdown
|
||||
menu-class="text-center"
|
||||
variant="link">
|
||||
|
||||
variant="link"
|
||||
>
|
||||
<template slot="button-content">
|
||||
<span class="text-dark font-weight-bold">
|
||||
<span :class="rootActiveClasses()">
|
||||
@ -28,7 +28,8 @@
|
||||
<span :class="activeClasses(v.attrs)">
|
||||
<font-awesome-icon
|
||||
v-if="format.icon"
|
||||
:icon="v.icon" />
|
||||
:icon="v.icon"
|
||||
/>
|
||||
<span v-else>
|
||||
{{ v.label }}
|
||||
</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 Table from './Table.vue'
|
||||
export default {
|
||||
Alignment,
|
||||
Table,
|
||||
}
|
||||
|
||||
@ -2,13 +2,13 @@
|
||||
<div class="d-flex flex-wrap">
|
||||
<component
|
||||
v-for="(f, i) of formats"
|
||||
:key="`${f.name}${i}}`"
|
||||
:key="`${f.name}${i}`"
|
||||
:is="getItem(f)"
|
||||
:format="f"
|
||||
v-bind="$props"
|
||||
:labels="labels"
|
||||
:current-value="currentValue"
|
||||
@click="(commands[$event.type])($event.attrs)" />
|
||||
@click="triggerCommand" />
|
||||
|
||||
<!-- Extra button to remove formatting -->
|
||||
<b-button
|
||||
@ -103,6 +103,10 @@ export default {
|
||||
removeMarks () {
|
||||
removeMark(null)(this.editor.view.state, this.editor.view.dispatch)
|
||||
},
|
||||
|
||||
triggerCommand (v) {
|
||||
this.commands[v.type](v.attrs)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -51,6 +51,7 @@ export default {
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
|
||||
labels: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
@ -62,7 +63,7 @@ export default {
|
||||
return {
|
||||
formats,
|
||||
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,
|
||||
editor: undefined,
|
||||
currentValue: '',
|
||||
|
||||
@ -23,6 +23,10 @@ import {
|
||||
ListItem,
|
||||
TodoItem,
|
||||
History,
|
||||
Table,
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
} from 'tiptap-extensions'
|
||||
|
||||
// Defines a set of formats that our document supports
|
||||
@ -46,6 +50,12 @@ export const getFormats = () => [
|
||||
new History(),
|
||||
new TextBackground(),
|
||||
new TextColor(),
|
||||
new Table({
|
||||
resizable: true,
|
||||
}),
|
||||
new TableHeader(),
|
||||
new TableCell(),
|
||||
new TableRow(),
|
||||
]
|
||||
|
||||
// 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 } },
|
||||
|
||||
// @note There is no free FA icon for this
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user