Allow changing of table class and attributes
Some checks failed
build / lint (16) (push) Has been cancelled
build / test (16) (push) Has been cancelled
deploy / deploy (push) Has been cancelled
build / build (16) (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2022-10-27 15:35:34 +13:00
parent 18ffa5e083
commit 8beb10c8b0
3 changed files with 384 additions and 155 deletions

View File

@@ -61,67 +61,158 @@ const MenuBar = ({ editor }) => {
return (
<>
<button onClick={() => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()}>
<button
onClick={() => editor
.chain()
.focus()
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
.run()
}
>
insertTable
</button>
<button onClick={() => editor.chain().focus().insertContent(tableHTML, {
parseOptions: {
preserveWhitespace: false,
},
}).run()}>
<button
onClick={() => editor
.chain()
.focus()
.insertContent(tableHTML, {
parseOptions: {
preserveWhitespace: false,
},
})
.run()
}
>
insertHTMLTable
</button>
<button onClick={() => editor.chain().focus().addColumnBefore().run()} disabled={!editor.can().addColumnBefore()}>
<button
onClick={() => editor.chain().focus().addColumnBefore().run()}
disabled={!editor.can().addColumnBefore()}
>
addColumnBefore
</button>
<button onClick={() => editor.chain().focus().addColumnAfter().run()} disabled={!editor.can().addColumnAfter()}>
<button
onClick={() => editor.chain().focus().addColumnAfter().run()}
disabled={!editor.can().addColumnAfter()}
>
addColumnAfter
</button>
<button onClick={() => editor.chain().focus().deleteColumn().run()} disabled={!editor.can().deleteColumn()}>
<button
onClick={() => editor.chain().focus().deleteColumn().run()}
disabled={!editor.can().deleteColumn()}
>
deleteColumn
</button>
<button onClick={() => editor.chain().focus().addRowBefore().run()} disabled={!editor.can().addRowBefore()}>
<button
onClick={() => editor.chain().focus().addRowBefore().run()}
disabled={!editor.can().addRowBefore()}
>
addRowBefore
</button>
<button onClick={() => editor.chain().focus().addRowAfter().run()} disabled={!editor.can().addRowAfter()}>
<button
onClick={() => editor.chain().focus().addRowAfter().run()}
disabled={!editor.can().addRowAfter()}
>
addRowAfter
</button>
<button onClick={() => editor.chain().focus().deleteRow().run()} disabled={!editor.can().deleteRow()}>
<button
onClick={() => editor.chain().focus().deleteRow().run()}
disabled={!editor.can().deleteRow()}
>
deleteRow
</button>
<button onClick={() => editor.chain().focus().deleteTable().run()} disabled={!editor.can().deleteTable()}>
<button
onClick={() => editor.chain().focus().deleteTable().run()}
disabled={!editor.can().deleteTable()}
>
deleteTable
</button>
<button onClick={() => editor.chain().focus().mergeCells().run()} disabled={!editor.can().mergeCells()}>
<button
onClick={() => editor.chain().focus().mergeCells().run()}
disabled={!editor.can().mergeCells()}
>
mergeCells
</button>
<button onClick={() => editor.chain().focus().splitCell().run()} disabled={!editor.can().splitCell()}>
<button
onClick={() => editor.chain().focus().splitCell().run()}
disabled={!editor.can().splitCell()}
>
splitCell
</button>
<button onClick={() => editor.chain().focus().toggleHeaderColumn().run()} disabled={!editor.can().toggleHeaderColumn()}>
<button
onClick={() => editor.chain().focus().toggleHeaderColumn().run()}
disabled={!editor.can().toggleHeaderColumn()}
>
toggleHeaderColumn
</button>
<button onClick={() => editor.chain().focus().toggleHeaderRow().run()} disabled={!editor.can().toggleHeaderRow()}>
toggleHeaderRow
<button
onClick={() => editor.chain().focus().toggleHeaderRow().run()}
disabled={!editor.can().toggleHeaderRow()}
>
{editor.can().tableHasHeader() && <>* toggleHeaderRow</>}
{!editor.can().tableHasHeader() && <>toggleHeaderRow</>}
</button>
<button onClick={() => editor.chain().focus().toggleHeaderCell().run()} disabled={!editor.can().toggleHeaderCell()}>
<button
onClick={() => editor.chain().focus().toggleHeaderCell().run()}
disabled={!editor.can().toggleHeaderCell()}
>
toggleHeaderCell
</button>
<button onClick={() => editor.chain().focus().mergeOrSplit().run()} disabled={!editor.can().mergeOrSplit()}>
<button
onClick={() => editor.chain().focus().mergeOrSplit().run()}
disabled={!editor.can().mergeOrSplit()}
>
mergeOrSplit
</button>
<button onClick={() => editor.chain().focus().setCellAttribute('backgroundColor', '#FAF594').run()} disabled={!editor.can().setCellAttribute('backgroundColor', '#FAF594')}>
<button
onClick={() => editor
.chain()
.focus()
.setCellAttribute('backgroundColor', '#FAF594')
.run()
}
disabled={!editor.can().setCellAttribute('backgroundColor', '#FAF594')}
>
setCellAttribute
</button>
<button onClick={() => editor.chain().focus().fixTables().run()} disabled={!editor.can().fixTables()}>
<button
onClick={() => editor.chain().focus().fixTables().run()}
disabled={!editor.can().fixTables()}
>
fixTables
</button>
<button onClick={() => editor.chain().focus().goToNextCell().run()} disabled={!editor.can().goToNextCell()}>
<button
onClick={() => editor.chain().focus().goToNextCell().run()}
disabled={!editor.can().goToNextCell()}
>
goToNextCell
</button>
<button onClick={() => editor.chain().focus().goToPreviousCell().run()} disabled={!editor.can().goToPreviousCell()}>
<button
onClick={() => editor.chain().focus().goToPreviousCell().run()}
disabled={!editor.can().goToPreviousCell()}
>
goToPreviousCell
</button>
<button
onClick={() => editor.chain().focus().toggleTableClass('table-fullwidth').run()
}
>
toggleTableClass(fullwidth)
</button>
<button
onClick={() => editor.chain().focus().toggleTableClass('table-centred').run()
}
>
{editor.can().tableHasClass('table-centred') && (
<>* toggleTableClass(centred)</>
)}
{!editor.can().tableHasClass('table-centred') && (
<>toggleTableClass(centred)</>
)}
</button>
<button onClick={() => console.log(editor.getHTML())}>Get HTML</button>
</>
)
}
@@ -152,10 +243,10 @@ export default () => {
<p>
Here is an example:
</p>
<table>
<table class="tablestyle tablestyle2" data-foo="bar">
<tbody>
<tr>
<th>Name</th>
<th>Name!</th>
<th colspan="3">Description</th>
</tr>
<tr>

View File

@@ -2,7 +2,14 @@
import { Node as ProseMirrorNode } from 'prosemirror-model'
import { NodeView } from 'prosemirror-view'
export function updateColumns(node: ProseMirrorNode, colgroup: Element, table: Element, cellMinWidth: number, overrideCol?: number, overrideValue?: any) {
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
@@ -50,7 +57,6 @@ export function updateColumns(node: ProseMirrorNode, colgroup: Element, table: E
}
export class TableView implements NodeView {
node: ProseMirrorNode
cellMinWidth: number
@@ -69,6 +75,8 @@ export class TableView implements NodeView {
this.dom = document.createElement('div')
this.dom.className = 'tableWrapper'
this.table = this.dom.appendChild(document.createElement('table'))
this.table.className = node.attrs?.class
this.table.setAttribute('data-ref', node.attrs?.ref)
this.colgroup = this.table.appendChild(document.createElement('colgroup'))
updateColumns(node, this.colgroup, this.table, cellMinWidth)
this.contentDOM = this.table.appendChild(document.createElement('tbody'))
@@ -85,7 +93,13 @@ export class TableView implements NodeView {
return true
}
ignoreMutation(mutation: MutationRecord | { type: 'selection'; target: Element }) {
return mutation.type === 'attributes' && (mutation.target === this.table || this.colgroup.contains(mutation.target))
ignoreMutation(
mutation: MutationRecord | { type: 'selection'; target: Element },
) {
return (
mutation.type === 'attributes'
&& (mutation.target === this.table
|| this.colgroup.contains(mutation.target))
)
}
}

View File

@@ -32,50 +32,62 @@ import { createTable } from './utilities/createTable'
import { deleteTableWhenAllCellsSelected } from './utilities/deleteTableWhenAllCellsSelected'
export interface TableOptions {
HTMLAttributes: Record<string, any>,
resizable: boolean,
handleWidth: number,
cellMinWidth: number,
View: NodeView,
lastColumnResizable: boolean,
allowTableNodeSelection: boolean,
HTMLAttributes: Record<string, any>;
resizable: boolean;
handleWidth: number;
cellMinWidth: number;
View: NodeView;
lastColumnResizable: boolean;
allowTableNodeSelection: boolean;
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
table: {
insertTable: (options?: { rows?: number, cols?: number, withHeaderRow?: boolean }) => ReturnType,
addColumnBefore: () => ReturnType,
addColumnAfter: () => ReturnType,
deleteColumn: () => ReturnType,
addRowBefore: () => ReturnType,
addRowAfter: () => ReturnType,
deleteRow: () => ReturnType,
deleteTable: () => ReturnType,
mergeCells: () => ReturnType,
splitCell: () => ReturnType,
toggleHeaderColumn: () => ReturnType,
toggleHeaderRow: () => ReturnType,
toggleHeaderCell: () => ReturnType,
mergeOrSplit: () => ReturnType,
setCellAttribute: (name: string, value: any) => ReturnType,
goToNextCell: () => ReturnType,
goToPreviousCell: () => ReturnType,
fixTables: () => ReturnType,
setCellSelection: (position: { anchorCell: number, headCell?: number }) => ReturnType,
}
insertTable: (options?: {
rows?: number;
cols?: number;
withHeaderRow?: boolean;
}) => ReturnType;
addColumnBefore: () => ReturnType;
addColumnAfter: () => ReturnType;
deleteColumn: () => ReturnType;
addRowBefore: () => ReturnType;
addRowAfter: () => ReturnType;
deleteRow: () => ReturnType;
deleteTable: () => ReturnType;
mergeCells: () => ReturnType;
splitCell: () => ReturnType;
toggleHeaderColumn: () => ReturnType;
toggleHeaderRow: () => ReturnType;
toggleHeaderCell: () => ReturnType;
mergeOrSplit: () => ReturnType;
setCellAttribute: (name: string, value: any) => ReturnType;
goToNextCell: () => ReturnType;
goToPreviousCell: () => ReturnType;
fixTables: () => ReturnType;
setCellSelection: (position: {
anchorCell: number;
headCell?: number;
}) => ReturnType;
toggleTableClass: (className: string) => ReturnType;
tableHasClass: (className: string) => ReturnType;
tableHasHeader: () => ReturnType;
};
}
interface NodeConfig<Options, Storage> {
/**
* Table Role
*/
tableRole?: string | ((this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options>>['tableRole'],
}) => string),
tableRole?:
| string
| ((this: {
name: string;
options: Options;
storage: Storage;
parent: ParentConfig<NodeConfig<Options>>['tableRole'];
}) => string);
}
}
@@ -105,99 +117,209 @@ export const Table = Node.create<TableOptions>({
group: 'block',
parseHTML() {
return [
{ tag: 'table' },
]
return [{ tag: 'table' }]
},
addAttributes() {
return {
...this.parent?.(),
class: {
default: null,
parseHTML: element => element.getAttribute('class'),
},
ref: {
default: `table${Math.random().toString().substring(2)}`,
},
}
},
renderHTML({ HTMLAttributes }) {
return ['table', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), ['tbody', 0]]
return [
'table',
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
['tbody', 0],
]
},
addCommands() {
return {
insertTable: ({ rows = 3, cols = 3, withHeaderRow = true } = {}) => ({ tr, dispatch, editor }) => {
const node = createTable(editor.schema, rows, cols, withHeaderRow)
insertTable:
({ rows = 3, cols = 3, withHeaderRow = true } = {}) => ({ tr, dispatch, editor }) => {
const node = createTable(editor.schema, rows, cols, withHeaderRow)
if (dispatch) {
const offset = tr.selection.anchor + 1
if (dispatch) {
const offset = tr.selection.anchor + 1
tr.replaceSelectionWith(node)
.scrollIntoView()
.setSelection(TextSelection.near(tr.doc.resolve(offset)))
}
tr.replaceSelectionWith(node)
.scrollIntoView()
.setSelection(TextSelection.near(tr.doc.resolve(offset)))
}
return true
},
addColumnBefore: () => ({ state, dispatch }) => {
return addColumnBefore(state, dispatch)
},
addColumnAfter: () => ({ state, dispatch }) => {
return addColumnAfter(state, dispatch)
},
deleteColumn: () => ({ state, dispatch }) => {
return deleteColumn(state, dispatch)
},
addRowBefore: () => ({ state, dispatch }) => {
return addRowBefore(state, dispatch)
},
addRowAfter: () => ({ state, dispatch }) => {
return addRowAfter(state, dispatch)
},
deleteRow: () => ({ state, dispatch }) => {
return deleteRow(state, dispatch)
},
deleteTable: () => ({ state, dispatch }) => {
return deleteTable(state, dispatch)
},
mergeCells: () => ({ state, dispatch }) => {
return mergeCells(state, dispatch)
},
splitCell: () => ({ state, dispatch }) => {
return splitCell(state, dispatch)
},
toggleHeaderColumn: () => ({ state, dispatch }) => {
return toggleHeader('column')(state, dispatch)
},
toggleHeaderRow: () => ({ state, dispatch }) => {
return toggleHeader('row')(state, dispatch)
},
toggleHeaderCell: () => ({ state, dispatch }) => {
return toggleHeaderCell(state, dispatch)
},
mergeOrSplit: () => ({ state, dispatch }) => {
if (mergeCells(state, dispatch)) {
return true
}
},
addColumnBefore:
() => ({ state, dispatch }) => {
return addColumnBefore(state, dispatch)
},
addColumnAfter:
() => ({ state, dispatch }) => {
return addColumnAfter(state, dispatch)
},
deleteColumn:
() => ({ state, dispatch }) => {
return deleteColumn(state, dispatch)
},
addRowBefore:
() => ({ state, dispatch }) => {
return addRowBefore(state, dispatch)
},
addRowAfter:
() => ({ state, dispatch }) => {
return addRowAfter(state, dispatch)
},
deleteRow:
() => ({ state, dispatch }) => {
return deleteRow(state, dispatch)
},
deleteTable:
() => ({ state, dispatch }) => {
return deleteTable(state, dispatch)
},
mergeCells:
() => ({ state, dispatch }) => {
return mergeCells(state, dispatch)
},
splitCell:
() => ({ state, dispatch }) => {
return splitCell(state, dispatch)
},
toggleHeaderColumn:
() => ({ state, dispatch }) => {
return toggleHeader('column')(state, dispatch)
},
toggleHeaderRow:
() => ({ state, dispatch }) => {
return toggleHeader('row')(state, dispatch)
},
toggleHeaderCell:
() => ({ state, dispatch }) => {
return toggleHeaderCell(state, dispatch)
},
mergeOrSplit:
() => ({ state, dispatch }) => {
if (mergeCells(state, dispatch)) {
return true
}
return splitCell(state, dispatch)
},
setCellAttribute: (name, value) => ({ state, dispatch }) => {
return setCellAttr(name, value)(state, dispatch)
},
goToNextCell: () => ({ state, dispatch }) => {
return goToNextCell(1)(state, dispatch)
},
goToPreviousCell: () => ({ state, dispatch }) => {
return goToNextCell(-1)(state, dispatch)
},
fixTables: () => ({ state, dispatch }) => {
if (dispatch) {
fixTables(state)
}
return splitCell(state, dispatch)
},
setCellAttribute:
(name, value) => ({ state, dispatch }) => {
return setCellAttr(name, value)(state, dispatch)
},
goToNextCell:
() => ({ state, dispatch }) => {
return goToNextCell(1)(state, dispatch)
},
goToPreviousCell:
() => ({ state, dispatch }) => {
return goToNextCell(-1)(state, dispatch)
},
fixTables:
() => ({ state, dispatch }) => {
if (dispatch) {
fixTables(state)
}
return true
},
setCellSelection: position => ({ tr, dispatch }) => {
if (dispatch) {
const selection = CellSelection.create(tr.doc, position.anchorCell, position.headCell)
return true
},
setCellSelection:
position => ({ tr, dispatch }) => {
if (dispatch) {
const selection = CellSelection.create(
tr.doc,
position.anchorCell,
position.headCell,
)
// @ts-ignore
tr.setSelection(selection)
}
// @ts-ignore
tr.setSelection(selection)
}
return true
},
return true
},
tableHasHeader:
() => ({ state }) => {
const $pos = state.selection.$anchor
for (let d = $pos.depth; d > 0; d -= 1) {
const node = $pos.node(d)
if (node.type.spec.tableRole === 'table') {
const ref = node.attrs?.ref
if (!ref) { return false }
const tableThDom = document.querySelector(
`div.ProseMirror table[data-ref=${node.attrs.ref}] > tbody > tr > th, div.ProseMirror table[data-ref=${node.attrs.ref}] > thead > tr > th`,
)
if (tableThDom) { return true }
}
}
return false
},
tableHasClass:
className => ({ state }) => {
const $pos = state.selection.$anchor
for (let d = $pos.depth; d > 0; d -= 1) {
const node = $pos.node(d)
if (node.type.spec.tableRole === 'table') {
const classStr = node.attrs?.class || ''
const classList = classStr?.split(' ') || []
const classIndex = classList.indexOf(className)
if (classIndex >= 0) {
return true
}
}
}
return false
},
toggleTableClass:
className => ({ state }) => {
const $pos = state.selection.$anchor
for (let d = $pos.depth; d > 0; d -= 1) {
const node = $pos.node(d)
if (node.type.spec.tableRole === 'table') {
const classStr = node.attrs?.class || ''
const classList = classStr?.split(' ') || []
const classIndex = classList.indexOf(className)
if (classIndex >= 0) {
classList.splice(classIndex, 1)
} else {
classList.push(className)
}
const newClassStr = classList.join(' ')
state.tr.setNodeAttribute($pos.before(d), 'class', newClassStr)
const tableDom = document.querySelector(
`div.ProseMirror table[data-ref=${node.attrs.ref}]`,
)
if (tableDom) { tableDom.className = newClassStr }
return true
}
}
return false
},
}
},
@@ -212,11 +334,7 @@ export const Table = Node.create<TableOptions>({
return false
}
return this.editor
.chain()
.addRowAfter()
.goToNextCell()
.run()
return this.editor.chain().addRowAfter().goToNextCell().run()
},
'Shift-Tab': () => this.editor.commands.goToPreviousCell(),
Backspace: deleteTableWhenAllCellsSelected,
@@ -230,14 +348,18 @@ export const Table = Node.create<TableOptions>({
const isResizable = this.options.resizable && this.editor.isEditable
return [
...(isResizable ? [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,
})] : []),
...(isResizable
? [
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,
}),
@@ -252,7 +374,9 @@ export const Table = Node.create<TableOptions>({
}
return {
tableRole: callOrReturn(getExtensionField(extension, 'tableRole', context)),
tableRole: callOrReturn(
getExtensionField(extension, 'tableRole', context),
),
}
},
})