@milkdown/preset-gfm 6.1.4 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/auto-link.d.ts.map +1 -1
- package/lib/footnote/definition.d.ts +1 -5
- package/lib/footnote/definition.d.ts.map +1 -1
- package/lib/footnote/reference.d.ts +1 -5
- package/lib/footnote/reference.d.ts.map +1 -1
- package/lib/index.d.ts +1 -3
- package/lib/index.d.ts.map +1 -1
- package/lib/index.es.js +1979 -891
- package/lib/index.es.js.map +1 -1
- package/lib/strike-through.d.ts +1 -5
- package/lib/strike-through.d.ts.map +1 -1
- package/lib/supported-keys.d.ts +1 -0
- package/lib/supported-keys.d.ts.map +1 -1
- package/lib/table/command.d.ts +1 -1
- package/lib/table/command.d.ts.map +1 -1
- package/lib/table/nodes/index.d.ts +2 -23
- package/lib/table/nodes/index.d.ts.map +1 -1
- package/lib/table/operator-plugin/actions.d.ts +1 -1
- package/lib/table/operator-plugin/actions.d.ts.map +1 -1
- package/lib/table/operator-plugin/calc-pos.d.ts.map +1 -1
- package/lib/table/operator-plugin/helper.d.ts +1 -1
- package/lib/table/operator-plugin/helper.d.ts.map +1 -1
- package/lib/table/operator-plugin/index.d.ts +1 -1
- package/lib/table/operator-plugin/index.d.ts.map +1 -1
- package/lib/table/operator-plugin/widget.d.ts +4 -4
- package/lib/table/operator-plugin/widget.d.ts.map +1 -1
- package/lib/table/plugin/auto-insert-zero-space.d.ts +3 -0
- package/lib/table/plugin/auto-insert-zero-space.d.ts.map +1 -0
- package/lib/table/plugin/cell-selection.d.ts +38 -0
- package/lib/table/plugin/cell-selection.d.ts.map +1 -0
- package/lib/table/plugin/column-resizing.d.ts +17 -0
- package/lib/table/plugin/column-resizing.d.ts.map +1 -0
- package/lib/table/plugin/commands.d.ts +30 -0
- package/lib/table/plugin/commands.d.ts.map +1 -0
- package/lib/table/plugin/copy-paste.d.ts +13 -0
- package/lib/table/plugin/copy-paste.d.ts.map +1 -0
- package/lib/table/plugin/fix-tables.d.ts +6 -0
- package/lib/table/plugin/fix-tables.d.ts.map +1 -0
- package/lib/table/plugin/index.d.ts +4 -0
- package/lib/table/plugin/index.d.ts.map +1 -0
- package/lib/table/plugin/schema.d.ts +4 -0
- package/lib/table/plugin/schema.d.ts.map +1 -0
- package/lib/table/plugin/table-editing.d.ts +9 -0
- package/lib/table/plugin/table-editing.d.ts.map +1 -0
- package/lib/table/plugin/table-map.d.ts +44 -0
- package/lib/table/plugin/table-map.d.ts.map +1 -0
- package/lib/table/plugin/table-view.d.ts +15 -0
- package/lib/table/plugin/table-view.d.ts.map +1 -0
- package/lib/table/plugin/types.d.ts +15 -0
- package/lib/table/plugin/types.d.ts.map +1 -0
- package/lib/table/plugin/util.d.ts +16 -0
- package/lib/table/plugin/util.d.ts.map +1 -0
- package/lib/table/utils.d.ts +6 -6
- package/lib/table/utils.d.ts.map +1 -1
- package/lib/task-list-item.d.ts +1 -5
- package/lib/task-list-item.d.ts.map +1 -1
- package/package.json +8 -7
- package/src/auto-link.ts +4 -3
- package/src/footnote/definition.ts +3 -1
- package/src/footnote/reference.ts +2 -1
- package/src/table/command.ts +3 -3
- package/src/table/nodes/index.ts +7 -3
- package/src/table/operator-plugin/actions.ts +4 -4
- package/src/table/operator-plugin/calc-pos.ts +4 -2
- package/src/table/operator-plugin/helper.ts +2 -1
- package/src/table/operator-plugin/index.ts +1 -1
- package/src/table/operator-plugin/widget.ts +4 -14
- package/src/table/plugin/auto-insert-zero-space.ts +51 -0
- package/src/table/plugin/cell-selection.ts +352 -0
- package/src/table/plugin/column-resizing.ts +260 -0
- package/src/table/plugin/commands.ts +551 -0
- package/src/table/plugin/copy-paste.ts +306 -0
- package/src/table/plugin/fix-tables.ts +117 -0
- package/src/table/plugin/index.ts +4 -0
- package/src/table/plugin/schema.ts +114 -0
- package/src/table/plugin/table-editing.ts +275 -0
- package/src/table/plugin/table-map.ts +280 -0
- package/src/table/plugin/table-view.ts +76 -0
- package/src/table/plugin/types.ts +16 -0
- package/src/table/plugin/util.ts +107 -0
- package/src/table/utils.ts +31 -22
- package/src/task-list-item.ts +4 -2
- package/lib/table/nodes/schema.d.ts +0 -2
- package/lib/table/nodes/schema.d.ts.map +0 -1
- package/src/table/nodes/schema.ts +0 -16
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/* Copyright 2021, Milkdown by Mirone. */
|
|
2
|
+
import { keydownHandler } from '@milkdown/prose/keymap';
|
|
3
|
+
import { Fragment, ResolvedPos, Slice } from '@milkdown/prose/model';
|
|
4
|
+
import { Command, EditorState, Plugin, PluginKey, Selection, TextSelection, Transaction } from '@milkdown/prose/state';
|
|
5
|
+
import { EditorView } from '@milkdown/prose/view';
|
|
6
|
+
|
|
7
|
+
import { CellSelection, drawCellSelection, normalizeSelection } from './cell-selection';
|
|
8
|
+
import { clipCells, fitSlice, insertCells, pastedCells } from './copy-paste';
|
|
9
|
+
import { fixTables } from './fix-tables';
|
|
10
|
+
import { tableNodeTypes } from './schema';
|
|
11
|
+
import { TableMap } from './table-map';
|
|
12
|
+
import { cellAround, inSameTable, isInTable, nextCell, selectionCell } from './util';
|
|
13
|
+
|
|
14
|
+
export const tableEditingKey = new PluginKey('selectingCells');
|
|
15
|
+
|
|
16
|
+
function domInCell(view: EditorView, dom: EventTarget | null) {
|
|
17
|
+
for (; dom && dom != view.dom; dom = (dom as Element).parentNode as Element)
|
|
18
|
+
if ((dom as Element).nodeName == 'TD' || (dom as Element).nodeName == 'TH') return dom;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function cellUnderMouse(view: EditorView, event: MouseEvent) {
|
|
23
|
+
const mousePos = view.posAtCoords({ left: event.clientX, top: event.clientY });
|
|
24
|
+
if (!mousePos) return null;
|
|
25
|
+
return mousePos ? cellAround(view.state.doc.resolve(mousePos.pos)) : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function handleMouseDown(view: EditorView, event: Event) {
|
|
29
|
+
const startEvent = event as MouseEvent;
|
|
30
|
+
if (startEvent.ctrlKey || startEvent.metaKey) return;
|
|
31
|
+
|
|
32
|
+
const startDOMCell = domInCell(view, startEvent.target as Element);
|
|
33
|
+
let $anchor;
|
|
34
|
+
if (startEvent.shiftKey && view.state.selection instanceof CellSelection) {
|
|
35
|
+
// Adding to an existing cell selection
|
|
36
|
+
setCellSelection(view.state.selection.$anchorCell, startEvent);
|
|
37
|
+
startEvent.preventDefault();
|
|
38
|
+
} else if (
|
|
39
|
+
startEvent.shiftKey &&
|
|
40
|
+
startDOMCell &&
|
|
41
|
+
($anchor = cellAround(view.state.selection.$anchor)) != null &&
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
43
|
+
cellUnderMouse(view, startEvent)!.pos != $anchor.pos
|
|
44
|
+
) {
|
|
45
|
+
// Adding to a selection that starts in another cell (causing a
|
|
46
|
+
// cell selection to be created).
|
|
47
|
+
setCellSelection($anchor, startEvent);
|
|
48
|
+
startEvent.preventDefault();
|
|
49
|
+
} else if (!startDOMCell) {
|
|
50
|
+
// Not in a cell, let the default behavior happen.
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Create and dispatch a cell selection between the given anchor and
|
|
55
|
+
// the position under the mouse.
|
|
56
|
+
function setCellSelection($anchor: ResolvedPos, event: MouseEvent) {
|
|
57
|
+
let $head = cellUnderMouse(view, event);
|
|
58
|
+
const starting = tableEditingKey.getState(view.state) == null;
|
|
59
|
+
if (!$head || !inSameTable($anchor, $head)) {
|
|
60
|
+
if (starting) $head = $anchor;
|
|
61
|
+
else return;
|
|
62
|
+
}
|
|
63
|
+
const selection = new CellSelection($anchor, $head);
|
|
64
|
+
if (starting || !view.state.selection.eq(selection)) {
|
|
65
|
+
const tr = view.state.tr.setSelection(selection);
|
|
66
|
+
if (starting) tr.setMeta(tableEditingKey, $anchor.pos);
|
|
67
|
+
view.dispatch(tr);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Stop listening to mouse motion events.
|
|
72
|
+
function stop() {
|
|
73
|
+
view.root.removeEventListener('mouseup', stop);
|
|
74
|
+
view.root.removeEventListener('dragstart', stop);
|
|
75
|
+
view.root.removeEventListener('mousemove', move);
|
|
76
|
+
if (tableEditingKey.getState(view.state) != null) view.dispatch(view.state.tr.setMeta(tableEditingKey, -1));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function move(event: Event) {
|
|
80
|
+
const anchor = tableEditingKey.getState(view.state);
|
|
81
|
+
let $anchor;
|
|
82
|
+
if (anchor != null) {
|
|
83
|
+
// Continuing an existing cross-cell selection
|
|
84
|
+
$anchor = view.state.doc.resolve(anchor);
|
|
85
|
+
} else if (domInCell(view, event.target) != startDOMCell) {
|
|
86
|
+
// Moving out of the initial cell -- start a new cell selection
|
|
87
|
+
$anchor = cellUnderMouse(view, startEvent);
|
|
88
|
+
if (!$anchor) return stop();
|
|
89
|
+
}
|
|
90
|
+
if ($anchor) setCellSelection($anchor, event as MouseEvent);
|
|
91
|
+
}
|
|
92
|
+
view.root.addEventListener('mouseup', stop);
|
|
93
|
+
view.root.addEventListener('dragstart', stop);
|
|
94
|
+
view.root.addEventListener('mousemove', move);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function handleTripleClick(view: EditorView, pos: number) {
|
|
98
|
+
const doc = view.state.doc,
|
|
99
|
+
$cell = cellAround(doc.resolve(pos));
|
|
100
|
+
if (!$cell) return false;
|
|
101
|
+
view.dispatch(view.state.tr.setSelection(new CellSelection($cell)));
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function maybeSetSelection(
|
|
106
|
+
state: EditorState,
|
|
107
|
+
dispatch: undefined | ((tr: Transaction) => void),
|
|
108
|
+
selection: Selection,
|
|
109
|
+
) {
|
|
110
|
+
if (selection.eq(state.selection)) return false;
|
|
111
|
+
if (dispatch) dispatch(state.tr.setSelection(selection).scrollIntoView());
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function atEndOfCell(view: EditorView, axis: string, dir: number) {
|
|
116
|
+
if (!(view.state.selection instanceof TextSelection)) return null;
|
|
117
|
+
const { $head } = view.state.selection;
|
|
118
|
+
for (let d = $head.depth - 1; d >= 0; d--) {
|
|
119
|
+
const parent = $head.node(d),
|
|
120
|
+
index = dir < 0 ? $head.index(d) : $head.indexAfter(d);
|
|
121
|
+
if (index != (dir < 0 ? 0 : parent.childCount)) return null;
|
|
122
|
+
if (parent.type.spec['tableRole'] == 'cell' || parent.type.spec['tableRole'] == 'header_cell') {
|
|
123
|
+
const cellPos = $head.before(d);
|
|
124
|
+
const dirStr = axis == 'vert' ? (dir > 0 ? 'down' : 'up') : dir > 0 ? 'right' : 'left';
|
|
125
|
+
return view.endOfTextblock(dirStr) ? cellPos : null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function arrow(axis: string, dir: number): Command {
|
|
132
|
+
return (state, dispatch, view) => {
|
|
133
|
+
const sel = state.selection;
|
|
134
|
+
if (sel instanceof CellSelection) {
|
|
135
|
+
return maybeSetSelection(state, dispatch, Selection.near(sel.$headCell, dir));
|
|
136
|
+
}
|
|
137
|
+
if (axis != 'horiz' && !sel.empty) return false;
|
|
138
|
+
const end = atEndOfCell(view as EditorView, axis, dir);
|
|
139
|
+
if (end == null) return false;
|
|
140
|
+
if (axis == 'horiz') {
|
|
141
|
+
return maybeSetSelection(state, dispatch, Selection.near(state.doc.resolve(sel.head + dir), dir));
|
|
142
|
+
} else {
|
|
143
|
+
const $cell = state.doc.resolve(end),
|
|
144
|
+
$next = nextCell($cell, axis, dir);
|
|
145
|
+
let newSel;
|
|
146
|
+
if ($next) newSel = Selection.near($next, 1);
|
|
147
|
+
else if (dir < 0) newSel = Selection.near(state.doc.resolve($cell.before(-1)), -1);
|
|
148
|
+
else newSel = Selection.near(state.doc.resolve($cell.after(-1)), 1);
|
|
149
|
+
return maybeSetSelection(state, dispatch, newSel);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function shiftArrow(axis: string, dir: number): Command {
|
|
155
|
+
return (state, dispatch, view) => {
|
|
156
|
+
let sel = state.selection;
|
|
157
|
+
if (!(sel instanceof CellSelection)) {
|
|
158
|
+
const end = atEndOfCell(view as EditorView, axis, dir);
|
|
159
|
+
if (end == null) return false;
|
|
160
|
+
sel = new CellSelection(state.doc.resolve(end));
|
|
161
|
+
}
|
|
162
|
+
const $head = nextCell((sel as CellSelection).$headCell, axis, dir);
|
|
163
|
+
if (!$head) return false;
|
|
164
|
+
return maybeSetSelection(state, dispatch, new CellSelection((sel as CellSelection).$anchorCell, $head));
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function deleteCellSelection(state: EditorState, dispatch?: (tr: Transaction) => void) {
|
|
169
|
+
const sel = state.selection;
|
|
170
|
+
if (!(sel instanceof CellSelection)) return false;
|
|
171
|
+
if (dispatch) {
|
|
172
|
+
const tr = state.tr,
|
|
173
|
+
baseContent = tableNodeTypes(state.schema).cell.createAndFill().content;
|
|
174
|
+
sel.forEachCell((cell, pos) => {
|
|
175
|
+
if (!cell.content.eq(baseContent))
|
|
176
|
+
tr.replace(
|
|
177
|
+
tr.mapping.map(pos + 1),
|
|
178
|
+
tr.mapping.map(pos + cell.nodeSize - 1),
|
|
179
|
+
new Slice(baseContent, 0, 0),
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
if (tr.docChanged) dispatch(tr);
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const handleKeyDown = keydownHandler({
|
|
188
|
+
ArrowLeft: arrow('horiz', -1),
|
|
189
|
+
ArrowRight: arrow('horiz', 1),
|
|
190
|
+
ArrowUp: arrow('vert', -1),
|
|
191
|
+
ArrowDown: arrow('vert', 1),
|
|
192
|
+
|
|
193
|
+
'Shift-ArrowLeft': shiftArrow('horiz', -1),
|
|
194
|
+
'Shift-ArrowRight': shiftArrow('horiz', 1),
|
|
195
|
+
'Shift-ArrowUp': shiftArrow('vert', -1),
|
|
196
|
+
'Shift-ArrowDown': shiftArrow('vert', 1),
|
|
197
|
+
|
|
198
|
+
Backspace: deleteCellSelection,
|
|
199
|
+
'Mod-Backspace': deleteCellSelection,
|
|
200
|
+
Delete: deleteCellSelection,
|
|
201
|
+
'Mod-Delete': deleteCellSelection,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
export function handlePaste(view: EditorView, _: Event, slice: Slice) {
|
|
205
|
+
if (!isInTable(view.state)) return false;
|
|
206
|
+
let cells = pastedCells(slice);
|
|
207
|
+
const sel = view.state.selection;
|
|
208
|
+
if (sel instanceof CellSelection) {
|
|
209
|
+
if (!cells)
|
|
210
|
+
cells = {
|
|
211
|
+
width: 1,
|
|
212
|
+
height: 1,
|
|
213
|
+
rows: [Fragment.from(fitSlice(tableNodeTypes(view.state.schema).cell, slice))],
|
|
214
|
+
};
|
|
215
|
+
const table = sel.$anchorCell.node(-1),
|
|
216
|
+
start = sel.$anchorCell.start(-1);
|
|
217
|
+
const rect = TableMap.get(table).rectBetween(sel.$anchorCell.pos - start, sel.$headCell.pos - start);
|
|
218
|
+
cells = clipCells(cells, rect.right - rect.left, rect.bottom - rect.top);
|
|
219
|
+
insertCells(view.state, view.dispatch, start, rect, cells);
|
|
220
|
+
return true;
|
|
221
|
+
} else if (cells) {
|
|
222
|
+
const $cell = selectionCell(view.state) as ResolvedPos,
|
|
223
|
+
start = $cell.start(-1);
|
|
224
|
+
insertCells(view.state, view.dispatch, start, TableMap.get($cell.node(-1)).findCell($cell.pos - start), cells);
|
|
225
|
+
return true;
|
|
226
|
+
} else {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function tableEditing({ allowTableNodeSelection = false } = {}) {
|
|
232
|
+
return new Plugin({
|
|
233
|
+
key: tableEditingKey,
|
|
234
|
+
|
|
235
|
+
// This piece of state is used to remember when a mouse-drag
|
|
236
|
+
// cell-selection is happening, so that it can continue even as
|
|
237
|
+
// transactions (which might move its anchor cell) come in.
|
|
238
|
+
state: {
|
|
239
|
+
init() {
|
|
240
|
+
return null;
|
|
241
|
+
},
|
|
242
|
+
apply(tr, cur) {
|
|
243
|
+
const set = tr.getMeta(tableEditingKey);
|
|
244
|
+
if (set != null) return set == -1 ? null : set;
|
|
245
|
+
if (cur == null || !tr.docChanged) return cur;
|
|
246
|
+
const { deleted, pos } = tr.mapping.mapResult(cur);
|
|
247
|
+
return deleted ? null : pos;
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
props: {
|
|
252
|
+
decorations: drawCellSelection,
|
|
253
|
+
|
|
254
|
+
handleDOMEvents: {
|
|
255
|
+
mousedown: handleMouseDown,
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
createSelectionBetween(view) {
|
|
259
|
+
if (tableEditingKey.getState(view.state) != null) return view.state.selection;
|
|
260
|
+
|
|
261
|
+
return null;
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
handleTripleClick,
|
|
265
|
+
|
|
266
|
+
handleKeyDown,
|
|
267
|
+
|
|
268
|
+
handlePaste,
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
appendTransaction(_, oldState, state) {
|
|
272
|
+
return normalizeSelection(state, fixTables(state, oldState), allowTableNodeSelection);
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/* Copyright 2021, Milkdown by Mirone. */
|
|
2
|
+
|
|
3
|
+
import { Attrs, Node } from '@milkdown/prose/model';
|
|
4
|
+
|
|
5
|
+
const cache = new WeakMap<Node, TableMap>();
|
|
6
|
+
const readFromCache = (key: Node) => cache.get(key);
|
|
7
|
+
const addToCache = (key: Node, value: TableMap) => {
|
|
8
|
+
cache.set(key, value);
|
|
9
|
+
return value;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export class Rect {
|
|
13
|
+
public tableStart?: number;
|
|
14
|
+
public map?: TableMap;
|
|
15
|
+
public table?: Node;
|
|
16
|
+
constructor(public left: number, public top: number, public right: number, public bottom: number) {}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ::- A table map describes the structore of a given table. To avoid
|
|
20
|
+
// recomputing them all the time, they are cached per table node. To
|
|
21
|
+
// be able to do that, positions saved in the map are relative to the
|
|
22
|
+
// start of the table, rather than the start of the document.
|
|
23
|
+
export class TableMap {
|
|
24
|
+
constructor(public width: number, public height: number, public map: number[], public problems?: Problem[]) {
|
|
25
|
+
// :: number The width of the table
|
|
26
|
+
this.width = width;
|
|
27
|
+
// :: number The table's height
|
|
28
|
+
this.height = height;
|
|
29
|
+
// :: [number] A width * height array with the start position of
|
|
30
|
+
// the cell covering that part of the table in each slot
|
|
31
|
+
this.map = map;
|
|
32
|
+
// An optional array of problems (cell overlap or non-rectangular
|
|
33
|
+
// shape) for the table, used by the table normalizer.
|
|
34
|
+
this.problems = problems;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Find the dimensions of the cell at the given position.
|
|
38
|
+
findCell(pos: number): Rect {
|
|
39
|
+
for (let i = 0; i < this.map.length; i++) {
|
|
40
|
+
const curPos = this.map[i];
|
|
41
|
+
if (curPos != pos) continue;
|
|
42
|
+
const left = i % this.width,
|
|
43
|
+
top = (i / this.width) | 0;
|
|
44
|
+
let right = left + 1,
|
|
45
|
+
bottom = top + 1;
|
|
46
|
+
for (let j = 1; right < this.width && this.map[i + j] == curPos; j++) right++;
|
|
47
|
+
for (let j = 1; bottom < this.height && this.map[i + this.width * j] == curPos; j++) bottom++;
|
|
48
|
+
return new Rect(left, top, right, bottom);
|
|
49
|
+
}
|
|
50
|
+
throw new RangeError('No cell with offset ' + pos + ' found');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
colCount(pos: number): number {
|
|
54
|
+
for (let i = 0; i < this.map.length; i++) if (this.map[i] == pos) return i % this.width;
|
|
55
|
+
throw new RangeError('No cell with offset ' + pos + ' found');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// :: (number, string, number) → ?number
|
|
59
|
+
// Find the next cell in the given direction, starting from the cell
|
|
60
|
+
// at `pos`, if any.
|
|
61
|
+
nextCell(pos: number, axis: string, dir: number): number | undefined {
|
|
62
|
+
const { left, right, top, bottom } = this.findCell(pos);
|
|
63
|
+
if (axis == 'horiz') {
|
|
64
|
+
if (dir < 0 ? left == 0 : right == this.width) return undefined;
|
|
65
|
+
return this.map[top * this.width + (dir < 0 ? left - 1 : right)];
|
|
66
|
+
} else {
|
|
67
|
+
if (dir < 0 ? top == 0 : bottom == this.height) return undefined;
|
|
68
|
+
return this.map[left + this.width * (dir < 0 ? top - 1 : bottom)];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// :: (number, number) → Rect
|
|
73
|
+
// Get the rectangle spanning the two given cells.
|
|
74
|
+
rectBetween(a: number, b: number): Rect {
|
|
75
|
+
const { left: leftA, right: rightA, top: topA, bottom: bottomA } = this.findCell(a);
|
|
76
|
+
const { left: leftB, right: rightB, top: topB, bottom: bottomB } = this.findCell(b);
|
|
77
|
+
return new Rect(
|
|
78
|
+
Math.min(leftA, leftB),
|
|
79
|
+
Math.min(topA, topB),
|
|
80
|
+
Math.max(rightA, rightB),
|
|
81
|
+
Math.max(bottomA, bottomB),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// :: (Rect) → [number]
|
|
86
|
+
// Return the position of all cells that have the top left corner in
|
|
87
|
+
// the given rectangle.
|
|
88
|
+
cellsInRect(rect: Rect): number[] {
|
|
89
|
+
const result: number[] = [],
|
|
90
|
+
seen: Record<number, boolean> = {};
|
|
91
|
+
for (let row = rect.top; row < rect.bottom; row++) {
|
|
92
|
+
for (let col = rect.left; col < rect.right; col++) {
|
|
93
|
+
const index = row * this.width + col,
|
|
94
|
+
pos = this.map[index] as number;
|
|
95
|
+
if (seen[pos]) continue;
|
|
96
|
+
seen[pos] = true;
|
|
97
|
+
if (
|
|
98
|
+
(col != rect.left || !col || this.map[index - 1] != pos) &&
|
|
99
|
+
(row != rect.top || !row || this.map[index - this.width] != pos)
|
|
100
|
+
)
|
|
101
|
+
result.push(pos);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// :: (number, number, Node) → number
|
|
108
|
+
// Return the position at which the cell at the given row and column
|
|
109
|
+
// starts, or would start, if a cell started there.
|
|
110
|
+
positionAt(row: number, col: number, table: Node): number {
|
|
111
|
+
for (let i = 0, rowStart = 0; ; i++) {
|
|
112
|
+
const rowEnd = rowStart + table.child(i).nodeSize;
|
|
113
|
+
if (i == row) {
|
|
114
|
+
let index = col + row * this.width;
|
|
115
|
+
const rowEndIndex = (row + 1) * this.width;
|
|
116
|
+
// Skip past cells from previous rows (via rowspan)
|
|
117
|
+
while (index < rowEndIndex && (this.map[index] as number) < rowStart) index++;
|
|
118
|
+
return index == rowEndIndex ? rowEnd - 1 : (this.map[index] as number);
|
|
119
|
+
}
|
|
120
|
+
rowStart = rowEnd;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Find the table map for the given table node.
|
|
125
|
+
static get(table: Node): TableMap {
|
|
126
|
+
return readFromCache(table) || addToCache(table, computeMap(table));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type Problem =
|
|
131
|
+
| {
|
|
132
|
+
type: 'missing';
|
|
133
|
+
row: number;
|
|
134
|
+
n: number;
|
|
135
|
+
}
|
|
136
|
+
| {
|
|
137
|
+
type: 'overlong_rowspan';
|
|
138
|
+
pos: number;
|
|
139
|
+
n: number;
|
|
140
|
+
}
|
|
141
|
+
| {
|
|
142
|
+
type: 'collision';
|
|
143
|
+
row: number;
|
|
144
|
+
pos: number;
|
|
145
|
+
n: number;
|
|
146
|
+
}
|
|
147
|
+
| {
|
|
148
|
+
type: 'colwidth mismatch';
|
|
149
|
+
pos: number;
|
|
150
|
+
colwidth: boolean;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Compute a table map.
|
|
154
|
+
function computeMap(table: Node) {
|
|
155
|
+
if (table.type.spec['tableRole'] != 'table') throw new RangeError('Not a table node: ' + table.type.name);
|
|
156
|
+
const width = findWidth(table),
|
|
157
|
+
height = table.childCount;
|
|
158
|
+
const map: number[] = [];
|
|
159
|
+
const colWidths: number[] = [];
|
|
160
|
+
let mapPos = 0,
|
|
161
|
+
problems: Problem[] | undefined = undefined;
|
|
162
|
+
for (let i = 0, e = width * height; i < e; i++) map[i] = 0;
|
|
163
|
+
|
|
164
|
+
for (let row = 0, pos = 0; row < height; row++) {
|
|
165
|
+
const rowNode = table.child(row);
|
|
166
|
+
pos++;
|
|
167
|
+
for (let i = 0; ; i++) {
|
|
168
|
+
while (mapPos < map.length && map[mapPos] != 0) mapPos++;
|
|
169
|
+
if (i == rowNode.childCount) break;
|
|
170
|
+
const cellNode = rowNode.child(i),
|
|
171
|
+
{ colspan, rowspan, colwidth } = cellNode.attrs;
|
|
172
|
+
for (let h = 0; h < rowspan; h++) {
|
|
173
|
+
if (h + row >= height) {
|
|
174
|
+
(problems || (problems = [])).push({
|
|
175
|
+
type: 'overlong_rowspan',
|
|
176
|
+
pos,
|
|
177
|
+
n: rowspan - h,
|
|
178
|
+
});
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
const start = mapPos + h * width;
|
|
182
|
+
for (let w = 0; w < colspan; w++) {
|
|
183
|
+
if (map[start + w] == 0) map[start + w] = pos;
|
|
184
|
+
else
|
|
185
|
+
(problems || (problems = [])).push({
|
|
186
|
+
type: 'collision',
|
|
187
|
+
row,
|
|
188
|
+
pos,
|
|
189
|
+
n: colspan - w,
|
|
190
|
+
});
|
|
191
|
+
const colW = colwidth && colwidth[w];
|
|
192
|
+
if (colW) {
|
|
193
|
+
const widthIndex = ((start + w) % width) * 2,
|
|
194
|
+
prev = colWidths[widthIndex];
|
|
195
|
+
if (prev == null || (prev != colW && colWidths[widthIndex + 1] == 1)) {
|
|
196
|
+
colWidths[widthIndex] = colW;
|
|
197
|
+
colWidths[widthIndex + 1] = 1;
|
|
198
|
+
} else if (prev == colW) {
|
|
199
|
+
colWidths[widthIndex + 1]++;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
mapPos += colspan;
|
|
205
|
+
pos += cellNode.nodeSize;
|
|
206
|
+
}
|
|
207
|
+
const expectedPos = (row + 1) * width;
|
|
208
|
+
let missing = 0;
|
|
209
|
+
while (mapPos < expectedPos) if (map[mapPos++] == 0) missing++;
|
|
210
|
+
if (missing) (problems || (problems = [])).push({ type: 'missing', row, n: missing });
|
|
211
|
+
pos++;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const tableMap = new TableMap(width, height, map, problems);
|
|
215
|
+
let badWidths = false;
|
|
216
|
+
|
|
217
|
+
// For columns that have defined widths, but whose widths disagree
|
|
218
|
+
// between rows, fix up the cells whose width doesn't match the
|
|
219
|
+
// computed one.
|
|
220
|
+
for (let i = 0; !badWidths && i < colWidths.length; i += 2)
|
|
221
|
+
if (colWidths[i] != null && (colWidths[i + 1] as number) < height) badWidths = true;
|
|
222
|
+
if (badWidths) findBadColWidths(tableMap, colWidths, table);
|
|
223
|
+
|
|
224
|
+
return tableMap;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function findWidth(table: Node) {
|
|
228
|
+
let width = -1,
|
|
229
|
+
hasRowSpan = false;
|
|
230
|
+
for (let row = 0; row < table.childCount; row++) {
|
|
231
|
+
const rowNode = table.child(row);
|
|
232
|
+
let rowWidth = 0;
|
|
233
|
+
if (hasRowSpan)
|
|
234
|
+
for (let j = 0; j < row; j++) {
|
|
235
|
+
const prevRow = table.child(j);
|
|
236
|
+
for (let i = 0; i < prevRow.childCount; i++) {
|
|
237
|
+
const cell = prevRow.child(i);
|
|
238
|
+
if (j + cell.attrs['rowspan'] > row) rowWidth += cell.attrs['colspan'];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
for (let i = 0; i < rowNode.childCount; i++) {
|
|
242
|
+
const cell = rowNode.child(i);
|
|
243
|
+
rowWidth += cell.attrs['colspan'];
|
|
244
|
+
if (cell.attrs['rowspan'] > 1) hasRowSpan = true;
|
|
245
|
+
}
|
|
246
|
+
if (width == -1) width = rowWidth;
|
|
247
|
+
else if (width != rowWidth) width = Math.max(width, rowWidth);
|
|
248
|
+
}
|
|
249
|
+
return width;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function findBadColWidths(map: TableMap, colWidths: number[], table: Node) {
|
|
253
|
+
if (!map.problems) map.problems = [];
|
|
254
|
+
for (let i = 0, seen: Record<number, boolean> = {}; i < map.map.length; i++) {
|
|
255
|
+
const pos = map.map[i] as number;
|
|
256
|
+
if (seen[pos]) continue;
|
|
257
|
+
seen[pos] = true;
|
|
258
|
+
const node = table.nodeAt(pos) as Node;
|
|
259
|
+
let updated = null;
|
|
260
|
+
for (let j = 0; j < node.attrs['colspan']; j++) {
|
|
261
|
+
const col = (i + j) % map.width,
|
|
262
|
+
colWidth = colWidths[col * 2];
|
|
263
|
+
if (colWidth != null && (!node.attrs['colwidth'] || node.attrs['colwidth'][j] != colWidth))
|
|
264
|
+
(updated || (updated = freshColWidth(node.attrs)))[j] = colWidth;
|
|
265
|
+
}
|
|
266
|
+
if (updated)
|
|
267
|
+
map.problems.unshift({
|
|
268
|
+
type: 'colwidth mismatch',
|
|
269
|
+
pos,
|
|
270
|
+
colwidth: updated,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function freshColWidth(attrs: Attrs) {
|
|
276
|
+
if (attrs['colwidth']) return attrs['colwidth'].slice();
|
|
277
|
+
const result = [];
|
|
278
|
+
for (let i = 0; i < attrs['colspan']; i++) result.push(0);
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/* Copyright 2021, Milkdown by Mirone. */
|
|
2
|
+
import { Node } from '@milkdown/prose/model';
|
|
3
|
+
import { NodeView } from '@milkdown/prose/view';
|
|
4
|
+
|
|
5
|
+
/* Copyright 2021, Milkdown by Mirone. */
|
|
6
|
+
export class TableView implements NodeView {
|
|
7
|
+
public dom: HTMLElement;
|
|
8
|
+
public contentDOM: HTMLElement;
|
|
9
|
+
public table: HTMLTableElement;
|
|
10
|
+
public colgroup: HTMLTableColElement;
|
|
11
|
+
|
|
12
|
+
constructor(public node: Node, public cellMinWidth: number) {
|
|
13
|
+
this.node = node;
|
|
14
|
+
this.cellMinWidth = cellMinWidth;
|
|
15
|
+
this.dom = document.createElement('div');
|
|
16
|
+
this.dom.className = 'tableWrapper';
|
|
17
|
+
this.table = this.dom.appendChild(document.createElement('table'));
|
|
18
|
+
this.colgroup = this.table.appendChild(document.createElement('colgroup'));
|
|
19
|
+
updateColumns(node, this.colgroup, this.table, cellMinWidth);
|
|
20
|
+
this.contentDOM = this.table.appendChild(document.createElement('tbody'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
update(node: Node) {
|
|
24
|
+
if (node.type != this.node.type) return false;
|
|
25
|
+
this.node = node;
|
|
26
|
+
updateColumns(node, this.colgroup, this.table, this.cellMinWidth);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
ignoreMutation(record: MutationRecord) {
|
|
31
|
+
return record.type == 'attributes' && (record.target == this.table || this.colgroup.contains(record.target));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function updateColumns(
|
|
36
|
+
node: Node,
|
|
37
|
+
colgroup: HTMLTableColElement,
|
|
38
|
+
table: HTMLTableElement,
|
|
39
|
+
cellMinWidth: number,
|
|
40
|
+
overrideCol?: number,
|
|
41
|
+
overrideValue?: number,
|
|
42
|
+
) {
|
|
43
|
+
let totalWidth = 0,
|
|
44
|
+
fixedWidth = true;
|
|
45
|
+
let nextDOM = colgroup.firstChild;
|
|
46
|
+
const row = node.firstChild as Node;
|
|
47
|
+
for (let i = 0, col = 0; i < row.childCount; i++) {
|
|
48
|
+
const { colspan, colwidth } = row.child(i).attrs;
|
|
49
|
+
for (let j = 0; j < colspan; j++, col++) {
|
|
50
|
+
const hasWidth = overrideCol == col ? overrideValue : colwidth && colwidth[j];
|
|
51
|
+
const cssWidth = hasWidth ? hasWidth + 'px' : '';
|
|
52
|
+
totalWidth += hasWidth || cellMinWidth;
|
|
53
|
+
if (!hasWidth) fixedWidth = false;
|
|
54
|
+
if (!nextDOM) {
|
|
55
|
+
colgroup.appendChild(document.createElement('col')).style.width = cssWidth;
|
|
56
|
+
} else {
|
|
57
|
+
if ((nextDOM as HTMLElement).style.width != cssWidth) (nextDOM as HTMLElement).style.width = cssWidth;
|
|
58
|
+
nextDOM = nextDOM.nextSibling;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
while (nextDOM) {
|
|
64
|
+
const after = nextDOM.nextSibling;
|
|
65
|
+
nextDOM.parentNode?.removeChild(nextDOM);
|
|
66
|
+
nextDOM = after;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (fixedWidth) {
|
|
70
|
+
table.style.width = totalWidth + 'px';
|
|
71
|
+
table.style.minWidth = '';
|
|
72
|
+
} else {
|
|
73
|
+
table.style.width = '';
|
|
74
|
+
table.style.minWidth = totalWidth + 'px';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* Copyright 2021, Milkdown by Mirone. */
|
|
2
|
+
|
|
3
|
+
export type getFromDOM<T> = (dom: Element) => T;
|
|
4
|
+
export type setDOMAttr = <Value>(value: Value, attrs: Record<string, unknown>) => void;
|
|
5
|
+
|
|
6
|
+
export interface CellAttributes<T = unknown> {
|
|
7
|
+
default: T;
|
|
8
|
+
getFromDOM?: getFromDOM<T>;
|
|
9
|
+
setDOMAttr?: setDOMAttr;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TableNodesOptions {
|
|
13
|
+
tableGroup?: string;
|
|
14
|
+
cellContent: string;
|
|
15
|
+
cellAttributes: { [key: string]: CellAttributes };
|
|
16
|
+
}
|