@fc3/mmcadi 0.1.49 → 0.1.50
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/.last-compile-time +1 -1
- package/dist/.last-publish-time +1 -1
- package/dist/client.js +879 -55
- package/dist/src/client/helper/drag.d.ts +29 -0
- package/dist/src/client/helper/drag.js +350 -0
- package/dist/src/client/helper/drag.js.map +1 -0
- package/dist/src/client/helper/interaction.d.ts +47 -0
- package/dist/src/client/helper/interaction.js +739 -0
- package/dist/src/client/helper/interaction.js.map +1 -0
- package/dist/src/client/page/cursor.d.ts +1 -0
- package/dist/src/client/page/cursor.js +17 -19
- package/dist/src/client/page/cursor.js.map +1 -1
- package/dist/src/client/utility/get-index-path-for-element.d.ts +2 -0
- package/dist/src/client/utility/get-index-path-for-element.js +12 -0
- package/dist/src/client/utility/get-index-path-for-element.js.map +1 -0
- package/dist/src/common/utility/role-is-public.d.ts +3 -0
- package/dist/src/common/utility/role-is-public.js +13 -0
- package/dist/src/common/utility/role-is-public.js.map +1 -0
- package/dist/src/enum/action-type.d.ts +1 -0
- package/dist/src/enum/action-type.js +1 -0
- package/dist/src/enum/action-type.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/server/endpoint/action/create.d.ts +1 -0
- package/dist/src/server/endpoint/action/create.js +20 -1
- package/dist/src/server/endpoint/action/create.js.map +1 -1
- package/dist/src/server/operation/reposition-block.d.ts +15 -0
- package/dist/src/server/operation/reposition-block.js +108 -0
- package/dist/src/server/operation/reposition-block.js.map +1 -0
- package/dist/src/type/action/reposition-block.d.ts +8 -0
- package/dist/src/type/action/reposition-block.js +3 -0
- package/dist/src/type/action/reposition-block.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const errors_1 = require("@fc3/errors");
|
|
7
|
+
const action_type_1 = __importDefault(require("./../../enum/action-type.js"));
|
|
8
|
+
const emoji_1 = __importDefault(require("./../../enum/emoji.js"));
|
|
9
|
+
const get_index_path_for_element_1 = __importDefault(require("./../utility/get-index-path-for-element.js"));
|
|
10
|
+
class InteractionHelper {
|
|
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;
|
|
27
|
+
}
|
|
28
|
+
attach() {
|
|
29
|
+
document.addEventListener('mousedown', (event) => {
|
|
30
|
+
this.onPressStart(event);
|
|
31
|
+
}, true);
|
|
32
|
+
document.addEventListener('touchstart', (event) => {
|
|
33
|
+
this.onPressStart(event);
|
|
34
|
+
}, { 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.
|
|
53
|
+
document.addEventListener('dragstart', (event) => {
|
|
54
|
+
this.onNativeDragStart(event);
|
|
55
|
+
}, true);
|
|
56
|
+
}
|
|
57
|
+
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
|
+
});
|
|
72
|
+
}
|
|
73
|
+
onPressStart(event) {
|
|
74
|
+
const target = event.target;
|
|
75
|
+
if (target === null) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
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;
|
|
82
|
+
}
|
|
83
|
+
const block = target.closest('section.block');
|
|
84
|
+
if (block === null) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
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
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const touch_event = event;
|
|
95
|
+
const is_touch = touch_event.type === 'touchstart';
|
|
96
|
+
const point = this.getEventPoint(event);
|
|
97
|
+
if (point === null) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
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;
|
|
106
|
+
}
|
|
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
|
+
}
|
|
113
|
+
onNativeDragStart(event) {
|
|
114
|
+
const target = event.target;
|
|
115
|
+
if (!target) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (target.closest('section.block')) {
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
onPointerMove(event) {
|
|
123
|
+
const point = this.getEventPoint(event);
|
|
124
|
+
if (point === null) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
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);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
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
|
+
}
|
|
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);
|
|
191
|
+
});
|
|
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`;
|
|
198
|
+
}
|
|
199
|
+
// Capture last positions and animate
|
|
200
|
+
const blocks_after = this.getSiblingBlocks(container, origin);
|
|
201
|
+
this.animateWithFlip(preceding_rects, blocks_after);
|
|
202
|
+
}
|
|
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]);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
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
|
+
}
|
|
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);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
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
|
+
}
|
|
315
|
+
this.resetSwipeState();
|
|
316
|
+
}
|
|
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();
|
|
333
|
+
}
|
|
334
|
+
getSwipeMenuFor(block) {
|
|
335
|
+
const wrapper = block.closest('main .section-wrapper');
|
|
336
|
+
if (!wrapper || !block.id) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
return wrapper.querySelector(`.swipe-actions[data-for="${block.id}"]`);
|
|
340
|
+
}
|
|
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';
|
|
360
|
+
}
|
|
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);
|
|
449
|
+
}
|
|
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;
|
|
466
|
+
}
|
|
467
|
+
this.enforceTimeVisibility(block);
|
|
468
|
+
return {
|
|
469
|
+
target,
|
|
470
|
+
menu,
|
|
471
|
+
menu_width
|
|
472
|
+
};
|
|
473
|
+
}
|
|
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);
|
|
482
|
+
});
|
|
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
|
+
}
|
|
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
|
+
}
|
|
514
|
+
}
|
|
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
|
+
}
|
|
526
|
+
}
|
|
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);
|
|
563
|
+
}
|
|
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
|
+
}
|
|
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
|
+
}
|
|
590
|
+
}
|
|
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
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
animateWithFlip(preceding_rects, blocks_after) {
|
|
602
|
+
blocks_after.forEach((element) => {
|
|
603
|
+
const first = preceding_rects.get(element);
|
|
604
|
+
if (!first) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const last_rect = element.getBoundingClientRect();
|
|
608
|
+
const delta_y = first.top - last_rect.top;
|
|
609
|
+
if (delta_y === 0) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
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
|
+
});
|
|
626
|
+
}
|
|
627
|
+
async postReposition(source_index_path, target_index_path, editing) {
|
|
628
|
+
const form = new FormData();
|
|
629
|
+
form.set('path', window.location.pathname);
|
|
630
|
+
form.set('action_type', action_type_1.default.REPOSITION_BLOCK);
|
|
631
|
+
form.set('source_index_path', source_index_path);
|
|
632
|
+
form.set('target_index_path', target_index_path);
|
|
633
|
+
const response = await fetch(`/actions?editing=${editing}`, {
|
|
634
|
+
method: 'POST',
|
|
635
|
+
body: form,
|
|
636
|
+
credentials: 'same-origin',
|
|
637
|
+
redirect: 'follow'
|
|
638
|
+
});
|
|
639
|
+
try {
|
|
640
|
+
const url_object = new URL(response.url, window.location.origin);
|
|
641
|
+
return url_object.searchParams.get('index_path');
|
|
642
|
+
}
|
|
643
|
+
catch (_) {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
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;
|
|
732
|
+
return {
|
|
733
|
+
clientX: mouse_event.clientX,
|
|
734
|
+
clientY: mouse_event.clientY
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
exports.default = InteractionHelper;
|
|
739
|
+
//# sourceMappingURL=interaction.js.map
|