@fc3/mmcadi 0.1.50 → 0.1.52

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.
@@ -3,634 +3,361 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ const time_1 = require("@fc3/time");
6
7
  const errors_1 = require("@fc3/errors");
7
8
  const action_type_1 = __importDefault(require("./../../enum/action-type.js"));
8
- const emoji_1 = __importDefault(require("./../../enum/emoji.js"));
9
9
  const get_index_path_for_element_1 = __importDefault(require("./../utility/get-index-path-for-element.js"));
10
10
  class InteractionHelper {
11
11
  constructor() {
12
- this.drag_active = false;
13
- this.drag_origin_block = null;
14
- this.drag_placeholder = null;
15
- this.drag_ghost = null;
16
- this.drag_start_y = null;
17
- this.drag_pointer_offset_y = null;
18
- this.drag_press_timer = null;
19
- this.swipe_active = false;
20
- this.swipe_block = null;
21
- this.swipe_start_x = null;
22
- this.swipe_start_y = null;
23
- this.swipe_content = null;
24
- this.swipe_menu_width = 0;
25
- this.open_swipe_block = null;
26
- this.open_swipe_content = null;
12
+ this.drag_start_coordinate = null;
13
+ this.drag_delay_timer = null;
27
14
  }
28
15
  attach() {
29
- document.addEventListener('mousedown', (event) => {
30
- this.onPressStart(event);
31
- }, true);
32
- document.addEventListener('touchstart', (event) => {
33
- this.onPressStart(event);
16
+ document.addEventListener('pointerdown', (event) => {
17
+ this.handlePointerDown(event);
18
+ }, { capture: true });
19
+ document.addEventListener('pointermove', (event) => {
20
+ this.handlePointerMove(event);
34
21
  }, { capture: true, passive: false });
35
- document.addEventListener('mousemove', (event) => {
36
- this.onPointerMove(event);
37
- }, true);
38
- document.addEventListener('touchmove', (event) => {
39
- this.onPointerMove(event);
40
- }, { capture: true, passive: false });
41
- document.addEventListener('mouseup', (event) => {
42
- this.onPressEnd(event);
43
- }, true);
44
- document.addEventListener('touchend', (event) => {
45
- this.onPressEnd(event);
46
- }, true);
47
- // Close any open swipe when touching outside
48
- document.addEventListener('touchstart', (event) => {
49
- this.onGlobalTouchStart(event);
50
- }, { capture: true, passive: true });
51
- // Prevent native browser drag for anchors/images inside blocks so our
52
- // custom drag logic receives mousemove events instead of dragover.
22
+ document.addEventListener('pointerup', (event) => {
23
+ this.handlePointerUp(event);
24
+ }, { capture: true });
25
+ document.addEventListener('pointercancel', (event) => {
26
+ this.handlePointerCancel(event);
27
+ }, { capture: true });
53
28
  document.addEventListener('dragstart', (event) => {
54
- this.onNativeDragStart(event);
55
- }, true);
29
+ this.handleNativeDragStart(event);
30
+ }, { capture: true });
56
31
  }
57
32
  isDragging() {
58
- return this.drag_active;
59
- }
60
- getBlockElements() {
61
- const elements = document.querySelectorAll('section.block');
62
- return Array.from(elements);
63
- }
64
- getSiblingBlocks(container, exclude) {
65
- const children = Array.from(container.children);
66
- return children.filter((element) => {
67
- if (element === exclude) {
68
- return false;
69
- }
70
- return element.classList.contains('block');
71
- });
33
+ const elements = Array.from(document.querySelectorAll('.dragging'));
34
+ return elements.length > 0;
72
35
  }
73
- onPressStart(event) {
74
- const target = event.target;
36
+ handlePointerDown(event) {
37
+ const { target } = event;
75
38
  if (target === null) {
76
39
  return;
77
40
  }
78
- // Allow starting drag from anchors (e.g., Folder blocks wrap content in <a>).
79
- // Still block on true form controls to avoid interfering with inputs/buttons.
80
- if (target.closest('button, input, textarea, select, label, form')) {
81
- return;
41
+ const target_element = target;
42
+ let current_element = target_element;
43
+ while (current_element) {
44
+ const attribute = current_element.getAttribute('data-role');
45
+ if (attribute === 'block_activation') {
46
+ break;
47
+ }
48
+ // Tricky. Bail out early if we detect an anchor element in the event
49
+ // ancestry that is NOT an all-block-encompassing link (eg, a folder block).
50
+ if (current_element instanceof HTMLAnchorElement) {
51
+ return;
52
+ }
53
+ current_element = current_element.parentNode;
54
+ if (!(current_element instanceof HTMLElement)) {
55
+ break;
56
+ }
82
57
  }
83
- const block = target.closest('section.block');
58
+ const block = target_element.closest('section.block');
84
59
  if (block === null) {
85
60
  return;
86
61
  }
87
- // Avoid native link-drag by preventing default on mousedown for anchors
88
- if (event instanceof MouseEvent) {
89
- const anchor = target.closest('a[href]');
90
- if (anchor) {
91
- event.preventDefault();
92
- }
62
+ if (target_element.closest('button, input, textarea, select, label, form')) {
63
+ return;
93
64
  }
94
- const touch_event = event;
95
- const is_touch = touch_event.type === 'touchstart';
96
- const point = this.getEventPoint(event);
97
- if (point === null) {
65
+ const coordinate = this.getEventCoordinate(event);
66
+ if (coordinate === null) {
98
67
  return;
99
68
  }
100
- this.drag_origin_block = block;
101
- this.drag_start_y = point.clientY;
102
- if (is_touch) {
103
- this.swipe_block = block;
104
- this.swipe_start_x = point.clientX;
105
- this.swipe_start_y = point.clientY;
69
+ this.drag_start_coordinate = coordinate;
70
+ block.classList.add('drag-candidate');
71
+ this.scheduleDelayedDrag();
72
+ }
73
+ scheduleDelayedDrag() {
74
+ this.cancelDelayedDrag();
75
+ const delay = time_1.TimeInterval.ONE_SECOND;
76
+ this.drag_delay_timer = setTimeout(this.beginDelayedDrag.bind(this), delay);
77
+ }
78
+ cancelDelayedDrag() {
79
+ if (this.drag_delay_timer !== null) {
80
+ clearTimeout(this.drag_delay_timer);
106
81
  }
107
- const threshold_ms = is_touch ? 350 : 200;
108
- this.clearPressTimer();
109
- this.drag_press_timer = window.setTimeout(() => {
110
- this.beginDrag(point.clientY);
111
- }, threshold_ms);
112
82
  }
113
- onNativeDragStart(event) {
83
+ removeClassFromAllElements(class_name) {
84
+ const raw_elements = document.querySelectorAll(`.${class_name}`);
85
+ const elements = Array.from(raw_elements);
86
+ elements.forEach((element) => {
87
+ element.classList.remove(class_name);
88
+ });
89
+ return elements;
90
+ }
91
+ beginDelayedDrag() {
92
+ const raw_candidates = document.querySelectorAll('.drag-candidate');
93
+ const candidates = Array.from(raw_candidates);
94
+ candidates.forEach((candidate) => {
95
+ const ghost = candidate.cloneNode(true);
96
+ const index_path = (0, get_index_path_for_element_1.default)(candidate);
97
+ ghost.classList.add('ghost');
98
+ ghost.setAttribute('data-for', index_path);
99
+ candidate.classList.remove('drag-candidate');
100
+ const { parentNode } = candidate;
101
+ if (parentNode === null) {
102
+ throw new errors_1.InvariantViolation(`
103
+ Expected drag candidate to be located in a parent, but it was null
104
+ `);
105
+ }
106
+ const rect = candidate.getBoundingClientRect();
107
+ Object.assign(candidate.style, {
108
+ top: `${rect.top}px`,
109
+ left: `${rect.left}px`,
110
+ width: `${rect.width}px`,
111
+ height: `${rect.height}px`
112
+ });
113
+ candidate.setAttribute('data-drag-start-x', rect.left.toString());
114
+ candidate.setAttribute('data-drag-start-y', rect.top.toString());
115
+ candidate.classList.add('dragging');
116
+ parentNode.insertBefore(ghost, candidate);
117
+ });
118
+ }
119
+ handleNativeDragStart(event) {
114
120
  const target = event.target;
115
121
  if (!target) {
116
122
  return;
117
123
  }
118
- if (target.closest('section.block')) {
124
+ const closest_block = target.closest('section.block');
125
+ if (closest_block !== null) {
119
126
  event.preventDefault();
120
127
  }
121
128
  }
122
- onPointerMove(event) {
123
- const point = this.getEventPoint(event);
124
- if (point === null) {
129
+ handlePointerMove(event) {
130
+ const current_coordinate = this.getEventCoordinate(event);
131
+ const start_coordinate = this.getDragStartCoordinate();
132
+ if (start_coordinate === null || current_coordinate === null) {
125
133
  return;
126
134
  }
127
- const touch_event = event;
128
- const { touches } = touch_event;
129
- const is_touch = touches !== undefined;
130
- // Touch-only swipe detection (left swipe reveals actions)
131
- if (is_touch && this.swipe_block) {
132
- if (this.swipe_start_x === null || this.swipe_start_y === null) {
133
- throw new errors_1.InvariantViolation('Swipe start coordinates were not initialized');
134
- }
135
- const start_x = this.swipe_start_x;
136
- const start_y = this.swipe_start_y;
137
- const delta_x = point.clientX - start_x;
138
- const delta_y = point.clientY - start_y;
139
- if (!this.swipe_active) {
140
- const abs_x = Math.abs(delta_x);
141
- const abs_y = Math.abs(delta_y);
142
- // Initiate swipe if horizontal dominates and exceeds threshold
143
- if (abs_x > 12 && abs_x > abs_y) {
144
- // Cancel pending drag long-press
145
- this.clearPressTimer();
146
- // Close any previously open swipe on another block
147
- this.closeOpenSwipeMenu(this.swipe_block);
148
- this.swipe_active = true;
149
- this.beginSwipe(this.swipe_block);
135
+ const { target } = event;
136
+ if (target !== null) {
137
+ const target_element = target;
138
+ const block = target_element.closest('section.block');
139
+ if (block) {
140
+ if (!block.classList.contains('dragging')) {
141
+ block.classList.add('drag-candidate');
150
142
  }
151
143
  }
152
- if (this.swipe_active) {
153
- event.preventDefault();
154
- const content_element = this.swipe_content;
155
- const menu_width = this.swipe_menu_width;
156
- // Only allow left swipe (negative delta_x)
157
- const translate_x = Math.max(-menu_width, Math.min(0, delta_x));
158
- content_element.style.transition = '';
159
- content_element.style.transform = `translateX(${translate_x}px)`;
160
- // Don't fall through to drag while swiping:
161
- return;
162
- }
163
- }
164
- if (!this.drag_active && this.drag_origin_block && this.drag_start_y !== null) {
165
- const delta_y = Math.abs(point.clientY - this.drag_start_y);
166
- if (delta_y > 8 && this.drag_press_timer !== null) {
167
- this.beginDrag(point.clientY);
168
- }
169
- }
170
- if (!this.drag_active) {
171
- return;
172
- }
173
- event.preventDefault();
174
- // FLIP: capture positions before DOM change (within current container)
175
- const origin = this.drag_origin_block;
176
- let container = null;
177
- if (this.drag_placeholder !== null) {
178
- container = this.drag_placeholder.parentElement;
179
- }
180
- if (container === null && origin !== null) {
181
- container = origin.parentElement;
182
144
  }
183
- if (container === null) {
184
- return;
185
- }
186
- const blocks_before = this.getSiblingBlocks(container, origin);
187
- const preceding_rects = new Map();
188
- blocks_before.forEach((element) => {
189
- const rect = element.getBoundingClientRect();
190
- preceding_rects.set(element, rect);
145
+ const { x: start_x, y: start_y } = start_coordinate;
146
+ const { x: current_x, y: current_y } = current_coordinate;
147
+ const distance = Math.hypot(current_x - start_x, current_y - start_y);
148
+ if (distance > 25) {
149
+ this.removeClassFromAllElements('drag-candidate');
150
+ }
151
+ this.scheduleDelayedDrag();
152
+ const raw_targets = document.querySelectorAll('.dragging');
153
+ const drag_target_blocks = Array.from(raw_targets);
154
+ drag_target_blocks.forEach((target_block) => {
155
+ this.translateDragTargetBlock(target_block, current_coordinate);
191
156
  });
192
- // Move placeholder to its new spot
193
- this.updatePlaceholderPosition(point.clientY);
194
- // Update ghost to follow pointer (glued to finger/mouse)
195
- if (this.drag_ghost && this.drag_pointer_offset_y !== null) {
196
- const top_position = point.clientY - this.drag_pointer_offset_y;
197
- this.drag_ghost.style.top = `${top_position}px`;
157
+ if (drag_target_blocks.length > 0) {
158
+ event.preventDefault();
198
159
  }
199
- // Capture last positions and animate
200
- const blocks_after = this.getSiblingBlocks(container, origin);
201
- this.animateWithFlip(preceding_rects, blocks_after);
202
160
  }
203
- onPressEnd(event) {
204
- this.clearPressTimer();
205
- // Finalize swipe if active
206
- if (this.swipe_active) {
207
- const content = this.swipe_content;
208
- const style = window.getComputedStyle(content);
209
- let current_x = 0;
210
- try {
211
- const css_matrix = new WebKitCSSMatrix(style.transform);
212
- current_x = css_matrix.m41;
213
- }
214
- catch {
215
- const translate_match = style.transform.match(/translateX\(([-0-9.]+)px\)/);
216
- if (translate_match) {
217
- current_x = parseFloat(translate_match[1]);
161
+ translateDragTargetBlock(block, current_coordinate) {
162
+ const x_attribute = block.getAttribute('data-drag-start-x');
163
+ const y_attribute = block.getAttribute('data-drag-start-y');
164
+ if (x_attribute === null || y_attribute === null) {
165
+ throw new errors_1.InvariantViolation(`
166
+ Drag coordinate attributes were invalid:
167
+ x: ${x_attribute}
168
+ y: ${y_attribute}
169
+ `);
170
+ }
171
+ const parsed_x = parseInt(x_attribute);
172
+ const parsed_y = parseInt(y_attribute);
173
+ if (isNaN(parsed_x) || isNaN(parsed_y)) {
174
+ throw new errors_1.InvariantViolation(`
175
+ Drag coordinate attributes were invalid:
176
+ x: ${x_attribute}
177
+ y: ${y_attribute}
178
+ `);
179
+ }
180
+ const start_coordinate = this.getDragStartCoordinate();
181
+ if (start_coordinate === null) {
182
+ throw new errors_1.InvariantViolation(`
183
+ Tried to translate drag target, but drag start coordinate was null
184
+ `);
185
+ }
186
+ const { x: start_x, y: start_y } = start_coordinate;
187
+ const { x: current_x, y: current_y } = current_coordinate;
188
+ const x_delta = current_x - start_x;
189
+ const y_delta = current_y - start_y;
190
+ const new_x = parsed_x + x_delta;
191
+ const new_y = parsed_y + y_delta;
192
+ block.style.left = `${new_x}px`;
193
+ block.style.top = `${new_y}px`;
194
+ const block_rect = block.getBoundingClientRect();
195
+ const previous_block = this.getPreviousBlock(block);
196
+ const next_block = this.getNextBlock(block);
197
+ const ghost = this.getGhostForBlock(block);
198
+ let swapped = false;
199
+ if (previous_block !== null) {
200
+ const previous_rect = previous_block.getBoundingClientRect();
201
+ const previous_block_y_threshold = previous_rect.top + (previous_rect.height * 0.4);
202
+ if (block_rect.top < previous_block_y_threshold) {
203
+ swapped = true;
204
+ const next_sibling = ghost.nextElementSibling;
205
+ previous_block.replaceWith(ghost);
206
+ if (next_sibling) {
207
+ next_sibling.before(previous_block);
218
208
  }
219
209
  }
220
- const width = this.swipe_menu_width;
221
- const open = Math.abs(current_x) > width / 2;
222
- content.style.transition = 'transform 150ms ease';
223
- content.style.transform = open ? `translateX(${-width}px)` : 'translateX(0)';
224
- if (!open) {
225
- this.teardownSwipe();
226
- }
227
- if (open) {
228
- this.open_swipe_block = this.swipe_block;
229
- this.open_swipe_content = content;
230
- }
231
- this.swipe_active = false;
232
- return;
233
- }
234
- if (!this.drag_active) {
235
- this.resetDragState();
236
- return;
237
210
  }
238
- event.preventDefault();
239
- const origin = this.drag_origin_block;
240
- const placeholder = this.drag_placeholder;
241
- const source_index_path = (0, get_index_path_for_element_1.default)(origin);
242
- const target_index_path = this.computeTargetIndexPath(placeholder);
243
- this.cleanupDragElements();
244
- if (target_index_path === null || target_index_path === source_index_path) {
245
- this.resetDragState();
246
- return;
247
- }
248
- const editing = new URL(window.location.href).searchParams.get('editing') === 'true';
249
- this.postReposition(source_index_path, target_index_path, editing)
250
- .then((new_index_path) => {
251
- if (new_index_path) {
252
- const url = new URL(window.location.href);
253
- url.searchParams.set('index_path', new_index_path);
254
- history.replaceState({}, '', url.toString());
255
- }
256
- const blocks = this.getBlockElements();
257
- blocks.forEach((element) => {
258
- element.classList.remove('selected');
259
- });
260
- origin.classList.add('selected');
261
- // Rewrite data-index-path for all top-level blocks to reflect new order
262
- this.updateTopLevelIndexPaths();
263
- })
264
- .finally(() => {
265
- this.resetDragState();
266
- });
267
- }
268
- beginSwipe(block) {
269
- // Ensure wrapper and actions exist
270
- const result = this.ensureSwipeUI(block);
271
- const target = result.target;
272
- const menu = result.menu;
273
- const menu_width = result.menu_width;
274
- this.swipe_content = target;
275
- this.swipe_menu_width = menu_width;
276
- if (menu) {
277
- menu.style.display = 'flex';
278
- menu.style.opacity = '1';
279
- }
280
- }
281
- closeOpenSwipeMenu(except) {
282
- if (!this.open_swipe_content) {
283
- return;
284
- }
285
- if (except && this.open_swipe_block === except) {
286
- return;
287
- }
288
- const content = this.open_swipe_content;
289
- content.style.transition = 'transform 150ms ease';
290
- content.style.transform = 'translateX(0)';
291
- const clear_callback = () => {
292
- content.removeEventListener('transitionend', clear_callback);
293
- if (this.open_swipe_block) {
294
- const menu_element = this.getSwipeMenuFor(this.open_swipe_block);
295
- if (menu_element && menu_element.parentElement) {
296
- menu_element.parentElement.removeChild(menu_element);
211
+ if (next_block !== null && swapped === false) {
212
+ const next_rect = next_block.getBoundingClientRect();
213
+ const next_block_y_threshold = next_rect.top + (next_rect.height * 0.6);
214
+ if (block_rect.bottom > next_block_y_threshold) {
215
+ const next_sibling = ghost.nextElementSibling === next_block ? ghost : ghost.nextElementSibling;
216
+ next_block.replaceWith(ghost);
217
+ if (next_sibling) {
218
+ next_sibling.before(next_block);
297
219
  }
298
220
  }
299
- this.open_swipe_block = null;
300
- this.open_swipe_content = null;
301
- };
302
- content.addEventListener('transitionend', clear_callback);
303
- }
304
- teardownSwipe() {
305
- if (!this.swipe_block || !this.swipe_content) {
306
- this.resetSwipeState();
307
- return;
308
- }
309
- this.swipe_content.style.transition = '';
310
- this.swipe_content.style.transform = 'translateX(0)';
311
- const menu = this.getSwipeMenuFor(this.swipe_block);
312
- if (menu && menu.parentElement) {
313
- menu.parentElement.removeChild(menu);
314
221
  }
315
- this.resetSwipeState();
316
222
  }
317
- onGlobalTouchStart(event) {
318
- if (!this.open_swipe_block || !this.open_swipe_content) {
319
- return;
320
- }
321
- const target = event.target;
322
- if (!target) {
323
- return;
324
- }
325
- // Ignore touches inside the open block or its action menu
326
- const menu = this.getSwipeMenuFor(this.open_swipe_block);
327
- const closest_block = target.closest('section.block');
328
- const swipe_actions = target.closest('.swipe-actions');
329
- if (closest_block === this.open_swipe_block || swipe_actions === menu) {
330
- return;
331
- }
332
- this.closeOpenSwipeMenu();
223
+ getGhostForBlock(block) {
224
+ const index_path = (0, get_index_path_for_element_1.default)(block);
225
+ const ghost = document.querySelector(`[data-for="${index_path}"]`);
226
+ if (ghost === null) {
227
+ throw new errors_1.InvariantViolation(`
228
+ Unable to find ghost for block with index path ${index_path}
229
+ `);
230
+ }
231
+ return ghost;
333
232
  }
334
- getSwipeMenuFor(block) {
335
- const wrapper = block.closest('main .section-wrapper');
336
- if (!wrapper || !block.id) {
337
- return null;
233
+ getPreviousBlock(block) {
234
+ const parent_node = block.parentNode;
235
+ const siblings = Array.from(parent_node.children);
236
+ const ghost = this.getGhostForBlock(block);
237
+ let index = siblings.indexOf(ghost);
238
+ if (index === -1) {
239
+ throw new errors_1.InvariantViolation('Unable to locate ghost within siblings');
240
+ }
241
+ while (index--) {
242
+ const sibling = siblings[index];
243
+ if (sibling === block) {
244
+ continue;
245
+ }
246
+ const { classList } = sibling;
247
+ if (!classList.contains('block')) {
248
+ continue;
249
+ }
250
+ if (classList.contains('ghost')) {
251
+ continue;
252
+ }
253
+ return sibling;
338
254
  }
339
- return wrapper.querySelector(`.swipe-actions[data-for="${block.id}"]`);
255
+ return null;
340
256
  }
341
- resetSwipeState() {
342
- this.swipe_block = null;
343
- this.swipe_start_x = null;
344
- this.swipe_start_y = null;
345
- this.swipe_content = null;
346
- this.swipe_menu_width = 0;
347
- }
348
- ensureSwipeUI(block) {
349
- const target = block;
350
- target.style.willChange = 'transform';
351
- target.style.transform = 'translateX(0)';
352
- target.style.transition = '';
353
- target.style.position = target.style.position || 'relative';
354
- target.style.zIndex = '1';
355
- const wrapper = block.closest('main .section-wrapper');
356
- let menu = null;
357
- if (wrapper) {
358
- if (getComputedStyle(wrapper).position === 'static') {
359
- wrapper.style.position = 'relative';
257
+ getNextBlock(block) {
258
+ const parent_node = block.parentNode;
259
+ const siblings = Array.from(parent_node.children);
260
+ const ghost = this.getGhostForBlock(block);
261
+ let index = siblings.indexOf(ghost);
262
+ if (index === -1) {
263
+ throw new errors_1.InvariantViolation('Unable to locate ghost within siblings');
264
+ }
265
+ while (index < siblings.length) {
266
+ index++;
267
+ const sibling = siblings[index];
268
+ if (sibling === block) {
269
+ continue;
360
270
  }
361
- let existing = wrapper.querySelector(`.swipe-actions[data-for="${block.id}"]`);
362
- if (!existing) {
363
- existing = document.createElement('div');
364
- existing.className = 'swipe-actions';
365
- existing.setAttribute('data-for', block.id);
366
- existing.style.position = 'absolute';
367
- existing.style.right = '0';
368
- existing.style.display = 'none';
369
- existing.style.opacity = '0';
370
- existing.style.gap = '8px';
371
- existing.style.alignItems = 'stretch';
372
- existing.style.padding = '0 8px';
373
- existing.style.background = 'transparent';
374
- existing.style.zIndex = '0';
375
- const edit_button = document.createElement('button');
376
- const delete_button = document.createElement('button');
377
- const add_button = document.createElement('button');
378
- edit_button.textContent = emoji_1.default.GEAR;
379
- edit_button.setAttribute('aria-label', 'Edit');
380
- edit_button.style.background = '#f0ad4e';
381
- edit_button.style.color = '#000';
382
- edit_button.style.border = 'none';
383
- edit_button.style.padding = '0';
384
- edit_button.style.fontSize = '20px';
385
- edit_button.style.width = '48px';
386
- edit_button.style.minWidth = '48px';
387
- edit_button.style.display = 'flex';
388
- edit_button.style.alignItems = 'center';
389
- edit_button.style.justifyContent = 'center';
390
- edit_button.style.cursor = 'pointer';
391
- delete_button.textContent = emoji_1.default.RED_X;
392
- delete_button.setAttribute('aria-label', 'Delete');
393
- delete_button.style.background = '#e74c3c';
394
- delete_button.style.color = '#fff';
395
- delete_button.style.border = 'none';
396
- delete_button.style.padding = '0';
397
- delete_button.style.fontSize = '20px';
398
- delete_button.style.width = '48px';
399
- delete_button.style.minWidth = '48px';
400
- delete_button.style.display = 'flex';
401
- delete_button.style.alignItems = 'center';
402
- delete_button.style.justifyContent = 'center';
403
- delete_button.style.cursor = 'pointer';
404
- add_button.textContent = emoji_1.default.PLUS_SIGN;
405
- add_button.setAttribute('aria-label', 'Add After');
406
- add_button.style.background = '#27ae60';
407
- add_button.style.color = '#fff';
408
- add_button.style.border = 'none';
409
- add_button.style.padding = '0';
410
- add_button.style.fontSize = '20px';
411
- add_button.style.width = '48px';
412
- add_button.style.minWidth = '48px';
413
- add_button.style.display = 'flex';
414
- add_button.style.alignItems = 'center';
415
- add_button.style.justifyContent = 'center';
416
- add_button.style.cursor = 'pointer';
417
- existing.appendChild(edit_button);
418
- existing.appendChild(delete_button);
419
- existing.appendChild(add_button);
420
- wrapper.appendChild(existing);
421
- const edit_handler = (event) => {
422
- event.preventDefault();
423
- event.stopPropagation();
424
- this.navigateToBlockLink(block, 'edit');
425
- };
426
- const delete_handler = (event) => {
427
- event.preventDefault();
428
- event.stopPropagation();
429
- this.navigateToBlockLink(block, 'delete');
430
- };
431
- const add_handler = (event) => {
432
- event.preventDefault();
433
- event.stopPropagation();
434
- this.navigateToAddAfter(block);
435
- };
436
- // Prevent global touchstart from closing when interacting with menu
437
- existing.addEventListener('touchstart', (event) => {
438
- event.stopPropagation();
439
- });
440
- existing.addEventListener('mousedown', (event) => {
441
- event.stopPropagation();
442
- });
443
- edit_button.addEventListener('click', edit_handler);
444
- edit_button.addEventListener('touchend', edit_handler);
445
- delete_button.addEventListener('click', delete_handler);
446
- delete_button.addEventListener('touchend', delete_handler);
447
- add_button.addEventListener('click', add_handler);
448
- add_button.addEventListener('touchend', add_handler);
271
+ const { classList } = sibling;
272
+ if (!classList.contains('block')) {
273
+ continue;
449
274
  }
450
- menu = existing;
451
- // align to block
452
- const top = block.offsetTop;
453
- const height = block.offsetHeight;
454
- menu.style.top = `${top}px`;
455
- menu.style.height = `${height}px`;
456
- }
457
- let menu_width = 160;
458
- if (menu) {
459
- const prevDisplay = menu.style.display;
460
- const prevOpacity = menu.style.opacity;
461
- menu.style.display = 'flex';
462
- menu.style.opacity = '0';
463
- menu_width = menu.offsetWidth || 160;
464
- menu.style.display = prevDisplay;
465
- menu.style.opacity = prevOpacity;
275
+ if (classList.contains('ghost')) {
276
+ continue;
277
+ }
278
+ return sibling;
466
279
  }
467
- this.enforceTimeVisibility(block);
468
- return {
469
- target,
470
- menu,
471
- menu_width
472
- };
280
+ return null;
473
281
  }
474
- navigateToAddAfter(block) {
475
- const current = (0, get_index_path_for_element_1.default)(block);
476
- // Compute after index by incrementing the last segment
477
- const parts = current.split('.').map((part) => {
478
- return parseInt(part, 10);
479
- });
480
- const has_invalid_number = parts.some((value) => {
481
- return isNaN(value);
282
+ handlePointerUp(event) {
283
+ this.stopDragging();
284
+ }
285
+ handlePointerCancel(event) {
286
+ this.stopDragging();
287
+ }
288
+ stopDragging() {
289
+ this.showFullscreenBlockingOverlay();
290
+ this.stopDraggingAsync().then(() => {
291
+ this.hideFullscreenBlockingOverlay();
482
292
  });
483
- if (parts.length === 0 || has_invalid_number) {
484
- throw new errors_1.InvariantViolation('Invalid index_path for Add After');
485
- }
486
- const last = parts.pop();
487
- if (last === undefined) {
488
- throw new errors_1.InvariantViolation('Unable to compute next index for Add After');
489
- }
490
- parts.push(last + 1);
491
- const index_path = parts.join('.');
492
- const url = new URL(window.location.href);
493
- const editing = url.searchParams.get('editing') === 'true';
494
- const path = window.location.pathname;
495
- const actionUrl = new URL('/actions', window.location.origin);
496
- actionUrl.searchParams.set('path', path);
497
- actionUrl.searchParams.set('editing', String(editing));
498
- actionUrl.searchParams.set('action_type', 'add_block');
499
- actionUrl.searchParams.set('index_path', index_path);
500
- window.location.href = actionUrl.toString();
501
293
  }
502
- enforceTimeVisibility(block) {
503
- const shows_time = block.classList.contains('with-time');
504
- const time_element = block.querySelector(':scope time');
505
- if (!time_element) {
506
- return;
507
- }
508
- if (!shows_time) {
509
- time_element.style.display = 'none';
510
- }
511
- else {
512
- time_element.style.display = '';
513
- }
294
+ showFullscreenBlockingOverlay() {
514
295
  }
515
- navigateToBlockLink(block, type) {
516
- const block_id = block.id;
517
- const link_id = `${block_id}-${type}`;
518
- const link_element = document.getElementById(link_id);
519
- if (!link_element) {
520
- return;
521
- }
522
- const href = link_element.getAttribute('href');
523
- if (href) {
524
- window.location.href = href;
525
- }
296
+ hideFullscreenBlockingOverlay() {
526
297
  }
527
- beginDrag(current_y) {
528
- if (this.drag_active) {
529
- return;
530
- }
531
- const block = this.drag_origin_block;
532
- if (!block) {
533
- return;
534
- }
535
- this.drag_active = true;
536
- // Reduce text selection and improve UX while dragging
537
- document.body.style.userSelect = 'none';
538
- document.body.style.cursor = 'grabbing';
539
- const rect = block.getBoundingClientRect();
540
- const ghost = block.cloneNode(true);
541
- ghost.style.position = 'fixed';
542
- ghost.style.top = `${rect.top}px`;
543
- ghost.style.left = `${rect.left}px`;
544
- ghost.style.width = `${rect.width}px`;
545
- ghost.style.pointerEvents = 'none';
546
- ghost.style.opacity = '0.9';
547
- ghost.style.boxShadow = '0 8px 16px rgba(0,0,0,0.2)';
548
- ghost.style.zIndex = '9999';
549
- // No transition on top; keep glued to pointer
550
- document.body.appendChild(ghost);
551
- this.drag_ghost = ghost;
552
- // Record pointer offset within the element so the ghost aligns under finger
553
- this.drag_pointer_offset_y = current_y - rect.top;
554
- // Replace the original block's layout with a placeholder in the same spot
555
- const placeholder = document.createElement('div');
556
- placeholder.style.height = `${rect.height}px`;
557
- placeholder.style.margin = getComputedStyle(block).margin;
558
- placeholder.style.border = '2px dashed #888';
559
- placeholder.style.borderRadius = '4px';
560
- placeholder.style.boxSizing = 'border-box';
561
- if (block.parentElement !== null) {
562
- block.parentElement.insertBefore(placeholder, block);
298
+ async stopDraggingAsync() {
299
+ this.cancelDelayedDrag();
300
+ this.removeClassFromAllElements('drag-candidate');
301
+ const dragging_elements = this.removeClassFromAllElements('dragging');
302
+ let index = 0;
303
+ while (index < dragging_elements.length) {
304
+ const element = dragging_elements[index++];
305
+ const ghost = this.getGhostForBlock(element);
306
+ const source_index_path = (0, get_index_path_for_element_1.default)(element);
307
+ const target_index_path = this.getIndexPathForGhost(ghost);
308
+ await this.submitReposition(source_index_path, target_index_path);
309
+ ghost.replaceWith(element);
310
+ Object.assign(element.style, {
311
+ left: '',
312
+ top: '',
313
+ right: '',
314
+ bottom: '',
315
+ width: '',
316
+ height: ''
317
+ });
563
318
  }
564
- this.drag_placeholder = placeholder;
565
- // Remove original from layout so we don't keep extra blank space
566
- block.style.display = 'none';
567
- this.updatePlaceholderPosition(current_y);
568
319
  }
569
- updatePlaceholderPosition(cursor_y) {
570
- const placeholder = this.drag_placeholder;
571
- const origin = this.drag_origin_block;
572
- if (!placeholder || !origin) {
573
- return;
574
- }
575
- const container = placeholder.parentElement;
576
- const blocks = this.getSiblingBlocks(container, origin);
577
- let inserted = false;
578
- for (let i = 0; i < blocks.length; i++) {
579
- const element = blocks[i];
580
- const rect = element.getBoundingClientRect();
581
- const midpoint = rect.top + rect.height / 2;
582
- if (cursor_y < midpoint) {
583
- const parent = element.parentElement;
584
- if (parent) {
585
- parent.insertBefore(placeholder, element);
586
- }
587
- inserted = true;
588
- break;
589
- }
320
+ getIndexPathSafe(element) {
321
+ const is_block = element.classList.contains('block');
322
+ if (!is_block) {
323
+ return null;
590
324
  }
591
- if (!inserted) {
592
- const last_element = blocks[blocks.length - 1];
593
- if (last_element) {
594
- const parent = last_element.parentElement;
595
- if (parent) {
596
- parent.appendChild(placeholder);
597
- }
598
- }
325
+ const is_ghost = element.classList.contains('ghost');
326
+ if (is_ghost) {
327
+ return null;
599
328
  }
329
+ return (0, get_index_path_for_element_1.default)(element);
600
330
  }
601
- animateWithFlip(preceding_rects, blocks_after) {
602
- blocks_after.forEach((element) => {
603
- const first = preceding_rects.get(element);
604
- if (!first) {
605
- return;
331
+ getIndexPathForGhost(ghost) {
332
+ let current_child = ghost;
333
+ while (current_child) {
334
+ const index_path = this.getIndexPathSafe(current_child);
335
+ if (index_path !== null) {
336
+ return index_path;
606
337
  }
607
- const last_rect = element.getBoundingClientRect();
608
- const delta_y = first.top - last_rect.top;
609
- if (delta_y === 0) {
610
- return;
338
+ current_child = current_child.nextElementSibling;
339
+ }
340
+ current_child = ghost;
341
+ while (current_child) {
342
+ const index_path = this.getIndexPathSafe(current_child);
343
+ if (index_path !== null) {
344
+ // TODO: This will need to change when I implement drag-reordering of
345
+ // multicolumn layouts.
346
+ const index_major_version = parseInt(index_path);
347
+ const next_version = index_major_version + 1;
348
+ return next_version.toString();
611
349
  }
612
- // Invert
613
- element.style.transition = '';
614
- element.style.transform = `translateY(${delta_y}px)`;
615
- // Play
616
- void element.getBoundingClientRect();
617
- element.style.transition = 'transform 120ms ease';
618
- element.style.transform = 'translateY(0)';
619
- const cleanup = () => {
620
- element.style.transition = '';
621
- element.style.transform = '';
622
- element.removeEventListener('transitionend', cleanup);
623
- };
624
- element.addEventListener('transitionend', cleanup);
625
- });
350
+ current_child = current_child.previousElementSibling;
351
+ }
352
+ return '0';
626
353
  }
627
- async postReposition(source_index_path, target_index_path, editing) {
354
+ async submitReposition(source_index_path, target_index_path) {
628
355
  const form = new FormData();
629
356
  form.set('path', window.location.pathname);
630
357
  form.set('action_type', action_type_1.default.REPOSITION_BLOCK);
631
358
  form.set('source_index_path', source_index_path);
632
359
  form.set('target_index_path', target_index_path);
633
- const response = await fetch(`/actions?editing=${editing}`, {
360
+ const response = await fetch(`/actions`, {
634
361
  method: 'POST',
635
362
  body: form,
636
363
  credentials: 'same-origin',
@@ -644,96 +371,15 @@ class InteractionHelper {
644
371
  return null;
645
372
  }
646
373
  }
647
- computeTargetIndexPath(placeholder) {
648
- if (!placeholder) {
649
- return null;
650
- }
651
- const parent = placeholder.parentElement;
652
- if (!parent) {
653
- return null;
654
- }
655
- let index = 0;
656
- const children_elements = Array.from(parent.children);
657
- for (let i = 0; i < children_elements.length; i++) {
658
- const child_element = children_elements[i];
659
- if (child_element === placeholder) {
660
- break;
661
- }
662
- if (child_element.classList.contains('block')) {
663
- index += 1;
664
- }
665
- }
666
- return index.toString();
667
- }
668
- updateTopLevelIndexPaths() {
669
- // Top-level blocks are rendered inside main > .section-wrapper
670
- const wrapper = document.querySelector('main .section-wrapper');
671
- let blocks;
672
- if (wrapper) {
673
- const node_list = wrapper.querySelectorAll(':scope > section.block');
674
- blocks = Array.from(node_list);
675
- }
676
- else {
677
- // Fallback: direct children of main if wrapper is not present
678
- const node_list = document.querySelectorAll('main > section.block');
679
- blocks = Array.from(node_list);
680
- }
681
- for (let i = 0; i < blocks.length; i++) {
682
- blocks[i].setAttribute('data-index-path', i.toString());
683
- }
684
- }
685
- cleanupDragElements() {
686
- if (this.drag_ghost && this.drag_ghost.parentElement) {
687
- this.drag_ghost.parentElement.removeChild(this.drag_ghost);
688
- }
689
- if (this.drag_placeholder && this.drag_placeholder.parentElement) {
690
- // Move the real element into the placeholder's final position
691
- if (this.drag_origin_block) {
692
- this.drag_placeholder.parentElement.insertBefore(this.drag_origin_block, this.drag_placeholder);
693
- }
694
- this.drag_placeholder.parentElement.removeChild(this.drag_placeholder);
695
- }
696
- if (this.drag_origin_block) {
697
- this.drag_origin_block.style.display = '';
698
- }
699
- this.drag_ghost = null;
700
- this.drag_placeholder = null;
701
- // Restore global styles
702
- document.body.style.userSelect = '';
703
- document.body.style.cursor = '';
704
- }
705
- resetDragState() {
706
- this.cleanupDragElements();
707
- this.drag_active = false;
708
- this.drag_origin_block = null;
709
- this.drag_start_y = null;
710
- this.drag_pointer_offset_y = null;
711
- }
712
- clearPressTimer() {
713
- if (this.drag_press_timer !== null) {
714
- window.clearTimeout(this.drag_press_timer);
715
- this.drag_press_timer = null;
716
- }
717
- }
718
- getEventPoint(event) {
719
- const touch_event = event;
720
- const has_touches = touch_event.touches !== undefined;
721
- if (has_touches) {
722
- const primary_touch = touch_event.touches[0] || touch_event.changedTouches[0];
723
- if (!primary_touch) {
724
- return null;
725
- }
726
- return {
727
- clientX: primary_touch.clientX,
728
- clientY: primary_touch.clientY
729
- };
730
- }
731
- const mouse_event = event;
374
+ getEventCoordinate(event) {
732
375
  return {
733
- clientX: mouse_event.clientX,
734
- clientY: mouse_event.clientY
376
+ x: event.clientX,
377
+ y: event.clientY
735
378
  };
736
379
  }
380
+ getDragStartCoordinate() {
381
+ return this.drag_start_coordinate;
382
+ }
737
383
  }
738
384
  exports.default = InteractionHelper;
739
385
  //# sourceMappingURL=interaction.js.map