Merge branch 'feature/tables' into main

This commit is contained in:
Hans Pagel
2021-01-25 11:08:41 +01:00
36 changed files with 1013 additions and 3 deletions

View File

@@ -0,0 +1,7 @@
context('/api/nodes/table', () => {
before(() => {
cy.visit('/api/nodes/table')
})
// TODO: Write tests
})

View File

@@ -0,0 +1,208 @@
<template>
<div v-if="editor">
<button @click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()">
insertTable
</button>
<button @click="editor.chain().focus().addColumnBefore().run()">
addColumnBefore
</button>
<button @click="editor.chain().focus().addColumnAfter().run()">
addColumnAfter
</button>
<button @click="editor.chain().focus().deleteColumn().run()">
deleteColumn
</button>
<button @click="editor.chain().focus().addRowBefore().run()">
addRowBefore
</button>
<button @click="editor.chain().focus().addRowAfter().run()">
addRowAfter
</button>
<button @click="editor.chain().focus().deleteRow().run()">
deleteRow
</button>
<button @click="editor.chain().focus().deleteTable().run()">
deleteTable
</button>
<button @click="editor.chain().focus().mergeCells().run()">
mergeCells
</button>
<button @click="editor.chain().focus().splitCell().run()">
splitCell
</button>
<button @click="editor.chain().focus().toggleHeaderColumn().run()">
toggleHeaderColumn
</button>
<button @click="editor.chain().focus().toggleHeaderRow().run()">
toggleHeaderRow
</button>
<button @click="editor.chain().focus().toggleHeaderCell().run()">
toggleHeaderCell
</button>
<button @click="editor.chain().focus().mergeOrSplit().run()">
mergeOrSplit
</button>
<button @click="editor.chain().focus().setCellAttributes({name: 'color', value: 'pink'}).run()">
setCellAttributes
</button>
<button @click="editor.chain().focus().fixTables().run()">
fixTables
</button>
<button @click="editor.chain().focus().goToNextCell().run()">
goToNextCell
</button>
<button @click="editor.chain().focus().goToPreviousCell().run()">
goToPreviousCell
</button>
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor } from '@tiptap/core'
import { EditorContent } from '@tiptap/vue'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
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 Gapcursor from '@tiptap/extension-gapcursor'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
Gapcursor,
Table.configure({
resizable: true,
}),
TableRow,
TableHeader,
TableCell.extend({
addAttributes() {
return {
// original attributes
colspan: {
default: 1,
},
rowspan: {
default: 1,
},
colwidth: {
default: null,
},
// add a color attribute to the table cell
color: {
default: null,
renderHTML: attributes => {
return {
style: `color: ${attributes.color}`,
}
},
},
}
},
}),
],
content: `
<p>
People
</p>
<table>
<tbody>
<tr>
<th>Name</th>
<th colspan="3">Description</th>
</tr>
<tr>
<td>Cyndi Lauper</td>
<td>singer</td>
<td>songwriter</td>
<td>actress</td>
</tr>
</tbody>
</table>
`,
})
},
beforeDestroy() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
.ProseMirror {
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;
}
.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 {
padding: 1rem 0;
overflow-x: auto;
}
.resize-cursor {
cursor: ew-resize;
cursor: col-resize;
}
</style>

View File

@@ -0,0 +1,8 @@
# TableHeader
:::pro Fund the development 💖
We need your support to maintain, update, support and develop tiptap 2. If youre waiting for this extension, [become a sponsor and fund open source](/sponsor).
:::
TODO

View File

@@ -5,3 +5,10 @@ We need your support to maintain, update, support and develop tiptap 2. If you
:::
TODO
⚠️ Preview
Tasks
- backspace: when all cells are selected, delete table
<demo name="Nodes/Table" />

View File

@@ -139,6 +139,9 @@
- title: TableCell
link: /api/nodes/table-cell
type: draft
- title: TableHeader
link: /api/nodes/table-header
type: draft
- title: TaskList
link: /api/nodes/task-list
- title: TaskItem

View File

@@ -30,6 +30,7 @@
"@types/prosemirror-model": "^1.11.2",
"@types/prosemirror-schema-list": "^1.0.2",
"@types/prosemirror-state": "^1.2.6",
"@types/prosemirror-tables": "^0.9.1",
"@types/prosemirror-transform": "^1.1.2",
"@types/prosemirror-view": "^1.17.1",
"prosemirror-commands": "^1.1.3",

