@sheetpane/grid 0.4.1 → 0.5.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/dist/excel/element.d.ts +44 -0
- package/dist/excel/element.d.ts.map +1 -1
- package/dist/excel/element.js +551 -41
- package/dist/excel/element.js.map +1 -1
- package/dist/excel/fill.d.ts +38 -0
- package/dist/excel/fill.d.ts.map +1 -0
- package/dist/excel/fill.js +78 -0
- package/dist/excel/fill.js.map +1 -0
- package/dist/excel/float.d.ts +23 -0
- package/dist/excel/float.d.ts.map +1 -0
- package/dist/excel/float.js +82 -0
- package/dist/excel/float.js.map +1 -0
- package/dist/excel/geometry.d.ts +7 -1
- package/dist/excel/geometry.d.ts.map +1 -1
- package/dist/excel/geometry.js +4 -3
- package/dist/excel/geometry.js.map +1 -1
- package/dist/excel/styles.d.ts.map +1 -1
- package/dist/excel/styles.js +16 -0
- package/dist/excel/styles.js.map +1 -1
- package/dist/llms-full.txt +111 -69
- package/dist/llms.txt +92 -92
- package/dist/web-types.json +1 -1
- package/package.json +4 -4
package/dist/excel/element.js
CHANGED
|
@@ -13,6 +13,8 @@ import { pluginsFromIds } from '../plugins/host.js';
|
|
|
13
13
|
import { COL_MAX, COL_MIN, COL_VIRTUAL_AFTER, DEFAULT_COL_PX, HEAD_H, ROWHEAD_W, ROW_H, ROW_MAX, ROW_MIN, colAt, colWidth, hiddenSpans, isHiddenAt, mergeAt, rowAt, rowHeight, rowTop, sheetGeometry, visibleWindow } from './geometry.js';
|
|
14
14
|
import { cellCount, clipOf, dataEdge, growForMerges, rangeText, refLabel, spanOf, splitByPanes, totalsOf, usedRange } from './selection.js';
|
|
15
15
|
import { EXCEL_CSS } from './styles.js';
|
|
16
|
+
import { fillDownTo, fillSeries } from './fill.js';
|
|
17
|
+
import { floatAll } from './float.js';
|
|
16
18
|
import { parseTarget, readLink, splitSheetRef, writeLink } from './link.js';
|
|
17
19
|
const ZOOMS = [0.75, 1, 1.25, 1.5];
|
|
18
20
|
const MAX_BYTES = 50 * 2 ** 20;
|
|
@@ -147,6 +149,18 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
147
149
|
/** A drag in progress (W-46): drawn by the selection layer each frame; the store hears of it on release. */
|
|
148
150
|
drag;
|
|
149
151
|
dragRaf = 0;
|
|
152
|
+
/** The fill handle's drag: the source, the range it now reaches, the pointer, the last cell's value. */
|
|
153
|
+
fill;
|
|
154
|
+
fillRaf = 0;
|
|
155
|
+
/** The in-cell editor (as in Excel): it sits on the active cell; the cell bar's input holds the same draft. */
|
|
156
|
+
ice;
|
|
157
|
+
/** `enter`: started by typing — an arrow commits and moves; `edit`: Enter, F2 or a double-click — arrows move the caret. */
|
|
158
|
+
iceMode;
|
|
159
|
+
/** Where a run of Tabs began: Enter then returns to this column, a row down (Excel). */
|
|
160
|
+
tabCol;
|
|
161
|
+
dropDepth = 0;
|
|
162
|
+
/** The last mouse press on a cell, to tell a double press (edit in place) from two clicks. */
|
|
163
|
+
lastDown;
|
|
150
164
|
/** The last pointer was a finger: a range shows its two handles. */
|
|
151
165
|
touchUi = false;
|
|
152
166
|
/** The range Ctrl/⌘+C copied, outlined until Esc, an edit or the next copy. */
|
|
@@ -157,6 +171,8 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
157
171
|
totalsKey = [];
|
|
158
172
|
/** Column widths and row heights the user set, per sheet, px at 100 % — a view setting, never a change (W-47). */
|
|
159
173
|
_sizes = {};
|
|
174
|
+
/** Per sheet, the least the grid shows after inserts and deletes (SR-GRID 11e): an insert always adds what it says it does. */
|
|
175
|
+
extent = {};
|
|
160
176
|
sizesVer = 0;
|
|
161
177
|
/** A header-edge drag in progress (W-47). */
|
|
162
178
|
rz;
|
|
@@ -535,15 +551,22 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
535
551
|
if (rsrc && this.rules === undefined)
|
|
536
552
|
void this.loadRules(rsrc);
|
|
537
553
|
addEventListener('beforeunload', this.onUnload);
|
|
554
|
+
addEventListener('scroll', this.onViewport, true);
|
|
555
|
+
addEventListener('resize', this.onViewport);
|
|
538
556
|
addEventListener('hashchange', this.onHash);
|
|
539
557
|
addEventListener('popstate', this.onHash);
|
|
540
558
|
this.seenHash = location.hash;
|
|
541
559
|
this.place();
|
|
542
560
|
this.schedule();
|
|
543
561
|
}
|
|
562
|
+
/** The page scrolled or resized: the cards in the top layer follow their anchors (SR-LAW-13). */
|
|
563
|
+
onViewport = () => { if (this.shadowRoot?.querySelector('.sp-float'))
|
|
564
|
+
floatAll(this.shadowRoot); };
|
|
544
565
|
/** Releases the pane's store. */
|
|
545
566
|
disconnectedCallback() {
|
|
546
567
|
removeEventListener('beforeunload', this.onUnload);
|
|
568
|
+
removeEventListener('scroll', this.onViewport, true);
|
|
569
|
+
removeEventListener('resize', this.onViewport);
|
|
547
570
|
removeEventListener('hashchange', this.onHash);
|
|
548
571
|
removeEventListener('popstate', this.onHash);
|
|
549
572
|
clearTimeout(this.linkTimer);
|
|
@@ -696,6 +719,7 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
696
719
|
}
|
|
697
720
|
if (e.type === 'load') {
|
|
698
721
|
this._sizes = {};
|
|
722
|
+
this.extent = {};
|
|
699
723
|
this.sizesVer++;
|
|
700
724
|
this.rz = undefined;
|
|
701
725
|
this.hdrMenu = undefined;
|
|
@@ -902,28 +926,30 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
902
926
|
if (typeof ResizeObserver !== 'undefined')
|
|
903
927
|
new ResizeObserver(() => this.fitDock()).observe(q.dock.parentElement);
|
|
904
928
|
// the grid's focus ring shows for keyboard use only
|
|
905
|
-
q.root.addEventListener('keydown', () =>
|
|
929
|
+
q.root.addEventListener('keydown', (e) => { if (e.key === 'Tab')
|
|
930
|
+
q.root.setAttribute('data-kbd', '');
|
|
931
|
+
else if (e.key.length === 1 || e.key === 'Enter' || e.key === 'Escape')
|
|
932
|
+
q.root.removeAttribute('data-kbd'); }, true);
|
|
906
933
|
q.root.addEventListener('pointerdown', () => q.root.removeAttribute('data-kbd'), true);
|
|
907
934
|
q.dock.addEventListener('keydown', (e) => this.onMenuKey(e, '.moremenu'));
|
|
908
935
|
q.help.addEventListener('click', () => { this.helpOpen = !this.helpOpen; q.help.setAttribute('aria-expanded', String(this.helpOpen)); this.schedule(); });
|
|
936
|
+
// Esc closes the keyboard help from its button or inside it; the grid's own Esc handles the grid
|
|
937
|
+
q.root.addEventListener('keydown', (e) => {
|
|
938
|
+
if (e.key !== 'Escape' || !this.helpOpen || e.target.closest?.('.grid, .ice'))
|
|
939
|
+
return;
|
|
940
|
+
this.helpOpen = false;
|
|
941
|
+
q.help.setAttribute('aria-expanded', 'false');
|
|
942
|
+
q.help.focus();
|
|
943
|
+
this.schedule();
|
|
944
|
+
});
|
|
909
945
|
q.revert.addEventListener('click', () => { const sel = s().getSnapshot().selection; s().revert(sel.sheet, sel.ref, this); });
|
|
910
946
|
// The cell bar edits a draft; Enter, Tab and blur commit it (W-26, events verdict decisions 1–3).
|
|
911
947
|
const cell = q.cell;
|
|
912
|
-
cell.addEventListener('focus', () => {
|
|
913
|
-
|
|
914
|
-
const rich = !!s().runs(sel.sheet, sel.ref); // W-53: the edit writes plain text, the runs' formatting goes
|
|
915
|
-
const allowed = fire(this, 'sheetpane-editstart', { sheet: sel.sheet, ref: sel.ref, column: this.column(sel.sheet, sel.ref), value: s().value(sel.sheet, sel.ref), ...(rich ? { richText: true } : {}) }, true);
|
|
916
|
-
if (!allowed) {
|
|
917
|
-
cell.readOnly = true;
|
|
918
|
-
this.editing = undefined;
|
|
919
|
-
queueMicrotask(() => q.grid.focus({ preventScroll: true }));
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
cell.readOnly = false;
|
|
923
|
-
this.editing = { sheet: sel.sheet, ref: sel.ref, start: cell.value };
|
|
924
|
-
this.schedule(); // the docked actions step aside while the draft is edited
|
|
925
|
-
});
|
|
948
|
+
cell.addEventListener('focus', () => { if (!this.beginEdit())
|
|
949
|
+
queueMicrotask(() => q.grid.focus({ preventScroll: true })); });
|
|
926
950
|
cell.addEventListener('input', () => {
|
|
951
|
+
if (this.iceOpen() && this.shadow.activeElement === cell)
|
|
952
|
+
this.ice.value = cell.value; // typed in the bar: the cell's editor follows
|
|
927
953
|
const ed = this.editing;
|
|
928
954
|
if (!ed)
|
|
929
955
|
return;
|
|
@@ -931,26 +957,73 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
931
957
|
this.checkDraft(ed.sheet, ed.ref, cell.value);
|
|
932
958
|
});
|
|
933
959
|
cell.addEventListener('keydown', (e) => {
|
|
934
|
-
if (e.key === 'Enter'
|
|
960
|
+
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
935
961
|
e.preventDefault();
|
|
936
|
-
this.
|
|
937
|
-
|
|
938
|
-
|
|
962
|
+
this.fillRangeWithDraft();
|
|
963
|
+
}
|
|
964
|
+
else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
965
|
+
e.preventDefault();
|
|
966
|
+
this.finishEdit(e.key === 'Tab' ? 'tab' : 'enter', e.shiftKey);
|
|
939
967
|
}
|
|
940
968
|
else if (e.key === 'Escape') {
|
|
941
969
|
e.preventDefault();
|
|
942
970
|
e.stopPropagation();
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
this.editing = undefined;
|
|
947
|
-
this.clearRuleMsg();
|
|
948
|
-
fire(this, 'sheetpane-editcancel', { sheet: ed.sheet, ref: ed.ref });
|
|
949
|
-
}
|
|
971
|
+
if (this.iceOpen())
|
|
972
|
+
this.ice.hidden = true;
|
|
973
|
+
this.cancelDraft();
|
|
950
974
|
q.grid.focus({ preventScroll: true });
|
|
951
975
|
}
|
|
952
976
|
});
|
|
953
|
-
cell.addEventListener('blur', () => {
|
|
977
|
+
cell.addEventListener('blur', (e) => {
|
|
978
|
+
if (e.relatedTarget === this.ice && this.iceOpen())
|
|
979
|
+
return; // the draft moves back into the cell's editor
|
|
980
|
+
if (this.iceOpen()) {
|
|
981
|
+
this.ice.hidden = true;
|
|
982
|
+
this.iceMode = undefined;
|
|
983
|
+
}
|
|
984
|
+
this.commitDraft();
|
|
985
|
+
this.schedule();
|
|
986
|
+
});
|
|
987
|
+
// the in-cell editor: one draft with the cell bar; typing, Enter, F2 or a double-click open it
|
|
988
|
+
const ice = this.ice = document.createElement('input');
|
|
989
|
+
ice.className = 'ice';
|
|
990
|
+
ice.hidden = true;
|
|
991
|
+
ice.autocomplete = 'off';
|
|
992
|
+
ice.spellcheck = false;
|
|
993
|
+
ice.setAttribute('aria-label', 'Cell value');
|
|
994
|
+
ice.dataset.sp = 'cell-editor';
|
|
995
|
+
q.sheet.append(ice);
|
|
996
|
+
for (const ev of ['pointerdown', 'mousedown', 'dblclick', 'click', 'contextmenu'])
|
|
997
|
+
ice.addEventListener(ev, (e) => e.stopPropagation()); // inside the editor a click moves the caret, never the selection
|
|
998
|
+
ice.addEventListener('input', () => { cell.value = ice.value; cell.dispatchEvent(new Event('input')); });
|
|
999
|
+
ice.addEventListener('keydown', (e) => this.onIceKey(e));
|
|
1000
|
+
ice.addEventListener('blur', (e) => {
|
|
1001
|
+
if (ice.hidden || e.relatedTarget === cell)
|
|
1002
|
+
return; // into the cell bar: the same draft carries on there
|
|
1003
|
+
ice.hidden = true;
|
|
1004
|
+
this.iceMode = undefined;
|
|
1005
|
+
this.commitDraft();
|
|
1006
|
+
this.schedule();
|
|
1007
|
+
});
|
|
1008
|
+
// a file dropped anywhere on a loaded sheet opens it — the empty state has its own drop zone
|
|
1009
|
+
const files = (e) => !!e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files') && !q.scroller.hidden;
|
|
1010
|
+
q.root.addEventListener('dragenter', (e) => { if (!files(e))
|
|
1011
|
+
return; e.preventDefault(); this.dropDepth++; this.showDrop(true); });
|
|
1012
|
+
q.root.addEventListener('dragover', (e) => { if (!files(e))
|
|
1013
|
+
return; e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; });
|
|
1014
|
+
q.root.addEventListener('dragleave', (e) => { if (!files(e))
|
|
1015
|
+
return; this.dropDepth = Math.max(0, this.dropDepth - 1); if (!this.dropDepth)
|
|
1016
|
+
this.showDrop(false); });
|
|
1017
|
+
q.root.addEventListener('drop', (e) => {
|
|
1018
|
+
if (!files(e))
|
|
1019
|
+
return;
|
|
1020
|
+
e.preventDefault();
|
|
1021
|
+
this.dropDepth = 0;
|
|
1022
|
+
this.showDrop(false);
|
|
1023
|
+
const f = e.dataTransfer?.files?.[0];
|
|
1024
|
+
if (f)
|
|
1025
|
+
void this.load(f);
|
|
1026
|
+
});
|
|
954
1027
|
q.grid.addEventListener('paste', (e) => this.onPaste(e));
|
|
955
1028
|
q.tabs.addEventListener('click', (e) => {
|
|
956
1029
|
const t = e.target;
|
|
@@ -1020,7 +1093,12 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
1020
1093
|
// W-46: mouse and pen drag with Pointer Events (captured, so the drag outlives the grid); a finger
|
|
1021
1094
|
// pans, and its tap arrives as the compatibility mousedown — as do synthetic clicks
|
|
1022
1095
|
q.scroller.addEventListener('pointerdown', (e) => this.onPointerDown(e));
|
|
1023
|
-
q.scroller.addEventListener('pointermove', (e) => { if (this.
|
|
1096
|
+
q.scroller.addEventListener('pointermove', (e) => { if (this.fill && e.pointerId === this.fill.id) {
|
|
1097
|
+
this.fill.x = e.clientX;
|
|
1098
|
+
this.fill.y = e.clientY;
|
|
1099
|
+
this.queueFill();
|
|
1100
|
+
return;
|
|
1101
|
+
} if (this.rz) {
|
|
1024
1102
|
this.moveResize(e);
|
|
1025
1103
|
return;
|
|
1026
1104
|
} if (this.drag && e.pointerId === this.drag.id) {
|
|
@@ -1028,17 +1106,26 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
1028
1106
|
this.drag.y = e.clientY;
|
|
1029
1107
|
this.queueDrag();
|
|
1030
1108
|
} });
|
|
1031
|
-
q.scroller.addEventListener('pointerup', (e) => { if (this.
|
|
1109
|
+
q.scroller.addEventListener('pointerup', (e) => { if (this.fill && e.pointerId === this.fill.id) {
|
|
1110
|
+
this.endFill(true, e.ctrlKey || e.metaKey);
|
|
1111
|
+
return;
|
|
1112
|
+
} if (this.rz && e.pointerId === this.rz.id) {
|
|
1032
1113
|
this.endResize(true);
|
|
1033
1114
|
return;
|
|
1034
1115
|
} if (this.drag && e.pointerId === this.drag.id)
|
|
1035
1116
|
this.endDrag(true); });
|
|
1036
|
-
q.scroller.addEventListener('pointercancel', (e) => { if (this.
|
|
1117
|
+
q.scroller.addEventListener('pointercancel', (e) => { if (this.fill && e.pointerId === this.fill.id) {
|
|
1118
|
+
this.endFill(false, false);
|
|
1119
|
+
return;
|
|
1120
|
+
} if (this.rz && e.pointerId === this.rz.id) {
|
|
1037
1121
|
this.endResize(false);
|
|
1038
1122
|
return;
|
|
1039
1123
|
} if (this.drag && e.pointerId === this.drag.id)
|
|
1040
1124
|
this.endDrag(false); });
|
|
1041
|
-
q.scroller.addEventListener('lostpointercapture', (e) => { if (this.
|
|
1125
|
+
q.scroller.addEventListener('lostpointercapture', (e) => { if (this.fill && e.pointerId === this.fill.id) {
|
|
1126
|
+
this.endFill(true, false);
|
|
1127
|
+
return;
|
|
1128
|
+
} if (this.rz && e.pointerId === this.rz.id) {
|
|
1042
1129
|
this.endResize(true);
|
|
1043
1130
|
return;
|
|
1044
1131
|
} if (this.drag && e.pointerId === this.drag.id)
|
|
@@ -1095,10 +1182,17 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
1095
1182
|
if (!c || c.dataset.r === undefined)
|
|
1096
1183
|
return;
|
|
1097
1184
|
e.preventDefault();
|
|
1098
|
-
|
|
1185
|
+
// a right-click inside the range keeps it (Copy values, Clear act on all of it), as in Excel
|
|
1186
|
+
const rr = Number(c.dataset.r), cc = Number(c.dataset.c), cur = s().getSnapshot().selection.range, inR = cur ? parseRange(cur) : undefined;
|
|
1187
|
+
if (!(inR && rr >= inR.top && rr <= inR.bottom && cc >= inR.left && cc <= inR.right))
|
|
1188
|
+
this.selectCell(rr, cc, false);
|
|
1099
1189
|
this.openContextMenu();
|
|
1100
1190
|
});
|
|
1101
1191
|
q.scroller.addEventListener('dblclick', (e) => {
|
|
1192
|
+
if (e.target.closest('.fh')) {
|
|
1193
|
+
this.autoFill();
|
|
1194
|
+
return;
|
|
1195
|
+
} // W: fill down beside the data, as Excel
|
|
1102
1196
|
const edge = e.target.closest('[data-rz-c], [data-rz-r]');
|
|
1103
1197
|
if (edge) {
|
|
1104
1198
|
const axis = edge.dataset.rzC !== undefined ? 'col' : 'row';
|
|
@@ -1429,8 +1523,8 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
1429
1523
|
}
|
|
1430
1524
|
return out.join('');
|
|
1431
1525
|
}
|
|
1432
|
-
contextMenuHtml(x, y, edit, err) {
|
|
1433
|
-
return `<div class="card ctxmenu" role="menu" aria-label="Selection actions" data-sp="menu" style="top:${y}px;left:${x}px">${this.actionItems(edit, err, { flag: true, plugins: true })}</div>`;
|
|
1526
|
+
contextMenuHtml(x, y, edit, err, anchor = 0) {
|
|
1527
|
+
return `<div class="card ctxmenu" role="menu" aria-label="Selection actions" data-sp="menu" data-anchor-h="${anchor}" style="top:${y}px;left:${x}px">${this.actionItems(edit, err, { flag: true, plugins: true })}</div>`;
|
|
1434
1528
|
}
|
|
1435
1529
|
/** Arrow keys move through a menu's items; Esc closes it and returns focus. True when handled. */
|
|
1436
1530
|
onMenuKey(e, sel) {
|
|
@@ -1552,7 +1646,10 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
1552
1646
|
['Enter / Tab in a range', 'Next cell of the range (Shift: back)'], ['Drag, Shift + click', 'Select a range'], ['Click a header', 'Select a column, a row or the sheet'],
|
|
1553
1647
|
['Drag a header edge', 'Resize the column or row'], ['Double-click a header edge', 'Fit it to the text'], ['Shift + F10 on a whole column or row', 'Fit · width… · reset to the file’s size'],
|
|
1554
1648
|
['Home / End', 'Row start / end'], ['Ctrl + Home / End', 'First / last cell'],
|
|
1555
|
-
['Page Up / Down', 'Move a screen'], ['Enter or
|
|
1649
|
+
['Page Up / Down', 'Move a screen'], ['Enter, F2 or double-click', 'Edit in the cell (Edit)'], ['Type', 'Replace the cell, in place (Edit)'], ['Delete', 'Clear the cell or range (Edit)'],
|
|
1650
|
+
['Arrows while typing', 'Commit and move (F2: move the caret instead)'], ['Tab … Enter', 'Across, then back to the first column, a row down'], ['Ctrl/⌘ + Enter', 'Put the entry in every cell of the range (Edit)'],
|
|
1651
|
+
['Drag the corner square', 'Fill: a series from numbers, copies otherwise; Ctrl/⌘ swaps (Edit)'], ['Double-click the corner square', 'Fill down beside the data (Edit)'], ['Ctrl + D / Ctrl + R', 'Copy down / right (Edit)'],
|
|
1652
|
+
['Drop a file on the sheet', 'Open it'],
|
|
1556
1653
|
['F', 'Flag issue (Edit)'], ['Esc', 'Cancel a drag or copy · close the pop-up'], ['Ctrl/⌘ + Z', 'Undo'], ['Ctrl + Y, ⌘ + Shift + Z', 'Redo'], ['Ctrl/⌘ + C', 'Copy the value or the range'], ['Shift + F10 or Menu', 'Selection actions'],
|
|
1557
1654
|
['Ctrl/⌘ + Shift + =', 'Insert rows above (whole columns: columns left) (Edit)'], ['Ctrl/⌘ + −', 'Delete the selected rows or columns (Edit)'],
|
|
1558
1655
|
].map(([keys, what]) => ({ keys: keys, what: what }));
|
|
@@ -1619,6 +1716,11 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
1619
1716
|
} // W-47: a header edge resizes, never selects
|
|
1620
1717
|
if (t.closest('[data-hid], [data-og], [data-olvl]'))
|
|
1621
1718
|
return; // W-49: markers and outline buttons act on click
|
|
1719
|
+
if (t.closest('.fh') && e.button === 0 && e.pointerType !== 'touch') {
|
|
1720
|
+
this.startFill(e);
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
this.tabCol = undefined;
|
|
1622
1724
|
const handle = t.closest('.sh');
|
|
1623
1725
|
if (handle && e.button === 0) {
|
|
1624
1726
|
this.startHandleDrag(e, handle.dataset.end === 'start');
|
|
@@ -1655,6 +1757,16 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
1655
1757
|
return;
|
|
1656
1758
|
}
|
|
1657
1759
|
const snap = s.getSnapshot(), before = { ...snap.selection };
|
|
1760
|
+
if (!head) {
|
|
1761
|
+
// a second press on the same cell within 400 ms edits it in place — Pointer Events do not always raise dblclick here
|
|
1762
|
+
const r0 = Number(c.dataset.r), c0 = Number(c.dataset.c), now = performance.now(), last = this.lastDown;
|
|
1763
|
+
this.lastDown = { row: r0, col: c0, t: now };
|
|
1764
|
+
if (last && last.row === r0 && last.col === c0 && now - last.t < 400 && snap.mode === 'edit') {
|
|
1765
|
+
this.lastDown = undefined;
|
|
1766
|
+
this.editCell();
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1658
1770
|
if (head)
|
|
1659
1771
|
this.selectHeader(head, false);
|
|
1660
1772
|
else
|
|
@@ -1788,6 +1900,7 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
1788
1900
|
if (!one)
|
|
1789
1901
|
this.say(r);
|
|
1790
1902
|
this.drawSelection();
|
|
1903
|
+
this.schedule(); // the name box drops the drag's "1R × 1C" even when the selection did not change
|
|
1791
1904
|
}
|
|
1792
1905
|
/** The range a drag spans: corners for cells (grown over merges), whole columns or rows for headers. */
|
|
1793
1906
|
dragRange(d) {
|
|
@@ -2463,6 +2576,12 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
2463
2576
|
if (j !== undefined)
|
|
2464
2577
|
out[j] = v;
|
|
2465
2578
|
} return out; };
|
|
2579
|
+
// the grid grows (or shrinks) by what was inserted (deleted), even at the edge where the data alone stays the same
|
|
2580
|
+
const was = this.geom?.sheet.name === e.sheet ? this.geom.g : undefined, ext = this.extent[e.sheet] ?? { rows: 0, cols: 0 };
|
|
2581
|
+
if (was) {
|
|
2582
|
+
const n = (rows ? was.rows : was.cols) + (o.op.startsWith('insert') ? o.count : -o.count);
|
|
2583
|
+
this.extent[e.sheet] = rows ? { ...ext, rows: Math.max(0, n) } : { ...ext, cols: Math.max(0, n) };
|
|
2584
|
+
}
|
|
2466
2585
|
const z = this._sizes[e.sheet];
|
|
2467
2586
|
if (z)
|
|
2468
2587
|
this._sizes[e.sheet] = rows ? { ...z, rows: move(z.rows) } : { ...z, cols: move(z.cols) };
|
|
@@ -2633,6 +2752,12 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
2633
2752
|
e.stopPropagation();
|
|
2634
2753
|
return;
|
|
2635
2754
|
}
|
|
2755
|
+
if (this.fill && k === 'Escape') {
|
|
2756
|
+
this.endFill(false, false);
|
|
2757
|
+
e.preventDefault();
|
|
2758
|
+
e.stopPropagation();
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2636
2761
|
if (this.rz && k === 'Escape') {
|
|
2637
2762
|
this.endResize(false);
|
|
2638
2763
|
e.preventDefault();
|
|
@@ -2682,6 +2807,8 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
2682
2807
|
s.redo(this);
|
|
2683
2808
|
else if (mod && (k === 'c' || k === 'C'))
|
|
2684
2809
|
this.copySelection();
|
|
2810
|
+
else if (edit && mod && !e.shiftKey && !e.altKey && (k === 'd' || k === 'D' || k === 'r' || k === 'R'))
|
|
2811
|
+
this.fillKey(k === 'd' || k === 'D');
|
|
2685
2812
|
else if (edit && mod && !e.altKey && (k === '+' || (e.shiftKey && k === '=')))
|
|
2686
2813
|
this.structureKey('insert');
|
|
2687
2814
|
else if (edit && mod && !e.altKey && !e.shiftKey && (k === '-' || k === '_'))
|
|
@@ -2892,6 +3019,10 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
2892
3019
|
} // W-62: the cell bar's chip says why
|
|
2893
3020
|
if (cell.hidden)
|
|
2894
3021
|
return;
|
|
3022
|
+
if (sel && !this.editing)
|
|
3023
|
+
cell.value = this._store.input(sel.sheet, sel.ref); // the draft starts from the cell's own text, even before the next frame
|
|
3024
|
+
if (this.openIce(first))
|
|
3025
|
+
return;
|
|
2895
3026
|
cell.focus();
|
|
2896
3027
|
if (first !== undefined) {
|
|
2897
3028
|
cell.value = first;
|
|
@@ -2900,6 +3031,371 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
2900
3031
|
else
|
|
2901
3032
|
cell.select();
|
|
2902
3033
|
}
|
|
3034
|
+
/** Starts a draft on the active cell (the cell's editor or the cell bar): `sheetpane-editstart` may refuse. */
|
|
3035
|
+
beginEdit() {
|
|
3036
|
+
const s = this._store, cell = this.q.cell, sel = s.getSnapshot().selection;
|
|
3037
|
+
if (this.editing && this.editing.sheet === sel.sheet && this.editing.ref === sel.ref)
|
|
3038
|
+
return true; // the same draft, between the cell and the bar
|
|
3039
|
+
const rich = !!s.runs(sel.sheet, sel.ref); // W-53: the edit writes plain text, the runs' formatting goes
|
|
3040
|
+
const allowed = fire(this, 'sheetpane-editstart', { sheet: sel.sheet, ref: sel.ref, column: this.column(sel.sheet, sel.ref), value: s.value(sel.sheet, sel.ref), ...(rich ? { richText: true } : {}) }, true);
|
|
3041
|
+
if (!allowed) {
|
|
3042
|
+
cell.readOnly = true;
|
|
3043
|
+
this.editing = undefined;
|
|
3044
|
+
return false;
|
|
3045
|
+
}
|
|
3046
|
+
cell.readOnly = false;
|
|
3047
|
+
this.editing = { sheet: sel.sheet, ref: sel.ref, start: cell.value };
|
|
3048
|
+
this.schedule(); // the docked actions step aside while the draft is edited
|
|
3049
|
+
return true;
|
|
3050
|
+
}
|
|
3051
|
+
/** Esc: the draft goes, the cell keeps its value. */
|
|
3052
|
+
cancelDraft() {
|
|
3053
|
+
const ed = this.editing, cell = this.q.cell;
|
|
3054
|
+
if (!ed)
|
|
3055
|
+
return;
|
|
3056
|
+
cell.value = ed.start;
|
|
3057
|
+
this.editing = undefined;
|
|
3058
|
+
this.clearRuleMsg();
|
|
3059
|
+
fire(this, 'sheetpane-editcancel', { sheet: ed.sheet, ref: ed.ref });
|
|
3060
|
+
}
|
|
3061
|
+
iceOpen() { return !!this.ice && !this.ice.hidden; }
|
|
3062
|
+
/** Opens the editor on the active cell: typing replaces the value (`first`); Enter, F2 and a double-click keep it, caret at the end. */
|
|
3063
|
+
openIce(first) {
|
|
3064
|
+
const q = this.q, cell = q.cell, p = parseRef(this._store.getSnapshot().selection.ref);
|
|
3065
|
+
if (!p || !this.ice || !q.grid.querySelector(`.c[data-r="${p.row}"][data-c="${p.col}"]`))
|
|
3066
|
+
return false;
|
|
3067
|
+
if (!this.beginEdit())
|
|
3068
|
+
return true; // refused: nothing opens
|
|
3069
|
+
const ice = this.ice;
|
|
3070
|
+
ice.value = first ?? cell.value;
|
|
3071
|
+
this.iceMode = first !== undefined ? 'enter' : 'edit';
|
|
3072
|
+
ice.hidden = false;
|
|
3073
|
+
this.placeIce();
|
|
3074
|
+
ice.focus({ preventScroll: true });
|
|
3075
|
+
ice.setSelectionRange(ice.value.length, ice.value.length);
|
|
3076
|
+
if (first !== undefined) {
|
|
3077
|
+
cell.value = first;
|
|
3078
|
+
cell.dispatchEvent(new Event('input'));
|
|
3079
|
+
}
|
|
3080
|
+
this.schedule();
|
|
3081
|
+
return true;
|
|
3082
|
+
}
|
|
3083
|
+
/** Keeps the editor on its cell through scrolling and zoom; a selection moved elsewhere (another pane, the errors list) commits it. */
|
|
3084
|
+
placeIce() {
|
|
3085
|
+
const ice = this.ice;
|
|
3086
|
+
if (!ice || ice.hidden)
|
|
3087
|
+
return;
|
|
3088
|
+
const ed = this.editing, sel = this._store?.getSnapshot().selection;
|
|
3089
|
+
if (!ed || !sel || sel.sheet !== ed.sheet || sel.ref !== ed.ref) {
|
|
3090
|
+
ice.hidden = true;
|
|
3091
|
+
this.iceMode = undefined;
|
|
3092
|
+
if (ed)
|
|
3093
|
+
this.commitDraft();
|
|
3094
|
+
return;
|
|
3095
|
+
}
|
|
3096
|
+
const p = parseRef(ed.ref);
|
|
3097
|
+
const el = this.q.grid.querySelector(`.c[data-r="${p.row}"][data-c="${p.col}"]`);
|
|
3098
|
+
if (!el) {
|
|
3099
|
+
ice.style.visibility = 'hidden';
|
|
3100
|
+
return;
|
|
3101
|
+
} // scrolled out of the rendered window: the bar still holds the draft
|
|
3102
|
+
const host = this.q.sheet.getBoundingClientRect(), r = el.getBoundingClientRect(), sc = this.q.scroller.getBoundingClientRect();
|
|
3103
|
+
const cs = getComputedStyle(el);
|
|
3104
|
+
ice.style.cssText = `left:${r.left - host.left}px;top:${r.top - host.top}px;min-width:${r.width}px;height:${r.height}px;max-width:${Math.max(r.width, sc.right - r.left - 12)}px;font-size:${cs.fontSize};font-weight:${cs.fontWeight};font-style:${cs.fontStyle}`;
|
|
3105
|
+
}
|
|
3106
|
+
/** The cell's editor: Enter and Tab commit and move, Esc cancels, an arrow commits when the draft was started by typing. */
|
|
3107
|
+
onIceKey(e) {
|
|
3108
|
+
const k = e.key, q = this.q;
|
|
3109
|
+
e.stopPropagation();
|
|
3110
|
+
if (k === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
3111
|
+
e.preventDefault();
|
|
3112
|
+
this.fillRangeWithDraft();
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
3115
|
+
if (k === 'Enter' || k === 'Tab') {
|
|
3116
|
+
e.preventDefault();
|
|
3117
|
+
this.finishEdit(k === 'Tab' ? 'tab' : 'enter', e.shiftKey);
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
if (k === 'Escape') {
|
|
3121
|
+
e.preventDefault();
|
|
3122
|
+
this.ice.hidden = true;
|
|
3123
|
+
this.iceMode = undefined;
|
|
3124
|
+
this.cancelDraft();
|
|
3125
|
+
q.grid.focus({ preventScroll: true });
|
|
3126
|
+
this.schedule();
|
|
3127
|
+
return;
|
|
3128
|
+
}
|
|
3129
|
+
if (k === 'F2') {
|
|
3130
|
+
e.preventDefault();
|
|
3131
|
+
this.iceMode = this.iceMode === 'enter' ? 'edit' : 'enter';
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
3134
|
+
const arrow = k === 'ArrowDown' ? [1, 0] : k === 'ArrowUp' ? [-1, 0] : k === 'ArrowRight' ? [0, 1] : k === 'ArrowLeft' ? [0, -1] : undefined;
|
|
3135
|
+
if (arrow && this.iceMode === 'enter' && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) {
|
|
3136
|
+
e.preventDefault();
|
|
3137
|
+
this.ice.hidden = true;
|
|
3138
|
+
this.iceMode = undefined;
|
|
3139
|
+
this.tabCol = undefined;
|
|
3140
|
+
this.commitDraft();
|
|
3141
|
+
this.move(arrow[0], arrow[1], false);
|
|
3142
|
+
q.grid.focus({ preventScroll: true });
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
/** Commit the draft, then move: Enter down (back to where a run of Tabs began), Tab across; inside a range, within it. */
|
|
3146
|
+
finishEdit(how, back) {
|
|
3147
|
+
const s = this._store, g = this.geometry();
|
|
3148
|
+
if (this.iceOpen()) {
|
|
3149
|
+
this.ice.hidden = true;
|
|
3150
|
+
this.iceMode = undefined;
|
|
3151
|
+
}
|
|
3152
|
+
this.commitDraft();
|
|
3153
|
+
const sel = s.getSnapshot().selection, p = parseRef(sel.ref), range = sel.range ? parseRange(sel.range) : undefined;
|
|
3154
|
+
if (range)
|
|
3155
|
+
this.stepInRange(range, how === 'tab', back);
|
|
3156
|
+
else if (how === 'tab') {
|
|
3157
|
+
if (this.tabCol === undefined && !back)
|
|
3158
|
+
this.tabCol = p.col;
|
|
3159
|
+
this.move(0, back ? -1 : 1, false);
|
|
3160
|
+
}
|
|
3161
|
+
else if (this.tabCol !== undefined && !back && g) {
|
|
3162
|
+
const col = this.tabCol;
|
|
3163
|
+
this.tabCol = undefined;
|
|
3164
|
+
this.selectCell(Math.min(g.g.rows - 1, p.row + 1), col, false);
|
|
3165
|
+
}
|
|
3166
|
+
else {
|
|
3167
|
+
this.tabCol = undefined;
|
|
3168
|
+
this.move(back ? -1 : 1, 0, false);
|
|
3169
|
+
}
|
|
3170
|
+
this.q.grid.focus({ preventScroll: true });
|
|
3171
|
+
}
|
|
3172
|
+
/** Ctrl/⌘+Enter: the draft goes into every shown cell of the range, one undo step; the selection stays. */
|
|
3173
|
+
fillRangeWithDraft() {
|
|
3174
|
+
const s = this._store, snap = s.getSnapshot(), sel = snap.selection, ed = this.editing;
|
|
3175
|
+
const range = sel.range ? parseRange(sel.range) : undefined;
|
|
3176
|
+
if (this.iceOpen()) {
|
|
3177
|
+
this.ice.hidden = true;
|
|
3178
|
+
this.iceMode = undefined;
|
|
3179
|
+
}
|
|
3180
|
+
if (!range || !ed) {
|
|
3181
|
+
this.commitDraft();
|
|
3182
|
+
this.q.grid.focus({ preventScroll: true });
|
|
3183
|
+
this.schedule();
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
3186
|
+
const v = this.q.cell.value, book = s.book, g = this.geometry().g;
|
|
3187
|
+
this.editing = undefined;
|
|
3188
|
+
this.clearRuleMsg();
|
|
3189
|
+
if (cellCount(range) > 1_000_000) {
|
|
3190
|
+
this.say0('Nothing filled — the range is over 1,000,000 cells');
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3193
|
+
this.lockedRefused = 0;
|
|
3194
|
+
const cells = [];
|
|
3195
|
+
for (let row = range.top; row <= range.bottom; row++)
|
|
3196
|
+
for (let col = range.left; col <= range.right; col++) {
|
|
3197
|
+
if (!this.shownRow(g, row) || !this.shownCol(g, col))
|
|
3198
|
+
continue;
|
|
3199
|
+
const ref = cellRef(row, col);
|
|
3200
|
+
if (this.allowChange(sel.sheet, ref, parseInput(v, s.original(sel.sheet, ref).value, book.source), v))
|
|
3201
|
+
cells.push({ sheet: sel.sheet, ref, input: v });
|
|
3202
|
+
}
|
|
3203
|
+
if (cells.length)
|
|
3204
|
+
s.setCells(cells, { origin: this });
|
|
3205
|
+
this.sayLocked();
|
|
3206
|
+
this.q.grid.focus({ preventScroll: true });
|
|
3207
|
+
this.schedule();
|
|
3208
|
+
}
|
|
3209
|
+
// ── the fill handle (Excel's AutoFill): drag the selection's corner to continue it ──
|
|
3210
|
+
startFill(e) {
|
|
3211
|
+
const s = this._store, sel = s.getSnapshot().selection, g = this.geometry();
|
|
3212
|
+
if (!g || s.getSnapshot().mode !== 'edit')
|
|
3213
|
+
return;
|
|
3214
|
+
const p = parseRef(sel.ref);
|
|
3215
|
+
const src = sel.range ? parseRange(sel.range) : (mergeAt(g.g, p.row, p.col) ?? { top: p.row, left: p.col, bottom: p.row, right: p.col });
|
|
3216
|
+
e.preventDefault();
|
|
3217
|
+
e.stopPropagation();
|
|
3218
|
+
this.q.grid.focus({ preventScroll: true });
|
|
3219
|
+
this.fill = { id: e.pointerId, src, to: src, x: e.clientX, y: e.clientY, tip: '' };
|
|
3220
|
+
try {
|
|
3221
|
+
this.q.scroller.setPointerCapture(e.pointerId);
|
|
3222
|
+
}
|
|
3223
|
+
catch { /* synthetic */ }
|
|
3224
|
+
}
|
|
3225
|
+
queueFill() {
|
|
3226
|
+
if (!this.fillRaf)
|
|
3227
|
+
this.fillRaf = requestAnimationFrame(() => { this.fillRaf = 0; this.fillStep(); });
|
|
3228
|
+
}
|
|
3229
|
+
/** One frame of a fill drag: autoscroll, the axis the pointer has gone furthest along, the preview and the last value. */
|
|
3230
|
+
fillStep() {
|
|
3231
|
+
const f = this.fill, g = this.geometry(), s = this._store;
|
|
3232
|
+
if (!f || !g || !s)
|
|
3233
|
+
return;
|
|
3234
|
+
const sc = this.q.scroller, z = this.zoom, HH = this.headH() * z, RW = this.rowHeadW() * z, rect = sc.getBoundingClientRect();
|
|
3235
|
+
const vx = f.x - rect.left - sc.clientLeft, vy = f.y - rect.top - sc.clientTop;
|
|
3236
|
+
const speed = (into) => Math.min(80, 4 + into * 0.8);
|
|
3237
|
+
let dy = 0, dx = 0;
|
|
3238
|
+
if (vy > sc.clientHeight - 24)
|
|
3239
|
+
dy = speed(vy - (sc.clientHeight - 24));
|
|
3240
|
+
else if (vy < HH + 24 && sc.scrollTop > 0)
|
|
3241
|
+
dy = -speed(HH + 24 - vy);
|
|
3242
|
+
if (vx > sc.clientWidth - 24)
|
|
3243
|
+
dx = speed(vx - (sc.clientWidth - 24));
|
|
3244
|
+
else if (vx < RW + 24 && sc.scrollLeft > 0)
|
|
3245
|
+
dx = -speed(RW + 24 - vx);
|
|
3246
|
+
if (dy)
|
|
3247
|
+
sc.scrollTop += dy;
|
|
3248
|
+
if (dx)
|
|
3249
|
+
sc.scrollLeft += dx;
|
|
3250
|
+
const row = Math.max(0, Math.min(g.g.rows - 1, rowAt(g.g, Math.max(0, (vy + sc.scrollTop - HH) / z))));
|
|
3251
|
+
const col = Math.max(0, Math.min(g.g.cols - 1, colAt(g.g, Math.max(0, (vx + sc.scrollLeft - RW) / z))));
|
|
3252
|
+
const r = f.src;
|
|
3253
|
+
const outV = row > r.bottom ? row - r.bottom : row < r.top ? r.top - row : 0;
|
|
3254
|
+
const outH = col > r.right ? col - r.right : col < r.left ? r.left - col : 0;
|
|
3255
|
+
let to = r;
|
|
3256
|
+
if (outV >= outH && outV > 0)
|
|
3257
|
+
to = row > r.bottom ? { ...r, bottom: row } : { ...r, top: row };
|
|
3258
|
+
else if (outH > 0)
|
|
3259
|
+
to = col > r.right ? { ...r, right: col } : { ...r, left: col };
|
|
3260
|
+
f.to = to;
|
|
3261
|
+
// the value the last cell will hold, shown beside the handle (Excel's fill tooltip)
|
|
3262
|
+
const vertical = to.top !== r.top || to.bottom !== r.bottom, back = vertical ? to.top < r.top : to.left < r.left;
|
|
3263
|
+
const dist = vertical ? (back ? r.top - to.top : to.bottom - r.bottom) : (back ? r.left - to.left : to.right - r.right);
|
|
3264
|
+
const lane = vertical ? r.left : r.top, sheet = s.getSnapshot().selection.sheet;
|
|
3265
|
+
const src = [];
|
|
3266
|
+
if (vertical)
|
|
3267
|
+
for (let rr = r.top; rr <= r.bottom; rr++)
|
|
3268
|
+
src.push(s.input(sheet, cellRef(rr, lane)));
|
|
3269
|
+
else
|
|
3270
|
+
for (let cc = r.left; cc <= r.right; cc++)
|
|
3271
|
+
src.push(s.input(sheet, cellRef(lane, cc)));
|
|
3272
|
+
f.tip = dist > 0 ? (fillSeries(src, dist, { back, unit: vertical ? { dr: 1, dc: 0 } : { dr: 0, dc: 1 } }).at(-1) ?? '') : '';
|
|
3273
|
+
this.selKey = '';
|
|
3274
|
+
this.drawSelection();
|
|
3275
|
+
if (dy || dx)
|
|
3276
|
+
this.queueFill();
|
|
3277
|
+
}
|
|
3278
|
+
endFill(commit, invert) {
|
|
3279
|
+
const f = this.fill;
|
|
3280
|
+
if (!f)
|
|
3281
|
+
return;
|
|
3282
|
+
if (this.fillRaf) {
|
|
3283
|
+
cancelAnimationFrame(this.fillRaf);
|
|
3284
|
+
this.fillRaf = 0;
|
|
3285
|
+
}
|
|
3286
|
+
this.fill = undefined;
|
|
3287
|
+
try {
|
|
3288
|
+
if (this.q.scroller.hasPointerCapture(f.id))
|
|
3289
|
+
this.q.scroller.releasePointerCapture(f.id);
|
|
3290
|
+
}
|
|
3291
|
+
catch { /* synthetic */ }
|
|
3292
|
+
this.selKey = '';
|
|
3293
|
+
if (commit && rangeText(f.to) !== rangeText(f.src))
|
|
3294
|
+
this.applyFill(f.src, f.to, { invert });
|
|
3295
|
+
else
|
|
3296
|
+
this.drawSelection();
|
|
3297
|
+
}
|
|
3298
|
+
/** Writes a fill: per column (or row) of the source, the series beyond it — `beforechange` per cell, hidden cells left alone, one undo step. */
|
|
3299
|
+
applyFill(src, to, o = {}) {
|
|
3300
|
+
const s = this._store, snap = s.getSnapshot(), sheet = snap.selection.sheet, book = s.book, g = this.geometry().g;
|
|
3301
|
+
const vertical = to.top !== src.top || to.bottom !== src.bottom;
|
|
3302
|
+
const back = vertical ? to.top < src.top : to.left < src.left;
|
|
3303
|
+
const cells = [];
|
|
3304
|
+
this.lockedRefused = 0;
|
|
3305
|
+
const [l0, l1] = vertical ? [src.left, src.right] : [src.top, src.bottom];
|
|
3306
|
+
for (let lane = l0; lane <= l1; lane++) {
|
|
3307
|
+
const inputs = [];
|
|
3308
|
+
if (vertical)
|
|
3309
|
+
for (let r = src.top; r <= src.bottom; r++)
|
|
3310
|
+
inputs.push(s.input(sheet, cellRef(r, lane)));
|
|
3311
|
+
else
|
|
3312
|
+
for (let c = src.left; c <= src.right; c++)
|
|
3313
|
+
inputs.push(s.input(sheet, cellRef(lane, c)));
|
|
3314
|
+
const targets = [];
|
|
3315
|
+
if (vertical) {
|
|
3316
|
+
if (back)
|
|
3317
|
+
for (let r = src.top - 1; r >= to.top; r--)
|
|
3318
|
+
targets.push(r);
|
|
3319
|
+
else
|
|
3320
|
+
for (let r = src.bottom + 1; r <= to.bottom; r++)
|
|
3321
|
+
targets.push(r);
|
|
3322
|
+
}
|
|
3323
|
+
else {
|
|
3324
|
+
if (back)
|
|
3325
|
+
for (let c = src.left - 1; c >= to.left; c--)
|
|
3326
|
+
targets.push(c);
|
|
3327
|
+
else
|
|
3328
|
+
for (let c = src.right + 1; c <= to.right; c++)
|
|
3329
|
+
targets.push(c);
|
|
3330
|
+
}
|
|
3331
|
+
const vals = fillSeries(inputs, targets.length, { back, invert: !!o.invert, copy: !!o.copy, unit: vertical ? { dr: 1, dc: 0 } : { dr: 0, dc: 1 } });
|
|
3332
|
+
targets.forEach((t, i) => {
|
|
3333
|
+
const row = vertical ? t : lane, col = vertical ? lane : t;
|
|
3334
|
+
if (!this.shownRow(g, row) || !this.shownCol(g, col))
|
|
3335
|
+
return; // hidden cells are left alone (W-49)
|
|
3336
|
+
const ref = cellRef(row, col), v = vals[i];
|
|
3337
|
+
if (this.allowChange(sheet, ref, parseInput(v, s.original(sheet, ref).value, book.source), v))
|
|
3338
|
+
cells.push({ sheet, ref, input: v });
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
if (cells.length)
|
|
3342
|
+
s.setCells(cells, { origin: this });
|
|
3343
|
+
this.sayLocked();
|
|
3344
|
+
if (!o.keep) {
|
|
3345
|
+
const all = { top: Math.min(src.top, to.top), left: Math.min(src.left, to.left), bottom: Math.max(src.bottom, to.bottom), right: Math.max(src.right, to.right) };
|
|
3346
|
+
s.select({ sheet, ref: snap.selection.ref, range: rangeText(all) }, this);
|
|
3347
|
+
this.say(all);
|
|
3348
|
+
}
|
|
3349
|
+
this.selKey = '';
|
|
3350
|
+
this.schedule();
|
|
3351
|
+
}
|
|
3352
|
+
/** Double-click on the handle: fill down as far as the column beside the source has data. */
|
|
3353
|
+
autoFill() {
|
|
3354
|
+
const s = this._store, snap = s.getSnapshot(), g = this.geometry();
|
|
3355
|
+
if (!g || snap.mode !== 'edit')
|
|
3356
|
+
return;
|
|
3357
|
+
const sel = snap.selection, p = parseRef(sel.ref);
|
|
3358
|
+
const src = sel.range ? parseRange(sel.range) : { top: p.row, left: p.col, bottom: p.row, right: p.col };
|
|
3359
|
+
const has = (r, c) => { const v = s.value(sel.sheet, cellRef(r, c)); return v !== null && v !== undefined && v !== ''; };
|
|
3360
|
+
const last = fillDownTo(has, src.bottom, src.left, src.right, g.g.rows);
|
|
3361
|
+
if (last > src.bottom)
|
|
3362
|
+
this.applyFill(src, { ...src, bottom: last });
|
|
3363
|
+
}
|
|
3364
|
+
/** Ctrl+D / Ctrl+R: copy the top row (left column) of the range over the rest — one cell copies from above (left). Formulas move. */
|
|
3365
|
+
fillKey(down) {
|
|
3366
|
+
const s = this._store, sel = s.getSnapshot().selection, p = parseRef(sel.ref);
|
|
3367
|
+
const r = sel.range ? parseRange(sel.range) : { top: p.row, left: p.col, bottom: p.row, right: p.col };
|
|
3368
|
+
if (down) {
|
|
3369
|
+
if (r.top === r.bottom) {
|
|
3370
|
+
if (r.top > 0)
|
|
3371
|
+
this.applyFill({ ...r, top: r.top - 1, bottom: r.top - 1 }, { ...r, top: r.top - 1 }, { copy: true, keep: true });
|
|
3372
|
+
}
|
|
3373
|
+
else
|
|
3374
|
+
this.applyFill({ ...r, bottom: r.top }, r, { copy: true, keep: true });
|
|
3375
|
+
}
|
|
3376
|
+
else if (r.left === r.right) {
|
|
3377
|
+
if (r.left > 0)
|
|
3378
|
+
this.applyFill({ ...r, left: r.left - 1, right: r.left - 1 }, { ...r, left: r.left - 1 }, { copy: true, keep: true });
|
|
3379
|
+
}
|
|
3380
|
+
else
|
|
3381
|
+
this.applyFill({ ...r, right: r.left }, r, { copy: true, keep: true });
|
|
3382
|
+
}
|
|
3383
|
+
/** A file dragged over the loaded sheet: what dropping it does. */
|
|
3384
|
+
showDrop(on) {
|
|
3385
|
+
let ov = this.q.body.querySelector('.dropov');
|
|
3386
|
+
if (!on) {
|
|
3387
|
+
ov?.remove();
|
|
3388
|
+
return;
|
|
3389
|
+
}
|
|
3390
|
+
if (!ov) {
|
|
3391
|
+
ov = document.createElement('div');
|
|
3392
|
+
ov.className = 'dropov';
|
|
3393
|
+
ov.setAttribute('role', 'status');
|
|
3394
|
+
this.q.body.append(ov);
|
|
3395
|
+
}
|
|
3396
|
+
const n = this._store?.getSnapshot().changes.size ?? 0;
|
|
3397
|
+
ov.innerHTML = `${ICON.upload}<span>Drop to open the file here</span><small>.xlsx or .csv — read in this tab, nothing is uploaded${n ? ` · it replaces this workbook and its ${n} change${n === 1 ? '' : 's'} not yet downloaded` : ''}</small>`;
|
|
3398
|
+
}
|
|
2903
3399
|
/** Flag issue (`F`): the store decides which form opens (contract decision 4). */
|
|
2904
3400
|
flag() {
|
|
2905
3401
|
const s = this._store, sel = s.getSnapshot().selection;
|
|
@@ -3202,7 +3698,7 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
3202
3698
|
// the file's hiding, less what this element reveals for viewing (W-49)
|
|
3203
3699
|
const vh = this.viewHidden[sheet.name];
|
|
3204
3700
|
const keep = (list, shown, view) => new Set([...(rev?.all ? [] : list.filter((i) => !shown?.has(i))), ...(view ?? [])]);
|
|
3205
|
-
this.geom = { sheet, g: sheetGeometry(sheet, merges, this._sizes[sheet.name], { rows: keep(hid.rows, rev?.rows, vh?.rows), cols: keep(hid.cols, rev?.cols, vh?.cols) }) };
|
|
3701
|
+
this.geom = { sheet, g: sheetGeometry(sheet, merges, this._sizes[sheet.name], { rows: keep(hid.rows, rev?.rows, vh?.rows), cols: keep(hid.cols, rev?.cols, vh?.cols) }, this.extent[sheet.name]) };
|
|
3206
3702
|
this.geomMerges = mk;
|
|
3207
3703
|
}
|
|
3208
3704
|
return this.geom;
|
|
@@ -3287,7 +3783,7 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
3287
3783
|
q.val.hidden = editable;
|
|
3288
3784
|
q.cellname.textContent = `Value of ${sel.ref}`;
|
|
3289
3785
|
const curText = s.input(sel.sheet, sel.ref);
|
|
3290
|
-
if (this.shadow.activeElement !== cell)
|
|
3786
|
+
if (this.shadow.activeElement !== cell && !this.iceOpen())
|
|
3291
3787
|
cell.value = curText;
|
|
3292
3788
|
q.val.textContent = s.text(sel.sheet, sel.ref) || '(empty)';
|
|
3293
3789
|
q.merge.hidden = !merge;
|
|
@@ -3337,7 +3833,7 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
3337
3833
|
q.lockmsg.textContent = `🔒 ${LOCKED_MESSAGE}`;
|
|
3338
3834
|
q.plugCellbar.innerHTML = this.controlsHtml(controlsFor(s.plugins, 'cellbar', snap.mode), 'btn btn-g');
|
|
3339
3835
|
// docked selection actions (W-31): Flag issue · selbar plugin controls · ⋯ — hidden while typing or composing
|
|
3340
|
-
const typing = !!this.editing && this.shadow.activeElement === cell;
|
|
3836
|
+
const typing = !!this.editing && (this.shadow.activeElement === cell || this.iceOpen());
|
|
3341
3837
|
q.dock.hidden = typing || composing;
|
|
3342
3838
|
if (!q.dock.hidden) {
|
|
3343
3839
|
const flagBtn = edit && !err && s.flagging ? `<button type="button" class="btn btn-g" data-act="flag" data-sp="flag" title="Flag issue (F)" aria-keyshortcuts="F">${ICON.flag}Flag issue<span class="kbd" aria-hidden="true">F</span></button>` : '';
|
|
@@ -3438,6 +3934,7 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
3438
3934
|
q.status.dataset.html = statusHtml;
|
|
3439
3935
|
}
|
|
3440
3936
|
this.renderGrid(g.g, sheet, snap.mode);
|
|
3937
|
+
floatAll(this.shadowRoot); // every menu and card on top, inside the viewport (SR-LAW-13)
|
|
3441
3938
|
}
|
|
3442
3939
|
/** Count · Sum · Average of a range of two or more cells (W-46 DoD 8), cached until the range or the cells change. */
|
|
3443
3940
|
totalsFor(g) {
|
|
@@ -3680,6 +4177,7 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
3680
4177
|
this.ariaOn.clear(); // new nodes: the layer's bands and aria-selected go on again
|
|
3681
4178
|
}
|
|
3682
4179
|
grid.setAttribute('aria-activedescendant', `c${selP.row}_${selP.col}`);
|
|
4180
|
+
this.placeIce();
|
|
3683
4181
|
this.drawSelection();
|
|
3684
4182
|
this.renderOverlay(g, sheet, z, RW, HH, Y, H, X, W, win);
|
|
3685
4183
|
}
|
|
@@ -3706,7 +4204,8 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
3706
4204
|
const copied = this.copied && this.copied.sheet === g.sheet.name ? this.copied.range : undefined;
|
|
3707
4205
|
if (d)
|
|
3708
4206
|
q.ref.textContent = `${range.bottom - range.top + 1}R × ${range.right - range.left + 1}C`;
|
|
3709
|
-
const
|
|
4207
|
+
const f = this.fill, handle = snap.mode === 'edit' && !d && !this.touchUi && !this.iceOpen() && !snap.composing;
|
|
4208
|
+
const key = [g.sheet.name, z, fr, fc, gg.rows, gg.cols, rangeText(range), rangeText(am), copied ? rangeText(copied) : '', this.touchUi && !d && multi ? 1 : 0, handle ? 1 : 0, f ? `${rangeText(f.to)}~${f.tip}` : ''].join('|');
|
|
3710
4209
|
if (key === this.selKey)
|
|
3711
4210
|
return;
|
|
3712
4211
|
this.selKey = key;
|
|
@@ -3753,6 +4252,16 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
3753
4252
|
layer += `<div class="sh" data-end="${end}" style="position:relative;z-index:3;margin:${HH + Y(row) - 12}px 0 0 ${RW + X(col) - 12}px"></div>`;
|
|
3754
4253
|
}
|
|
3755
4254
|
}
|
|
4255
|
+
// the fill handle at the corner (Edit), and while it is dragged, the dashed reach and the last cell's value
|
|
4256
|
+
if (f && rangeText(f.to) !== rangeText(f.src))
|
|
4257
|
+
for (const p of splitByPanes(f.to, fr, fc))
|
|
4258
|
+
put(p.pane, p.top, p.bottom, p.left, p.right, 'sfp', (row) => borders(row === undefined ? p.edges : { ...p.edges, top: p.edges.top && row === p.top, bottom: p.edges.bottom && row === p.bottom }));
|
|
4259
|
+
const reach = f ? f.to : range;
|
|
4260
|
+
if (handle && reach.bottom >= fr && reach.right >= fc) {
|
|
4261
|
+
layer += `<div class="fh" title="Drag to fill — double-click to fill down, Ctrl/⌘ to copy instead" style="position:relative;z-index:4;margin:${HH + Y(reach.bottom + 1) - 4}px 0 0 ${RW + X(reach.right + 1) - 4}px"></div>`;
|
|
4262
|
+
if (f?.tip)
|
|
4263
|
+
layer += `<div class="ftip" style="position:relative;z-index:5;margin:${HH + Y(reach.bottom + 1) + 6}px 0 0 ${RW + X(reach.right + 1) + 6}px">${esc(f.tip)}</div>`;
|
|
4264
|
+
}
|
|
3756
4265
|
q.sellayer.innerHTML = layer;
|
|
3757
4266
|
for (const b of Array.from(q.frozen.querySelectorAll('.sband')))
|
|
3758
4267
|
b.remove();
|
|
@@ -3850,7 +4359,7 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
3850
4359
|
}
|
|
3851
4360
|
else if (this.menu && this.menu.sheet === sheet.name && this.menu.ref === sel.ref) {
|
|
3852
4361
|
const m = this.menu;
|
|
3853
|
-
card = `<div class="card menu" role="listbox" aria-label="Choices" style="top:${p.y + H(selP.row)}px;left:${p.x}px;min-width:${Math.max(cw, 140)}px;max-height:260px;overflow:auto">${m.options.map((o) => `<button type="button" role="option" class="btn btn-g" style="justify-content:flex-start;width:100%;border-radius:0" data-act="pick-option" data-v="${esc(o)}">${esc(o) || '(empty)'}</button>`).join('')}</div>`;
|
|
4362
|
+
card = `<div class="card menu" role="listbox" aria-label="Choices" data-anchor-h="${H(selP.row)}" style="top:${p.y + H(selP.row)}px;left:${p.x}px;min-width:${Math.max(cw, 140)}px;max-height:260px;overflow:auto">${m.options.map((o) => `<button type="button" role="option" class="btn btn-g" style="justify-content:flex-start;width:100%;border-radius:0" data-act="pick-option" data-v="${esc(o)}">${esc(o) || '(empty)'}</button>`).join('')}</div>`;
|
|
3854
4363
|
}
|
|
3855
4364
|
else if (err && s.flagging) {
|
|
3856
4365
|
card = this.calloutHtml(err, sideLeft(300), sideTop(260));
|
|
@@ -3872,7 +4381,7 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
3872
4381
|
// the context menu (right-click, Shift+F10, the Menu key) sits just under the cell, above any card
|
|
3873
4382
|
if (this.ctxOpen && sel.sheet === sheet.name) {
|
|
3874
4383
|
const mg = mergeAt(g, selP.row, selP.col);
|
|
3875
|
-
card = this.contextMenuHtml(p.x, (mg ? HH + Y(mg.bottom + 1) : p.y + H(selP.row)) + 2, edit, err);
|
|
4384
|
+
card = this.contextMenuHtml(p.x, (mg ? HH + Y(mg.bottom + 1) : p.y + H(selP.row)) + 2, edit, err, (mg ? Y(mg.bottom + 1) - Y(mg.top) : H(selP.row)) + 4);
|
|
3876
4385
|
}
|
|
3877
4386
|
if (!card && this.helpOpen) {
|
|
3878
4387
|
const sc = this.q.scroller;
|
|
@@ -3919,6 +4428,7 @@ export class SheetpaneExcel extends ElementBase {
|
|
|
3919
4428
|
this.detailRegion.sync(cardEl, card.startsWith('<div class="card callout"') ? err : undefined); // the host's region in the card (W-68)
|
|
3920
4429
|
this.renderHighlight(g, sheet, RW, HH, Y, X, ov, fixed);
|
|
3921
4430
|
this.renderPluginOverlays(sheet, z, RW, HH, ov);
|
|
4431
|
+
floatAll(this.shadowRoot);
|
|
3922
4432
|
}
|
|
3923
4433
|
/**
|
|
3924
4434
|
* The host's highlight (W-56): one box per range on this sheet, 2 px outside the cells so the
|