@qalma/kit 0.3.0 → 0.4.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/README.md +185 -0
- package/fesm2022/qalma-kit-headless.mjs +932 -0
- package/fesm2022/qalma-kit-headless.mjs.map +1 -0
- package/fesm2022/qalma-kit.mjs +90 -1146
- package/fesm2022/qalma-kit.mjs.map +1 -1
- package/package.json +5 -1
- package/types/qalma-kit-headless.d.ts +303 -0
- package/types/qalma-kit-headless.d.ts.map +1 -0
- package/types/qalma-kit.d.ts +32 -316
- package/types/qalma-kit.d.ts.map +1 -1
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, ElementRef, input, output, Directive, signal, DestroyRef, afterNextRender } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_GAP$1 = 8;
|
|
5
|
+
const DEFAULT_EDGE_MARGIN = 8;
|
|
6
|
+
function anchorToRect(anchor, options) {
|
|
7
|
+
const gap = options.gap ?? DEFAULT_GAP$1;
|
|
8
|
+
const edgeMargin = options.edgeMargin ?? DEFAULT_EDGE_MARGIN;
|
|
9
|
+
const { boundary, size } = options;
|
|
10
|
+
if (options.placement === 'left' || options.placement === 'right') {
|
|
11
|
+
const mainAxis = options.placement === 'left'
|
|
12
|
+
? anchor.left - gap - size.width
|
|
13
|
+
: anchor.right + gap;
|
|
14
|
+
const crossAxis = resolveCrossAxis(options.align, anchor.top, anchor.bottom, size.height);
|
|
15
|
+
return {
|
|
16
|
+
left: clamp$1(mainAxis, boundary.left + edgeMargin, boundary.right - edgeMargin - size.width),
|
|
17
|
+
top: clamp$1(crossAxis, boundary.top + edgeMargin, boundary.bottom - edgeMargin - size.height),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const mainAxis = options.placement === 'bottom'
|
|
21
|
+
? anchor.bottom + gap
|
|
22
|
+
: anchor.top - gap - size.height;
|
|
23
|
+
const crossAxis = resolveCrossAxis(options.align, anchor.left, anchor.right, size.width);
|
|
24
|
+
return {
|
|
25
|
+
top: clamp$1(mainAxis, boundary.top + edgeMargin, boundary.bottom - edgeMargin - size.height),
|
|
26
|
+
left: clamp$1(crossAxis, boundary.left + edgeMargin, boundary.right - edgeMargin - size.width),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function resolveCrossAxis(align, start, end, size) {
|
|
30
|
+
const span = end - start;
|
|
31
|
+
if (size >= span) {
|
|
32
|
+
// The floating element doesn't fit within the anchor (e.g. a
|
|
33
|
+
// single-line block shorter than the button anchored to it). Center it
|
|
34
|
+
// instead of clamping to an arbitrary edge.
|
|
35
|
+
return start + span / 2 - size / 2;
|
|
36
|
+
}
|
|
37
|
+
if (typeof align === 'number') {
|
|
38
|
+
return clamp$1(align - size / 2, start, end - size);
|
|
39
|
+
}
|
|
40
|
+
switch (align) {
|
|
41
|
+
case 'end':
|
|
42
|
+
return end - size;
|
|
43
|
+
case 'center':
|
|
44
|
+
return start + (end - start) / 2 - size / 2;
|
|
45
|
+
case 'start':
|
|
46
|
+
default:
|
|
47
|
+
return start;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function clamp$1(value, min, max) {
|
|
51
|
+
const lo = Math.min(min, max);
|
|
52
|
+
const hi = Math.max(min, max);
|
|
53
|
+
return Math.min(Math.max(value, lo), hi);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const DEFAULT_MARGIN = 12;
|
|
57
|
+
const DEFAULT_GAP = 8;
|
|
58
|
+
/**
|
|
59
|
+
* Positions a floating list under an anchor rect, flipping it above when there
|
|
60
|
+
* is not enough room below, and clamping its height and horizontal position to
|
|
61
|
+
* the viewport. Shared by the mention and slash-command menus, whose flip
|
|
62
|
+
* behavior `anchorToRect` does not cover (it has no flip / dynamic max-height).
|
|
63
|
+
*/
|
|
64
|
+
function flipAbovePlacement(rect, options) {
|
|
65
|
+
const { width, desiredHeight, minHeight, margin = DEFAULT_MARGIN, gap = DEFAULT_GAP, } = options;
|
|
66
|
+
const availableBelow = window.innerHeight - rect.bottom - margin - gap;
|
|
67
|
+
const availableAbove = rect.top - margin - gap;
|
|
68
|
+
const openAbove = availableBelow < desiredHeight && availableAbove > availableBelow;
|
|
69
|
+
const availableHeight = Math.max(minHeight, openAbove ? availableAbove : availableBelow);
|
|
70
|
+
const maxHeight = Math.min(desiredHeight, availableHeight);
|
|
71
|
+
const leftBoundary = Math.max(margin, window.innerWidth - width - margin);
|
|
72
|
+
return {
|
|
73
|
+
left: Math.min(Math.max(rect.left, margin), leftBoundary),
|
|
74
|
+
top: openAbove ? null : rect.bottom + gap,
|
|
75
|
+
bottom: openAbove ? window.innerHeight - rect.top + gap : null,
|
|
76
|
+
maxHeight,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Shared outside-click / Escape dismiss behavior for floating overlays
|
|
82
|
+
* (popovers, contextual menus, autocomplete lists). Generalizes the
|
|
83
|
+
* ad-hoc click-outside/Escape handling that used to be reimplemented
|
|
84
|
+
* separately per feature.
|
|
85
|
+
*/
|
|
86
|
+
class DismissibleOverlay {
|
|
87
|
+
options;
|
|
88
|
+
pointerDown = (event) => {
|
|
89
|
+
if (!this.options.isInside(event.target)) {
|
|
90
|
+
this.options.onDismiss();
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
keydown = (event) => {
|
|
94
|
+
if (event.key === 'Escape') {
|
|
95
|
+
this.options.onDismiss();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
constructor(options) {
|
|
99
|
+
this.options = options;
|
|
100
|
+
}
|
|
101
|
+
connect(destroyRef) {
|
|
102
|
+
document.addEventListener('pointerdown', this.pointerDown, true);
|
|
103
|
+
document.addEventListener('keydown', this.keydown);
|
|
104
|
+
destroyRef.onDestroy(() => {
|
|
105
|
+
document.removeEventListener('pointerdown', this.pointerDown, true);
|
|
106
|
+
document.removeEventListener('keydown', this.keydown);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Circular index step: move `index` by `delta` within `[0, length)`, wrapping
|
|
113
|
+
* around both ends; returns `index` unchanged for an empty list. This is the
|
|
114
|
+
* single source of truth for wrap-around list navigation — both
|
|
115
|
+
* `KeyboardNavigableList` (BYO-markup lists) and `QalmaSuggestionMenu` (the
|
|
116
|
+
* shipped mention/slash menus) delegate their arrow-key math to it.
|
|
117
|
+
*/
|
|
118
|
+
function wrapIndex(index, delta, length) {
|
|
119
|
+
return length > 0 ? (index + delta + length) % length : index;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Arrow-key navigation + Enter-to-select for autocomplete-style popovers
|
|
123
|
+
* (mention menu, slash command menu). Shared so both features stop
|
|
124
|
+
* reimplementing the same wrap-around index math.
|
|
125
|
+
*
|
|
126
|
+
* Takes a plain key string rather than a `KeyboardEvent` — some callers
|
|
127
|
+
* relay keys through a `CustomEvent` (no real `KeyboardEvent` to read),
|
|
128
|
+
* so preventing the default action is left to the caller.
|
|
129
|
+
*/
|
|
130
|
+
class KeyboardNavigableList {
|
|
131
|
+
options;
|
|
132
|
+
constructor(options) {
|
|
133
|
+
this.options = options;
|
|
134
|
+
}
|
|
135
|
+
/** Returns true when the key was handled (caller should stop further propagation / preventDefault). */
|
|
136
|
+
handleKey(key) {
|
|
137
|
+
const items = this.options.items();
|
|
138
|
+
if (items.length === 0) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
switch (key) {
|
|
142
|
+
case 'ArrowDown':
|
|
143
|
+
this.options.setActiveIndex(wrapIndex(this.options.activeIndex(), 1, items.length));
|
|
144
|
+
return true;
|
|
145
|
+
case 'ArrowUp':
|
|
146
|
+
this.options.setActiveIndex(wrapIndex(this.options.activeIndex(), -1, items.length));
|
|
147
|
+
return true;
|
|
148
|
+
case 'Enter': {
|
|
149
|
+
const index = this.options.activeIndex();
|
|
150
|
+
const item = items[index];
|
|
151
|
+
if (item === undefined) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
this.options.onSelect(item, index);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
default:
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Option buttons carry this attribute so the base can focus/scroll to one by index. */
|
|
164
|
+
const SUGGESTION_OPTION_INDEX_ATTR = 'data-suggestion-index';
|
|
165
|
+
/** The scrollable options container carries this attribute (optional). */
|
|
166
|
+
const SUGGESTION_OPTIONS_SCROLLER_ATTR = 'data-suggestion-options';
|
|
167
|
+
/**
|
|
168
|
+
* Shared behavior for caret-anchored suggestion menus (mention, slash command):
|
|
169
|
+
* flip-above placement input, active-option highlighting, and in-menu keyboard
|
|
170
|
+
* handling (Escape dismisses, Arrow moves + focuses, Enter/Space picks) with
|
|
171
|
+
* wrap-around and optional scroll-into-view. Concrete menus only supply their
|
|
172
|
+
* own item markup and expose their option list via `optionList`.
|
|
173
|
+
*/
|
|
174
|
+
class QalmaSuggestionMenu {
|
|
175
|
+
elementRef = inject(ElementRef);
|
|
176
|
+
placement = input(null, ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
|
|
177
|
+
activeIndex = input(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
|
|
178
|
+
activate = output();
|
|
179
|
+
pick = output();
|
|
180
|
+
dismiss = output();
|
|
181
|
+
preserveSelection(event) {
|
|
182
|
+
event.preventDefault();
|
|
183
|
+
}
|
|
184
|
+
handleOptionKeydown(event, option, index) {
|
|
185
|
+
if (event.key === 'Escape') {
|
|
186
|
+
event.preventDefault();
|
|
187
|
+
this.dismiss.emit();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
191
|
+
event.preventDefault();
|
|
192
|
+
const nextIndex = this.getNextIndex(index, event.key === 'ArrowDown' ? 1 : -1);
|
|
193
|
+
this.activate.emit(nextIndex);
|
|
194
|
+
queueMicrotask(() => this.focusOption(nextIndex));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {
|
|
198
|
+
event.preventDefault();
|
|
199
|
+
this.pick.emit(option);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
focusOption(index) {
|
|
203
|
+
this.optionElement(index)?.focus();
|
|
204
|
+
// No-op for menus without a marked scroller (native focus handles scroll).
|
|
205
|
+
this.scrollOptionIntoView(index);
|
|
206
|
+
}
|
|
207
|
+
scrollOptionIntoView(index) {
|
|
208
|
+
const scroller = this.optionsScroller();
|
|
209
|
+
const option = this.optionElement(index);
|
|
210
|
+
if (!scroller || !option) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const optionTop = option.offsetTop - scroller.offsetTop;
|
|
214
|
+
const optionBottom = optionTop + option.offsetHeight;
|
|
215
|
+
const visibleTop = scroller.scrollTop;
|
|
216
|
+
const visibleBottom = visibleTop + scroller.clientHeight;
|
|
217
|
+
if (optionTop < visibleTop) {
|
|
218
|
+
scroller.scrollTop = optionTop;
|
|
219
|
+
}
|
|
220
|
+
else if (optionBottom > visibleBottom) {
|
|
221
|
+
scroller.scrollTop = optionBottom - scroller.clientHeight;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
getNextIndex(index, delta) {
|
|
225
|
+
return wrapIndex(index, delta, this.optionList().length);
|
|
226
|
+
}
|
|
227
|
+
optionElement(index) {
|
|
228
|
+
return this.elementRef.nativeElement.querySelector(`[${SUGGESTION_OPTION_INDEX_ATTR}="${index}"]`);
|
|
229
|
+
}
|
|
230
|
+
optionsScroller() {
|
|
231
|
+
return this.elementRef.nativeElement.querySelector(`[${SUGGESTION_OPTIONS_SCROLLER_ATTR}]`);
|
|
232
|
+
}
|
|
233
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaSuggestionMenu, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
234
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.17", type: QalmaSuggestionMenu, isStandalone: true, inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, activeIndex: { classPropertyName: "activeIndex", publicName: "activeIndex", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activate: "activate", pick: "pick", dismiss: "dismiss" }, ngImport: i0 });
|
|
235
|
+
}
|
|
236
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaSuggestionMenu, decorators: [{
|
|
237
|
+
type: Directive
|
|
238
|
+
}], propDecorators: { placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], activeIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeIndex", required: false }] }], activate: [{ type: i0.Output, args: ["activate"] }], pick: [{ type: i0.Output, args: ["pick"] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }] } });
|
|
239
|
+
|
|
240
|
+
const DRAG_START_DISTANCE = 8;
|
|
241
|
+
const HANDLE_EDGE_MARGIN = 8;
|
|
242
|
+
const HANDLE_GAP = 8;
|
|
243
|
+
const HANDLE_BUTTON_SIZE = 30;
|
|
244
|
+
const DROP_LINE_EDGE_MARGIN = 8;
|
|
245
|
+
class QalmaDragHandleController {
|
|
246
|
+
editor;
|
|
247
|
+
handleState = signal(null, ...(ngDevMode ? [{ debugName: "handleState" }] : /* istanbul ignore next */ []));
|
|
248
|
+
dropIndicatorState = signal(null, ...(ngDevMode ? [{ debugName: "dropIndicatorState" }] : /* istanbul ignore next */ []));
|
|
249
|
+
draggedBlockHighlightState = signal(null, ...(ngDevMode ? [{ debugName: "draggedBlockHighlightState" }] : /* istanbul ignore next */ []));
|
|
250
|
+
handle = this.handleState.asReadonly();
|
|
251
|
+
dropIndicator = this.dropIndicatorState.asReadonly();
|
|
252
|
+
draggedBlockHighlight = this.draggedBlockHighlightState.asReadonly();
|
|
253
|
+
surface = null;
|
|
254
|
+
currentBlock = null;
|
|
255
|
+
pendingBlock = null;
|
|
256
|
+
lastPointerClientY = null;
|
|
257
|
+
refreshFrame = null;
|
|
258
|
+
handleKey = null;
|
|
259
|
+
dragSession = null;
|
|
260
|
+
constructor(editor) {
|
|
261
|
+
this.editor = editor;
|
|
262
|
+
}
|
|
263
|
+
connect(surface, destroyRef) {
|
|
264
|
+
this.surface = surface;
|
|
265
|
+
const pointerMove = (event) => this.handlePointerMove(event);
|
|
266
|
+
const pointerLeave = (event) => this.handlePointerLeave(event);
|
|
267
|
+
const scheduleRefresh = () => this.scheduleRefresh();
|
|
268
|
+
surface.addEventListener('pointermove', pointerMove, {
|
|
269
|
+
passive: true,
|
|
270
|
+
});
|
|
271
|
+
surface.addEventListener('mouseleave', pointerLeave);
|
|
272
|
+
surface.addEventListener('scroll', scheduleRefresh, {
|
|
273
|
+
passive: true,
|
|
274
|
+
});
|
|
275
|
+
window.addEventListener('scroll', scheduleRefresh, {
|
|
276
|
+
passive: true,
|
|
277
|
+
});
|
|
278
|
+
window.addEventListener('resize', scheduleRefresh, {
|
|
279
|
+
passive: true,
|
|
280
|
+
});
|
|
281
|
+
destroyRef.onDestroy(() => {
|
|
282
|
+
this.finishDrag(false);
|
|
283
|
+
this.cancelScheduledRefresh();
|
|
284
|
+
this.surface = null;
|
|
285
|
+
this.currentBlock = null;
|
|
286
|
+
this.pendingBlock = null;
|
|
287
|
+
surface.removeEventListener('pointermove', pointerMove);
|
|
288
|
+
surface.removeEventListener('mouseleave', pointerLeave);
|
|
289
|
+
surface.removeEventListener('scroll', scheduleRefresh);
|
|
290
|
+
window.removeEventListener('scroll', scheduleRefresh);
|
|
291
|
+
window.removeEventListener('resize', scheduleRefresh);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
hide() {
|
|
295
|
+
this.currentBlock = null;
|
|
296
|
+
this.pendingBlock = null;
|
|
297
|
+
this.lastPointerClientY = null;
|
|
298
|
+
this.cancelScheduledRefresh();
|
|
299
|
+
this.setHandle(null);
|
|
300
|
+
}
|
|
301
|
+
startDrag(event, handle) {
|
|
302
|
+
if (event.button !== 0 || this.dragSession) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const surface = this.surface;
|
|
306
|
+
const sourceBlock = surface && findDragHandleBlockByPos(surface, handle.target.pos);
|
|
307
|
+
if (!surface || !sourceBlock) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
event.preventDefault();
|
|
311
|
+
const session = {
|
|
312
|
+
handle,
|
|
313
|
+
sourceBlock,
|
|
314
|
+
originX: event.clientX,
|
|
315
|
+
originY: event.clientY,
|
|
316
|
+
dragging: false,
|
|
317
|
+
previousBodyCursor: document.body.style.cursor,
|
|
318
|
+
previousBodyUserSelect: document.body.style.userSelect,
|
|
319
|
+
pointerMove: (moveEvent) => this.handleDragPointerMove(moveEvent),
|
|
320
|
+
pointerUp: (upEvent) => this.handleDragPointerUp(upEvent),
|
|
321
|
+
pointerCancel: () => this.finishDrag(false),
|
|
322
|
+
};
|
|
323
|
+
this.dragSession = session;
|
|
324
|
+
window.addEventListener('pointermove', session.pointerMove, {
|
|
325
|
+
passive: false,
|
|
326
|
+
});
|
|
327
|
+
window.addEventListener('pointerup', session.pointerUp, {
|
|
328
|
+
passive: false,
|
|
329
|
+
});
|
|
330
|
+
window.addEventListener('pointercancel', session.pointerCancel);
|
|
331
|
+
}
|
|
332
|
+
handlePointerMove(event) {
|
|
333
|
+
if (this.dragSession?.dragging) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const surface = this.surface;
|
|
337
|
+
const target = event.target;
|
|
338
|
+
if (!surface || !(target instanceof Element)) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const block = findDragHandleBlock(target, surface);
|
|
342
|
+
if (!block) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (event instanceof MouseEvent) {
|
|
346
|
+
this.lastPointerClientY = event.clientY;
|
|
347
|
+
}
|
|
348
|
+
this.pendingBlock = block;
|
|
349
|
+
this.scheduleRefresh();
|
|
350
|
+
}
|
|
351
|
+
handlePointerLeave(event) {
|
|
352
|
+
if (this.dragSession?.dragging) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const relatedTarget = event instanceof MouseEvent ? event.relatedTarget : null;
|
|
356
|
+
if (relatedTarget instanceof Element &&
|
|
357
|
+
relatedTarget.closest('[data-qalma-drag-handle]')) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
this.hide();
|
|
361
|
+
}
|
|
362
|
+
handleDragPointerMove(event) {
|
|
363
|
+
const session = this.dragSession;
|
|
364
|
+
if (!session || !(event instanceof MouseEvent)) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const distance = Math.hypot(event.clientX - session.originX, event.clientY - session.originY);
|
|
368
|
+
if (!session.dragging && distance < DRAG_START_DISTANCE) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
event.preventDefault();
|
|
372
|
+
if (!session.dragging) {
|
|
373
|
+
this.activateDragSession(session);
|
|
374
|
+
}
|
|
375
|
+
this.updateDropIndicator(event.clientY);
|
|
376
|
+
}
|
|
377
|
+
handleDragPointerUp(event) {
|
|
378
|
+
if (this.dragSession?.dragging && event instanceof MouseEvent) {
|
|
379
|
+
event.preventDefault();
|
|
380
|
+
}
|
|
381
|
+
this.finishDrag(true);
|
|
382
|
+
}
|
|
383
|
+
activateDragSession(session) {
|
|
384
|
+
session.dragging = true;
|
|
385
|
+
this.surface?.setAttribute('data-qalma-drag-active', 'true');
|
|
386
|
+
document.body.style.cursor = 'grabbing';
|
|
387
|
+
document.body.style.userSelect = 'none';
|
|
388
|
+
this.updateDraggedBlockHighlight(session.sourceBlock);
|
|
389
|
+
}
|
|
390
|
+
updateDropIndicator(clientY) {
|
|
391
|
+
const surface = this.surface;
|
|
392
|
+
const session = this.dragSession;
|
|
393
|
+
const candidate = surface && session
|
|
394
|
+
? findDragDropCandidate(surface, session.handle.target.pos, clientY)
|
|
395
|
+
: null;
|
|
396
|
+
if (!candidate ||
|
|
397
|
+
!this.editor().canExecute('moveBlockTo', candidate.target)) {
|
|
398
|
+
this.setDropIndicator(null);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
this.setDropIndicator({
|
|
402
|
+
target: candidate.target,
|
|
403
|
+
transform: `translate3d(${Math.round(candidate.lineX)}px, ${Math.round(candidate.lineY)}px, 0)`,
|
|
404
|
+
width: Math.round(candidate.lineWidth),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
updateDraggedBlockHighlight(block) {
|
|
408
|
+
const rect = block.getBoundingClientRect();
|
|
409
|
+
this.draggedBlockHighlightState.set({
|
|
410
|
+
transform: `translate3d(${Math.round(rect.left)}px, ${Math.round(rect.top)}px, 0)`,
|
|
411
|
+
width: Math.round(rect.width),
|
|
412
|
+
height: Math.round(rect.height),
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
finishDrag(shouldDrop) {
|
|
416
|
+
const session = this.dragSession;
|
|
417
|
+
if (!session) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const dropTarget = this.dropIndicatorState()?.target ?? null;
|
|
421
|
+
window.removeEventListener('pointermove', session.pointerMove);
|
|
422
|
+
window.removeEventListener('pointerup', session.pointerUp);
|
|
423
|
+
window.removeEventListener('pointercancel', session.pointerCancel);
|
|
424
|
+
this.surface?.removeAttribute('data-qalma-drag-active');
|
|
425
|
+
document.body.style.cursor = session.previousBodyCursor;
|
|
426
|
+
document.body.style.userSelect = session.previousBodyUserSelect;
|
|
427
|
+
this.dragSession = null;
|
|
428
|
+
this.setDropIndicator(null);
|
|
429
|
+
this.draggedBlockHighlightState.set(null);
|
|
430
|
+
if (shouldDrop && session.dragging && dropTarget) {
|
|
431
|
+
this.editor().execute('moveBlockTo', dropTarget);
|
|
432
|
+
this.hide();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
scheduleRefresh() {
|
|
436
|
+
if (this.refreshFrame !== null) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
this.refreshFrame = requestAnimationFrame(() => {
|
|
440
|
+
this.refreshFrame = null;
|
|
441
|
+
this.refresh();
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
refresh() {
|
|
445
|
+
const block = this.pendingBlock ?? this.currentBlock;
|
|
446
|
+
this.pendingBlock = null;
|
|
447
|
+
if (!block) {
|
|
448
|
+
this.hide();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
this.updateFromBlock(block);
|
|
452
|
+
}
|
|
453
|
+
updateFromBlock(block) {
|
|
454
|
+
const surface = this.surface;
|
|
455
|
+
const target = parseDragHandleTarget(block);
|
|
456
|
+
if (!surface || !target) {
|
|
457
|
+
this.hide();
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const rect = block.getBoundingClientRect();
|
|
461
|
+
if (!isRectVisibleInsideSurface$1(rect, surface)) {
|
|
462
|
+
this.hide();
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const surfaceRect = surface.getBoundingClientRect();
|
|
466
|
+
const position = anchorToRect(rect, {
|
|
467
|
+
placement: 'left',
|
|
468
|
+
boundary: surfaceRect,
|
|
469
|
+
size: { width: HANDLE_BUTTON_SIZE, height: HANDLE_BUTTON_SIZE },
|
|
470
|
+
gap: HANDLE_GAP,
|
|
471
|
+
edgeMargin: HANDLE_EDGE_MARGIN,
|
|
472
|
+
align: this.lastPointerClientY ?? rect.top + rect.height / 2,
|
|
473
|
+
});
|
|
474
|
+
const canMoveUp = this.editor().canExecute('moveBlockUp', target);
|
|
475
|
+
const canMoveDown = this.editor().canExecute('moveBlockDown', target);
|
|
476
|
+
this.currentBlock = block;
|
|
477
|
+
this.setHandle({
|
|
478
|
+
target,
|
|
479
|
+
transform: `translate3d(${Math.round(position.left)}px, ${Math.round(position.top)}px, 0)`,
|
|
480
|
+
blockType: block.dataset['qalmaDragHandleType'] ?? 'block',
|
|
481
|
+
canMoveUp,
|
|
482
|
+
canMoveDown,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
cancelScheduledRefresh() {
|
|
486
|
+
if (this.refreshFrame === null) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
cancelAnimationFrame(this.refreshFrame);
|
|
490
|
+
this.refreshFrame = null;
|
|
491
|
+
}
|
|
492
|
+
setHandle(handle) {
|
|
493
|
+
const nextKey = handle
|
|
494
|
+
? [
|
|
495
|
+
handle.target.pos,
|
|
496
|
+
handle.transform,
|
|
497
|
+
handle.blockType,
|
|
498
|
+
handle.canMoveUp,
|
|
499
|
+
handle.canMoveDown,
|
|
500
|
+
].join(':')
|
|
501
|
+
: null;
|
|
502
|
+
if (nextKey === this.handleKey) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
this.handleKey = nextKey;
|
|
506
|
+
this.handleState.set(handle);
|
|
507
|
+
}
|
|
508
|
+
setDropIndicator(indicator) {
|
|
509
|
+
this.dropIndicatorState.set(indicator);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function findDragDropCandidate(surface, sourcePos, clientY) {
|
|
513
|
+
const blocks = Array.from(surface.querySelectorAll('[data-qalma-drag-handle-block]'));
|
|
514
|
+
const lineRect = getDropLineRect(surface);
|
|
515
|
+
let lastCandidate = null;
|
|
516
|
+
for (const block of blocks) {
|
|
517
|
+
const range = parseDragHandleBlockRange(block);
|
|
518
|
+
if (!range) {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
const rect = block.getBoundingClientRect();
|
|
522
|
+
const midpoint = rect.top + rect.height / 2;
|
|
523
|
+
if (clientY < midpoint) {
|
|
524
|
+
return {
|
|
525
|
+
target: {
|
|
526
|
+
pos: sourcePos,
|
|
527
|
+
targetPos: range.pos,
|
|
528
|
+
},
|
|
529
|
+
lineX: lineRect.left,
|
|
530
|
+
lineY: rect.top,
|
|
531
|
+
lineWidth: lineRect.width,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
lastCandidate = {
|
|
535
|
+
target: {
|
|
536
|
+
pos: sourcePos,
|
|
537
|
+
targetPos: range.to,
|
|
538
|
+
},
|
|
539
|
+
lineX: lineRect.left,
|
|
540
|
+
lineY: rect.bottom,
|
|
541
|
+
lineWidth: lineRect.width,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
return lastCandidate;
|
|
545
|
+
}
|
|
546
|
+
function getDropLineRect(surface) {
|
|
547
|
+
const content = surface.querySelector('.ProseMirror') ?? surface;
|
|
548
|
+
const rect = content.getBoundingClientRect();
|
|
549
|
+
const width = Math.max(0, rect.width - DROP_LINE_EDGE_MARGIN * 2);
|
|
550
|
+
return {
|
|
551
|
+
left: rect.left + DROP_LINE_EDGE_MARGIN,
|
|
552
|
+
width,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function findDragHandleBlock(target, surface) {
|
|
556
|
+
const block = target.closest('[data-qalma-drag-handle-block]');
|
|
557
|
+
return block && surface.contains(block) ? block : null;
|
|
558
|
+
}
|
|
559
|
+
function findDragHandleBlockByPos(surface, pos) {
|
|
560
|
+
const blocks = Array.from(surface.querySelectorAll('[data-qalma-drag-handle-block]'));
|
|
561
|
+
return (blocks.find((block) => parseDragHandleTarget(block)?.pos === pos) ?? null);
|
|
562
|
+
}
|
|
563
|
+
function parseDragHandleTarget(block) {
|
|
564
|
+
const pos = Number(block.dataset['qalmaDragHandlePos']);
|
|
565
|
+
return Number.isInteger(pos) ? { pos } : null;
|
|
566
|
+
}
|
|
567
|
+
function parseDragHandleBlockRange(block) {
|
|
568
|
+
const target = parseDragHandleTarget(block);
|
|
569
|
+
const to = Number(block.dataset['qalmaDragHandleTo']);
|
|
570
|
+
return target && Number.isInteger(to) ? { ...target, to } : null;
|
|
571
|
+
}
|
|
572
|
+
function isRectVisibleInsideSurface$1(rect, surface) {
|
|
573
|
+
const surfaceRect = surface.getBoundingClientRect();
|
|
574
|
+
return (rect.bottom > surfaceRect.top &&
|
|
575
|
+
rect.top < surfaceRect.bottom &&
|
|
576
|
+
rect.right > surfaceRect.left &&
|
|
577
|
+
rect.left < surfaceRect.right);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
class QalmaDragHandleDirective {
|
|
581
|
+
editor = input.required({ ...(ngDevMode ? { debugName: "editor" } : /* istanbul ignore next */ {}), alias: 'qalmaDragHandle' });
|
|
582
|
+
destroyRef = inject(DestroyRef);
|
|
583
|
+
host = inject(ElementRef);
|
|
584
|
+
controller = new QalmaDragHandleController(() => this.editor());
|
|
585
|
+
handle = this.controller.handle;
|
|
586
|
+
dropIndicator = this.controller.dropIndicator;
|
|
587
|
+
draggedBlockHighlight = this.controller.draggedBlockHighlight;
|
|
588
|
+
constructor() {
|
|
589
|
+
afterNextRender(() => {
|
|
590
|
+
this.controller.connect(this.host.nativeElement, this.destroyRef);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
hide() {
|
|
594
|
+
this.controller.hide();
|
|
595
|
+
}
|
|
596
|
+
startDrag(event, handle) {
|
|
597
|
+
this.controller.startDrag(event, handle);
|
|
598
|
+
}
|
|
599
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaDragHandleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
600
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.17", type: QalmaDragHandleDirective, isStandalone: true, selector: "[qalmaDragHandle]", inputs: { editor: { classPropertyName: "editor", publicName: "qalmaDragHandle", isSignal: true, isRequired: true, transformFunction: null } }, exportAs: ["qalmaDragHandle"], ngImport: i0 });
|
|
601
|
+
}
|
|
602
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaDragHandleDirective, decorators: [{
|
|
603
|
+
type: Directive,
|
|
604
|
+
args: [{
|
|
605
|
+
exportAs: 'qalmaDragHandle',
|
|
606
|
+
selector: '[qalmaDragHandle]',
|
|
607
|
+
}]
|
|
608
|
+
}], ctorParameters: () => [], propDecorators: { editor: [{ type: i0.Input, args: [{ isSignal: true, alias: "qalmaDragHandle", required: true }] }] } });
|
|
609
|
+
|
|
610
|
+
const TOOLBAR_EDGE_MARGIN = 16;
|
|
611
|
+
const TOOLBAR_GAP = 8;
|
|
612
|
+
class QalmaSelectionToolbarController {
|
|
613
|
+
editor;
|
|
614
|
+
placementState = signal(null, ...(ngDevMode ? [{ debugName: "placementState" }] : /* istanbul ignore next */ []));
|
|
615
|
+
placement = this.placementState.asReadonly();
|
|
616
|
+
surface = null;
|
|
617
|
+
refreshFrame = null;
|
|
618
|
+
placementKey = null;
|
|
619
|
+
constructor(editor) {
|
|
620
|
+
this.editor = editor;
|
|
621
|
+
}
|
|
622
|
+
connect(surface, destroyRef) {
|
|
623
|
+
this.surface = surface;
|
|
624
|
+
const refresh = () => this.refresh();
|
|
625
|
+
const scheduleRefresh = () => this.scheduleRefresh();
|
|
626
|
+
const handleKeydown = (event) => this.handleKeydown(event);
|
|
627
|
+
surface.addEventListener('qalma-selection-update', refresh);
|
|
628
|
+
surface.addEventListener('keydown', handleKeydown);
|
|
629
|
+
surface.addEventListener('keyup', refresh);
|
|
630
|
+
surface.addEventListener('mouseup', refresh);
|
|
631
|
+
surface.addEventListener('scroll', scheduleRefresh, {
|
|
632
|
+
passive: true,
|
|
633
|
+
});
|
|
634
|
+
window.addEventListener('scroll', scheduleRefresh, {
|
|
635
|
+
passive: true,
|
|
636
|
+
});
|
|
637
|
+
window.addEventListener('resize', scheduleRefresh, {
|
|
638
|
+
passive: true,
|
|
639
|
+
});
|
|
640
|
+
destroyRef.onDestroy(() => {
|
|
641
|
+
this.cancelScheduledRefresh();
|
|
642
|
+
this.surface = null;
|
|
643
|
+
surface.removeEventListener('qalma-selection-update', refresh);
|
|
644
|
+
surface.removeEventListener('keydown', handleKeydown);
|
|
645
|
+
surface.removeEventListener('keyup', refresh);
|
|
646
|
+
surface.removeEventListener('mouseup', refresh);
|
|
647
|
+
surface.removeEventListener('scroll', scheduleRefresh);
|
|
648
|
+
window.removeEventListener('scroll', scheduleRefresh);
|
|
649
|
+
window.removeEventListener('resize', scheduleRefresh);
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
refresh() {
|
|
653
|
+
const surface = this.surface;
|
|
654
|
+
const selection = this.editor().query('selection');
|
|
655
|
+
if (!surface ||
|
|
656
|
+
!selection ||
|
|
657
|
+
selection.empty ||
|
|
658
|
+
selection.text.trim().length === 0) {
|
|
659
|
+
this.hide();
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const domSelection = window.getSelection();
|
|
663
|
+
if (!domSelection ||
|
|
664
|
+
domSelection.rangeCount === 0 ||
|
|
665
|
+
!isSelectionInsideSurface(domSelection, surface)) {
|
|
666
|
+
this.hide();
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const rect = getVisibleSelectionRect(domSelection.getRangeAt(0));
|
|
670
|
+
if (!rect || !isRectVisibleInsideSurface(rect, surface)) {
|
|
671
|
+
this.hide();
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
675
|
+
const x = Math.round(clamp(rect.left + rect.width / 2, TOOLBAR_EDGE_MARGIN, viewportWidth - TOOLBAR_EDGE_MARGIN));
|
|
676
|
+
const y = Math.round(Math.max(TOOLBAR_EDGE_MARGIN, rect.top - TOOLBAR_GAP));
|
|
677
|
+
this.setPlacement({
|
|
678
|
+
transform: `translate3d(${x}px, ${y}px, 0) translate(-50%, -100%)`,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
hide() {
|
|
682
|
+
this.setPlacement(null);
|
|
683
|
+
}
|
|
684
|
+
handleKeydown(event) {
|
|
685
|
+
if (!(event instanceof KeyboardEvent) ||
|
|
686
|
+
event.key !== 'Escape' ||
|
|
687
|
+
!this.placement()) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
event.preventDefault();
|
|
691
|
+
this.hide();
|
|
692
|
+
}
|
|
693
|
+
scheduleRefresh() {
|
|
694
|
+
if (!this.placement() || this.refreshFrame !== null) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
this.refreshFrame = requestAnimationFrame(() => {
|
|
698
|
+
this.refreshFrame = null;
|
|
699
|
+
this.refresh();
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
cancelScheduledRefresh() {
|
|
703
|
+
if (this.refreshFrame === null) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
cancelAnimationFrame(this.refreshFrame);
|
|
707
|
+
this.refreshFrame = null;
|
|
708
|
+
}
|
|
709
|
+
setPlacement(placement) {
|
|
710
|
+
const nextKey = placement?.transform ?? null;
|
|
711
|
+
if (nextKey === this.placementKey) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
this.placementKey = nextKey;
|
|
715
|
+
this.placementState.set(placement);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
function getVisibleSelectionRect(range) {
|
|
719
|
+
const rect = range.getBoundingClientRect();
|
|
720
|
+
if (rect.width > 0 || rect.height > 0) {
|
|
721
|
+
return rect;
|
|
722
|
+
}
|
|
723
|
+
for (const clientRect of Array.from(range.getClientRects())) {
|
|
724
|
+
if (clientRect.width > 0 || clientRect.height > 0) {
|
|
725
|
+
return clientRect;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
function isSelectionInsideSurface(selection, surface) {
|
|
731
|
+
return (isNodeInsideSurface(selection.anchorNode, surface) &&
|
|
732
|
+
isNodeInsideSurface(selection.focusNode, surface));
|
|
733
|
+
}
|
|
734
|
+
function isNodeInsideSurface(node, surface) {
|
|
735
|
+
const element = node instanceof Element ? node : node?.parentElement;
|
|
736
|
+
return Boolean(element && surface.contains(element));
|
|
737
|
+
}
|
|
738
|
+
function isRectVisibleInsideSurface(rect, surface) {
|
|
739
|
+
const surfaceRect = surface.getBoundingClientRect();
|
|
740
|
+
return (rect.bottom > surfaceRect.top &&
|
|
741
|
+
rect.top < surfaceRect.bottom &&
|
|
742
|
+
rect.right > surfaceRect.left &&
|
|
743
|
+
rect.left < surfaceRect.right);
|
|
744
|
+
}
|
|
745
|
+
function clamp(value, min, max) {
|
|
746
|
+
return Math.min(Math.max(value, min), max);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
class QalmaSelectionToolbarDirective {
|
|
750
|
+
editor = input.required({ ...(ngDevMode ? { debugName: "editor" } : /* istanbul ignore next */ {}), alias: 'qalmaSelectionToolbar' });
|
|
751
|
+
destroyRef = inject(DestroyRef);
|
|
752
|
+
host = inject(ElementRef);
|
|
753
|
+
controller = new QalmaSelectionToolbarController(() => this.editor());
|
|
754
|
+
placement = this.controller.placement;
|
|
755
|
+
constructor() {
|
|
756
|
+
afterNextRender(() => {
|
|
757
|
+
this.controller.connect(this.host.nativeElement, this.destroyRef);
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
refresh() {
|
|
761
|
+
this.controller.refresh();
|
|
762
|
+
}
|
|
763
|
+
hide() {
|
|
764
|
+
this.controller.hide();
|
|
765
|
+
}
|
|
766
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaSelectionToolbarDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
767
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.17", type: QalmaSelectionToolbarDirective, isStandalone: true, selector: "[qalmaSelectionToolbar]", inputs: { editor: { classPropertyName: "editor", publicName: "qalmaSelectionToolbar", isSignal: true, isRequired: true, transformFunction: null } }, exportAs: ["qalmaSelectionToolbar"], ngImport: i0 });
|
|
768
|
+
}
|
|
769
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaSelectionToolbarDirective, decorators: [{
|
|
770
|
+
type: Directive,
|
|
771
|
+
args: [{
|
|
772
|
+
exportAs: 'qalmaSelectionToolbar',
|
|
773
|
+
selector: '[qalmaSelectionToolbar]',
|
|
774
|
+
}]
|
|
775
|
+
}], ctorParameters: () => [], propDecorators: { editor: [{ type: i0.Input, args: [{ isSignal: true, alias: "qalmaSelectionToolbar", required: true }] }] } });
|
|
776
|
+
|
|
777
|
+
const LINK_POPOVER_WIDTH = 360;
|
|
778
|
+
// Both the editing and preview rows are a single h-9 (36px) control plus
|
|
779
|
+
// padding/border; this is an estimate since the actual element isn't
|
|
780
|
+
// rendered yet when the placement is computed.
|
|
781
|
+
const LINK_POPOVER_HEIGHT_ESTIMATE = 56;
|
|
782
|
+
function createLinkPopoverPlacement(element) {
|
|
783
|
+
const rect = element.getBoundingClientRect();
|
|
784
|
+
return anchorToRect(rect, {
|
|
785
|
+
placement: 'bottom',
|
|
786
|
+
boundary: {
|
|
787
|
+
top: 0,
|
|
788
|
+
bottom: window.innerHeight,
|
|
789
|
+
left: 0,
|
|
790
|
+
right: window.innerWidth,
|
|
791
|
+
},
|
|
792
|
+
size: { width: LINK_POPOVER_WIDTH, height: LINK_POPOVER_HEIGHT_ESTIMATE },
|
|
793
|
+
gap: 8,
|
|
794
|
+
edgeMargin: 16,
|
|
795
|
+
align: 'start',
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
function findEditorLinkElement(target) {
|
|
799
|
+
if (!(target instanceof Element)) {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
const element = target.closest('.ProseMirror a[href]');
|
|
803
|
+
return element instanceof HTMLAnchorElement ? element : null;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
class LinkPopoverController {
|
|
807
|
+
editor;
|
|
808
|
+
popover = signal(null, ...(ngDevMode ? [{ debugName: "popover" }] : /* istanbul ignore next */ []));
|
|
809
|
+
href = signal('', ...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
|
|
810
|
+
hideTimeout;
|
|
811
|
+
constructor(editor) {
|
|
812
|
+
this.editor = editor;
|
|
813
|
+
}
|
|
814
|
+
showToolbarEditor(event) {
|
|
815
|
+
const linkState = this.editor.query('link');
|
|
816
|
+
const target = event.currentTarget;
|
|
817
|
+
if (!(target instanceof HTMLElement)) {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
this.keepOpen();
|
|
821
|
+
this.href.set(linkState?.href ?? 'https://');
|
|
822
|
+
this.popover.set({
|
|
823
|
+
...createLinkPopoverPlacement(target),
|
|
824
|
+
editing: true,
|
|
825
|
+
element: null,
|
|
826
|
+
href: linkState?.href ?? '',
|
|
827
|
+
rel: linkState?.rel ?? null,
|
|
828
|
+
target: linkState?.target ?? null,
|
|
829
|
+
text: linkState?.text ?? '',
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
showPreview(event) {
|
|
833
|
+
const element = findEditorLinkElement(event.target);
|
|
834
|
+
if (!element) {
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
this.keepOpen();
|
|
838
|
+
this.popover.set({
|
|
839
|
+
...createLinkPopoverPlacement(element),
|
|
840
|
+
editing: false,
|
|
841
|
+
element,
|
|
842
|
+
href: element.href,
|
|
843
|
+
rel: element.rel || null,
|
|
844
|
+
target: element.target === '_blank' ? '_blank' : null,
|
|
845
|
+
text: element.textContent?.trim() ?? '',
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
scheduleHideFromEvent(event) {
|
|
849
|
+
const nextTarget = event.relatedTarget;
|
|
850
|
+
if (nextTarget instanceof Element &&
|
|
851
|
+
(findEditorLinkElement(nextTarget) ||
|
|
852
|
+
nextTarget.closest('[data-qalma-link-popover]'))) {
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
if (!this.popover()?.editing) {
|
|
856
|
+
this.scheduleHide();
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
edit(popover) {
|
|
860
|
+
this.keepOpen();
|
|
861
|
+
this.selectLink(popover);
|
|
862
|
+
this.href.set(popover.href);
|
|
863
|
+
this.popover.set({
|
|
864
|
+
...popover,
|
|
865
|
+
editing: true,
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
save(popover) {
|
|
869
|
+
const href = this.href().trim();
|
|
870
|
+
if (!href) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
this.selectLink(popover);
|
|
874
|
+
this.editor.execute('setLink', href);
|
|
875
|
+
this.hide();
|
|
876
|
+
}
|
|
877
|
+
remove(popover) {
|
|
878
|
+
this.selectLink(popover);
|
|
879
|
+
this.editor.execute('unsetLink');
|
|
880
|
+
this.hide();
|
|
881
|
+
}
|
|
882
|
+
hide() {
|
|
883
|
+
this.clearHideTimeout();
|
|
884
|
+
this.popover.set(null);
|
|
885
|
+
}
|
|
886
|
+
keepOpen() {
|
|
887
|
+
this.clearHideTimeout();
|
|
888
|
+
}
|
|
889
|
+
scheduleHide() {
|
|
890
|
+
this.clearHideTimeout();
|
|
891
|
+
if (this.popover()?.editing) {
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
this.hideTimeout = setTimeout(() => {
|
|
895
|
+
if (this.popover()?.editing) {
|
|
896
|
+
this.hideTimeout = undefined;
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
this.popover.set(null);
|
|
900
|
+
this.hideTimeout = undefined;
|
|
901
|
+
}, 160);
|
|
902
|
+
}
|
|
903
|
+
selectLink(popover) {
|
|
904
|
+
if (popover.element) {
|
|
905
|
+
this.editor.execute('selectLink', {
|
|
906
|
+
element: popover.element,
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
clearHideTimeout() {
|
|
911
|
+
if (!this.hideTimeout) {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
clearTimeout(this.hideTimeout);
|
|
915
|
+
this.hideTimeout = undefined;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// @qalma/kit/headless — dependency-free behavior for the Qalma editor.
|
|
920
|
+
//
|
|
921
|
+
// Controllers, geometry helpers, dismiss/keyboard primitives and unstyled
|
|
922
|
+
// directives, with zero styling dependencies — no CSS-class or icon libraries.
|
|
923
|
+
// Import from here when you bring your own design system and only want the
|
|
924
|
+
// editor behavior wired up; import from `@qalma/kit` for the styled components.
|
|
925
|
+
// Positioning geometry
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Generated bundle index. Do not edit.
|
|
929
|
+
*/
|
|
930
|
+
|
|
931
|
+
export { DismissibleOverlay, KeyboardNavigableList, LinkPopoverController, QalmaDragHandleController, QalmaDragHandleDirective, QalmaSelectionToolbarController, QalmaSelectionToolbarDirective, QalmaSuggestionMenu, SUGGESTION_OPTIONS_SCROLLER_ATTR, SUGGESTION_OPTION_INDEX_ATTR, anchorToRect, createLinkPopoverPlacement, findEditorLinkElement, flipAbovePlacement, wrapIndex };
|
|
932
|
+
//# sourceMappingURL=qalma-kit-headless.mjs.map
|