2022-09-09 11:53:35 +00:00
|
|
|
import { EditorState, Selection } from 'prosemirror-state'
|
|
|
|
import type { Node, Schema, ResolvedPos } from 'prosemirror-model'
|
|
|
|
import { InputRule, inputRules } from 'prosemirror-inputrules'
|
|
|
|
import { keymap } from 'prosemirror-keymap'
|
2022-10-08 16:40:58 +00:00
|
|
|
import type { ProseMirrorExtension } from '../../store/state'
|
2022-09-09 11:53:35 +00:00
|
|
|
|
|
|
|
export const tableInputRule = (schema: Schema) =>
|
|
|
|
new InputRule(
|
|
|
|
new RegExp('^\\|{2,}\\s$'),
|
|
|
|
(state: EditorState, match: string[], start: number, end: number) => {
|
|
|
|
const tr = state.tr
|
|
|
|
const columns = [...Array.from({ length: match[0].trim().length - 1 })]
|
|
|
|
const headers = columns.map(() => schema.node(schema.nodes.table_header, {}))
|
|
|
|
const cells = columns.map(() => schema.node(schema.nodes.table_cell, {}))
|
|
|
|
const table = schema.node(schema.nodes.table, {}, [
|
|
|
|
schema.node(schema.nodes.table_head, {}, schema.node(schema.nodes.table_row, {}, headers)),
|
|
|
|
schema.node(schema.nodes.table_body, {}, schema.node(schema.nodes.table_row, {}, cells))
|
|
|
|
])
|
|
|
|
|
|
|
|
tr.delete(start - 1, end)
|
|
|
|
tr.insert(start - 1, table)
|
|
|
|
tr.setSelection(Selection.near(tr.doc.resolve(start + 3)))
|
|
|
|
return tr
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
const tableSchema = {
|
|
|
|
table: {
|
|
|
|
content: '(table_head | table_body)*',
|
|
|
|
isolating: true,
|
|
|
|
selectable: false,
|
|
|
|
group: 'block',
|
|
|
|
parseDOM: [{ tag: 'div[data-type="table"]' }],
|
|
|
|
toDOM: () => [
|
|
|
|
'div',
|
|
|
|
{
|
|
|
|
class: 'table-container',
|
|
|
|
'data-type': 'table'
|
|
|
|
},
|
|
|
|
['table', 0]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
table_head: {
|
|
|
|
content: 'table_row',
|
|
|
|
isolating: true,
|
|
|
|
group: 'table_block',
|
|
|
|
selectable: false,
|
|
|
|
parseDOM: [{ tag: 'thead' }],
|
|
|
|
toDOM: () => ['thead', 0]
|
|
|
|
},
|
|
|
|
table_body: {
|
|
|
|
content: 'table_row+',
|
|
|
|
isolating: true,
|
|
|
|
group: 'table_block',
|
|
|
|
selectable: false,
|
|
|
|
parseDOM: [{ tag: 'tbody' }],
|
|
|
|
toDOM: () => ['tbody', 0]
|
|
|
|
},
|
|
|
|
table_row: {
|
|
|
|
content: '(table_cell | table_header)*',
|
|
|
|
parseDOM: [{ tag: 'tr' }],
|
|
|
|
toDOM: () => ['tr', 0]
|
|
|
|
},
|
|
|
|
table_cell: {
|
|
|
|
content: 'inline*',
|
|
|
|
isolating: true,
|
|
|
|
group: 'table_block',
|
|
|
|
selectable: false,
|
|
|
|
attrs: { style: { default: null } },
|
|
|
|
parseDOM: [
|
|
|
|
{
|
|
|
|
tag: 'td',
|
|
|
|
getAttrs: (dom: HTMLElement) => {
|
|
|
|
const textAlign = dom.style.textAlign
|
|
|
|
return textAlign ? { style: `text-align: ${textAlign}` } : null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
],
|
|
|
|
toDOM: (node: Node) => ['td', node.attrs, 0]
|
|
|
|
},
|
|
|
|
table_header: {
|
|
|
|
content: 'inline*',
|
|
|
|
isolating: true,
|
|
|
|
group: 'table_block',
|
|
|
|
selectable: false,
|
|
|
|
attrs: { style: { default: null } },
|
|
|
|
parseDOM: [
|
|
|
|
{
|
|
|
|
tag: 'th',
|
|
|
|
getAttrs: (dom: HTMLElement) => {
|
|
|
|
const textAlign = dom.style.textAlign
|
|
|
|
return textAlign ? { style: `text-align: ${textAlign}` } : null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
],
|
|
|
|
toDOM: (node: Node) => ['th', node.attrs, 0]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const findParentPos = ($pos: ResolvedPos, fn: (n: Node) => boolean) => {
|
|
|
|
for (let d = $pos.depth; d > 0; d--) {
|
|
|
|
if (fn($pos.node(d))) return $pos.doc.resolve($pos.before(d + 1))
|
|
|
|
}
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
const findTableCellPos = ($pos: ResolvedPos, header = true) =>
|
|
|
|
findParentPos($pos, (n) => n.type.name === 'table_cell' || (header && n.type.name === 'table_header'))
|
|
|
|
|
|
|
|
const findTableRowPos = ($pos: ResolvedPos) => findParentPos($pos, (n) => n.type.name === 'table_row')
|
|
|
|
|
|
|
|
const findTableHeadPos = ($pos: ResolvedPos) => findParentPos($pos, (n) => n.type.name === 'table_head')
|
|
|
|
|
|
|
|
const findTablePos = ($pos: ResolvedPos) => findParentPos($pos, (n) => n.type.name === 'table')
|
|
|
|
|
|
|
|
const findNodePosition = (node: Node, fn: (n: Node, p: Node) => boolean) => {
|
|
|
|
let result = -1
|
|
|
|
node.descendants((n, pos, p) => {
|
|
|
|
if (result !== -1) {
|
|
|
|
return false
|
|
|
|
} else if (fn(n, p)) {
|
|
|
|
result = pos
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
const findVertTableCellPos = ($pos: ResolvedPos, dir = 'up') => {
|
|
|
|
const cellPos = findTableCellPos($pos)
|
|
|
|
const rowPos = findTableRowPos($pos)
|
|
|
|
const offset = cellPos.pos - ($pos.before() + 1)
|
|
|
|
|
|
|
|
const add = dir === 'up' ? -1 : rowPos.node().nodeSize + 1
|
|
|
|
const nodeBeforePos = $pos.doc.resolve(rowPos.before() + add)
|
|
|
|
let rowBeforePos = findTableRowPos(nodeBeforePos)
|
|
|
|
|
|
|
|
if (!rowBeforePos) {
|
|
|
|
const table = $pos.node(0)
|
|
|
|
const tablePos = findTablePos($pos)
|
|
|
|
const inTableHead = !!findTableHeadPos($pos)
|
|
|
|
|
|
|
|
if (dir === 'up' && inTableHead) {
|
|
|
|
return $pos.doc.resolve(Math.max(0, tablePos.before() - 1))
|
|
|
|
} else if (dir === 'down' && !inTableHead) {
|
|
|
|
return $pos.doc.resolve(tablePos.after())
|
|
|
|
}
|
|
|
|
|
|
|
|
const pos = findNodePosition(table, (n, p) => {
|
|
|
|
return inTableHead
|
|
|
|
? p.type.name === 'table_body' && n.type.name === 'table_row'
|
|
|
|
: p.type.name === 'table_head' && n.type.name === 'table_row'
|
|
|
|
})
|
|
|
|
|
|
|
|
rowBeforePos = $pos.doc.resolve(pos + 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
const targetCell = $pos.doc.resolve(rowBeforePos.posAtIndex(rowPos.index()) + 1)
|
|
|
|
const targetCellTextSize = getTextSize(targetCell.node())
|
|
|
|
const cellOffset = offset > targetCellTextSize ? targetCellTextSize : offset
|
|
|
|
return $pos.doc.resolve(targetCell.pos + cellOffset)
|
|
|
|
}
|
|
|
|
|
|
|
|
const getTextSize = (n: Node) => {
|
|
|
|
let size = 0
|
|
|
|
n.descendants((d: Node) => {
|
|
|
|
size += d.text?.length ?? 0
|
|
|
|
})
|
|
|
|
|
|
|
|
return size
|
|
|
|
}
|
|
|
|
|
|
|
|
export default (): ProseMirrorExtension => ({
|
|
|
|
schema: (prev) => ({
|
|
|
|
...prev,
|
|
|
|
nodes: (prev.nodes as any).append(tableSchema)
|
|
|
|
}),
|
|
|
|
// FIXME (extract functions)
|
|
|
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
|
|
plugins: (prev, schema) => [
|
|
|
|
keymap({
|
|
|
|
'Ctrl-Enter': (state, dispatch) => {
|
|
|
|
const tablePos = findTablePos(state.selection.$head)
|
|
|
|
if (!tablePos) return false
|
|
|
|
const targetPos = tablePos.after()
|
|
|
|
const tr = state.tr
|
|
|
|
tr.insert(targetPos, state.schema.node('paragraph'))
|
|
|
|
tr.setSelection(Selection.near(tr.doc.resolve(targetPos)))
|
|
|
|
dispatch(tr)
|
|
|
|
return true
|
|
|
|
},
|
|
|
|
Backspace: (state, dispatch) => {
|
|
|
|
const sel = state.selection
|
|
|
|
if (!sel.empty) return false
|
|
|
|
const cellPos = findTableCellPos(sel.$head)
|
|
|
|
if (!cellPos) return false
|
|
|
|
|
|
|
|
if (getTextSize(cellPos.node()) === 0) {
|
|
|
|
const rowPos = findTableRowPos(sel.$head)
|
|
|
|
const tablePos = findTablePos(sel.$head)
|
|
|
|
const before = state.doc.resolve(cellPos.before() - 1)
|
|
|
|
|
|
|
|
const cellBeforePos = findTableCellPos(before)
|
|
|
|
const inTableHead = !!findTableHeadPos(sel.$head)
|
|
|
|
|
|
|
|
if (cellBeforePos) {
|
|
|
|
const tr = state.tr
|
|
|
|
tr.setSelection(Selection.near(before))
|
|
|
|
dispatch(tr)
|
|
|
|
return true
|
|
|
|
} else if (!inTableHead && getTextSize(rowPos.node()) === 0) {
|
|
|
|
const tr = state.tr
|
|
|
|
tr.delete(before.pos - 1, before.pos + rowPos.node().nodeSize)
|
|
|
|
tr.setSelection(Selection.near(tr.doc.resolve(before.pos - 4)))
|
|
|
|
dispatch(tr)
|
|
|
|
return true
|
|
|
|
} else if (getTextSize(tablePos.node()) === 0) {
|
|
|
|
const tr = state.tr
|
|
|
|
tr.delete(tablePos.before(), tablePos.before() + tablePos.node().nodeSize)
|
|
|
|
dispatch(tr)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
},
|
|
|
|
Enter: (state, dispatch) => {
|
|
|
|
const sel = state.selection
|
|
|
|
if (!sel.empty) return false
|
|
|
|
const cellPos = findTableCellPos(sel.$head)
|
|
|
|
if (!cellPos) return false
|
|
|
|
|
|
|
|
const rowPos = findTableRowPos(sel.$head)
|
|
|
|
const cells = []
|
|
|
|
rowPos.node().forEach((cell) => {
|
|
|
|
cells.push(schema.nodes.table_cell.create(cell.attrs))
|
|
|
|
})
|
|
|
|
const newRow = schema.nodes.table_row.create(null, cells)
|
|
|
|
|
|
|
|
const theadPos = findTableHeadPos(sel.$head)
|
|
|
|
if (theadPos) {
|
|
|
|
const tablePos = findTablePos(sel.$head)
|
|
|
|
let tbodyPos: number
|
|
|
|
tablePos.node().descendants((node, pos) => {
|
|
|
|
if (node.type.name === 'table_body') {
|
|
|
|
tbodyPos = tablePos.pos + pos
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
if (tbodyPos) {
|
|
|
|
const tbody = state.doc.resolve(tbodyPos + 1)
|
|
|
|
const tr = state.tr.insert(tbody.pos, newRow)
|
|
|
|
tr.setSelection(Selection.near(tr.doc.resolve(tbody.pos)))
|
|
|
|
dispatch(tr)
|
|
|
|
} else {
|
|
|
|
const tbody = schema.nodes.table_body.create(null, [newRow])
|
|
|
|
const targetPos = theadPos.after()
|
|
|
|
const tr = state.tr.insert(targetPos, tbody)
|
|
|
|
tr.setSelection(Selection.near(tr.doc.resolve(targetPos)))
|
|
|
|
dispatch(tr)
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
const targetPos = sel.$head.after(-1)
|
|
|
|
const tr = state.tr.insert(targetPos, newRow)
|
|
|
|
tr.setSelection(Selection.near(tr.doc.resolve(targetPos)))
|
|
|
|
|
|
|
|
dispatch(tr)
|
|
|
|
return true
|
|
|
|
},
|
|
|
|
ArrowUp: (state, dispatch) => {
|
|
|
|
const sel = state.selection
|
|
|
|
if (!sel.empty) return false
|
|
|
|
const cellPos = findTableCellPos(sel.$head)
|
|
|
|
if (!cellPos) return false
|
|
|
|
const abovePos = findVertTableCellPos(sel.$head)
|
|
|
|
if (abovePos) {
|
|
|
|
const tr = state.tr
|
|
|
|
let selection = Selection.near(abovePos)
|
|
|
|
if (abovePos.pos === 0 && cellPos.parentOffset === 0) {
|
|
|
|
tr.insert(0, state.schema.node('paragraph'))
|
|
|
|
selection = Selection.near(tr.doc.resolve(0))
|
|
|
|
}
|
|
|
|
tr.setSelection(selection)
|
|
|
|
dispatch(tr)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
},
|
|
|
|
ArrowDown: (state, dispatch) => {
|
|
|
|
const sel = state.selection
|
|
|
|
if (!sel.empty) return false
|
|
|
|
const cellPos = findTableCellPos(sel.$head)
|
|
|
|
if (!cellPos) return false
|
|
|
|
const belowPos = findVertTableCellPos(sel.$head, 'down')
|
|
|
|
if (belowPos) {
|
|
|
|
const tr = state.tr
|
|
|
|
tr.setSelection(Selection.near(belowPos))
|
|
|
|
dispatch(tr)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
...prev,
|
|
|
|
inputRules({ rules: [tableInputRule(schema)] })
|
|
|
|
]
|
|
|
|
})
|