@griddle/vue 0.1.2 → 0.1.3

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.
@@ -191,6 +191,7 @@ function setSelection(next: Set<string>) {
191
191
  }
192
192
 
193
193
  const DEFAULT_DRAG_IGNORE = 'a, button, input, textarea, select, [contenteditable]';
194
+ const DRAG_START_THRESHOLD_PX = 12;
194
195
 
195
196
  const dragController = new DragController(props.api.grid);
196
197
  const groupDragController = new GroupDragController(props.api.grid);
@@ -249,18 +250,35 @@ interface DrawState {
249
250
  }
250
251
  const drawState = ref<DrawState | null>(null);
251
252
 
252
- function onTilePointerDown(e: PointerEvent, tile: Tile) {
253
- if (tile.draggable === false) return;
254
- if ((e.target as HTMLElement).dataset.griddleHandle) return;
253
+ interface PendingDragState {
254
+ pointerId: number;
255
+ tileId: string;
256
+ tileElement: HTMLDivElement;
257
+ startPointerX: number;
258
+ startPointerY: number;
259
+ mode: 'pin' | 'group' | 'single';
260
+ groupTileIds: string[];
261
+ }
262
+ let pendingDrag: PendingDragState | null = null;
263
+
264
+ function beginPendingDrag(pending: PendingDragState): boolean {
265
+ const tile = props.api.grid.getTile(pending.tileId);
266
+ if (!tile) return false;
267
+
268
+ // Capture only once movement proves this is a drag. Capturing on
269
+ // pointer-down retargets pointer-up away from editable descendants and turns
270
+ // ordinary clicks into drag start/end cycles.
271
+ try {
272
+ pending.tileElement.setPointerCapture(pending.pointerId);
273
+ } catch {
274
+ // The pointer may already have ended or the tile may have unmounted.
275
+ return false;
276
+ }
255
277
 
256
- const ignoreSelector = config.value.dragIgnoreFrom ?? DEFAULT_DRAG_IGNORE;
257
- if (ignoreSelector && (e.target as HTMLElement).closest(ignoreSelector)) return;
278
+ dragStartPointerX = pending.startPointerX;
279
+ dragStartPointerY = pending.startPointerY;
258
280
 
259
- const metaKey = e.metaKey || e.ctrlKey;
260
-
261
- // Out-of-flow tiles get free-pixel drag (no selection).
262
- if (config.value.enablePositioning && isOutOfFlow(tile)) {
263
- (e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId);
281
+ if (pending.mode === 'pin') {
264
282
  const layout = computeTileLayout({
265
283
  tile,
266
284
  config: config.value,
@@ -276,10 +294,61 @@ function onTilePointerDown(e: PointerEvent, tile: Tile) {
276
294
  pinDrag.value = {
277
295
  tileId: tile.id,
278
296
  startPinPx,
297
+ startPointerX: pending.startPointerX,
298
+ startPointerY: pending.startPointerY,
299
+ };
300
+ emit('dragStart', tile.id);
301
+ return true;
302
+ }
303
+
304
+ if (pending.mode === 'group') {
305
+ if (!groupDragController.start(pending.groupTileIds)) return false;
306
+ groupDrag.value = {
307
+ tileIds: pending.groupTileIds,
308
+ deltaX: 0,
309
+ deltaY: 0,
310
+ committedDcol: 0,
311
+ committedDrow: 0,
312
+ };
313
+ emit('dragStart', tile.id);
314
+ return true;
315
+ }
316
+
317
+ if (!dragController.start(tile.id)) return false;
318
+ drag.value = {
319
+ tileId: tile.id,
320
+ pickupCol: tile.col,
321
+ pickupRow: tile.row,
322
+ deltaX: 0,
323
+ deltaY: 0,
324
+ indicatorCol: tile.col,
325
+ indicatorRow: tile.row,
326
+ };
327
+ emit('dragStart', tile.id);
328
+ return true;
329
+ }
330
+
331
+ function onTilePointerDown(e: PointerEvent, tile: Tile) {
332
+ if (e.button !== 0) return;
333
+ if (tile.draggable === false) return;
334
+ if ((e.target as HTMLElement).dataset.griddleHandle) return;
335
+
336
+ const ignoreSelector = config.value.dragIgnoreFrom ?? DEFAULT_DRAG_IGNORE;
337
+ if (ignoreSelector && (e.target as HTMLElement).closest(ignoreSelector)) return;
338
+
339
+ const metaKey = e.metaKey || e.ctrlKey;
340
+
341
+ // Out-of-flow tiles get free-pixel drag (no selection).
342
+ if (config.value.enablePositioning && isOutOfFlow(tile)) {
343
+ pendingDrag = {
344
+ pointerId: e.pointerId,
345
+ tileId: tile.id,
346
+ tileElement: e.currentTarget as HTMLDivElement,
279
347
  startPointerX: e.clientX,
280
348
  startPointerY: e.clientY,
349
+ mode: 'pin',
350
+ groupTileIds: [],
281
351
  };
282
- emit('dragStart', tile.id);
283
352
  e.stopPropagation();
284
353
  return;
285
354
  }
@@ -304,42 +373,20 @@ function onTilePointerDown(e: PointerEvent, tile: Tile) {
304
373
  setSelection(new Set([tile.id]));
305
374
  }
306
375
 
307
- // Capture pointer only when starting a drag.
308
- (e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId);
309
-
310
- // Group drag if 2+ tiles selected including this one.
376
+ // Defer capture and drag state until the pointer crosses the movement
377
+ // threshold. A stationary pointer-down/up remains a normal click, including
378
+ // when it starts inside contenteditable tile content.
311
379
  const effectiveSelection = tileIsSelected ? selection.value : new Set([tile.id]);
312
- if (effectiveSelection.size > 1) {
313
- const ids = Array.from(effectiveSelection);
314
- if (!groupDragController.start(ids)) return;
315
- dragStartPointerX = e.clientX;
316
- dragStartPointerY = e.clientY;
317
- groupDrag.value = {
318
- tileIds: ids,
319
- deltaX: 0,
320
- deltaY: 0,
321
- committedDcol: 0,
322
- committedDrow: 0,
323
- };
324
- emit('dragStart', tile.id);
325
- e.stopPropagation();
326
- return;
327
- }
328
-
329
- // Single tile drag.
330
- if (!dragController.start(tile.id)) return;
331
- dragStartPointerX = e.clientX;
332
- dragStartPointerY = e.clientY;
333
- drag.value = {
380
+ const groupTileIds = Array.from(effectiveSelection);
381
+ pendingDrag = {
382
+ pointerId: e.pointerId,
334
383
  tileId: tile.id,
335
- pickupCol: tile.col,
336
- pickupRow: tile.row,
337
- deltaX: 0,
338
- deltaY: 0,
339
- indicatorCol: tile.col,
340
- indicatorRow: tile.row,
384
+ tileElement: e.currentTarget as HTMLDivElement,
385
+ startPointerX: e.clientX,
386
+ startPointerY: e.clientY,
387
+ mode: groupTileIds.length > 1 ? 'group' : 'single',
388
+ groupTileIds,
341
389
  };
342
- emit('dragStart', tile.id);
343
390
  e.stopPropagation();
344
391
  }
345
392
 
@@ -395,6 +442,20 @@ function onPointerMove(e: PointerEvent) {
395
442
  }
396
443
  return;
397
444
  }
445
+
446
+ const pending = pendingDrag;
447
+ if (pending && e.pointerId === pending.pointerId) {
448
+ const distance = Math.hypot(
449
+ e.clientX - pending.startPointerX,
450
+ e.clientY - pending.startPointerY,
451
+ );
452
+ if (distance <= DRAG_START_THRESHOLD_PX) return;
453
+
454
+ pendingDrag = null;
455
+ if (!beginPendingDrag(pending)) return;
456
+ e.preventDefault();
457
+ }
458
+
398
459
  const pd = pinDrag.value;
399
460
  const gd = groupDrag.value;
400
461
  const d = drag.value;
@@ -472,7 +533,11 @@ function syncTiles() {
472
533
  props.api.version.value++;
473
534
  }
474
535
 
475
- function onPointerUp() {
536
+ function onPointerUp(e: PointerEvent) {
537
+ if (pendingDrag?.pointerId === e.pointerId) {
538
+ pendingDrag = null;
539
+ return;
540
+ }
476
541
  if (drawState.value) {
477
542
  const ds = drawState.value;
478
543
  const col = Math.min(ds.anchorCol, ds.currentCol);
@@ -554,6 +619,7 @@ onMounted(() => {
554
619
  window.addEventListener('keydown', onKeyDown);
555
620
  });
556
621
  onBeforeUnmount(() => {
622
+ pendingDrag = null;
557
623
  const el = scrollEl.value;
558
624
  if (el) el.removeEventListener('scroll', updateViewport);
559
625
  ro?.disconnect();