Add formula "point mode": click/drag a cell to insert its reference
Excel/Numbers-style reference picking: while editing a formula (buffer
starts with "="), a click on any grid cell -- in the same table or a
completely different one -- inserts its reference at the caret instead of
changing the selection; a drag inserts/grows a range reference live as you
drag. A further click/drag before typing anything replaces the just-inserted
reference; typing anything (";" to separate SUM() arguments included, but
not special-cased -- any character ends the window, same as Excel) makes the
next click append a fresh one instead.
- document.ts: whichever editor is open (in-cell editor or formula bar)
registers a small handle (home tableId + read/replace the buffer);
pointClick()/pointDrag() insert or replace a reference into it, qualifying
with `Name!` (quoted if needed) when the clicked table differs from the
editor's own. formulaPointModeActive() gates it on the buffer being a
formula; noteFormulaTyped() (wired to real typing, not the programmatic
inserts) ends the "replace" window.
- TableGrid.vue: onCellMouseDown checks point mode before its normal
selection/edit handling, tracking a drag the same way normal selection
does; dblclick during point mode no longer hijacks into editing that cell.
- FormulaBar.vue: registers on focus, same click/drag targeting works from
there without disturbing the grid's own selection.
Verified interactively against the dev server with Playwright, including a
real bug the store-level tests (a mocked editor handle, no real DOM) missed
entirely: preventDefault() alone kept focus on the editor, but the mousedown
still bubbled to CanvasObject.vue's own handler, which selects/brings the
clicked table to front -- closing the very editor being pointed into. Fixed
with stopPropagation() alongside it. After that fix: same-table and
cross-table click/drag both insert correctly-qualified references, a second
click replaces before typing and appends after, and the live selection/
active object are untouched by a point-mode click.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -415,6 +415,17 @@ function startDrag(mode: 'replace' | 'additive') {
|
||||
}
|
||||
function onCellMouseDown(e: MouseEvent, c: number, r: number) {
|
||||
if (e.button !== 0) return;
|
||||
if (store.formulaPointModeActive()) {
|
||||
e.preventDefault(); // keep focus on the formula editor -- must not blur/commit it
|
||||
// ... and don't let it bubble to the canvas object's own mousedown
|
||||
// (CanvasObject.vue selects/brings-to-front the clicked table on any
|
||||
// mousedown; a point-mode click must stay invisible to that, same as
|
||||
// Excel doesn't change your object selection while you're just picking
|
||||
// a reference for a formula elsewhere).
|
||||
e.stopPropagation();
|
||||
startPointDrag(c, r);
|
||||
return;
|
||||
}
|
||||
if (editing.value) commitEdit();
|
||||
clearFlow();
|
||||
lastPointer = { x: e.clientX, y: e.clientY };
|
||||
@@ -451,6 +462,37 @@ function onDragUp() {
|
||||
window.removeEventListener('blur', onDragUp);
|
||||
}
|
||||
|
||||
// ---- formula "point mode" (click/drag a cell to insert its reference) ---
|
||||
let pointDragStart: { c: number; r: number } | null = null;
|
||||
function startPointDrag(c: number, r: number) {
|
||||
pointDragStart = { c, r };
|
||||
store.pointClick(props.tableId, c, r);
|
||||
window.addEventListener('mousemove', onPointDragMove, true);
|
||||
window.addEventListener('mouseup', onPointDragUp, true);
|
||||
window.addEventListener('blur', onPointDragUp);
|
||||
}
|
||||
function onPointDragMove(e: MouseEvent) {
|
||||
if (!pointDragStart) return;
|
||||
if (e.buttons === 0) {
|
||||
onPointDragUp(); // a mouseup we never saw -- self-heal a stuck drag
|
||||
return;
|
||||
}
|
||||
const hit = cellFromPoint(e.clientX, e.clientY);
|
||||
if (!hit) return;
|
||||
if (hit.c === pointDragStart.c && hit.r === pointDragStart.r) {
|
||||
store.pointClick(props.tableId, hit.c, hit.r);
|
||||
} else {
|
||||
store.pointDrag(props.tableId, pointDragStart.c, pointDragStart.r, hit.c, hit.r);
|
||||
}
|
||||
}
|
||||
function onPointDragUp() {
|
||||
if (!pointDragStart) return;
|
||||
pointDragStart = null;
|
||||
window.removeEventListener('mousemove', onPointDragMove, true);
|
||||
window.removeEventListener('mouseup', onPointDragUp, true);
|
||||
window.removeEventListener('blur', onPointDragUp);
|
||||
}
|
||||
|
||||
// ---- fill handle (drag to copy/fill, Excel/Numbers-style) ---------------
|
||||
/** Small square at the selection's bottom-right corner; dragging it repeats
|
||||
* the selection's cells (values, formulas -- reference-adjusted like a
|
||||
@@ -624,9 +666,35 @@ function resizeEditor() {
|
||||
ta.style.height = Math.min(mirror.offsetHeight, EDIT_MAX_H) + 'px';
|
||||
}
|
||||
|
||||
/** Lets the store insert a reference into this editor when the user clicks
|
||||
* a grid cell mid-formula ("point mode") -- see store.pointClick/pointDrag. */
|
||||
function formulaEditorHandle() {
|
||||
return {
|
||||
tableId: props.tableId,
|
||||
getState: () => {
|
||||
const el = editInput.value;
|
||||
return {
|
||||
text: editValue.value,
|
||||
start: el?.selectionStart ?? editValue.value.length,
|
||||
end: el?.selectionEnd ?? editValue.value.length,
|
||||
};
|
||||
},
|
||||
replace: (start: number, end: number, value: string) => {
|
||||
editValue.value = editValue.value.slice(0, start) + value + editValue.value.slice(end);
|
||||
nextTick(() => {
|
||||
const el = editInput.value;
|
||||
el?.focus();
|
||||
el?.setSelectionRange(start + value.length, start + value.length);
|
||||
resizeEditor();
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function beginEdit(c: number, r: number, initial?: string) {
|
||||
editing.value = { col: c, row: r };
|
||||
editValue.value = initial ?? store.cellInput(props.tableId, c, r);
|
||||
store.registerFormulaEditor(formulaEditorHandle());
|
||||
nextTick(() => {
|
||||
const el = editInput.value;
|
||||
if (!el) return;
|
||||
@@ -642,11 +710,13 @@ function commitEdit() {
|
||||
const { col, row } = editing.value;
|
||||
store.setCellInput(props.tableId, col, row, editValue.value);
|
||||
editing.value = null;
|
||||
store.registerFormulaEditor(null);
|
||||
focusRoot();
|
||||
setTimeout(() => (committing = false), 0);
|
||||
}
|
||||
function cancelEdit() {
|
||||
editing.value = null;
|
||||
store.registerFormulaEditor(null);
|
||||
focusRoot();
|
||||
}
|
||||
function onEditKeydown(e: KeyboardEvent) {
|
||||
@@ -709,6 +779,18 @@ function toggleReferenceLock() {
|
||||
function onEditBlur() {
|
||||
if (!committing && editing.value) commitEdit();
|
||||
}
|
||||
/** Actual typing (not a point-mode click inserting a reference programmatically)
|
||||
* ends the "next click replaces the last reference" window. */
|
||||
function onEditInput() {
|
||||
resizeEditor();
|
||||
store.noteFormulaTyped();
|
||||
}
|
||||
/** Clicking inside the field to reposition the caret should also end that
|
||||
* window (mousedown so it fires before any selection change is read). */
|
||||
function onEditMousedown(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
store.noteFormulaTyped();
|
||||
}
|
||||
|
||||
// ---- keyboard navigation (Excel-like) ------------------------
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
@@ -889,6 +971,7 @@ const totalWidth = computed(() => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
onDragUp();
|
||||
onPointDragUp();
|
||||
onFillDragUp();
|
||||
onResizeUp();
|
||||
onRowResizeUp();
|
||||
@@ -960,7 +1043,7 @@ defineExpose({
|
||||
}"
|
||||
:style="cellStyle(c - 1, r - 1)"
|
||||
@mousedown="onCellMouseDown($event, c - 1, r - 1)"
|
||||
@dblclick="beginEdit(c - 1, r - 1)"
|
||||
@dblclick="!store.formulaPointModeActive() && beginEdit(c - 1, r - 1)"
|
||||
>
|
||||
<div
|
||||
class="cell-content"
|
||||
@@ -998,9 +1081,9 @@ defineExpose({
|
||||
spellcheck="false"
|
||||
rows="1"
|
||||
@keydown="onEditKeydown"
|
||||
@input="resizeEditor"
|
||||
@input="onEditInput"
|
||||
@blur="onEditBlur"
|
||||
@mousedown.stop
|
||||
@mousedown="onEditMousedown"
|
||||
/>
|
||||
<div v-if="editing" ref="editMirrorEl" class="cell-input-mirror" :style="editMirrorStyle" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user