You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
147 lines
4.7 KiB
147 lines
4.7 KiB
|
3 years ago
|
// This file defines helpers for normalizing tables, making sure no
|
||
|
|
// cells overlap (which can happen, if you have the wrong col- and
|
||
|
|
// rowspans) and that each row has the same width. Uses the problems
|
||
|
|
// reported by `TableMap`.
|
||
|
|
|
||
|
|
import { EditorState, PluginKey, Transaction } from 'prosemirror-state';
|
||
|
|
import { TableMap } from './tablemap';
|
||
|
|
import { removeColSpan, _setAttr } from './util';
|
||
|
|
import { tableNodeTypes } from './schema';
|
||
|
|
import { Node } from 'prosemirror-model';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @public
|
||
|
|
*/
|
||
|
|
export const fixTablesKey = new PluginKey<{ fixTables: boolean }>('fix-tables');
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Helper for iterating through the nodes in a document that changed
|
||
|
|
* compared to the given previous document. Useful for avoiding
|
||
|
|
* duplicate work on each transaction.
|
||
|
|
*
|
||
|
|
* @public
|
||
|
|
*/
|
||
|
|
function changedDescendants(
|
||
|
|
old: Node,
|
||
|
|
cur: Node,
|
||
|
|
offset: number,
|
||
|
|
f: (node: Node, pos: number) => void,
|
||
|
|
): void {
|
||
|
|
const oldSize = old.childCount,
|
||
|
|
curSize = cur.childCount;
|
||
|
|
outer: for (let i = 0, j = 0; i < curSize; i++) {
|
||
|
|
const child = cur.child(i);
|
||
|
|
for (let scan = j, e = Math.min(oldSize, i + 3); scan < e; scan++) {
|
||
|
|
if (old.child(scan) == child) {
|
||
|
|
j = scan + 1;
|
||
|
|
offset += child.nodeSize;
|
||
|
|
continue outer;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
f(child, offset);
|
||
|
|
if (j < oldSize && old.child(j).sameMarkup(child))
|
||
|
|
changedDescendants(old.child(j), child, offset + 1, f);
|
||
|
|
else child.nodesBetween(0, child.content.size, f, offset + 1);
|
||
|
|
offset += child.nodeSize;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Inspect all tables in the given state's document and return a
|
||
|
|
* transaction that fixes them, if necessary. If `oldState` was
|
||
|
|
* provided, that is assumed to hold a previous, known-good state,
|
||
|
|
* which will be used to avoid re-scanning unchanged parts of the
|
||
|
|
* document.
|
||
|
|
*
|
||
|
|
* @public
|
||
|
|
*/
|
||
|
|
export function fixTables(
|
||
|
|
state: EditorState,
|
||
|
|
oldState?: EditorState,
|
||
|
|
): Transaction | undefined {
|
||
|
|
let tr;
|
||
|
|
const check = (node, pos) => {
|
||
|
|
if (node.type.spec.tableRole == 'table')
|
||
|
|
tr = fixTable(state, node, pos, tr);
|
||
|
|
};
|
||
|
|
if (!oldState) state.doc.descendants(check);
|
||
|
|
else if (oldState.doc != state.doc)
|
||
|
|
changedDescendants(oldState.doc, state.doc, 0, check);
|
||
|
|
return tr;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fix the given table, if necessary. Will append to the transaction
|
||
|
|
// it was given, if non-null, or create a new one if necessary.
|
||
|
|
export function fixTable(
|
||
|
|
state: EditorState,
|
||
|
|
table: Node,
|
||
|
|
tablePos: number,
|
||
|
|
tr: Transaction | undefined,
|
||
|
|
): Transaction | undefined {
|
||
|
|
const map = TableMap.get(table);
|
||
|
|
if (!map.problems) return tr;
|
||
|
|
if (!tr) tr = state.tr;
|
||
|
|
|
||
|
|
// Track which rows we must add cells to, so that we can adjust that
|
||
|
|
// when fixing collisions.
|
||
|
|
const mustAdd = [];
|
||
|
|
for (let i = 0; i < map.height; i++) mustAdd.push(0);
|
||
|
|
for (let i = 0; i < map.problems.length; i++) {
|
||
|
|
const prob = map.problems[i];
|
||
|
|
if (prob.type == 'collision') {
|
||
|
|
const cell = table.nodeAt(prob.pos);
|
||
|
|
for (let j = 0; j < cell.attrs.rowspan; j++)
|
||
|
|
mustAdd[prob.row + j] += prob.n;
|
||
|
|
tr.setNodeMarkup(
|
||
|
|
tr.mapping.map(tablePos + 1 + prob.pos),
|
||
|
|
null,
|
||
|
|
removeColSpan(cell.attrs, cell.attrs.colspan - prob.n, prob.n),
|
||
|
|
);
|
||
|
|
} else if (prob.type == 'missing') {
|
||
|
|
mustAdd[prob.row] += prob.n;
|
||
|
|
} else if (prob.type == 'overlong_rowspan') {
|
||
|
|
const cell = table.nodeAt(prob.pos);
|
||
|
|
tr.setNodeMarkup(
|
||
|
|
tr.mapping.map(tablePos + 1 + prob.pos),
|
||
|
|
null,
|
||
|
|
_setAttr(cell.attrs, 'rowspan', cell.attrs.rowspan - prob.n),
|
||
|
|
);
|
||
|
|
} else if (prob.type == 'colwidth mismatch') {
|
||
|
|
const cell = table.nodeAt(prob.pos);
|
||
|
|
tr.setNodeMarkup(
|
||
|
|
tr.mapping.map(tablePos + 1 + prob.pos),
|
||
|
|
null,
|
||
|
|
_setAttr(cell.attrs, 'colwidth', prob.colwidth),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
let first, last;
|
||
|
|
for (let i = 0; i < mustAdd.length; i++)
|
||
|
|
if (mustAdd[i]) {
|
||
|
|
if (first == null) first = i;
|
||
|
|
last = i;
|
||
|
|
}
|
||
|
|
// Add the necessary cells, using a heuristic for whether to add the
|
||
|
|
// cells at the start or end of the rows (if it looks like a 'bite'
|
||
|
|
// was taken out of the table, add cells at the start of the row
|
||
|
|
// after the bite. Otherwise add them at the end).
|
||
|
|
for (let i = 0, pos = tablePos + 1; i < map.height; i++) {
|
||
|
|
const row = table.child(i);
|
||
|
|
const end = pos + row.nodeSize;
|
||
|
|
const add = mustAdd[i];
|
||
|
|
if (add > 0) {
|
||
|
|
let tableNodeType = 'cell';
|
||
|
|
if (row.firstChild) {
|
||
|
|
tableNodeType = row.firstChild.type.spec.tableRole;
|
||
|
|
}
|
||
|
|
const nodes = [];
|
||
|
|
for (let j = 0; j < add; j++)
|
||
|
|
nodes.push(tableNodeTypes(state.schema)[tableNodeType].createAndFill());
|
||
|
|
const side = (i == 0 || first == i - 1) && last == i ? pos + 1 : end - 1;
|
||
|
|
tr.insert(tr.mapping.map(side), nodes);
|
||
|
|
}
|
||
|
|
pos = end;
|
||
|
|
}
|
||
|
|
return tr.setMeta(fixTablesKey, { fixTables: true });
|
||
|
|
}
|