View File

@@ -68,6 +68,11 @@ export interface NodeConfig<Options = any, Commands = {}> extends Overwrite<Exte
*/
isolating?: NodeSpec['isolating'] | ((this: { options: Options }) => NodeSpec['isolating']),
/**
* Table Role
*/
tableRole?: NodeSpec['tableRole'] | ((this: { options: Options }) => NodeSpec['tableRole']),
/**
* Parse HTML
*/

View File

@@ -1,6 +1,7 @@
import { EditorState, TextSelection } from 'prosemirror-state'
import { Command, FocusPosition } from '../types'
import minMax from '../utilities/minMax'
import isTextSelection from '../helpers/isTextSelection'
function resolveSelection(state: EditorState, position: FocusPosition = null) {
if (!position) {
@@ -42,6 +43,12 @@ export const focus = (position: FocusPosition = null): Command => ({
return true
}
// we dont try to resolve a NodeSelection or CellSelection
if (dispatch && position === null && !isTextSelection(editor.state.selection)) {
view.focus()
return true
}
const { from, to } = resolveSelection(editor.state, position) || editor.state.selection
const { doc } = tr
const resolvedFrom = minMax(from, 0, doc.content.size)

View File

@@ -24,7 +24,7 @@ export const FocusEvents = Extension.create({
view.dispatch(transaction)
return true
return false
},
blur: (view, event) => {
editor.isFocused = false
@@ -35,7 +35,7 @@ export const FocusEvents = Extension.create({
view.dispatch(transaction)
return true
return false
},
},
},

View File

@@ -0,0 +1,23 @@
import { ResolvedPos, Node as ProsemirrorNode } from 'prosemirror-model'
export type Predicate = (node: ProsemirrorNode) => boolean
export default function findParentNodeClosestToPos($pos: ResolvedPos, predicate: Predicate): ({
pos: number,
start: number,
depth: number,
node: ProsemirrorNode,
} | undefined) {
for (let i = $pos.depth; i > 0; i -= 1) {
const node = $pos.node(i)
if (predicate(node)) {
return {
pos: i > 0 ? $pos.before(i) : 0,
start: $pos.start(i),
depth: i,
node,
}
}
}
}

View File

@@ -36,6 +36,7 @@ export default function getSchema(extensions: Extensions): Schema {
code: callOrReturn(extension.config.code, context),
defining: callOrReturn(extension.config.defining, context),
isolating: callOrReturn(extension.config.isolating, context),
tableRole: callOrReturn(extension.config.tableRole, context),
attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => {
return [extensionAttribute.name, { default: extensionAttribute?.attribute?.default }]
})),

View File

@@ -0,0 +1,6 @@
import { CellSelection } from 'prosemirror-tables'
import isObject from '../utilities/isObject'
export default function isCellSelection(value: unknown): value is CellSelection {
return isObject(value) && value instanceof CellSelection
}

View File

@@ -0,0 +1,6 @@
import { NodeSelection } from 'prosemirror-state'
import isObject from '../utilities/isObject'
export default function isNodeSelection(value: unknown): value is NodeSelection {
return isObject(value) && value instanceof NodeSelection
}

View File

@@ -0,0 +1,6 @@
import { TextSelection } from 'prosemirror-state'
import isObject from '../utilities/isObject'
export default function isTextSelection(value: unknown): value is TextSelection {
return isObject(value) && value instanceof TextSelection
}

View File

@@ -16,5 +16,9 @@ export { default as mergeAttributes } from './utilities/mergeAttributes'
export { default as isActive } from './helpers/isActive'
export { default as isMarkActive } from './helpers/isMarkActive'
export { default as isNodeActive } from './helpers/isNodeActive'
export { default as isNodeSelection } from './helpers/isNodeSelection'
export { default as isTextSelection } from './helpers/isTextSelection'
export { default as isCellSelection } from './helpers/isCellSelection'
export { default as findParentNodeClosestToPos } from './helpers/findParentNodeClosestToPos'
export interface AllExtensions {}

View File

@@ -0,0 +1,14 @@
# @tiptap/extension-table-cell
[![Version](https://img.shields.io/npm/v/@tiptap/extension-table-cell.svg?label=version)](https://www.npmjs.com/package/@tiptap/extension-table-cell)
[![Downloads](https://img.shields.io/npm/dm/@tiptap/extension-table-cell.svg)](https://npmcharts.com/compare/tiptap?minimal=true)
[![License](https://img.shields.io/npm/l/@tiptap/extension-table-cell.svg)](https://www.npmjs.com/package/@tiptap/extension-table-cell)
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis)
## Introduction
tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as *New York Times*, *The Guardian* or *Atlassian*.
## Offical Documentation
Documentation can be found on the [tiptap website](https://tiptap.dev).
## License
tiptap is open-sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap-next/blob/main/LICENSE.md).

View File

@@ -0,0 +1,27 @@
{
"name": "@tiptap/extension-table-cell",
"description": "table cell extension for tiptap",
"version": "2.0.0-alpha.5",
"homepage": "https://tiptap.dev",
"keywords": [
"tiptap",
"tiptap extension"
],
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"main": "dist/tiptap-extension-table-cell.cjs.js",
"umd": "dist/tiptap-extension-table-cell.umd.js",
"module": "dist/tiptap-extension-table-cell.esm.js",
"unpkg": "dist/tiptap-extension-table-cell.bundle.umd.min.js",
"types": "dist/packages/extension-table-cell/src/index.d.ts",
"files": [
"src",
"dist"
],
"peerDependencies": {
"@tiptap/core": "^2.0.0-alpha.6"
}
}

View File

@@ -0,0 +1,5 @@
import { TableCell } from './table-cell'
export * from './table-cell'
export default TableCell

View File

@@ -0,0 +1,51 @@
import { Node, mergeAttributes } from '@tiptap/core'
export interface TableCellOptions {
HTMLAttributes: {
[key: string]: any
},
}
export const TableCell = Node.create({
name: 'tableCell',
defaultOptions: <TableCellOptions>{
HTMLAttributes: {},
},
content: 'block+',
addAttributes() {
return {
colspan: {
default: 1,
},
rowspan: {
default: 1,
},
colwidth: {
default: null,
},
}
},
tableRole: 'cell',
isolating: true,
parseHTML() {
return [
{ tag: 'td' },
]
},
renderHTML({ HTMLAttributes }) {
return ['td', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
},
})
declare module '@tiptap/core' {
interface AllExtensions {
TableCell: typeof TableCell,
}
}

View File

@@ -0,0 +1,14 @@
# @tiptap/extension-table-header
[![Version](https://img.shields.io/npm/v/@tiptap/extension-table-header.svg?label=version)](https://www.npmjs.com/package/@tiptap/extension-table-header)
[![Downloads](https://img.shields.io/npm/dm/@tiptap/extension-table-header.svg)](https://npmcharts.com/compare/tiptap?minimal=true)
[![License](https://img.shields.io/npm/l/@tiptap/extension-table-header.svg)](https://www.npmjs.com/package/@tiptap/extension-table-header)
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis)
## Introduction
tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as *New York Times*, *The Guardian* or *Atlassian*.
## Offical Documentation
Documentation can be found on the [tiptap website](https://tiptap.dev).
## License
tiptap is open-sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap-next/blob/main/LICENSE.md).

View File

@@ -0,0 +1,27 @@
{
"name": "@tiptap/extension-table-header",
"description": "table cell extension for tiptap",
"version": "2.0.0-alpha.5",
"homepage": "https://tiptap.dev",
"keywords": [
"tiptap",
"tiptap extension"
],
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"main": "dist/tiptap-extension-table-header.cjs.js",
"umd": "dist/tiptap-extension-table-header.umd.js",
"module": "dist/tiptap-extension-table-header.esm.js",
"unpkg": "dist/tiptap-extension-table-header.bundle.umd.min.js",
"types": "dist/packages/extension-table-header/src/index.d.ts",
"files": [
"src",
"dist"
],
"peerDependencies": {
"@tiptap/core": "^2.0.0-alpha.6"
}
}

View File

@@ -0,0 +1,5 @@
import { TableHeader } from './table-header'
export * from './table-header'
export default TableHeader

View File

@@ -0,0 +1,51 @@
import { Node, mergeAttributes } from '@tiptap/core'
export interface TableHeaderOptions {
HTMLAttributes: {
[key: string]: any
},
}
export const TableHeader = Node.create({
name: 'tableHeader',
defaultOptions: <TableHeaderOptions>{
HTMLAttributes: {},
},
content: 'block+',
addAttributes() {
return {
colspan: {
default: 1,
},
rowspan: {
default: 1,
},
colwidth: {
default: null,
},
}
},
tableRole: 'header_cell',
isolating: true,
parseHTML() {
return [
{ tag: 'th' },
]
},
renderHTML({ HTMLAttributes }) {
return ['th', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
},
})
declare module '@tiptap/core' {
interface AllExtensions {
TableHeader: typeof TableHeader,
}
}

View File

@@ -0,0 +1,14 @@
# @tiptap/extension-table-row
[![Version](https://img.shields.io/npm/v/@tiptap/extension-table-row.svg?label=version)](https://www.npmjs.com/package/@tiptap/extension-table-row)
[![Downloads](https://img.shields.io/npm/dm/@tiptap/extension-table-row.svg)](https://npmcharts.com/compare/tiptap?minimal=true)
[![License](https://img.shields.io/npm/l/@tiptap/extension-table-row.svg)](https://www.npmjs.com/package/@tiptap/extension-table-row)
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis)
## Introduction
tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as *New York Times*, *The Guardian* or *Atlassian*.
## Offical Documentation
Documentation can be found on the [tiptap website](https://tiptap.dev).
## License
tiptap is open-sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap-next/blob/main/LICENSE.md).

View File

@@ -0,0 +1,27 @@
{
"name": "@tiptap/extension-table-row",
"description": "table row extension for tiptap",
"version": "2.0.0-alpha.5",
"homepage": "https://tiptap.dev",
"keywords": [
"tiptap",
"tiptap extension"
],
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"main": "dist/tiptap-extension-table-row.cjs.js",
"umd": "dist/tiptap-extension-table-row.umd.js",
"module": "dist/tiptap-extension-table-row.esm.js",
"unpkg": "dist/tiptap-extension-table-row.bundle.umd.min.js",
"types": "dist/packages/extension-table-row/src/index.d.ts",
"files": [
"src",
"dist"
],
"peerDependencies": {
"@tiptap/core": "^2.0.0-alpha.6"
}
}

View File

@@ -0,0 +1,5 @@
import { TableRow } from './table-row'
export * from './table-row'
export default TableRow

View File

@@ -0,0 +1,35 @@
import { Node, mergeAttributes } from '@tiptap/core'
export interface TableRowOptions {
HTMLAttributes: {
[key: string]: any
},
}
export const TableRow = Node.create({
name: 'tableRow',
defaultOptions: <TableRowOptions>{
HTMLAttributes: {},
},
content: '(tableCell | tableHeader)*',
tableRole: 'row',
parseHTML() {
return [
{ tag: 'tr' },
]
},
renderHTML({ HTMLAttributes }) {
return ['tr', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
},
})
declare module '@tiptap/core' {
interface AllExtensions {
TableRow: typeof TableRow,
}
}

View File

@@ -0,0 +1,14 @@
# @tiptap/extension-table
[![Version](https://img.shields.io/npm/v/@tiptap/extension-table.svg?label=version)](https://www.npmjs.com/package/@tiptap/extension-table)
[![Downloads](https://img.shields.io/npm/dm/@tiptap/extension-table.svg)](https://npmcharts.com/compare/tiptap?minimal=true)
[![License](https://img.shields.io/npm/l/@tiptap/extension-table.svg)](https://www.npmjs.com/package/@tiptap/extension-table)
[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis)
## Introduction
tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as *New York Times*, *The Guardian* or *Atlassian*.
## Offical Documentation
Documentation can be found on the [tiptap website](https://tiptap.dev).
## License
tiptap is open-sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap-next/blob/main/LICENSE.md).

View File

@@ -0,0 +1,31 @@
{
"name": "@tiptap/extension-table",
"description": "table extension for tiptap",
"version": "2.0.0-alpha.5",
"homepage": "https://tiptap.dev",
"keywords": [
"tiptap",
"tiptap extension"
],
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"main": "dist/tiptap-extension-table.cjs.js",
"umd": "dist/tiptap-extension-table.umd.js",
"module": "dist/tiptap-extension-table.esm.js",
"unpkg": "dist/tiptap-extension-table.bundle.umd.min.js",
"types": "dist/packages/extension-table/src/index.d.ts",
"files": [
"src",
"dist"
],
"peerDependencies": {
"@tiptap/core": "^2.0.0-alpha.6"
},
"dependencies": {
"prosemirror-tables": "^1.1.1",
"prosemirror-view": "^1.16.3"
}
}

View File

@@ -0,0 +1,89 @@
// @ts-nocheck
import { NodeView } from 'prosemirror-view'
import { Node as ProseMirrorNode } from 'prosemirror-model'
export function updateColumns(node: ProseMirrorNode, colgroup: Element, table: Element, cellMinWidth: number, overrideCol?: number, overrideValue?: any) {
let totalWidth = 0
let fixedWidth = true
let nextDOM = colgroup.firstChild
const row = node.firstChild
for (let i = 0, col = 0; i < row.childCount; i += 1) {
const { colspan, colwidth } = row.child(i).attrs
for (let j = 0; j < colspan; j += 1, col += 1) {
const hasWidth = overrideCol === col ? overrideValue : colwidth && colwidth[j]
const cssWidth = hasWidth ? `${hasWidth}px` : ''
totalWidth += hasWidth || cellMinWidth
if (!hasWidth) {
fixedWidth = false
}
if (!nextDOM) {
colgroup.appendChild(document.createElement('col')).style.width = cssWidth
} else {
if (nextDOM.style.width !== cssWidth) {
nextDOM.style.width = cssWidth
}
nextDOM = nextDOM.nextSibling
}
}
}
while (nextDOM) {
const after = nextDOM.nextSibling
nextDOM.parentNode.removeChild(nextDOM)
nextDOM = after
}
if (fixedWidth) {
table.style.width = `${totalWidth}px`
table.style.minWidth = ''
} else {
table.style.width = ''
table.style.minWidth = `${totalWidth}px`
}
}
export class TableView implements NodeView {
node: ProseMirrorNode
cellMinWidth: number
dom: Element
table: Element
colgroup: Element
contentDOM: Element
constructor(node: ProseMirrorNode, cellMinWidth: number) {
this.node = node
this.cellMinWidth = cellMinWidth
this.dom = document.createElement('div')
this.dom.className = 'tableWrapper'
this.table = this.dom.appendChild(document.createElement('table'))
this.colgroup = this.table.appendChild(document.createElement('colgroup'))
updateColumns(node, this.colgroup, this.table, cellMinWidth)
this.contentDOM = this.table.appendChild(document.createElement('tbody'))
}
update(node: ProseMirrorNode) {
if (node.type !== this.node.type) {
return false
}
this.node = node
updateColumns(node, this.colgroup, this.table, this.cellMinWidth)
return true
}
ignoreMutation(mutation: MutationRecord | { type: 'selection'; target: Element }) {
return mutation.type === 'attributes' && (mutation.target === this.table || this.colgroup.contains(mutation.target))
}
}

View File

@@ -0,0 +1,5 @@
import { Table } from './table'
export * from './table'
export default Table

View File

@@ -0,0 +1,230 @@
import {
Command,
Node,
mergeAttributes,
isCellSelection,
findParentNodeClosestToPos,
} from '@tiptap/core'
import {
tableEditing,
columnResizing,
goToNextCell,
addColumnBefore,
addColumnAfter,
deleteColumn,
addRowBefore,
addRowAfter,
deleteRow,
deleteTable,
mergeCells,
splitCell,
toggleHeaderColumn,
toggleHeaderRow,
toggleHeaderCell,
setCellAttr,
fixTables,
} from 'prosemirror-tables'
import { NodeView } from 'prosemirror-view'
import { TextSelection } from 'prosemirror-state'
import { createTable } from './utilities/createTable'
import { TableView } from './TableView'
export interface TableOptions {
HTMLAttributes: {
[key: string]: any
},
resizable: boolean,
handleWidth: number,
cellMinWidth: number,
View: NodeView,
lastColumnResizable: boolean,
allowTableNodeSelection: boolean,
}
export const Table = Node.create({
name: 'table',
defaultOptions: <TableOptions>{
HTMLAttributes: {},
resizable: false,
handleWidth: 5,
cellMinWidth: 25,
View: TableView,
lastColumnResizable: true,
allowTableNodeSelection: false,
},
content: 'tableRow+',
tableRole: 'table',
isolating: true,
group: 'block',
parseHTML() {
return [
{ tag: 'table' },
]
},
renderHTML({ HTMLAttributes }) {
return ['table', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), ['tbody', 0]]
},
addCommands() {
return {
insertTable: ({ rows = 3, cols = 3, withHeaderRow = true }): Command => ({ tr, dispatch, editor }) => {
const offset = tr.selection.anchor + 1
const node = createTable(editor.schema, rows, cols, withHeaderRow)
if (dispatch) {
tr.replaceSelectionWith(node)
.scrollIntoView()
.setSelection(TextSelection.near(tr.doc.resolve(offset)))
}
return true
},
addColumnBefore: (): Command => ({ state, dispatch }) => {
return addColumnBefore(state, dispatch)
},
addColumnAfter: (): Command => ({ state, dispatch }) => {
return addColumnAfter(state, dispatch)
},
deleteColumn: (): Command => ({ state, dispatch }) => {
return deleteColumn(state, dispatch)
},
addRowBefore: (): Command => ({ state, dispatch }) => {
return addRowBefore(state, dispatch)
},
addRowAfter: (): Command => ({ state, dispatch }) => {
return addRowAfter(state, dispatch)
},
deleteRow: (): Command => ({ state, dispatch }) => {
return deleteRow(state, dispatch)
},
deleteTable: (): Command => ({ state, dispatch }) => {
return deleteTable(state, dispatch)
},
mergeCells: (): Command => ({ state, dispatch }) => {
return mergeCells(state, dispatch)
},
splitCell: (): Command => ({ state, dispatch }) => {
return splitCell(state, dispatch)
},
toggleHeaderColumn: (): Command => ({ state, dispatch }) => {
return toggleHeaderColumn(state, dispatch)
},
toggleHeaderRow: (): Command => ({ state, dispatch }) => {
return toggleHeaderRow(state, dispatch)
},
toggleHeaderCell: (): Command => ({ state, dispatch }) => {
return toggleHeaderCell(state, dispatch)
},
mergeOrSplit: (): Command => ({ state, dispatch }) => {
if (mergeCells(state, dispatch)) {
return true
}
return splitCell(state, dispatch)
},
setCellAttributes: ({ name, value }: { name: string, value: any }): Command => ({ state, dispatch }) => {
return setCellAttr(name, value)(state, dispatch)
},
goToNextCell: (): Command => ({ state, dispatch }) => {
return goToNextCell(1)(state, dispatch)
},
goToPreviousCell: (): Command => ({ state, dispatch }) => {
return goToNextCell(-1)(state, dispatch)
},
fixTables: (): Command => ({ state, dispatch }) => {
if (dispatch) {
fixTables(state)
}
return true
},
}
},
addKeyboardShortcuts() {
const deleteTableWhenAllCellsSelected = () => {
const { selection } = this.editor.state
if (!isCellSelection(selection)) {
return false
}
let cellCount = 0
const table = findParentNodeClosestToPos(selection.ranges[0].$from, node => {
return node.type.name === 'table'
})
table?.node.descendants(node => {
if (node.type.name === 'table') {
return false
}
if (['tableCell', 'tableHeader'].includes(node.type.name)) {
cellCount += 1
}
})
const allCellsSelected = cellCount === selection.ranges.length
if (!allCellsSelected) {
return false
}
this.editor.commands.deleteTable()
return true
}
return {
Tab: () => {
if (this.editor.commands.goToNextCell()) {
return true
}
if (!this.editor.can().addRowAfter()) {
return false
}
return this.editor
.chain()
.addRowAfter()
.goToNextCell()
.run()
},
'Shift-Tab': () => this.editor.commands.goToPreviousCell(),
Backspace: deleteTableWhenAllCellsSelected,
'Mod-Backspace': deleteTableWhenAllCellsSelected,
Delete: deleteTableWhenAllCellsSelected,
'Mod-Delete': deleteTableWhenAllCellsSelected,
}
},
addProseMirrorPlugins() {
return [
...(this.options.resizable ? [columnResizing({
handleWidth: this.options.handleWidth,
cellMinWidth: this.options.cellMinWidth,
View: this.options.View,
// TODO: PR for @types/prosemirror-tables
// @ts-ignore (incorrect type)
lastColumnResizable: this.options.lastColumnResizable,
})] : []),
tableEditing({
allowTableNodeSelection: this.options.allowTableNodeSelection,
}),
]
},
})
declare module '@tiptap/core' {
interface AllExtensions {
Table: typeof Table,
}
}

View File

@@ -0,0 +1,13 @@
import {
NodeType, Fragment,
Node as ProsemirrorNode,
Schema,
} from 'prosemirror-model'
export function createCell(cellType: NodeType, cellContent?: Fragment<Schema> | ProsemirrorNode<Schema> | Array<ProsemirrorNode<Schema>>): ProsemirrorNode | null | undefined {
if (cellContent) {
return cellType.createChecked(null, cellContent)
}
return cellType.createAndFill()
}

View File

@@ -0,0 +1,33 @@
import { Schema, Fragment, Node as ProsemirrorNode } from 'prosemirror-model'
import { createCell } from './createCell'
import { getTableNodeTypes } from './getTableNodeTypes'
export function createTable(schema: Schema, rowsCount: number, colsCount: number, withHeaderRow: boolean, cellContent?: Fragment<Schema> | ProsemirrorNode<Schema> | Array<ProsemirrorNode<Schema>>): ProsemirrorNode {
const types = getTableNodeTypes(schema)
const headerCells = []
const cells = []
for (let index = 0; index < colsCount; index += 1) {
const cell = createCell(types.cell, cellContent)
if (cell) {
cells.push(cell)
}
if (withHeaderRow) {
const headerCell = createCell(types.header_cell, cellContent)
if (headerCell) {
headerCells.push(headerCell)
}
}
}
const rows = []
for (let index = 0; index < rowsCount; index += 1) {
rows.push(types.row.createChecked(null, withHeaderRow && index === 0 ? headerCells : cells))
}
return types.table.createChecked(null, rows)
}

View File

@@ -0,0 +1,21 @@
import { Schema, NodeType } from 'prosemirror-model'
export function getTableNodeTypes(schema: Schema): { [key: string]: NodeType } {
if (schema.cached.tableNodeTypes) {
return schema.cached.tableNodeTypes
}
const roles: { [key: string]: NodeType } = {}
Object.keys(schema.nodes).forEach(type => {
const nodeType = schema.nodes[type]
if (nodeType.spec.tableRole) {
roles[nodeType.spec.tableRole] = nodeType
}
})
schema.cached.tableNodeTypes = roles
return roles
}

View File

@@ -2486,6 +2486,13 @@
"@types/prosemirror-transform" "*"
"@types/prosemirror-view" "*"
"@types/prosemirror-tables@^0.9.1":
version "0.9.1"
resolved "https://registry.yarnpkg.com/@types/prosemirror-tables/-/prosemirror-tables-0.9.1.tgz#d2203330f0fa1161c04152bf02c39e152082d408"
integrity sha512-zoY1qcAC6kG4UjnaQQXuoyYQdDJMQmY9uzRKdyUppP8rWRR5/kXBHOd84CD9ZvrYUBo3uDmS20qQnc3knr2j9A==
dependencies:
prosemirror-tables "*"
"@types/prosemirror-transform@*":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/prosemirror-transform/-/prosemirror-transform-1.1.1.tgz#5a0de16e8e0123b4c3d9559235e19f39cee85e5c"
@@ -11842,7 +11849,7 @@ prosemirror-state@^1.3.4:
prosemirror-model "^1.0.0"
prosemirror-transform "^1.0.0"
prosemirror-tables@^1.1.1:
prosemirror-tables@*, prosemirror-tables@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-1.1.1.tgz#ad66300cc49500455cf1243bb129c9e7d883321e"
integrity sha512-LmCz4jrlqQZRsYRDzCRYf/pQ5CUcSOyqZlAj5kv67ZWBH1SVLP2U9WJEvQfimWgeRlIz0y0PQVqO1arRm1+woA==