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.

261 lines
8.1 KiB

// This file defines a number of helpers for wiring up user input to
// table-related functionality.
import { Slice, Fragment } from 'prosemirror-model';
import { Selection, TextSelection } from 'prosemirror-state';
import { keydownHandler } from 'prosemirror-keymap';
import {
key,
nextCell,
cellAround,
inSameTable,
isInTable,
selectionCell,
} from './util';
import { CellSelection } from './cellselection';
import { TableMap } from './tablemap';
import { pastedCells, fitSlice, clipCells, insertCells } from './copypaste';
import { tableNodeTypes } from './schema';
export const handleKeyDown = keydownHandler({
ArrowLeft: arrow('horiz', -1),
ArrowRight: arrow('horiz', 1),
ArrowUp: arrow('vert', -1),
ArrowDown: arrow('vert', 1),
'Shift-ArrowLeft': shiftArrow('horiz', -1),
'Shift-ArrowRight': shiftArrow('horiz', 1),
'Shift-ArrowUp': shiftArrow('vert', -1),
'Shift-ArrowDown': shiftArrow('vert', 1),
Backspace: deleteCellSelection,
'Mod-Backspace': deleteCellSelection,
Delete: deleteCellSelection,
'Mod-Delete': deleteCellSelection,
});
function maybeSetSelection(state, dispatch, selection) {
if (selection.eq(state.selection)) return false;
if (dispatch) dispatch(state.tr.setSelection(selection).scrollIntoView());
return true;
}
function arrow(axis, dir) {
return (state, dispatch, view) => {
let sel = state.selection;
if (sel instanceof CellSelection) {
return maybeSetSelection(
state,
dispatch,
Selection.near(sel.$headCell, dir),
);
}
if (axis != 'horiz' && !sel.empty) return false;
let end = atEndOfCell(view, axis, dir);
if (end == null) return false;
if (axis == 'horiz') {
return maybeSetSelection(
state,
dispatch,
Selection.near(state.doc.resolve(sel.head + dir), dir),
);
} else {
let $cell = state.doc.resolve(end),
$next = nextCell($cell, axis, dir),
newSel;
if ($next) newSel = Selection.near($next, 1);
else if (dir < 0)
newSel = Selection.near(state.doc.resolve($cell.before(-1)), -1);
else newSel = Selection.near(state.doc.resolve($cell.after(-1)), 1);
return maybeSetSelection(state, dispatch, newSel);
}
};
}
function shiftArrow(axis, dir) {
return (state, dispatch, view) => {
let sel = state.selection;
if (!(sel instanceof CellSelection)) {
let end = atEndOfCell(view, axis, dir);
if (end == null) return false;
sel = new CellSelection(state.doc.resolve(end));
}
let $head = nextCell(sel.$headCell, axis, dir);
if (!$head) return false;
return maybeSetSelection(
state,
dispatch,
new CellSelection(sel.$anchorCell, $head),
);
};
}
function deleteCellSelection(state, dispatch) {
let sel = state.selection;
if (!(sel instanceof CellSelection)) return false;
if (dispatch) {
let tr = state.tr,
baseContent = tableNodeTypes(state.schema).cell.createAndFill().content;
sel.forEachCell((cell, pos) => {
if (!cell.content.eq(baseContent))
tr.replace(
tr.mapping.map(pos + 1),
tr.mapping.map(pos + cell.nodeSize - 1),
new Slice(baseContent, 0, 0),
);
});
if (tr.docChanged) dispatch(tr);
}
return true;
}
export function handleTripleClick(view, pos) {
let doc = view.state.doc,
$cell = cellAround(doc.resolve(pos));
if (!$cell) return false;
view.dispatch(view.state.tr.setSelection(new CellSelection($cell)));
return true;
}
export function handlePaste(view, _, slice) {
if (!isInTable(view.state)) return false;
let cells = pastedCells(slice),
sel = view.state.selection;
if (sel instanceof CellSelection) {
if (!cells)
cells = {
width: 1,
height: 1,
rows: [
Fragment.from(
fitSlice(tableNodeTypes(view.state.schema).cell, slice),
),
],
};
let table = sel.$anchorCell.node(-1),
start = sel.$anchorCell.start(-1);
let rect = TableMap.get(table).rectBetween(
sel.$anchorCell.pos - start,
sel.$headCell.pos - start,
);
cells = clipCells(cells, rect.right - rect.left, rect.bottom - rect.top);
insertCells(view.state, view.dispatch, start, rect, cells);
return true;
} else if (cells) {
let $cell = selectionCell(view.state),
start = $cell.start(-1);
insertCells(
view.state,
view.dispatch,
start,
TableMap.get($cell.node(-1)).findCell($cell.pos - start),
cells,
);
return true;
} else {
return false;
}
}
export function handleMouseDown(view, startEvent) {
if (startEvent.ctrlKey || startEvent.metaKey) return;
let startDOMCell = domInCell(view, startEvent.target),
$anchor;
if (startEvent.shiftKey && view.state.selection instanceof CellSelection) {
// Adding to an existing cell selection
setCellSelection(view.state.selection.$anchorCell, startEvent);
startEvent.preventDefault();
} else if (
startEvent.shiftKey &&
startDOMCell &&
($anchor = cellAround(view.state.selection.$anchor)) != null &&
cellUnderMouse(view, startEvent).pos != $anchor.pos
) {
// Adding to a selection that starts in another cell (causing a
// cell selection to be created).
setCellSelection($anchor, startEvent);
startEvent.preventDefault();
} else if (!startDOMCell) {
// Not in a cell, let the default behavior happen.
return;
}
// Create and dispatch a cell selection between the given anchor and
// the position under the mouse.
function setCellSelection($anchor, event) {
let $head = cellUnderMouse(view, event);
let starting = key.getState(view.state) == null;
if (!$head || !inSameTable($anchor, $head)) {
if (starting) $head = $anchor;
else return;
}
let selection = new CellSelection($anchor, $head);
if (starting || !view.state.selection.eq(selection)) {
let tr = view.state.tr.setSelection(selection);
if (starting) tr.setMeta(key, $anchor.pos);
view.dispatch(tr);
}
}
// Stop listening to mouse motion events.
function stop() {
view.root.removeEventListener('mouseup', stop);
view.root.removeEventListener('dragstart', stop);
view.root.removeEventListener('mousemove', move);
if (key.getState(view.state) != null)
view.dispatch(view.state.tr.setMeta(key, -1));
}
function move(event) {
let anchor = key.getState(view.state),
$anchor;
if (anchor != null) {
// Continuing an existing cross-cell selection
$anchor = view.state.doc.resolve(anchor);
} else if (domInCell(view, event.target) != startDOMCell) {
// Moving out of the initial cell -- start a new cell selection
$anchor = cellUnderMouse(view, startEvent);
if (!$anchor) return stop();
}
if ($anchor) setCellSelection($anchor, event);
}
view.root.addEventListener('mouseup', stop);
view.root.addEventListener('dragstart', stop);
view.root.addEventListener('mousemove', move);
}
// Check whether the cursor is at the end of a cell (so that further
// motion would move out of the cell)
function atEndOfCell(view, axis, dir) {
if (!(view.state.selection instanceof TextSelection)) return null;
let { $head } = view.state.selection;
for (let d = $head.depth - 1; d >= 0; d--) {
let parent = $head.node(d),
index = dir < 0 ? $head.index(d) : $head.indexAfter(d);
if (index != (dir < 0 ? 0 : parent.childCount)) return null;
if (
parent.type.spec.tableRole == 'cell' ||
parent.type.spec.tableRole == 'header_cell'
) {
let cellPos = $head.before(d);
let dirStr =
axis == 'vert' ? (dir > 0 ? 'down' : 'up') : dir > 0 ? 'right' : 'left';
return view.endOfTextblock(dirStr) ? cellPos : null;
}
}
return null;
}
function domInCell(view, dom) {
for (; dom && dom != view.dom; dom = dom.parentNode)
if (dom.nodeName == 'TD' || dom.nodeName == 'TH') return dom;
}
function cellUnderMouse(view, event) {
let mousePos = view.posAtCoords({ left: event.clientX, top: event.clientY });
if (!mousePos) return null;
return mousePos ? cellAround(view.state.doc.resolve(mousePos.pos)) : null;
}