@jackuait/blok 0.10.0-beta.15 → 0.10.0-beta.17

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.
@@ -1053,7 +1053,11 @@ export class TableCellBlocks {
1053
1053
  }
1054
1054
 
1055
1055
  /**
1056
- * Delete blocks by their IDs (in reverse index order to avoid shifting issues)
1056
+ * Delete blocks by their IDs (in reverse index order to avoid shifting issues).
1057
+ * Preserves scroll position because api.blocks.delete() is async — its internal
1058
+ * `await` defers Caret.setToBlock() to microtasks that run AFTER this method returns,
1059
+ * causing unwanted page jumps via element.focus() and window.scrollBy().
1060
+ * We use Promise.all().then() to schedule the scroll restore after all those microtasks.
1057
1061
  */
1058
1062
  public deleteBlocks(blockIds: string[]): void {
1059
1063
  const blockIndices = blockIds
@@ -1061,8 +1065,16 @@ export class TableCellBlocks {
1061
1065
  .filter((index): index is number => index !== undefined)
1062
1066
  .sort((a, b) => b - a);
1063
1067
 
1064
- blockIndices.forEach(index => {
1065
- void this.api.blocks.delete(index);
1068
+ const savedScrollY = window.scrollY;
1069
+
1070
+ const deletePromises = blockIndices.map(index => {
1071
+ return this.api.blocks.delete(index);
1072
+ });
1073
+
1074
+ void Promise.all(deletePromises).then(() => {
1075
+ if (window.scrollY !== savedScrollY) {
1076
+ window.scrollTo(0, savedScrollY);
1077
+ }
1066
1078
  });
1067
1079
  }
1068
1080
 
@@ -0,0 +1,247 @@
1
+ import { show as showTooltip, hide as hideTooltip } from '../../components/utils/tooltip';
2
+
3
+ const CORNER_DRAG_ATTR = 'data-blok-table-corner-drag';
4
+
5
+ export interface TableCornerDragOptions {
6
+ wrapper: HTMLElement;
7
+ gridEl: HTMLElement;
8
+ onAddRow: () => void;
9
+ onAddColumn: () => void;
10
+ onRemoveLastRow: () => void;
11
+ onRemoveLastColumn: () => void;
12
+ onDragStart: () => void;
13
+ onDragEnd: () => void;
14
+ getTableSize: () => { rows: number; cols: number };
15
+ canRemoveLastRow: () => boolean;
16
+ canRemoveLastColumn: () => boolean;
17
+ onClickAdd?: () => void;
18
+ }
19
+
20
+ const DRAG_THRESHOLD = 5;
21
+
22
+ interface DragState {
23
+ startX: number;
24
+ startY: number;
25
+ unitWidth: number;
26
+ unitHeight: number;
27
+ addedRows: number;
28
+ addedCols: number;
29
+ pointerId: number;
30
+ didDrag: boolean;
31
+ }
32
+
33
+ export class TableCornerDrag {
34
+ private wrapper: HTMLElement;
35
+ private gridEl: HTMLElement;
36
+ private hitZone: HTMLElement;
37
+ private getTableSize: () => { rows: number; cols: number };
38
+ private onAddRow: () => void;
39
+ private onAddColumn: () => void;
40
+ private onRemoveLastRow: () => void;
41
+ private onRemoveLastColumn: () => void;
42
+ private onDragStart: () => void;
43
+ private onDragEnd: () => void;
44
+ private canRemoveLastRow: () => boolean;
45
+ private canRemoveLastColumn: () => boolean;
46
+ private onClickAdd: (() => void) | null;
47
+ private dragState: DragState | null = null;
48
+ private readonly boundMouseEnter: () => void;
49
+ private readonly boundMouseLeave: () => void;
50
+ private readonly boundPointerDown: (e: PointerEvent) => void;
51
+ private readonly boundPointerMove: (e: PointerEvent) => void;
52
+ private readonly boundPointerUp: (e: PointerEvent) => void;
53
+
54
+ constructor(options: TableCornerDragOptions) {
55
+ this.wrapper = options.wrapper;
56
+ this.gridEl = options.gridEl;
57
+ this.getTableSize = options.getTableSize;
58
+ this.onAddRow = options.onAddRow;
59
+ this.onAddColumn = options.onAddColumn;
60
+ this.onRemoveLastRow = options.onRemoveLastRow;
61
+ this.onRemoveLastColumn = options.onRemoveLastColumn;
62
+ this.onDragStart = options.onDragStart;
63
+ this.onDragEnd = options.onDragEnd;
64
+ this.canRemoveLastRow = options.canRemoveLastRow;
65
+ this.canRemoveLastColumn = options.canRemoveLastColumn;
66
+ this.onClickAdd = options.onClickAdd ?? null;
67
+
68
+ this.hitZone = document.createElement('div');
69
+ this.hitZone.setAttribute(CORNER_DRAG_ATTR, '');
70
+ this.hitZone.setAttribute('contenteditable', 'false');
71
+ this.hitZone.style.position = 'absolute';
72
+ this.hitZone.style.width = '36px';
73
+ this.hitZone.style.height = '36px';
74
+ this.hitZone.style.cursor = 'nwse-resize';
75
+ this.hitZone.style.zIndex = '2';
76
+ this.hitZone.style.pointerEvents = 'auto';
77
+ this.hitZone.style.bottom = '-36px';
78
+ this.hitZone.style.right = '-16px';
79
+
80
+ this.boundMouseEnter = this.handleMouseEnter.bind(this);
81
+ this.boundMouseLeave = this.handleMouseLeave.bind(this);
82
+ this.boundPointerDown = this.handlePointerDown.bind(this);
83
+ this.boundPointerMove = this.handlePointerMove.bind(this);
84
+ this.boundPointerUp = this.handlePointerUp.bind(this);
85
+
86
+ this.hitZone.addEventListener('mouseenter', this.boundMouseEnter);
87
+ this.hitZone.addEventListener('mouseleave', this.boundMouseLeave);
88
+ this.hitZone.addEventListener('pointerdown', this.boundPointerDown);
89
+
90
+ this.wrapper.appendChild(this.hitZone);
91
+ }
92
+
93
+ private updateTooltip(): void {
94
+ const size = this.getTableSize();
95
+
96
+ showTooltip(this.hitZone, `${size.cols}\u00D7${size.rows}`, { placement: 'bottom' });
97
+ }
98
+
99
+ private handleMouseEnter(): void {
100
+ this.updateTooltip();
101
+ }
102
+
103
+ private handleMouseLeave(): void {
104
+ if (this.dragState !== null) {
105
+ return;
106
+ }
107
+ hideTooltip();
108
+ }
109
+
110
+ private measureUnitHeight(): number {
111
+ const rows = this.gridEl.querySelectorAll('[data-blok-table-row]');
112
+ const lastRow = rows[rows.length - 1] as HTMLElement | undefined;
113
+
114
+ return lastRow?.offsetHeight || 30;
115
+ }
116
+
117
+ private measureUnitWidth(): number {
118
+ const firstRow = this.gridEl.querySelector('[data-blok-table-row]');
119
+
120
+ if (!firstRow) {
121
+ return 100;
122
+ }
123
+
124
+ const cells = firstRow.querySelectorAll('[data-blok-table-cell]');
125
+ const lastCell = cells[cells.length - 1] as HTMLElement | undefined;
126
+
127
+ return lastCell?.offsetWidth || 100;
128
+ }
129
+
130
+ private handlePointerDown(e: PointerEvent): void {
131
+ this.dragState = {
132
+ startX: e.clientX,
133
+ startY: e.clientY,
134
+ unitWidth: this.measureUnitWidth(),
135
+ unitHeight: this.measureUnitHeight(),
136
+ addedRows: 0,
137
+ addedCols: 0,
138
+ pointerId: e.pointerId,
139
+ didDrag: false,
140
+ };
141
+
142
+ this.updateTooltip();
143
+
144
+ this.hitZone.setPointerCapture(e.pointerId);
145
+ this.hitZone.addEventListener('pointermove', this.boundPointerMove);
146
+ this.hitZone.addEventListener('pointerup', this.boundPointerUp);
147
+ }
148
+
149
+ private handlePointerMove(e: PointerEvent): void {
150
+ if (this.dragState === null) {
151
+ return;
152
+ }
153
+
154
+ const dx = e.clientX - this.dragState.startX;
155
+ const dy = e.clientY - this.dragState.startY;
156
+
157
+ if (!this.dragState.didDrag) {
158
+ const distance = Math.sqrt(dx * dx + dy * dy);
159
+
160
+ if (distance < DRAG_THRESHOLD) {
161
+ return;
162
+ }
163
+
164
+ this.dragState.didDrag = true;
165
+ document.body.style.cursor = 'nwse-resize';
166
+ document.body.style.userSelect = 'none';
167
+ this.onDragStart();
168
+ }
169
+
170
+ const { unitHeight, unitWidth } = this.dragState;
171
+
172
+ const targetRows = Math.trunc(dy / unitHeight);
173
+ const targetCols = Math.trunc(dx / unitWidth);
174
+
175
+ while (this.dragState.addedRows < targetRows) {
176
+ this.onAddRow();
177
+ this.dragState.addedRows++;
178
+ }
179
+
180
+ while (this.dragState.addedRows > targetRows && this.canRemoveLastRow()) {
181
+ this.onRemoveLastRow();
182
+ this.dragState.addedRows--;
183
+ }
184
+
185
+ while (this.dragState.addedCols < targetCols) {
186
+ this.onAddColumn();
187
+ this.dragState.addedCols++;
188
+ }
189
+
190
+ while (this.dragState.addedCols > targetCols && this.canRemoveLastColumn()) {
191
+ this.onRemoveLastColumn();
192
+ this.dragState.addedCols--;
193
+ }
194
+
195
+ this.updateTooltip();
196
+ }
197
+
198
+ private handlePointerUp(_e: PointerEvent): void {
199
+ if (this.dragState === null) {
200
+ return;
201
+ }
202
+
203
+ const { didDrag, pointerId } = this.dragState;
204
+
205
+ this.dragState = null;
206
+ hideTooltip();
207
+ this.hitZone.releasePointerCapture(pointerId);
208
+ this.hitZone.removeEventListener('pointermove', this.boundPointerMove);
209
+ this.hitZone.removeEventListener('pointerup', this.boundPointerUp);
210
+
211
+ if (!didDrag) {
212
+ if (this.onClickAdd) {
213
+ this.onClickAdd();
214
+ } else {
215
+ this.onAddRow();
216
+ this.onAddColumn();
217
+ }
218
+ } else {
219
+ document.body.style.cursor = '';
220
+ document.body.style.userSelect = '';
221
+ this.onDragEnd();
222
+ }
223
+ }
224
+
225
+ public setDisplay(visible: boolean): void {
226
+ this.hitZone.style.display = visible ? '' : 'none';
227
+ }
228
+
229
+ public setInteractive(interactive: boolean): void {
230
+ this.hitZone.style.pointerEvents = interactive ? 'auto' : 'none';
231
+ }
232
+
233
+ public destroy(): void {
234
+ this.hitZone.removeEventListener('mouseenter', this.boundMouseEnter);
235
+ this.hitZone.removeEventListener('mouseleave', this.boundMouseLeave);
236
+ this.hitZone.removeEventListener('pointerdown', this.boundPointerDown);
237
+ this.hitZone.removeEventListener('pointermove', this.boundPointerMove);
238
+ this.hitZone.removeEventListener('pointerup', this.boundPointerUp);
239
+ if (this.dragState?.didDrag) {
240
+ document.body.style.cursor = '';
241
+ document.body.style.userSelect = '';
242
+ }
243
+ this.dragState = null;
244
+ hideTooltip();
245
+ this.hitZone.remove();
246
+ }
247
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Declaration for jsdom (used in CLI for HTML conversion)
3
+ */
4
+ declare module 'jsdom' {
5
+ export class JSDOM {
6
+ constructor(html: string);
7
+ window: typeof globalThis;
8
+ }
9
+ }
package/bin/blok.mjs DELETED
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { readFileSync } from 'node:fs';
4
- import { run } from '../dist/cli.mjs';
5
-
6
- const version = process.env.npm_package_version
7
- || JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8')).version;
8
- const args = process.argv.slice(2);
9
-
10
- run(args, version);
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import '../dist/convert-html.mjs';