Merge branch 'feature/tables' into main
This commit is contained in:
7
docs/src/demos/Nodes/Table/index.spec.js
Normal file
7
docs/src/demos/Nodes/Table/index.spec.js
Normal file
@@ -0,0 +1,7 @@
|
||||
context('/api/nodes/table', () => {
|
||||
before(() => {
|
||||
cy.visit('/api/nodes/table')
|
||||
})
|
||||
|
||||
// TODO: Write tests
|
||||
})
|
||||
208
docs/src/demos/Nodes/Table/index.vue
Normal file
208
docs/src/demos/Nodes/Table/index.vue
Normal 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>
|
||||
8
docs/src/docPages/api/nodes/table-header.md
Normal file
8
docs/src/docPages/api/nodes/table-header.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# TableHeader
|
||||
|
||||
:::pro Fund the development 💖
|
||||
We need your support to maintain, update, support and develop tiptap 2. If you’re waiting for this extension, [become a sponsor and fund open source](/sponsor).
|
||||
:::
|
||||
|
||||
TODO
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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 don’t 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)
|
||||
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
23
packages/core/src/helpers/findParentNodeClosestToPos.ts
Normal file
23
packages/core/src/helpers/findParentNodeClosestToPos.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }]
|
||||
})),
|
||||
|
||||
6
packages/core/src/helpers/isCellSelection.ts
Normal file
6
packages/core/src/helpers/isCellSelection.ts
Normal 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
|
||||
}
|
||||
6
packages/core/src/helpers/isNodeSelection.ts
Normal file
6
packages/core/src/helpers/isNodeSelection.ts
Normal 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
|
||||
}
|
||||
6
packages/core/src/helpers/isTextSelection.ts
Normal file
6
packages/core/src/helpers/isTextSelection.ts
Normal 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
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
14
packages/extension-table-cell/README.md
Normal file
14
packages/extension-table-cell/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# @tiptap/extension-table-cell
|
||||
[](https://www.npmjs.com/package/@tiptap/extension-table-cell)
|
||||
[](https://npmcharts.com/compare/tiptap?minimal=true)
|
||||
[](https://www.npmjs.com/package/@tiptap/extension-table-cell)
|
||||
[](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).
|
||||
27
packages/extension-table-cell/package.json
Normal file
27
packages/extension-table-cell/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
5
packages/extension-table-cell/src/index.ts
Normal file
5
packages/extension-table-cell/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { TableCell } from './table-cell'
|
||||
|
||||
export * from './table-cell'
|
||||
|
||||
export default TableCell
|
||||
51
packages/extension-table-cell/src/table-cell.ts
Normal file
51
packages/extension-table-cell/src/table-cell.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
14
packages/extension-table-header/README.md
Normal file
14
packages/extension-table-header/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# @tiptap/extension-table-header
|
||||
[](https://www.npmjs.com/package/@tiptap/extension-table-header)
|
||||
[](https://npmcharts.com/compare/tiptap?minimal=true)
|
||||
[](https://www.npmjs.com/package/@tiptap/extension-table-header)
|
||||
[](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).
|
||||
27
packages/extension-table-header/package.json
Normal file
27
packages/extension-table-header/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
5
packages/extension-table-header/src/index.ts
Normal file
5
packages/extension-table-header/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { TableHeader } from './table-header'
|
||||
|
||||
export * from './table-header'
|
||||
|
||||
export default TableHeader
|
||||
51
packages/extension-table-header/src/table-header.ts
Normal file
51
packages/extension-table-header/src/table-header.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
14
packages/extension-table-row/README.md
Normal file
14
packages/extension-table-row/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# @tiptap/extension-table-row
|
||||
[](https://www.npmjs.com/package/@tiptap/extension-table-row)
|
||||
[](https://npmcharts.com/compare/tiptap?minimal=true)
|
||||
[](https://www.npmjs.com/package/@tiptap/extension-table-row)
|
||||
[](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).
|
||||
27
packages/extension-table-row/package.json
Normal file
27
packages/extension-table-row/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
5
packages/extension-table-row/src/index.ts
Normal file
5
packages/extension-table-row/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { TableRow } from './table-row'
|
||||
|
||||
export * from './table-row'
|
||||
|
||||
export default TableRow
|
||||
35
packages/extension-table-row/src/table-row.ts
Normal file
35
packages/extension-table-row/src/table-row.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
14
packages/extension-table/README.md
Normal file
14
packages/extension-table/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# @tiptap/extension-table
|
||||
[](https://www.npmjs.com/package/@tiptap/extension-table)
|
||||
[](https://npmcharts.com/compare/tiptap?minimal=true)
|
||||
[](https://www.npmjs.com/package/@tiptap/extension-table)
|
||||
[](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).
|
||||
31
packages/extension-table/package.json
Normal file
31
packages/extension-table/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
89
packages/extension-table/src/TableView.ts
Normal file
89
packages/extension-table/src/TableView.ts
Normal 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))
|
||||
}
|
||||
}
|
||||
5
packages/extension-table/src/index.ts
Normal file
5
packages/extension-table/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Table } from './table'
|
||||
|
||||
export * from './table'
|
||||
|
||||
export default Table
|
||||
230
packages/extension-table/src/table.ts
Normal file
230
packages/extension-table/src/table.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
13
packages/extension-table/src/utilities/createCell.ts
Normal file
13
packages/extension-table/src/utilities/createCell.ts
Normal 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()
|
||||
}
|
||||
33
packages/extension-table/src/utilities/createTable.ts
Normal file
33
packages/extension-table/src/utilities/createTable.ts
Normal 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)
|
||||
}
|
||||
21
packages/extension-table/src/utilities/getTableNodeTypes.ts
Normal file
21
packages/extension-table/src/utilities/getTableNodeTypes.ts
Normal 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
|
||||
}
|
||||
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user