@qalma/kit 0.1.0-beta.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 +6 -2
- 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
package/fesm2022/qalma-kit.mjs
CHANGED
|
@@ -1,61 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { DismissibleOverlay, QalmaSuggestionMenu } from '@qalma/kit/headless';
|
|
2
|
+
export * from '@qalma/kit/headless';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
|
-
import {
|
|
5
|
-
import { cva } from 'class-variance-authority';
|
|
4
|
+
import { input, ChangeDetectionStrategy, Component, computed, inject, ElementRef, Directive, output, signal, DestroyRef, effect } from '@angular/core';
|
|
6
5
|
import { NgIcon, provideIcons } from '@ng-icons/core';
|
|
7
6
|
import { QalmaCommand, QalmaToolbar } from '@qalma/editor';
|
|
8
7
|
import { NgTemplateOutlet } from '@angular/common';
|
|
9
8
|
import { lucideUnlink, lucideUndo2, lucideUnderline, lucideTrash2, lucideTextQuote, lucideTable, lucideSuperscript, lucideSubscript, lucideStrikethrough, lucideSquareCode, lucideRows3, lucideRedo2, lucidePilcrow, lucideOutdent, lucideListTodo, lucideListOrdered, lucideList, lucideLetterText, lucideItalic, lucideIndent, lucideHeading3, lucideHeading2, lucideHeading1, lucideHeading, lucideEraser, lucideColumns3, lucideCode, lucideBold, lucideBetweenVerticalEnd, lucideBetweenHorizontalEnd, lucideAlignRight, lucideAlignLeft, lucideAlignJustify, lucideAlignCenter, lucideMousePointer2, lucideGripVertical, lucideCopy, lucideArrowUp, lucideArrowDown, lucideMinus, lucideX, lucidePencil, lucideLink, lucideExternalLink, lucideCheck } from '@ng-icons/lucide';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const buttonVariants = cva('inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_ng-icon]:shrink-0', {
|
|
16
|
-
variants: {
|
|
17
|
-
variant: {
|
|
18
|
-
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
19
|
-
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
20
|
-
outline: 'border border-border bg-card text-foreground hover:bg-secondary hover:text-secondary-foreground',
|
|
21
|
-
ghost: 'text-foreground hover:bg-secondary hover:text-secondary-foreground',
|
|
22
|
-
accent: 'bg-accent text-accent-foreground hover:bg-accent/90',
|
|
23
|
-
link: 'text-accent underline-offset-4 hover:underline',
|
|
24
|
-
},
|
|
25
|
-
size: {
|
|
26
|
-
default: 'h-9 px-4 py-2',
|
|
27
|
-
sm: 'h-8 rounded-md px-3 text-xs',
|
|
28
|
-
lg: 'h-11 rounded-md px-6 text-base',
|
|
29
|
-
icon: 'h-9 w-9',
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
defaultVariants: {
|
|
33
|
-
variant: 'default',
|
|
34
|
-
size: 'default',
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
/**
|
|
38
|
-
* Headless-styled button directive so it composes onto native `<button>` and
|
|
39
|
-
* `<a>` elements while reading from the host's Tailwind design tokens.
|
|
40
|
-
*/
|
|
41
|
-
class QalmaButton {
|
|
42
|
-
host = inject(ElementRef);
|
|
43
|
-
initialClass = this.host.nativeElement.getAttribute('class') ?? '';
|
|
44
|
-
variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
45
|
-
size = input('default', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
46
|
-
computedClass = computed(() => cn(buttonVariants({ variant: this.variant(), size: this.size() }), this.initialClass), ...(ngDevMode ? [{ debugName: "computedClass" }] : /* istanbul ignore next */ []));
|
|
47
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaButton, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
48
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.17", type: QalmaButton, isStandalone: true, selector: "button[qalmaBtn], a[qalmaBtn]", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "computedClass()" } }, ngImport: i0 });
|
|
49
|
-
}
|
|
50
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaButton, decorators: [{
|
|
51
|
-
type: Directive,
|
|
52
|
-
args: [{
|
|
53
|
-
selector: 'button[qalmaBtn], a[qalmaBtn]',
|
|
54
|
-
host: {
|
|
55
|
-
'[class]': 'computedClass()',
|
|
56
|
-
},
|
|
57
|
-
}]
|
|
58
|
-
}], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
|
|
9
|
+
import { cva } from 'class-variance-authority';
|
|
10
|
+
import { clsx } from 'clsx';
|
|
11
|
+
import { twMerge } from 'tailwind-merge';
|
|
59
12
|
|
|
60
13
|
/**
|
|
61
14
|
* Default styling for a toolbar command button. Icon-only, square, and driven
|
|
@@ -368,500 +321,54 @@ function provideQalmaToolbarIcons() {
|
|
|
368
321
|
});
|
|
369
322
|
}
|
|
370
323
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
function anchorToRect(anchor, options) {
|
|
374
|
-
const gap = options.gap ?? DEFAULT_GAP$1;
|
|
375
|
-
const edgeMargin = options.edgeMargin ?? DEFAULT_EDGE_MARGIN;
|
|
376
|
-
const { boundary, size } = options;
|
|
377
|
-
if (options.placement === 'left' || options.placement === 'right') {
|
|
378
|
-
const mainAxis = options.placement === 'left'
|
|
379
|
-
? anchor.left - gap - size.width
|
|
380
|
-
: anchor.right + gap;
|
|
381
|
-
const crossAxis = resolveCrossAxis(options.align, anchor.top, anchor.bottom, size.height);
|
|
382
|
-
return {
|
|
383
|
-
left: clamp$1(mainAxis, boundary.left + edgeMargin, boundary.right - edgeMargin - size.width),
|
|
384
|
-
top: clamp$1(crossAxis, boundary.top + edgeMargin, boundary.bottom - edgeMargin - size.height),
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
const mainAxis = options.placement === 'bottom'
|
|
388
|
-
? anchor.bottom + gap
|
|
389
|
-
: anchor.top - gap - size.height;
|
|
390
|
-
const crossAxis = resolveCrossAxis(options.align, anchor.left, anchor.right, size.width);
|
|
391
|
-
return {
|
|
392
|
-
top: clamp$1(mainAxis, boundary.top + edgeMargin, boundary.bottom - edgeMargin - size.height),
|
|
393
|
-
left: clamp$1(crossAxis, boundary.left + edgeMargin, boundary.right - edgeMargin - size.width),
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
function resolveCrossAxis(align, start, end, size) {
|
|
397
|
-
const span = end - start;
|
|
398
|
-
if (size >= span) {
|
|
399
|
-
// The floating element doesn't fit within the anchor (e.g. a
|
|
400
|
-
// single-line block shorter than the button anchored to it). Center it
|
|
401
|
-
// instead of clamping to an arbitrary edge.
|
|
402
|
-
return start + span / 2 - size / 2;
|
|
403
|
-
}
|
|
404
|
-
if (typeof align === 'number') {
|
|
405
|
-
return clamp$1(align - size / 2, start, end - size);
|
|
406
|
-
}
|
|
407
|
-
switch (align) {
|
|
408
|
-
case 'end':
|
|
409
|
-
return end - size;
|
|
410
|
-
case 'center':
|
|
411
|
-
return start + (end - start) / 2 - size / 2;
|
|
412
|
-
case 'start':
|
|
413
|
-
default:
|
|
414
|
-
return start;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
function clamp$1(value, min, max) {
|
|
418
|
-
const lo = Math.min(min, max);
|
|
419
|
-
const hi = Math.max(min, max);
|
|
420
|
-
return Math.min(Math.max(value, lo), hi);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Shared outside-click / Escape dismiss behavior for floating overlays
|
|
425
|
-
* (popovers, contextual menus, autocomplete lists). Generalizes the
|
|
426
|
-
* ad-hoc click-outside/Escape handling that used to be reimplemented
|
|
427
|
-
* separately per feature.
|
|
428
|
-
*/
|
|
429
|
-
class DismissibleOverlay {
|
|
430
|
-
options;
|
|
431
|
-
pointerDown = (event) => {
|
|
432
|
-
if (!this.options.isInside(event.target)) {
|
|
433
|
-
this.options.onDismiss();
|
|
434
|
-
}
|
|
435
|
-
};
|
|
436
|
-
keydown = (event) => {
|
|
437
|
-
if (event.key === 'Escape') {
|
|
438
|
-
this.options.onDismiss();
|
|
439
|
-
}
|
|
440
|
-
};
|
|
441
|
-
constructor(options) {
|
|
442
|
-
this.options = options;
|
|
443
|
-
}
|
|
444
|
-
connect(destroyRef) {
|
|
445
|
-
document.addEventListener('pointerdown', this.pointerDown, true);
|
|
446
|
-
document.addEventListener('keydown', this.keydown);
|
|
447
|
-
destroyRef.onDestroy(() => {
|
|
448
|
-
document.removeEventListener('pointerdown', this.pointerDown, true);
|
|
449
|
-
document.removeEventListener('keydown', this.keydown);
|
|
450
|
-
});
|
|
451
|
-
}
|
|
324
|
+
function cn(...inputs) {
|
|
325
|
+
return twMerge(clsx(inputs));
|
|
452
326
|
}
|
|
453
327
|
|
|
328
|
+
const buttonVariants = cva('inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_ng-icon]:shrink-0', {
|
|
329
|
+
variants: {
|
|
330
|
+
variant: {
|
|
331
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
332
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
333
|
+
outline: 'border border-border bg-card text-foreground hover:bg-secondary hover:text-secondary-foreground',
|
|
334
|
+
ghost: 'text-foreground hover:bg-secondary hover:text-secondary-foreground',
|
|
335
|
+
accent: 'bg-accent text-accent-foreground hover:bg-accent/90',
|
|
336
|
+
link: 'text-accent underline-offset-4 hover:underline',
|
|
337
|
+
},
|
|
338
|
+
size: {
|
|
339
|
+
default: 'h-9 px-4 py-2',
|
|
340
|
+
sm: 'h-8 rounded-md px-3 text-xs',
|
|
341
|
+
lg: 'h-11 rounded-md px-6 text-base',
|
|
342
|
+
icon: 'h-9 w-9',
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
defaultVariants: {
|
|
346
|
+
variant: 'default',
|
|
347
|
+
size: 'default',
|
|
348
|
+
},
|
|
349
|
+
});
|
|
454
350
|
/**
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
* reimplementing the same wrap-around index math.
|
|
458
|
-
*
|
|
459
|
-
* Takes a plain key string rather than a `KeyboardEvent` — some callers
|
|
460
|
-
* relay keys through a `CustomEvent` (no real `KeyboardEvent` to read),
|
|
461
|
-
* so preventing the default action is left to the caller.
|
|
351
|
+
* Headless-styled button directive so it composes onto native `<button>` and
|
|
352
|
+
* `<a>` elements while reading from the host's Tailwind design tokens.
|
|
462
353
|
*/
|
|
463
|
-
class
|
|
464
|
-
options;
|
|
465
|
-
constructor(options) {
|
|
466
|
-
this.options = options;
|
|
467
|
-
}
|
|
468
|
-
/** Returns true when the key was handled (caller should stop further propagation / preventDefault). */
|
|
469
|
-
handleKey(key) {
|
|
470
|
-
const items = this.options.items();
|
|
471
|
-
if (items.length === 0) {
|
|
472
|
-
return false;
|
|
473
|
-
}
|
|
474
|
-
switch (key) {
|
|
475
|
-
case 'ArrowDown':
|
|
476
|
-
this.options.setActiveIndex((this.options.activeIndex() + 1) % items.length);
|
|
477
|
-
return true;
|
|
478
|
-
case 'ArrowUp':
|
|
479
|
-
this.options.setActiveIndex((this.options.activeIndex() - 1 + items.length) % items.length);
|
|
480
|
-
return true;
|
|
481
|
-
case 'Enter': {
|
|
482
|
-
const index = this.options.activeIndex();
|
|
483
|
-
const item = items[index];
|
|
484
|
-
if (item === undefined) {
|
|
485
|
-
return false;
|
|
486
|
-
}
|
|
487
|
-
this.options.onSelect(item, index);
|
|
488
|
-
return true;
|
|
489
|
-
}
|
|
490
|
-
default:
|
|
491
|
-
return false;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
const DRAG_START_DISTANCE = 8;
|
|
497
|
-
const HANDLE_EDGE_MARGIN = 8;
|
|
498
|
-
const HANDLE_GAP = 8;
|
|
499
|
-
const HANDLE_BUTTON_SIZE = 30;
|
|
500
|
-
const DROP_LINE_EDGE_MARGIN = 8;
|
|
501
|
-
class QalmaDragHandleController {
|
|
502
|
-
editor;
|
|
503
|
-
handleState = signal(null, ...(ngDevMode ? [{ debugName: "handleState" }] : /* istanbul ignore next */ []));
|
|
504
|
-
dropIndicatorState = signal(null, ...(ngDevMode ? [{ debugName: "dropIndicatorState" }] : /* istanbul ignore next */ []));
|
|
505
|
-
draggedBlockHighlightState = signal(null, ...(ngDevMode ? [{ debugName: "draggedBlockHighlightState" }] : /* istanbul ignore next */ []));
|
|
506
|
-
handle = this.handleState.asReadonly();
|
|
507
|
-
dropIndicator = this.dropIndicatorState.asReadonly();
|
|
508
|
-
draggedBlockHighlight = this.draggedBlockHighlightState.asReadonly();
|
|
509
|
-
surface = null;
|
|
510
|
-
currentBlock = null;
|
|
511
|
-
pendingBlock = null;
|
|
512
|
-
lastPointerClientY = null;
|
|
513
|
-
refreshFrame = null;
|
|
514
|
-
handleKey = null;
|
|
515
|
-
dragSession = null;
|
|
516
|
-
constructor(editor) {
|
|
517
|
-
this.editor = editor;
|
|
518
|
-
}
|
|
519
|
-
connect(surface, destroyRef) {
|
|
520
|
-
this.surface = surface;
|
|
521
|
-
const pointerMove = (event) => this.handlePointerMove(event);
|
|
522
|
-
const pointerLeave = (event) => this.handlePointerLeave(event);
|
|
523
|
-
const scheduleRefresh = () => this.scheduleRefresh();
|
|
524
|
-
surface.addEventListener('pointermove', pointerMove, {
|
|
525
|
-
passive: true,
|
|
526
|
-
});
|
|
527
|
-
surface.addEventListener('mouseleave', pointerLeave);
|
|
528
|
-
surface.addEventListener('scroll', scheduleRefresh, {
|
|
529
|
-
passive: true,
|
|
530
|
-
});
|
|
531
|
-
window.addEventListener('scroll', scheduleRefresh, {
|
|
532
|
-
passive: true,
|
|
533
|
-
});
|
|
534
|
-
window.addEventListener('resize', scheduleRefresh, {
|
|
535
|
-
passive: true,
|
|
536
|
-
});
|
|
537
|
-
destroyRef.onDestroy(() => {
|
|
538
|
-
this.finishDrag(false);
|
|
539
|
-
this.cancelScheduledRefresh();
|
|
540
|
-
this.surface = null;
|
|
541
|
-
this.currentBlock = null;
|
|
542
|
-
this.pendingBlock = null;
|
|
543
|
-
surface.removeEventListener('pointermove', pointerMove);
|
|
544
|
-
surface.removeEventListener('mouseleave', pointerLeave);
|
|
545
|
-
surface.removeEventListener('scroll', scheduleRefresh);
|
|
546
|
-
window.removeEventListener('scroll', scheduleRefresh);
|
|
547
|
-
window.removeEventListener('resize', scheduleRefresh);
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
hide() {
|
|
551
|
-
this.currentBlock = null;
|
|
552
|
-
this.pendingBlock = null;
|
|
553
|
-
this.lastPointerClientY = null;
|
|
554
|
-
this.cancelScheduledRefresh();
|
|
555
|
-
this.setHandle(null);
|
|
556
|
-
}
|
|
557
|
-
startDrag(event, handle) {
|
|
558
|
-
if (event.button !== 0 || this.dragSession) {
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
const surface = this.surface;
|
|
562
|
-
const sourceBlock = surface && findDragHandleBlockByPos(surface, handle.target.pos);
|
|
563
|
-
if (!surface || !sourceBlock) {
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
event.preventDefault();
|
|
567
|
-
const session = {
|
|
568
|
-
handle,
|
|
569
|
-
sourceBlock,
|
|
570
|
-
originX: event.clientX,
|
|
571
|
-
originY: event.clientY,
|
|
572
|
-
dragging: false,
|
|
573
|
-
previousBodyCursor: document.body.style.cursor,
|
|
574
|
-
previousBodyUserSelect: document.body.style.userSelect,
|
|
575
|
-
pointerMove: (moveEvent) => this.handleDragPointerMove(moveEvent),
|
|
576
|
-
pointerUp: (upEvent) => this.handleDragPointerUp(upEvent),
|
|
577
|
-
pointerCancel: () => this.finishDrag(false),
|
|
578
|
-
};
|
|
579
|
-
this.dragSession = session;
|
|
580
|
-
window.addEventListener('pointermove', session.pointerMove, {
|
|
581
|
-
passive: false,
|
|
582
|
-
});
|
|
583
|
-
window.addEventListener('pointerup', session.pointerUp, {
|
|
584
|
-
passive: false,
|
|
585
|
-
});
|
|
586
|
-
window.addEventListener('pointercancel', session.pointerCancel);
|
|
587
|
-
}
|
|
588
|
-
handlePointerMove(event) {
|
|
589
|
-
if (this.dragSession?.dragging) {
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
const surface = this.surface;
|
|
593
|
-
const target = event.target;
|
|
594
|
-
if (!surface || !(target instanceof Element)) {
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
const block = findDragHandleBlock(target, surface);
|
|
598
|
-
if (!block) {
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
if (event instanceof MouseEvent) {
|
|
602
|
-
this.lastPointerClientY = event.clientY;
|
|
603
|
-
}
|
|
604
|
-
this.pendingBlock = block;
|
|
605
|
-
this.scheduleRefresh();
|
|
606
|
-
}
|
|
607
|
-
handlePointerLeave(event) {
|
|
608
|
-
if (this.dragSession?.dragging) {
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
const relatedTarget = event instanceof MouseEvent ? event.relatedTarget : null;
|
|
612
|
-
if (relatedTarget instanceof Element &&
|
|
613
|
-
relatedTarget.closest('[data-qalma-drag-handle]')) {
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
this.hide();
|
|
617
|
-
}
|
|
618
|
-
handleDragPointerMove(event) {
|
|
619
|
-
const session = this.dragSession;
|
|
620
|
-
if (!session || !(event instanceof MouseEvent)) {
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
const distance = Math.hypot(event.clientX - session.originX, event.clientY - session.originY);
|
|
624
|
-
if (!session.dragging && distance < DRAG_START_DISTANCE) {
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
event.preventDefault();
|
|
628
|
-
if (!session.dragging) {
|
|
629
|
-
this.activateDragSession(session);
|
|
630
|
-
}
|
|
631
|
-
this.updateDropIndicator(event.clientY);
|
|
632
|
-
}
|
|
633
|
-
handleDragPointerUp(event) {
|
|
634
|
-
if (this.dragSession?.dragging && event instanceof MouseEvent) {
|
|
635
|
-
event.preventDefault();
|
|
636
|
-
}
|
|
637
|
-
this.finishDrag(true);
|
|
638
|
-
}
|
|
639
|
-
activateDragSession(session) {
|
|
640
|
-
session.dragging = true;
|
|
641
|
-
this.surface?.setAttribute('data-qalma-drag-active', 'true');
|
|
642
|
-
document.body.style.cursor = 'grabbing';
|
|
643
|
-
document.body.style.userSelect = 'none';
|
|
644
|
-
this.updateDraggedBlockHighlight(session.sourceBlock);
|
|
645
|
-
}
|
|
646
|
-
updateDropIndicator(clientY) {
|
|
647
|
-
const surface = this.surface;
|
|
648
|
-
const session = this.dragSession;
|
|
649
|
-
const candidate = surface && session
|
|
650
|
-
? findDragDropCandidate(surface, session.handle.target.pos, clientY)
|
|
651
|
-
: null;
|
|
652
|
-
if (!candidate ||
|
|
653
|
-
!this.editor().canExecute('moveBlockTo', candidate.target)) {
|
|
654
|
-
this.setDropIndicator(null);
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
this.setDropIndicator({
|
|
658
|
-
target: candidate.target,
|
|
659
|
-
transform: `translate3d(${Math.round(candidate.lineX)}px, ${Math.round(candidate.lineY)}px, 0)`,
|
|
660
|
-
width: Math.round(candidate.lineWidth),
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
updateDraggedBlockHighlight(block) {
|
|
664
|
-
const rect = block.getBoundingClientRect();
|
|
665
|
-
this.draggedBlockHighlightState.set({
|
|
666
|
-
transform: `translate3d(${Math.round(rect.left)}px, ${Math.round(rect.top)}px, 0)`,
|
|
667
|
-
width: Math.round(rect.width),
|
|
668
|
-
height: Math.round(rect.height),
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
finishDrag(shouldDrop) {
|
|
672
|
-
const session = this.dragSession;
|
|
673
|
-
if (!session) {
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
const dropTarget = this.dropIndicatorState()?.target ?? null;
|
|
677
|
-
window.removeEventListener('pointermove', session.pointerMove);
|
|
678
|
-
window.removeEventListener('pointerup', session.pointerUp);
|
|
679
|
-
window.removeEventListener('pointercancel', session.pointerCancel);
|
|
680
|
-
this.surface?.removeAttribute('data-qalma-drag-active');
|
|
681
|
-
document.body.style.cursor = session.previousBodyCursor;
|
|
682
|
-
document.body.style.userSelect = session.previousBodyUserSelect;
|
|
683
|
-
this.dragSession = null;
|
|
684
|
-
this.setDropIndicator(null);
|
|
685
|
-
this.draggedBlockHighlightState.set(null);
|
|
686
|
-
if (shouldDrop && session.dragging && dropTarget) {
|
|
687
|
-
this.editor().execute('moveBlockTo', dropTarget);
|
|
688
|
-
this.hide();
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
scheduleRefresh() {
|
|
692
|
-
if (this.refreshFrame !== null) {
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
this.refreshFrame = requestAnimationFrame(() => {
|
|
696
|
-
this.refreshFrame = null;
|
|
697
|
-
this.refresh();
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
refresh() {
|
|
701
|
-
const block = this.pendingBlock ?? this.currentBlock;
|
|
702
|
-
this.pendingBlock = null;
|
|
703
|
-
if (!block) {
|
|
704
|
-
this.hide();
|
|
705
|
-
return;
|
|
706
|
-
}
|
|
707
|
-
this.updateFromBlock(block);
|
|
708
|
-
}
|
|
709
|
-
updateFromBlock(block) {
|
|
710
|
-
const surface = this.surface;
|
|
711
|
-
const target = parseDragHandleTarget(block);
|
|
712
|
-
if (!surface || !target) {
|
|
713
|
-
this.hide();
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
const rect = block.getBoundingClientRect();
|
|
717
|
-
if (!isRectVisibleInsideSurface$1(rect, surface)) {
|
|
718
|
-
this.hide();
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
const surfaceRect = surface.getBoundingClientRect();
|
|
722
|
-
const position = anchorToRect(rect, {
|
|
723
|
-
placement: 'left',
|
|
724
|
-
boundary: surfaceRect,
|
|
725
|
-
size: { width: HANDLE_BUTTON_SIZE, height: HANDLE_BUTTON_SIZE },
|
|
726
|
-
gap: HANDLE_GAP,
|
|
727
|
-
edgeMargin: HANDLE_EDGE_MARGIN,
|
|
728
|
-
align: this.lastPointerClientY ?? rect.top + rect.height / 2,
|
|
729
|
-
});
|
|
730
|
-
const canMoveUp = this.editor().canExecute('moveBlockUp', target);
|
|
731
|
-
const canMoveDown = this.editor().canExecute('moveBlockDown', target);
|
|
732
|
-
this.currentBlock = block;
|
|
733
|
-
this.setHandle({
|
|
734
|
-
target,
|
|
735
|
-
transform: `translate3d(${Math.round(position.left)}px, ${Math.round(position.top)}px, 0)`,
|
|
736
|
-
blockType: block.dataset['qalmaDragHandleType'] ?? 'block',
|
|
737
|
-
canMoveUp,
|
|
738
|
-
canMoveDown,
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
cancelScheduledRefresh() {
|
|
742
|
-
if (this.refreshFrame === null) {
|
|
743
|
-
return;
|
|
744
|
-
}
|
|
745
|
-
cancelAnimationFrame(this.refreshFrame);
|
|
746
|
-
this.refreshFrame = null;
|
|
747
|
-
}
|
|
748
|
-
setHandle(handle) {
|
|
749
|
-
const nextKey = handle
|
|
750
|
-
? [
|
|
751
|
-
handle.target.pos,
|
|
752
|
-
handle.transform,
|
|
753
|
-
handle.blockType,
|
|
754
|
-
handle.canMoveUp,
|
|
755
|
-
handle.canMoveDown,
|
|
756
|
-
].join(':')
|
|
757
|
-
: null;
|
|
758
|
-
if (nextKey === this.handleKey) {
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
this.handleKey = nextKey;
|
|
762
|
-
this.handleState.set(handle);
|
|
763
|
-
}
|
|
764
|
-
setDropIndicator(indicator) {
|
|
765
|
-
this.dropIndicatorState.set(indicator);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
function findDragDropCandidate(surface, sourcePos, clientY) {
|
|
769
|
-
const blocks = Array.from(surface.querySelectorAll('[data-qalma-drag-handle-block]'));
|
|
770
|
-
const lineRect = getDropLineRect(surface);
|
|
771
|
-
let lastCandidate = null;
|
|
772
|
-
for (const block of blocks) {
|
|
773
|
-
const range = parseDragHandleBlockRange(block);
|
|
774
|
-
if (!range) {
|
|
775
|
-
continue;
|
|
776
|
-
}
|
|
777
|
-
const rect = block.getBoundingClientRect();
|
|
778
|
-
const midpoint = rect.top + rect.height / 2;
|
|
779
|
-
if (clientY < midpoint) {
|
|
780
|
-
return {
|
|
781
|
-
target: {
|
|
782
|
-
pos: sourcePos,
|
|
783
|
-
targetPos: range.pos,
|
|
784
|
-
},
|
|
785
|
-
lineX: lineRect.left,
|
|
786
|
-
lineY: rect.top,
|
|
787
|
-
lineWidth: lineRect.width,
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
lastCandidate = {
|
|
791
|
-
target: {
|
|
792
|
-
pos: sourcePos,
|
|
793
|
-
targetPos: range.to,
|
|
794
|
-
},
|
|
795
|
-
lineX: lineRect.left,
|
|
796
|
-
lineY: rect.bottom,
|
|
797
|
-
lineWidth: lineRect.width,
|
|
798
|
-
};
|
|
799
|
-
}
|
|
800
|
-
return lastCandidate;
|
|
801
|
-
}
|
|
802
|
-
function getDropLineRect(surface) {
|
|
803
|
-
const content = surface.querySelector('.ProseMirror') ?? surface;
|
|
804
|
-
const rect = content.getBoundingClientRect();
|
|
805
|
-
const width = Math.max(0, rect.width - DROP_LINE_EDGE_MARGIN * 2);
|
|
806
|
-
return {
|
|
807
|
-
left: rect.left + DROP_LINE_EDGE_MARGIN,
|
|
808
|
-
width,
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
function findDragHandleBlock(target, surface) {
|
|
812
|
-
const block = target.closest('[data-qalma-drag-handle-block]');
|
|
813
|
-
return block && surface.contains(block) ? block : null;
|
|
814
|
-
}
|
|
815
|
-
function findDragHandleBlockByPos(surface, pos) {
|
|
816
|
-
const blocks = Array.from(surface.querySelectorAll('[data-qalma-drag-handle-block]'));
|
|
817
|
-
return (blocks.find((block) => parseDragHandleTarget(block)?.pos === pos) ?? null);
|
|
818
|
-
}
|
|
819
|
-
function parseDragHandleTarget(block) {
|
|
820
|
-
const pos = Number(block.dataset['qalmaDragHandlePos']);
|
|
821
|
-
return Number.isInteger(pos) ? { pos } : null;
|
|
822
|
-
}
|
|
823
|
-
function parseDragHandleBlockRange(block) {
|
|
824
|
-
const target = parseDragHandleTarget(block);
|
|
825
|
-
const to = Number(block.dataset['qalmaDragHandleTo']);
|
|
826
|
-
return target && Number.isInteger(to) ? { ...target, to } : null;
|
|
827
|
-
}
|
|
828
|
-
function isRectVisibleInsideSurface$1(rect, surface) {
|
|
829
|
-
const surfaceRect = surface.getBoundingClientRect();
|
|
830
|
-
return (rect.bottom > surfaceRect.top &&
|
|
831
|
-
rect.top < surfaceRect.bottom &&
|
|
832
|
-
rect.right > surfaceRect.left &&
|
|
833
|
-
rect.left < surfaceRect.right);
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
class QalmaDragHandleDirective {
|
|
837
|
-
editor = input.required({ ...(ngDevMode ? { debugName: "editor" } : /* istanbul ignore next */ {}), alias: 'qalmaDragHandle' });
|
|
838
|
-
destroyRef = inject(DestroyRef);
|
|
354
|
+
class QalmaButton {
|
|
839
355
|
host = inject(ElementRef);
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
this.controller.connect(this.host.nativeElement, this.destroyRef);
|
|
847
|
-
});
|
|
848
|
-
}
|
|
849
|
-
hide() {
|
|
850
|
-
this.controller.hide();
|
|
851
|
-
}
|
|
852
|
-
startDrag(event, handle) {
|
|
853
|
-
this.controller.startDrag(event, handle);
|
|
854
|
-
}
|
|
855
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaDragHandleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
856
|
-
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 });
|
|
356
|
+
initialClass = this.host.nativeElement.getAttribute('class') ?? '';
|
|
357
|
+
variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
358
|
+
size = input('default', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
359
|
+
computedClass = computed(() => cn(buttonVariants({ variant: this.variant(), size: this.size() }), this.initialClass), ...(ngDevMode ? [{ debugName: "computedClass" }] : /* istanbul ignore next */ []));
|
|
360
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaButton, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
361
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.17", type: QalmaButton, isStandalone: true, selector: "button[qalmaBtn], a[qalmaBtn]", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "computedClass()" } }, ngImport: i0 });
|
|
857
362
|
}
|
|
858
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type:
|
|
363
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaButton, decorators: [{
|
|
859
364
|
type: Directive,
|
|
860
365
|
args: [{
|
|
861
|
-
|
|
862
|
-
|
|
366
|
+
selector: 'button[qalmaBtn], a[qalmaBtn]',
|
|
367
|
+
host: {
|
|
368
|
+
'[class]': 'computedClass()',
|
|
369
|
+
},
|
|
863
370
|
}]
|
|
864
|
-
}],
|
|
371
|
+
}], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
|
|
865
372
|
|
|
866
373
|
class QalmaDragHandle {
|
|
867
374
|
editor = input.required(...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
|
|
@@ -1170,108 +677,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
|
|
|
1170
677
|
}]
|
|
1171
678
|
}], ctorParameters: () => [], propDecorators: { editor: [{ type: i0.Input, args: [{ isSignal: true, alias: "editor", required: true }] }], handle: [{ type: i0.Input, args: [{ isSignal: true, alias: "handle", required: false }] }], dropIndicator: [{ type: i0.Input, args: [{ isSignal: true, alias: "dropIndicator", required: false }] }], draggedBlockHighlight: [{ type: i0.Input, args: [{ isSignal: true, alias: "draggedBlockHighlight", required: false }] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }], dragStart: [{ type: i0.Output, args: ["dragStart"] }] } });
|
|
1172
679
|
|
|
1173
|
-
const DEFAULT_MARGIN = 12;
|
|
1174
|
-
const DEFAULT_GAP = 8;
|
|
1175
|
-
/**
|
|
1176
|
-
* Positions a floating list under an anchor rect, flipping it above when there
|
|
1177
|
-
* is not enough room below, and clamping its height and horizontal position to
|
|
1178
|
-
* the viewport. Shared by the mention and slash-command menus, whose flip
|
|
1179
|
-
* behavior `anchorToRect` does not cover (it has no flip / dynamic max-height).
|
|
1180
|
-
*/
|
|
1181
|
-
function flipAbovePlacement(rect, options) {
|
|
1182
|
-
const { width, desiredHeight, minHeight, margin = DEFAULT_MARGIN, gap = DEFAULT_GAP, } = options;
|
|
1183
|
-
const availableBelow = window.innerHeight - rect.bottom - margin - gap;
|
|
1184
|
-
const availableAbove = rect.top - margin - gap;
|
|
1185
|
-
const openAbove = availableBelow < desiredHeight && availableAbove > availableBelow;
|
|
1186
|
-
const availableHeight = Math.max(minHeight, openAbove ? availableAbove : availableBelow);
|
|
1187
|
-
const maxHeight = Math.min(desiredHeight, availableHeight);
|
|
1188
|
-
const leftBoundary = Math.max(margin, window.innerWidth - width - margin);
|
|
1189
|
-
return {
|
|
1190
|
-
left: Math.min(Math.max(rect.left, margin), leftBoundary),
|
|
1191
|
-
top: openAbove ? null : rect.bottom + gap,
|
|
1192
|
-
bottom: openAbove ? window.innerHeight - rect.top + gap : null,
|
|
1193
|
-
maxHeight,
|
|
1194
|
-
};
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
/** Option buttons carry this attribute so the base can focus/scroll to one by index. */
|
|
1198
|
-
const SUGGESTION_OPTION_INDEX_ATTR = 'data-suggestion-index';
|
|
1199
|
-
/** The scrollable options container carries this attribute (optional). */
|
|
1200
|
-
const SUGGESTION_OPTIONS_SCROLLER_ATTR = 'data-suggestion-options';
|
|
1201
|
-
/**
|
|
1202
|
-
* Shared behavior for caret-anchored suggestion menus (mention, slash command):
|
|
1203
|
-
* flip-above placement input, active-option highlighting, and in-menu keyboard
|
|
1204
|
-
* handling (Escape dismisses, Arrow moves + focuses, Enter/Space picks) with
|
|
1205
|
-
* wrap-around and optional scroll-into-view. Concrete menus only supply their
|
|
1206
|
-
* own item markup and expose their option list via `optionList`.
|
|
1207
|
-
*/
|
|
1208
|
-
class QalmaSuggestionMenu {
|
|
1209
|
-
elementRef = inject(ElementRef);
|
|
1210
|
-
placement = input(null, ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
|
|
1211
|
-
activeIndex = input(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
|
|
1212
|
-
activate = output();
|
|
1213
|
-
pick = output();
|
|
1214
|
-
dismiss = output();
|
|
1215
|
-
preserveSelection(event) {
|
|
1216
|
-
event.preventDefault();
|
|
1217
|
-
}
|
|
1218
|
-
handleOptionKeydown(event, option, index) {
|
|
1219
|
-
if (event.key === 'Escape') {
|
|
1220
|
-
event.preventDefault();
|
|
1221
|
-
this.dismiss.emit();
|
|
1222
|
-
return;
|
|
1223
|
-
}
|
|
1224
|
-
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
1225
|
-
event.preventDefault();
|
|
1226
|
-
const nextIndex = this.getNextIndex(index, event.key === 'ArrowDown' ? 1 : -1);
|
|
1227
|
-
this.activate.emit(nextIndex);
|
|
1228
|
-
queueMicrotask(() => this.focusOption(nextIndex));
|
|
1229
|
-
return;
|
|
1230
|
-
}
|
|
1231
|
-
if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {
|
|
1232
|
-
event.preventDefault();
|
|
1233
|
-
this.pick.emit(option);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
focusOption(index) {
|
|
1237
|
-
this.optionElement(index)?.focus();
|
|
1238
|
-
// No-op for menus without a marked scroller (native focus handles scroll).
|
|
1239
|
-
this.scrollOptionIntoView(index);
|
|
1240
|
-
}
|
|
1241
|
-
scrollOptionIntoView(index) {
|
|
1242
|
-
const scroller = this.optionsScroller();
|
|
1243
|
-
const option = this.optionElement(index);
|
|
1244
|
-
if (!scroller || !option) {
|
|
1245
|
-
return;
|
|
1246
|
-
}
|
|
1247
|
-
const optionTop = option.offsetTop - scroller.offsetTop;
|
|
1248
|
-
const optionBottom = optionTop + option.offsetHeight;
|
|
1249
|
-
const visibleTop = scroller.scrollTop;
|
|
1250
|
-
const visibleBottom = visibleTop + scroller.clientHeight;
|
|
1251
|
-
if (optionTop < visibleTop) {
|
|
1252
|
-
scroller.scrollTop = optionTop;
|
|
1253
|
-
}
|
|
1254
|
-
else if (optionBottom > visibleBottom) {
|
|
1255
|
-
scroller.scrollTop = optionBottom - scroller.clientHeight;
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
getNextIndex(index, delta) {
|
|
1259
|
-
const length = this.optionList().length;
|
|
1260
|
-
return length > 0 ? (index + delta + length) % length : index;
|
|
1261
|
-
}
|
|
1262
|
-
optionElement(index) {
|
|
1263
|
-
return this.elementRef.nativeElement.querySelector(`[${SUGGESTION_OPTION_INDEX_ATTR}="${index}"]`);
|
|
1264
|
-
}
|
|
1265
|
-
optionsScroller() {
|
|
1266
|
-
return this.elementRef.nativeElement.querySelector(`[${SUGGESTION_OPTIONS_SCROLLER_ATTR}]`);
|
|
1267
|
-
}
|
|
1268
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaSuggestionMenu, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1269
|
-
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 });
|
|
1270
|
-
}
|
|
1271
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaSuggestionMenu, decorators: [{
|
|
1272
|
-
type: Directive
|
|
1273
|
-
}], 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"] }] } });
|
|
1274
|
-
|
|
1275
680
|
/**
|
|
1276
681
|
* Caret-anchored mention suggestion menu: an avatar-initial list with a loading
|
|
1277
682
|
* state. Positioning, keyboard handling and active highlighting come from
|
|
@@ -1666,148 +1071,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
|
|
|
1666
1071
|
}]
|
|
1667
1072
|
}], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }] } });
|
|
1668
1073
|
|
|
1669
|
-
const LINK_POPOVER_WIDTH = 360;
|
|
1670
|
-
// Both the editing and preview rows are a single h-9 (36px) control plus
|
|
1671
|
-
// padding/border; this is an estimate since the actual element isn't
|
|
1672
|
-
// rendered yet when the placement is computed.
|
|
1673
|
-
const LINK_POPOVER_HEIGHT_ESTIMATE = 56;
|
|
1674
|
-
function createLinkPopoverPlacement(element) {
|
|
1675
|
-
const rect = element.getBoundingClientRect();
|
|
1676
|
-
return anchorToRect(rect, {
|
|
1677
|
-
placement: 'bottom',
|
|
1678
|
-
boundary: {
|
|
1679
|
-
top: 0,
|
|
1680
|
-
bottom: window.innerHeight,
|
|
1681
|
-
left: 0,
|
|
1682
|
-
right: window.innerWidth,
|
|
1683
|
-
},
|
|
1684
|
-
size: { width: LINK_POPOVER_WIDTH, height: LINK_POPOVER_HEIGHT_ESTIMATE },
|
|
1685
|
-
gap: 8,
|
|
1686
|
-
edgeMargin: 16,
|
|
1687
|
-
align: 'start',
|
|
1688
|
-
});
|
|
1689
|
-
}
|
|
1690
|
-
function findEditorLinkElement(target) {
|
|
1691
|
-
if (!(target instanceof Element)) {
|
|
1692
|
-
return null;
|
|
1693
|
-
}
|
|
1694
|
-
const element = target.closest('.ProseMirror a[href]');
|
|
1695
|
-
return element instanceof HTMLAnchorElement ? element : null;
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
class LinkPopoverController {
|
|
1699
|
-
editor;
|
|
1700
|
-
popover = signal(null, ...(ngDevMode ? [{ debugName: "popover" }] : /* istanbul ignore next */ []));
|
|
1701
|
-
href = signal('', ...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
|
|
1702
|
-
hideTimeout;
|
|
1703
|
-
constructor(editor) {
|
|
1704
|
-
this.editor = editor;
|
|
1705
|
-
}
|
|
1706
|
-
showToolbarEditor(event) {
|
|
1707
|
-
const linkState = this.editor.query('link');
|
|
1708
|
-
const target = event.currentTarget;
|
|
1709
|
-
if (!(target instanceof HTMLElement)) {
|
|
1710
|
-
return;
|
|
1711
|
-
}
|
|
1712
|
-
this.keepOpen();
|
|
1713
|
-
this.href.set(linkState?.href ?? 'https://');
|
|
1714
|
-
this.popover.set({
|
|
1715
|
-
...createLinkPopoverPlacement(target),
|
|
1716
|
-
editing: true,
|
|
1717
|
-
element: null,
|
|
1718
|
-
href: linkState?.href ?? '',
|
|
1719
|
-
rel: linkState?.rel ?? null,
|
|
1720
|
-
target: linkState?.target ?? null,
|
|
1721
|
-
text: linkState?.text ?? '',
|
|
1722
|
-
});
|
|
1723
|
-
}
|
|
1724
|
-
showPreview(event) {
|
|
1725
|
-
const element = findEditorLinkElement(event.target);
|
|
1726
|
-
if (!element) {
|
|
1727
|
-
return;
|
|
1728
|
-
}
|
|
1729
|
-
this.keepOpen();
|
|
1730
|
-
this.popover.set({
|
|
1731
|
-
...createLinkPopoverPlacement(element),
|
|
1732
|
-
editing: false,
|
|
1733
|
-
element,
|
|
1734
|
-
href: element.href,
|
|
1735
|
-
rel: element.rel || null,
|
|
1736
|
-
target: element.target === '_blank' ? '_blank' : null,
|
|
1737
|
-
text: element.textContent?.trim() ?? '',
|
|
1738
|
-
});
|
|
1739
|
-
}
|
|
1740
|
-
scheduleHideFromEvent(event) {
|
|
1741
|
-
const nextTarget = event.relatedTarget;
|
|
1742
|
-
if (nextTarget instanceof Element &&
|
|
1743
|
-
(findEditorLinkElement(nextTarget) ||
|
|
1744
|
-
nextTarget.closest('[data-qalma-link-popover]'))) {
|
|
1745
|
-
return;
|
|
1746
|
-
}
|
|
1747
|
-
if (!this.popover()?.editing) {
|
|
1748
|
-
this.scheduleHide();
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
edit(popover) {
|
|
1752
|
-
this.keepOpen();
|
|
1753
|
-
this.selectLink(popover);
|
|
1754
|
-
this.href.set(popover.href);
|
|
1755
|
-
this.popover.set({
|
|
1756
|
-
...popover,
|
|
1757
|
-
editing: true,
|
|
1758
|
-
});
|
|
1759
|
-
}
|
|
1760
|
-
save(popover) {
|
|
1761
|
-
const href = this.href().trim();
|
|
1762
|
-
if (!href) {
|
|
1763
|
-
return;
|
|
1764
|
-
}
|
|
1765
|
-
this.selectLink(popover);
|
|
1766
|
-
this.editor.execute('setLink', href);
|
|
1767
|
-
this.hide();
|
|
1768
|
-
}
|
|
1769
|
-
remove(popover) {
|
|
1770
|
-
this.selectLink(popover);
|
|
1771
|
-
this.editor.execute('unsetLink');
|
|
1772
|
-
this.hide();
|
|
1773
|
-
}
|
|
1774
|
-
hide() {
|
|
1775
|
-
this.clearHideTimeout();
|
|
1776
|
-
this.popover.set(null);
|
|
1777
|
-
}
|
|
1778
|
-
keepOpen() {
|
|
1779
|
-
this.clearHideTimeout();
|
|
1780
|
-
}
|
|
1781
|
-
scheduleHide() {
|
|
1782
|
-
this.clearHideTimeout();
|
|
1783
|
-
if (this.popover()?.editing) {
|
|
1784
|
-
return;
|
|
1785
|
-
}
|
|
1786
|
-
this.hideTimeout = setTimeout(() => {
|
|
1787
|
-
if (this.popover()?.editing) {
|
|
1788
|
-
this.hideTimeout = undefined;
|
|
1789
|
-
return;
|
|
1790
|
-
}
|
|
1791
|
-
this.popover.set(null);
|
|
1792
|
-
this.hideTimeout = undefined;
|
|
1793
|
-
}, 160);
|
|
1794
|
-
}
|
|
1795
|
-
selectLink(popover) {
|
|
1796
|
-
if (popover.element) {
|
|
1797
|
-
this.editor.execute('selectLink', {
|
|
1798
|
-
element: popover.element,
|
|
1799
|
-
});
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
clearHideTimeout() {
|
|
1803
|
-
if (!this.hideTimeout) {
|
|
1804
|
-
return;
|
|
1805
|
-
}
|
|
1806
|
-
clearTimeout(this.hideTimeout);
|
|
1807
|
-
this.hideTimeout = undefined;
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
1074
|
class QalmaLinkPopover {
|
|
1812
1075
|
popover = input(null, ...(ngDevMode ? [{ debugName: "popover" }] : /* istanbul ignore next */ []));
|
|
1813
1076
|
href = input.required(...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
|
|
@@ -2056,13 +1319,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
|
|
|
2056
1319
|
}]
|
|
2057
1320
|
}], propDecorators: { popover: [{ type: i0.Input, args: [{ isSignal: true, alias: "popover", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: true }] }], hrefChange: [{ type: i0.Output, args: ["hrefChange"] }], edit: [{ type: i0.Output, args: ["edit"] }], save: [{ type: i0.Output, args: ["save"] }], remove: [{ type: i0.Output, args: ["remove"] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }], keepOpen: [{ type: i0.Output, args: ["keepOpen"] }], scheduleHide: [{ type: i0.Output, args: ["scheduleHide"] }] } });
|
|
2058
1321
|
|
|
1322
|
+
/**
|
|
1323
|
+
* Floating, selection-anchored toolbar container. It owns the hard parts — the
|
|
1324
|
+
* fixed-position `transform` from a {@link QalmaContextualToolbarPlacement}
|
|
1325
|
+
* (see `QalmaSelectionToolbarDirective`), preserving the editor selection on
|
|
1326
|
+
* pointer-down, and Escape-to-dismiss — and projects whatever controls you put
|
|
1327
|
+
* inside it. Nothing about the button set is baked in: compose
|
|
1328
|
+
* `<qalma-toolbar-button>` for commands and your own buttons for app actions
|
|
1329
|
+
* (opening a link editor, a color picker, an AI action…).
|
|
1330
|
+
*
|
|
1331
|
+
* The container's `mousedown` handler preserves the selection for every
|
|
1332
|
+
* projected control via event bubbling, so inner buttons don't each need to.
|
|
1333
|
+
*
|
|
1334
|
+
* ```html
|
|
1335
|
+
* <qalma-contextual-toolbar
|
|
1336
|
+
* [placement]="selection.placement()"
|
|
1337
|
+
* (dismiss)="selection.hide()"
|
|
1338
|
+
* >
|
|
1339
|
+
* <qalma-toolbar-button command="toggleBold" icon="lucideBold" label="Bold" />
|
|
1340
|
+
* <qalma-toolbar-button command="toggleItalic" icon="lucideItalic" label="Italic" />
|
|
1341
|
+
* <button type="button" [class]="TOOLBAR_BUTTON_CLASS" (click)="openLinkEditor()">…</button>
|
|
1342
|
+
* </qalma-contextual-toolbar>
|
|
1343
|
+
* ```
|
|
1344
|
+
*/
|
|
2059
1345
|
class QalmaContextualToolbar {
|
|
2060
|
-
editor = input.required(...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
|
|
2061
1346
|
placement = input(null, ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
|
|
2062
|
-
|
|
1347
|
+
/** Accessible name for the toolbar region. */
|
|
1348
|
+
label = input('Selection formatting', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
2063
1349
|
dismiss = output();
|
|
2064
|
-
canSetLink = computed(() => this.editor().canExecute('setLink', 'https://angular.dev'), ...(ngDevMode ? [{ debugName: "canSetLink" }] : /* istanbul ignore next */ []));
|
|
2065
|
-
linkActive = computed(() => this.editor().isCommandActive('setLink'), ...(ngDevMode ? [{ debugName: "linkActive" }] : /* istanbul ignore next */ []));
|
|
2066
1350
|
preserveSelection(event) {
|
|
2067
1351
|
event.preventDefault();
|
|
2068
1352
|
}
|
|
@@ -2071,397 +1355,57 @@ class QalmaContextualToolbar {
|
|
|
2071
1355
|
this.dismiss.emit();
|
|
2072
1356
|
}
|
|
2073
1357
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaContextualToolbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2074
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: QalmaContextualToolbar, isStandalone: true, selector: "qalma-contextual-toolbar", inputs: {
|
|
2075
|
-
provideIcons({
|
|
2076
|
-
lucideBold,
|
|
2077
|
-
lucideCode,
|
|
2078
|
-
lucideItalic,
|
|
2079
|
-
lucideLetterText,
|
|
2080
|
-
lucideLink,
|
|
2081
|
-
}),
|
|
2082
|
-
], ngImport: i0, template: `
|
|
1358
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: QalmaContextualToolbar, isStandalone: true, selector: "qalma-contextual-toolbar", inputs: { placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismiss: "dismiss" }, ngImport: i0, template: `
|
|
2083
1359
|
@if (placement(); as placement) {
|
|
2084
1360
|
<qalma-toolbar
|
|
2085
|
-
label="
|
|
1361
|
+
[label]="label()"
|
|
2086
1362
|
class="fixed left-0 top-0 z-30 flex items-center gap-0.5 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg will-change-transform"
|
|
2087
1363
|
[style.transform]="placement.transform"
|
|
2088
1364
|
(mousedown)="preserveSelection($event)"
|
|
2089
1365
|
(keydown.escape)="dismissFromKeyboard($event)"
|
|
2090
1366
|
>
|
|
2091
|
-
<
|
|
2092
|
-
qalmaBtn
|
|
2093
|
-
variant="ghost"
|
|
2094
|
-
size="icon"
|
|
2095
|
-
type="button"
|
|
2096
|
-
class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
|
|
2097
|
-
qalmaCommand="toggleBold"
|
|
2098
|
-
title="Bold"
|
|
2099
|
-
aria-label="Bold"
|
|
2100
|
-
>
|
|
2101
|
-
<ng-icon
|
|
2102
|
-
class="text-[0.9rem]"
|
|
2103
|
-
name="lucideBold"
|
|
2104
|
-
aria-hidden="true"
|
|
2105
|
-
/>
|
|
2106
|
-
</button>
|
|
2107
|
-
<button
|
|
2108
|
-
qalmaBtn
|
|
2109
|
-
variant="ghost"
|
|
2110
|
-
size="icon"
|
|
2111
|
-
type="button"
|
|
2112
|
-
class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
|
|
2113
|
-
qalmaCommand="toggleItalic"
|
|
2114
|
-
title="Italic"
|
|
2115
|
-
aria-label="Italic"
|
|
2116
|
-
>
|
|
2117
|
-
<ng-icon
|
|
2118
|
-
class="text-[0.9rem]"
|
|
2119
|
-
name="lucideItalic"
|
|
2120
|
-
aria-hidden="true"
|
|
2121
|
-
/>
|
|
2122
|
-
</button>
|
|
2123
|
-
<button
|
|
2124
|
-
qalmaBtn
|
|
2125
|
-
variant="ghost"
|
|
2126
|
-
size="icon"
|
|
2127
|
-
type="button"
|
|
2128
|
-
class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
|
|
2129
|
-
qalmaCommand="toggleInlineCode"
|
|
2130
|
-
title="Inline code"
|
|
2131
|
-
aria-label="Inline code"
|
|
2132
|
-
>
|
|
2133
|
-
<ng-icon
|
|
2134
|
-
class="text-[0.9rem]"
|
|
2135
|
-
name="lucideCode"
|
|
2136
|
-
aria-hidden="true"
|
|
2137
|
-
/>
|
|
2138
|
-
</button>
|
|
2139
|
-
<button
|
|
2140
|
-
qalmaBtn
|
|
2141
|
-
variant="ghost"
|
|
2142
|
-
size="icon"
|
|
2143
|
-
type="button"
|
|
2144
|
-
class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
|
|
2145
|
-
qalmaCommand="toggleMonospace"
|
|
2146
|
-
title="Monospace"
|
|
2147
|
-
aria-label="Monospace"
|
|
2148
|
-
>
|
|
2149
|
-
<ng-icon
|
|
2150
|
-
class="text-[0.9rem]"
|
|
2151
|
-
name="lucideLetterText"
|
|
2152
|
-
aria-hidden="true"
|
|
2153
|
-
/>
|
|
2154
|
-
</button>
|
|
2155
|
-
<span class="mx-0.5 h-5 w-px bg-border" aria-hidden="true"></span>
|
|
2156
|
-
<button
|
|
2157
|
-
qalmaBtn
|
|
2158
|
-
variant="ghost"
|
|
2159
|
-
size="icon"
|
|
2160
|
-
type="button"
|
|
2161
|
-
class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
|
|
2162
|
-
[class.qalma-command-active]="linkActive()"
|
|
2163
|
-
[attr.aria-pressed]="linkActive()"
|
|
2164
|
-
[disabled]="!canSetLink()"
|
|
2165
|
-
(mousedown)="preserveSelection($event)"
|
|
2166
|
-
(click)="requestLink.emit($event)"
|
|
2167
|
-
title="Link"
|
|
2168
|
-
aria-label="Link"
|
|
2169
|
-
>
|
|
2170
|
-
<ng-icon
|
|
2171
|
-
class="text-[0.9rem]"
|
|
2172
|
-
name="lucideLink"
|
|
2173
|
-
aria-hidden="true"
|
|
2174
|
-
/>
|
|
2175
|
-
</button>
|
|
1367
|
+
<ng-content />
|
|
2176
1368
|
</qalma-toolbar>
|
|
2177
1369
|
}
|
|
2178
|
-
`, isInline: true, dependencies: [{ kind: "component", type:
|
|
1370
|
+
`, isInline: true, dependencies: [{ kind: "component", type: QalmaToolbar, selector: "qalma-toolbar", inputs: ["label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2179
1371
|
}
|
|
2180
1372
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaContextualToolbar, decorators: [{
|
|
2181
1373
|
type: Component,
|
|
2182
1374
|
args: [{
|
|
2183
1375
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2184
|
-
imports: [
|
|
2185
|
-
providers: [
|
|
2186
|
-
provideIcons({
|
|
2187
|
-
lucideBold,
|
|
2188
|
-
lucideCode,
|
|
2189
|
-
lucideItalic,
|
|
2190
|
-
lucideLetterText,
|
|
2191
|
-
lucideLink,
|
|
2192
|
-
}),
|
|
2193
|
-
],
|
|
1376
|
+
imports: [QalmaToolbar],
|
|
2194
1377
|
selector: 'qalma-contextual-toolbar',
|
|
2195
1378
|
template: `
|
|
2196
1379
|
@if (placement(); as placement) {
|
|
2197
1380
|
<qalma-toolbar
|
|
2198
|
-
label="
|
|
1381
|
+
[label]="label()"
|
|
2199
1382
|
class="fixed left-0 top-0 z-30 flex items-center gap-0.5 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg will-change-transform"
|
|
2200
1383
|
[style.transform]="placement.transform"
|
|
2201
1384
|
(mousedown)="preserveSelection($event)"
|
|
2202
1385
|
(keydown.escape)="dismissFromKeyboard($event)"
|
|
2203
1386
|
>
|
|
2204
|
-
<
|
|
2205
|
-
qalmaBtn
|
|
2206
|
-
variant="ghost"
|
|
2207
|
-
size="icon"
|
|
2208
|
-
type="button"
|
|
2209
|
-
class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
|
|
2210
|
-
qalmaCommand="toggleBold"
|
|
2211
|
-
title="Bold"
|
|
2212
|
-
aria-label="Bold"
|
|
2213
|
-
>
|
|
2214
|
-
<ng-icon
|
|
2215
|
-
class="text-[0.9rem]"
|
|
2216
|
-
name="lucideBold"
|
|
2217
|
-
aria-hidden="true"
|
|
2218
|
-
/>
|
|
2219
|
-
</button>
|
|
2220
|
-
<button
|
|
2221
|
-
qalmaBtn
|
|
2222
|
-
variant="ghost"
|
|
2223
|
-
size="icon"
|
|
2224
|
-
type="button"
|
|
2225
|
-
class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
|
|
2226
|
-
qalmaCommand="toggleItalic"
|
|
2227
|
-
title="Italic"
|
|
2228
|
-
aria-label="Italic"
|
|
2229
|
-
>
|
|
2230
|
-
<ng-icon
|
|
2231
|
-
class="text-[0.9rem]"
|
|
2232
|
-
name="lucideItalic"
|
|
2233
|
-
aria-hidden="true"
|
|
2234
|
-
/>
|
|
2235
|
-
</button>
|
|
2236
|
-
<button
|
|
2237
|
-
qalmaBtn
|
|
2238
|
-
variant="ghost"
|
|
2239
|
-
size="icon"
|
|
2240
|
-
type="button"
|
|
2241
|
-
class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
|
|
2242
|
-
qalmaCommand="toggleInlineCode"
|
|
2243
|
-
title="Inline code"
|
|
2244
|
-
aria-label="Inline code"
|
|
2245
|
-
>
|
|
2246
|
-
<ng-icon
|
|
2247
|
-
class="text-[0.9rem]"
|
|
2248
|
-
name="lucideCode"
|
|
2249
|
-
aria-hidden="true"
|
|
2250
|
-
/>
|
|
2251
|
-
</button>
|
|
2252
|
-
<button
|
|
2253
|
-
qalmaBtn
|
|
2254
|
-
variant="ghost"
|
|
2255
|
-
size="icon"
|
|
2256
|
-
type="button"
|
|
2257
|
-
class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
|
|
2258
|
-
qalmaCommand="toggleMonospace"
|
|
2259
|
-
title="Monospace"
|
|
2260
|
-
aria-label="Monospace"
|
|
2261
|
-
>
|
|
2262
|
-
<ng-icon
|
|
2263
|
-
class="text-[0.9rem]"
|
|
2264
|
-
name="lucideLetterText"
|
|
2265
|
-
aria-hidden="true"
|
|
2266
|
-
/>
|
|
2267
|
-
</button>
|
|
2268
|
-
<span class="mx-0.5 h-5 w-px bg-border" aria-hidden="true"></span>
|
|
2269
|
-
<button
|
|
2270
|
-
qalmaBtn
|
|
2271
|
-
variant="ghost"
|
|
2272
|
-
size="icon"
|
|
2273
|
-
type="button"
|
|
2274
|
-
class="!h-[1.85rem] !w-[1.85rem] cursor-pointer rounded-[0.4rem] disabled:cursor-not-allowed [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent"
|
|
2275
|
-
[class.qalma-command-active]="linkActive()"
|
|
2276
|
-
[attr.aria-pressed]="linkActive()"
|
|
2277
|
-
[disabled]="!canSetLink()"
|
|
2278
|
-
(mousedown)="preserveSelection($event)"
|
|
2279
|
-
(click)="requestLink.emit($event)"
|
|
2280
|
-
title="Link"
|
|
2281
|
-
aria-label="Link"
|
|
2282
|
-
>
|
|
2283
|
-
<ng-icon
|
|
2284
|
-
class="text-[0.9rem]"
|
|
2285
|
-
name="lucideLink"
|
|
2286
|
-
aria-hidden="true"
|
|
2287
|
-
/>
|
|
2288
|
-
</button>
|
|
1387
|
+
<ng-content />
|
|
2289
1388
|
</qalma-toolbar>
|
|
2290
1389
|
}
|
|
2291
1390
|
`,
|
|
2292
1391
|
}]
|
|
2293
|
-
}], propDecorators: {
|
|
2294
|
-
|
|
2295
|
-
const TOOLBAR_EDGE_MARGIN = 16;
|
|
2296
|
-
const TOOLBAR_GAP = 8;
|
|
2297
|
-
class QalmaSelectionToolbarController {
|
|
2298
|
-
editor;
|
|
2299
|
-
placementState = signal(null, ...(ngDevMode ? [{ debugName: "placementState" }] : /* istanbul ignore next */ []));
|
|
2300
|
-
placement = this.placementState.asReadonly();
|
|
2301
|
-
surface = null;
|
|
2302
|
-
refreshFrame = null;
|
|
2303
|
-
placementKey = null;
|
|
2304
|
-
constructor(editor) {
|
|
2305
|
-
this.editor = editor;
|
|
2306
|
-
}
|
|
2307
|
-
connect(surface, destroyRef) {
|
|
2308
|
-
this.surface = surface;
|
|
2309
|
-
const refresh = () => this.refresh();
|
|
2310
|
-
const scheduleRefresh = () => this.scheduleRefresh();
|
|
2311
|
-
const handleKeydown = (event) => this.handleKeydown(event);
|
|
2312
|
-
surface.addEventListener('qalma-selection-update', refresh);
|
|
2313
|
-
surface.addEventListener('keydown', handleKeydown);
|
|
2314
|
-
surface.addEventListener('keyup', refresh);
|
|
2315
|
-
surface.addEventListener('mouseup', refresh);
|
|
2316
|
-
surface.addEventListener('scroll', scheduleRefresh, {
|
|
2317
|
-
passive: true,
|
|
2318
|
-
});
|
|
2319
|
-
window.addEventListener('scroll', scheduleRefresh, {
|
|
2320
|
-
passive: true,
|
|
2321
|
-
});
|
|
2322
|
-
window.addEventListener('resize', scheduleRefresh, {
|
|
2323
|
-
passive: true,
|
|
2324
|
-
});
|
|
2325
|
-
destroyRef.onDestroy(() => {
|
|
2326
|
-
this.cancelScheduledRefresh();
|
|
2327
|
-
this.surface = null;
|
|
2328
|
-
surface.removeEventListener('qalma-selection-update', refresh);
|
|
2329
|
-
surface.removeEventListener('keydown', handleKeydown);
|
|
2330
|
-
surface.removeEventListener('keyup', refresh);
|
|
2331
|
-
surface.removeEventListener('mouseup', refresh);
|
|
2332
|
-
surface.removeEventListener('scroll', scheduleRefresh);
|
|
2333
|
-
window.removeEventListener('scroll', scheduleRefresh);
|
|
2334
|
-
window.removeEventListener('resize', scheduleRefresh);
|
|
2335
|
-
});
|
|
2336
|
-
}
|
|
2337
|
-
refresh() {
|
|
2338
|
-
const surface = this.surface;
|
|
2339
|
-
const selection = this.editor().query('selection');
|
|
2340
|
-
if (!surface ||
|
|
2341
|
-
!selection ||
|
|
2342
|
-
selection.empty ||
|
|
2343
|
-
selection.text.trim().length === 0) {
|
|
2344
|
-
this.hide();
|
|
2345
|
-
return;
|
|
2346
|
-
}
|
|
2347
|
-
const domSelection = window.getSelection();
|
|
2348
|
-
if (!domSelection ||
|
|
2349
|
-
domSelection.rangeCount === 0 ||
|
|
2350
|
-
!isSelectionInsideSurface(domSelection, surface)) {
|
|
2351
|
-
this.hide();
|
|
2352
|
-
return;
|
|
2353
|
-
}
|
|
2354
|
-
const rect = getVisibleSelectionRect(domSelection.getRangeAt(0));
|
|
2355
|
-
if (!rect || !isRectVisibleInsideSurface(rect, surface)) {
|
|
2356
|
-
this.hide();
|
|
2357
|
-
return;
|
|
2358
|
-
}
|
|
2359
|
-
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
2360
|
-
const x = Math.round(clamp(rect.left + rect.width / 2, TOOLBAR_EDGE_MARGIN, viewportWidth - TOOLBAR_EDGE_MARGIN));
|
|
2361
|
-
const y = Math.round(Math.max(TOOLBAR_EDGE_MARGIN, rect.top - TOOLBAR_GAP));
|
|
2362
|
-
this.setPlacement({
|
|
2363
|
-
transform: `translate3d(${x}px, ${y}px, 0) translate(-50%, -100%)`,
|
|
2364
|
-
});
|
|
2365
|
-
}
|
|
2366
|
-
hide() {
|
|
2367
|
-
this.setPlacement(null);
|
|
2368
|
-
}
|
|
2369
|
-
handleKeydown(event) {
|
|
2370
|
-
if (!(event instanceof KeyboardEvent) ||
|
|
2371
|
-
event.key !== 'Escape' ||
|
|
2372
|
-
!this.placement()) {
|
|
2373
|
-
return;
|
|
2374
|
-
}
|
|
2375
|
-
event.preventDefault();
|
|
2376
|
-
this.hide();
|
|
2377
|
-
}
|
|
2378
|
-
scheduleRefresh() {
|
|
2379
|
-
if (!this.placement() || this.refreshFrame !== null) {
|
|
2380
|
-
return;
|
|
2381
|
-
}
|
|
2382
|
-
this.refreshFrame = requestAnimationFrame(() => {
|
|
2383
|
-
this.refreshFrame = null;
|
|
2384
|
-
this.refresh();
|
|
2385
|
-
});
|
|
2386
|
-
}
|
|
2387
|
-
cancelScheduledRefresh() {
|
|
2388
|
-
if (this.refreshFrame === null) {
|
|
2389
|
-
return;
|
|
2390
|
-
}
|
|
2391
|
-
cancelAnimationFrame(this.refreshFrame);
|
|
2392
|
-
this.refreshFrame = null;
|
|
2393
|
-
}
|
|
2394
|
-
setPlacement(placement) {
|
|
2395
|
-
const nextKey = placement?.transform ?? null;
|
|
2396
|
-
if (nextKey === this.placementKey) {
|
|
2397
|
-
return;
|
|
2398
|
-
}
|
|
2399
|
-
this.placementKey = nextKey;
|
|
2400
|
-
this.placementState.set(placement);
|
|
2401
|
-
}
|
|
2402
|
-
}
|
|
2403
|
-
function getVisibleSelectionRect(range) {
|
|
2404
|
-
const rect = range.getBoundingClientRect();
|
|
2405
|
-
if (rect.width > 0 || rect.height > 0) {
|
|
2406
|
-
return rect;
|
|
2407
|
-
}
|
|
2408
|
-
for (const clientRect of Array.from(range.getClientRects())) {
|
|
2409
|
-
if (clientRect.width > 0 || clientRect.height > 0) {
|
|
2410
|
-
return clientRect;
|
|
2411
|
-
}
|
|
2412
|
-
}
|
|
2413
|
-
return null;
|
|
2414
|
-
}
|
|
2415
|
-
function isSelectionInsideSurface(selection, surface) {
|
|
2416
|
-
return (isNodeInsideSurface(selection.anchorNode, surface) &&
|
|
2417
|
-
isNodeInsideSurface(selection.focusNode, surface));
|
|
2418
|
-
}
|
|
2419
|
-
function isNodeInsideSurface(node, surface) {
|
|
2420
|
-
const element = node instanceof Element ? node : node?.parentElement;
|
|
2421
|
-
return Boolean(element && surface.contains(element));
|
|
2422
|
-
}
|
|
2423
|
-
function isRectVisibleInsideSurface(rect, surface) {
|
|
2424
|
-
const surfaceRect = surface.getBoundingClientRect();
|
|
2425
|
-
return (rect.bottom > surfaceRect.top &&
|
|
2426
|
-
rect.top < surfaceRect.bottom &&
|
|
2427
|
-
rect.right > surfaceRect.left &&
|
|
2428
|
-
rect.left < surfaceRect.right);
|
|
2429
|
-
}
|
|
2430
|
-
function clamp(value, min, max) {
|
|
2431
|
-
return Math.min(Math.max(value, min), max);
|
|
2432
|
-
}
|
|
1392
|
+
}], propDecorators: { placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }] } });
|
|
2433
1393
|
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
refresh() {
|
|
2446
|
-
this.controller.refresh();
|
|
2447
|
-
}
|
|
2448
|
-
hide() {
|
|
2449
|
-
this.controller.hide();
|
|
2450
|
-
}
|
|
2451
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaSelectionToolbarDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
2452
|
-
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 });
|
|
2453
|
-
}
|
|
2454
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaSelectionToolbarDirective, decorators: [{
|
|
2455
|
-
type: Directive,
|
|
2456
|
-
args: [{
|
|
2457
|
-
exportAs: 'qalmaSelectionToolbar',
|
|
2458
|
-
selector: '[qalmaSelectionToolbar]',
|
|
2459
|
-
}]
|
|
2460
|
-
}], ctorParameters: () => [], propDecorators: { editor: [{ type: i0.Input, args: [{ isSignal: true, alias: "qalmaSelectionToolbar", required: true }] }] } });
|
|
1394
|
+
// @qalma/kit — Tailwind/shadcn-flavored UI for the Qalma editor.
|
|
1395
|
+
//
|
|
1396
|
+
// This entry point is the styled tier: components that pull `cva` + `@ng-icons`
|
|
1397
|
+
// and read the host's design tokens. It re-exports the entire dependency-free
|
|
1398
|
+
// `@qalma/kit/headless` surface (controllers, geometry, keyboard nav) so it
|
|
1399
|
+
// stays a superset — bring-your-own-design-system consumers can import the
|
|
1400
|
+
// leaner `@qalma/kit/headless` directly instead.
|
|
1401
|
+
//
|
|
1402
|
+
// The internal `QalmaButton` directive and `cn()` helper are intentionally NOT
|
|
1403
|
+
// exported: a host already has a button and a class-merge utility in its design
|
|
1404
|
+
// system. They remain private implementation details of the styled components.
|
|
2461
1405
|
|
|
2462
1406
|
/**
|
|
2463
1407
|
* Generated bundle index. Do not edit.
|
|
2464
1408
|
*/
|
|
2465
1409
|
|
|
2466
|
-
export {
|
|
1410
|
+
export { QALMA_TOOLBAR_ALIGN, QALMA_TOOLBAR_CLEAR_FORMATTING, QALMA_TOOLBAR_HEADINGS, QALMA_TOOLBAR_HISTORY, QALMA_TOOLBAR_INLINE_MARKS, QALMA_TOOLBAR_LISTS, QALMA_TOOLBAR_TABLE_INSERT, QALMA_TOOLBAR_TABLE_OPS, QALMA_TOOLBAR_UNSET_LINK, QalmaContextualToolbar, QalmaDragHandle, QalmaLinkPopover, QalmaMentionMenu, QalmaSlashCommandMenu, QalmaToolbarButton, QalmaToolbarRegistry, TOOLBAR_BUTTON_CLASS, TOOLBAR_SEPARATOR_CLASS, isToolbarCommandItem, provideQalmaToolbarIcons };
|
|
2467
1411
|
//# sourceMappingURL=qalma-kit.mjs.map
|