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 ( 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 insertTable
</button> </button>
<button onClick={() => editor.chain().focus().insertContent(tableHTML, { <button
parseOptions: { onClick={() => editor
preserveWhitespace: false, .chain()
}, .focus()
}).run()}> .insertContent(tableHTML, {
parseOptions: {
preserveWhitespace: false,
},
})
.run()
}
>
insertHTMLTable insertHTMLTable
</button> </button>
<button onClick={() => editor.chain().focus().addColumnBefore().run()} disabled={!editor.can().addColumnBefore()}> <button
onClick={() => editor.chain().focus().addColumnBefore().run()}
disabled={!editor.can().addColumnBefore()}
>
addColumnBefore addColumnBefore
</button> </button>
<button onClick={() => editor.chain().focus().addColumnAfter().run()} disabled={!editor.can().addColumnAfter()}> <button
onClick={() => editor.chain().focus().addColumnAfter().run()}
disabled={!editor.can().addColumnAfter()}
>
addColumnAfter addColumnAfter
</button> </button>
<button onClick={() => editor.chain().focus().deleteColumn().run()} disabled={!editor.can().deleteColumn()}> <button
onClick={() => editor.chain().focus().deleteColumn().run()}
disabled={!editor.can().deleteColumn()}
>
deleteColumn deleteColumn
</button> </button>
<button onClick={() => editor.chain().focus().addRowBefore().run()} disabled={!editor.can().addRowBefore()}> <button
onClick={() => editor.chain().focus().addRowBefore().run()}
disabled={!editor.can().addRowBefore()}
>
addRowBefore addRowBefore
</button> </button>
<button onClick={() => editor.chain().focus().addRowAfter().run()} disabled={!editor.can().addRowAfter()}> <button
onClick={() => editor.chain().focus().addRowAfter().run()}
disabled={!editor.can().addRowAfter()}
>
addRowAfter addRowAfter
</button> </button>
<button onClick={() => editor.chain().focus().deleteRow().run()} disabled={!editor.can().deleteRow()}> <button
onClick={() => editor.chain().focus().deleteRow().run()}
disabled={!editor.can().deleteRow()}
>
deleteRow deleteRow
</button> </button>
<button onClick={() => editor.chain().focus().deleteTable().run()} disabled={!editor.can().deleteTable()}> <button
onClick={() => editor.chain().focus().deleteTable().run()}
disabled={!editor.can().deleteTable()}
>
deleteTable deleteTable
</button> </button>
<button onClick={() => editor.chain().focus().mergeCells().run()} disabled={!editor.can().mergeCells()}> <button
onClick={() => editor.chain().focus().mergeCells().run()}
disabled={!editor.can().mergeCells()}
>
mergeCells mergeCells
</button> </button>
<button onClick={() => editor.chain().focus().splitCell().run()} disabled={!editor.can().splitCell()}> <button
onClick={() => editor.chain().focus().splitCell().run()}
disabled={!editor.can().splitCell()}
>
splitCell splitCell
</button> </button>
<button onClick={() => editor.chain().focus().toggleHeaderColumn().run()} disabled={!editor.can().toggleHeaderColumn()}> <button
onClick={() => editor.chain().focus().toggleHeaderColumn().run()}
disabled={!editor.can().toggleHeaderColumn()}
>
toggleHeaderColumn toggleHeaderColumn
</button> </button>
<button onClick={() => editor.chain().focus().toggleHeaderRow().run()} disabled={!editor.can().toggleHeaderRow()}> <button
toggleHeaderRow onClick={() => editor.chain().focus().toggleHeaderRow().run()}
disabled={!editor.can().toggleHeaderRow()}
>
{editor.can().tableHasHeader() && <>* toggleHeaderRow</>}
{!editor.can().tableHasHeader() && <>toggleHeaderRow</>}
</button> </button>
<button onClick={() => editor.chain().focus().toggleHeaderCell().run()} disabled={!editor.can().toggleHeaderCell()}> <button
onClick={() => editor.chain().focus().toggleHeaderCell().run()}
disabled={!editor.can().toggleHeaderCell()}
>
toggleHeaderCell toggleHeaderCell
</button> </button>
<button onClick={() => editor.chain().focus().mergeOrSplit().run()} disabled={!editor.can().mergeOrSplit()}> <button
onClick={() => editor.chain().focus().mergeOrSplit().run()}
disabled={!editor.can().mergeOrSplit()}
>
mergeOrSplit mergeOrSplit
</button> </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 setCellAttribute
</button> </button>
<button onClick={() => editor.chain().focus().fixTables().run()} disabled={!editor.can().fixTables()}> <button
onClick={() => editor.chain().focus().fixTables().run()}
disabled={!editor.can().fixTables()}
>
fixTables fixTables
</button> </button>
<button onClick={() => editor.chain().focus().goToNextCell().run()} disabled={!editor.can().goToNextCell()}> <button
onClick={() => editor.chain().focus().goToNextCell().run()}
disabled={!editor.can().goToNextCell()}
>
goToNextCell goToNextCell
</button> </button>
<button onClick={() => editor.chain().focus().goToPreviousCell().run()} disabled={!editor.can().goToPreviousCell()}> <button
onClick={() => editor.chain().focus().goToPreviousCell().run()}
disabled={!editor.can().goToPreviousCell()}
>
goToPreviousCell goToPreviousCell
</button> </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> <p>
Here is an example: Here is an example:
</p> </p>
<table> <table class="tablestyle tablestyle2" data-foo="bar">
<tbody> <tbody>
<tr> <tr>
<th>Name</th> <th>Name!</th>
<th colspan="3">Description</th> <th colspan="3">Description</th>
</tr> </tr>
<tr> <tr>

View File

@@ -2,7 +2,14 @@
import { Node as ProseMirrorNode } from 'prosemirror-model' import { Node as ProseMirrorNode } from 'prosemirror-model'
import { NodeView } from 'prosemirror-view' 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 totalWidth = 0
let fixedWidth = true let fixedWidth = true
let nextDOM = colgroup.firstChild let nextDOM = colgroup.firstChild
@@ -50,7 +57,6 @@ export function updateColumns(node: ProseMirrorNode, colgroup: Element, table: E
} }
export class TableView implements NodeView { export class TableView implements NodeView {
node: ProseMirrorNode node: ProseMirrorNode
cellMinWidth: number cellMinWidth: number
@@ -69,6 +75,8 @@ export class TableView implements NodeView {
this.dom = document.createElement('div') this.dom = document.createElement('div')
this.dom.className = 'tableWrapper' this.dom.className = 'tableWrapper'
this.table = this.dom.appendChild(document.createElement('table')) 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')) this.colgroup = this.table.appendChild(document.createElement('colgroup'))
updateColumns(node, this.colgroup, this.table, cellMinWidth) updateColumns(node, this.colgroup, this.table, cellMinWidth)
this.contentDOM = this.table.appendChild(document.createElement('tbody')) this.contentDOM = this.table.appendChild(document.createElement('tbody'))
@@ -85,7 +93,13 @@ export class TableView implements NodeView {
return true return true
} }
ignoreMutation(mutation: MutationRecord | { type: 'selection'; target: Element }) { ignoreMutation(
return mutation.type === 'attributes' && (mutation.target === this.table || this.colgroup.contains(mutation.target)) 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' import { deleteTableWhenAllCellsSelected } from './utilities/deleteTableWhenAllCellsSelected'
export interface TableOptions { export interface TableOptions {
HTMLAttributes: Record<string, any>, HTMLAttributes: Record<string, any>;
resizable: boolean, resizable: boolean;
handleWidth: number, handleWidth: number;
cellMinWidth: number, cellMinWidth: number;
View: NodeView, View: NodeView;
lastColumnResizable: boolean, lastColumnResizable: boolean;
allowTableNodeSelection: boolean, allowTableNodeSelection: boolean;
} }
declare module '@tiptap/core' { declare module '@tiptap/core' {
interface Commands<ReturnType> { interface Commands<ReturnType> {
table: { table: {
insertTable: (options?: { rows?: number, cols?: number, withHeaderRow?: boolean }) => ReturnType, insertTable: (options?: {
addColumnBefore: () => ReturnType, rows?: number;
addColumnAfter: () => ReturnType, cols?: number;
deleteColumn: () => ReturnType, withHeaderRow?: boolean;
addRowBefore: () => ReturnType, }) => ReturnType;
addRowAfter: () => ReturnType, addColumnBefore: () => ReturnType;
deleteRow: () => ReturnType, addColumnAfter: () => ReturnType;
deleteTable: () => ReturnType, deleteColumn: () => ReturnType;
mergeCells: () => ReturnType, addRowBefore: () => ReturnType;
splitCell: () => ReturnType, addRowAfter: () => ReturnType;
toggleHeaderColumn: () => ReturnType, deleteRow: () => ReturnType;
toggleHeaderRow: () => ReturnType, deleteTable: () => ReturnType;
toggleHeaderCell: () => ReturnType, mergeCells: () => ReturnType;
mergeOrSplit: () => ReturnType, splitCell: () => ReturnType;
setCellAttribute: (name: string, value: any) => ReturnType, toggleHeaderColumn: () => ReturnType;
goToNextCell: () => ReturnType, toggleHeaderRow: () => ReturnType;
goToPreviousCell: () => ReturnType, toggleHeaderCell: () => ReturnType;
fixTables: () => ReturnType, mergeOrSplit: () => ReturnType;
setCellSelection: (position: { anchorCell: number, headCell?: number }) => 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> { interface NodeConfig<Options, Storage> {
/** /**
* Table Role * Table Role
*/ */
tableRole?: string | ((this: { tableRole?:
name: string, | string
options: Options, | ((this: {
storage: Storage, name: string;
parent: ParentConfig<NodeConfig<Options>>['tableRole'], options: Options;
}) => string), storage: Storage;
parent: ParentConfig<NodeConfig<Options>>['tableRole'];
}) => string);
} }
} }
@@ -105,99 +117,209 @@ export const Table = Node.create<TableOptions>({
group: 'block', group: 'block',
parseHTML() { parseHTML() {
return [ return [{ tag: 'table' }]
{ tag: 'table' }, },
]
addAttributes() {
return {
...this.parent?.(),
class: {
default: null,
parseHTML: element => element.getAttribute('class'),
},
ref: {
default: `table${Math.random().toString().substring(2)}`,
},
}
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['table', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), ['tbody', 0]] return [
'table',
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
['tbody', 0],
]
}, },
addCommands() { addCommands() {
return { return {
insertTable: ({ rows = 3, cols = 3, withHeaderRow = true } = {}) => ({ tr, dispatch, editor }) => { insertTable:
const node = createTable(editor.schema, rows, cols, withHeaderRow) ({ rows = 3, cols = 3, withHeaderRow = true } = {}) => ({ tr, dispatch, editor }) => {
const node = createTable(editor.schema, rows, cols, withHeaderRow)
if (dispatch) { if (dispatch) {
const offset = tr.selection.anchor + 1 const offset = tr.selection.anchor + 1
tr.replaceSelectionWith(node) tr.replaceSelectionWith(node)
.scrollIntoView() .scrollIntoView()
.setSelection(TextSelection.near(tr.doc.resolve(offset))) .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 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) return splitCell(state, dispatch)
}, },
setCellAttribute: (name, value) => ({ state, dispatch }) => { setCellAttribute:
return setCellAttr(name, value)(state, dispatch) (name, value) => ({ state, dispatch }) => {
}, return setCellAttr(name, value)(state, dispatch)
goToNextCell: () => ({ state, dispatch }) => { },
return goToNextCell(1)(state, dispatch) goToNextCell:
}, () => ({ state, dispatch }) => {
goToPreviousCell: () => ({ state, dispatch }) => { return goToNextCell(1)(state, dispatch)
return goToNextCell(-1)(state, dispatch) },
}, goToPreviousCell:
fixTables: () => ({ state, dispatch }) => { () => ({ state, dispatch }) => {
if (dispatch) { return goToNextCell(-1)(state, dispatch)
fixTables(state) },
} fixTables:
() => ({ state, dispatch }) => {
if (dispatch) {
fixTables(state)
}
return true return true
}, },
setCellSelection: position => ({ tr, dispatch }) => { setCellSelection:
if (dispatch) { position => ({ tr, dispatch }) => {
const selection = CellSelection.create(tr.doc, position.anchorCell, position.headCell) if (dispatch) {
const selection = CellSelection.create(
tr.doc,
position.anchorCell,
position.headCell,
)
// @ts-ignore // @ts-ignore
tr.setSelection(selection) 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 false
} }
return this.editor return this.editor.chain().addRowAfter().goToNextCell().run()
.chain()
.addRowAfter()
.goToNextCell()
.run()
}, },
'Shift-Tab': () => this.editor.commands.goToPreviousCell(), 'Shift-Tab': () => this.editor.commands.goToPreviousCell(),
Backspace: deleteTableWhenAllCellsSelected, Backspace: deleteTableWhenAllCellsSelected,
@@ -230,14 +348,18 @@ export const Table = Node.create<TableOptions>({
const isResizable = this.options.resizable && this.editor.isEditable const isResizable = this.options.resizable && this.editor.isEditable
return [ return [
...(isResizable ? [columnResizing({ ...(isResizable
handleWidth: this.options.handleWidth, ? [
cellMinWidth: this.options.cellMinWidth, columnResizing({
View: this.options.View, handleWidth: this.options.handleWidth,
// TODO: PR for @types/prosemirror-tables cellMinWidth: this.options.cellMinWidth,
// @ts-ignore (incorrect type) View: this.options.View,
lastColumnResizable: this.options.lastColumnResizable, // TODO: PR for @types/prosemirror-tables
})] : []), // @ts-ignore (incorrect type)
lastColumnResizable: this.options.lastColumnResizable,
}),
]
: []),
tableEditing({ tableEditing({
allowTableNodeSelection: this.options.allowTableNodeSelection, allowTableNodeSelection: this.options.allowTableNodeSelection,
}), }),
@@ -252,7 +374,9 @@ export const Table = Node.create<TableOptions>({
} }
return { return {
tableRole: callOrReturn(getExtensionField(extension, 'tableRole', context)), tableRole: callOrReturn(
getExtensionField(extension, 'tableRole', context),
),
} }
}, },
}) })