@qalma/kit 0.3.0 → 0.5.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 +196 -0
- package/fesm2022/qalma-kit-headless.mjs +932 -0
- package/fesm2022/qalma-kit-headless.mjs.map +1 -0
- package/fesm2022/qalma-kit.mjs +93 -1148
- 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 +36 -319
- package/types/qalma-kit.d.ts.map +1 -1
package/fesm2022/qalma-kit.mjs
CHANGED
|
@@ -1,70 +1,24 @@
|
|
|
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
|
|
62
15
|
* entirely by the host's shadcn-style design tokens (`text-muted-foreground`,
|
|
63
|
-
* `bg-secondary`, `bg-accent-subtle`…) so it themes with the
|
|
16
|
+
* `bg-card`, `bg-secondary`, `bg-accent-subtle`…) so it themes with the
|
|
17
|
+
* surrounding app.
|
|
64
18
|
* The `qalma-command-active` class is toggled by the `QalmaCommand` directive
|
|
65
19
|
* when the underlying command is active.
|
|
66
20
|
*/
|
|
67
|
-
const TOOLBAR_BUTTON_CLASS = 'inline-flex h-[1.85rem] w-[1.85rem] cursor-pointer items-center justify-center rounded-[0.4rem] border border-
|
|
21
|
+
const TOOLBAR_BUTTON_CLASS = 'inline-flex h-[1.85rem] w-[1.85rem] cursor-pointer items-center justify-center rounded-[0.4rem] border border-border bg-card text-muted-foreground transition hover:border-accent/50 hover:bg-secondary hover:text-foreground disabled:cursor-not-allowed disabled:opacity-45 [&.qalma-command-active]:border-accent/40 [&.qalma-command-active]:bg-accent-subtle [&.qalma-command-active]:text-accent';
|
|
68
22
|
/**
|
|
69
23
|
* Generic, declarative toolbar button for a single editor command.
|
|
70
24
|
*
|
|
@@ -368,500 +322,54 @@ function provideQalmaToolbarIcons() {
|
|
|
368
322
|
});
|
|
369
323
|
}
|
|
370
324
|
|
|
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
|
-
}
|
|
325
|
+
function cn(...inputs) {
|
|
326
|
+
return twMerge(clsx(inputs));
|
|
452
327
|
}
|
|
453
328
|
|
|
329
|
+
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', {
|
|
330
|
+
variants: {
|
|
331
|
+
variant: {
|
|
332
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
333
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
334
|
+
outline: 'border border-border bg-card text-foreground hover:bg-secondary hover:text-secondary-foreground',
|
|
335
|
+
ghost: 'text-foreground hover:bg-secondary hover:text-secondary-foreground',
|
|
336
|
+
accent: 'bg-accent text-accent-foreground hover:bg-accent/90',
|
|
337
|
+
link: 'text-accent underline-offset-4 hover:underline',
|
|
338
|
+
},
|
|
339
|
+
size: {
|
|
340
|
+
default: 'h-9 px-4 py-2',
|
|
341
|
+
sm: 'h-8 rounded-md px-3 text-xs',
|
|
342
|
+
lg: 'h-11 rounded-md px-6 text-base',
|
|
343
|
+
icon: 'h-9 w-9',
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
defaultVariants: {
|
|
347
|
+
variant: 'default',
|
|
348
|
+
size: 'default',
|
|
349
|
+
},
|
|
350
|
+
});
|
|
454
351
|
/**
|
|
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.
|
|
352
|
+
* Headless-styled button directive so it composes onto native `<button>` and
|
|
353
|
+
* `<a>` elements while reading from the host's Tailwind design tokens.
|
|
462
354
|
*/
|
|
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);
|
|
355
|
+
class QalmaButton {
|
|
839
356
|
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 });
|
|
357
|
+
initialClass = this.host.nativeElement.getAttribute('class') ?? '';
|
|
358
|
+
variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
359
|
+
size = input('default', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
360
|
+
computedClass = computed(() => cn(buttonVariants({ variant: this.variant(), size: this.size() }), this.initialClass), ...(ngDevMode ? [{ debugName: "computedClass" }] : /* istanbul ignore next */ []));
|
|
361
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaButton, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
362
|
+
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
363
|
}
|
|
858
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type:
|
|
364
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaButton, decorators: [{
|
|
859
365
|
type: Directive,
|
|
860
366
|
args: [{
|
|
861
|
-
|
|
862
|
-
|
|
367
|
+
selector: 'button[qalmaBtn], a[qalmaBtn]',
|
|
368
|
+
host: {
|
|
369
|
+
'[class]': 'computedClass()',
|
|
370
|
+
},
|
|
863
371
|
}]
|
|
864
|
-
}],
|
|
372
|
+
}], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
|
|
865
373
|
|
|
866
374
|
class QalmaDragHandle {
|
|
867
375
|
editor = input.required(...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
|
|
@@ -1170,108 +678,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
|
|
|
1170
678
|
}]
|
|
1171
679
|
}], 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
680
|
|
|
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
681
|
/**
|
|
1276
682
|
* Caret-anchored mention suggestion menu: an avatar-initial list with a loading
|
|
1277
683
|
* state. Positioning, keyboard handling and active highlighting come from
|
|
@@ -1666,148 +1072,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
|
|
|
1666
1072
|
}]
|
|
1667
1073
|
}], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }] } });
|
|
1668
1074
|
|
|
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
1075
|
class QalmaLinkPopover {
|
|
1812
1076
|
popover = input(null, ...(ngDevMode ? [{ debugName: "popover" }] : /* istanbul ignore next */ []));
|
|
1813
1077
|
href = input.required(...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
|
|
@@ -2056,13 +1320,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
|
|
|
2056
1320
|
}]
|
|
2057
1321
|
}], 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
1322
|
|
|
1323
|
+
/**
|
|
1324
|
+
* Floating, selection-anchored toolbar container. It owns the hard parts — the
|
|
1325
|
+
* fixed-position `transform` from a {@link QalmaContextualToolbarPlacement}
|
|
1326
|
+
* (see `QalmaSelectionToolbarDirective`), preserving the editor selection on
|
|
1327
|
+
* pointer-down, and Escape-to-dismiss — and projects whatever controls you put
|
|
1328
|
+
* inside it. Nothing about the button set is baked in: compose
|
|
1329
|
+
* `<qalma-toolbar-button>` for commands and your own buttons for app actions
|
|
1330
|
+
* (opening a link editor, a color picker, an AI action…).
|
|
1331
|
+
*
|
|
1332
|
+
* The container's `mousedown` handler preserves the selection for every
|
|
1333
|
+
* projected control via event bubbling, so inner buttons don't each need to.
|
|
1334
|
+
*
|
|
1335
|
+
* ```html
|
|
1336
|
+
* <qalma-contextual-toolbar
|
|
1337
|
+
* [placement]="selection.placement()"
|
|
1338
|
+
* (dismiss)="selection.hide()"
|
|
1339
|
+
* >
|
|
1340
|
+
* <qalma-toolbar-button command="toggleBold" icon="lucideBold" label="Bold" />
|
|
1341
|
+
* <qalma-toolbar-button command="toggleItalic" icon="lucideItalic" label="Italic" />
|
|
1342
|
+
* <button type="button" [class]="TOOLBAR_BUTTON_CLASS" (click)="openLinkEditor()">…</button>
|
|
1343
|
+
* </qalma-contextual-toolbar>
|
|
1344
|
+
* ```
|
|
1345
|
+
*/
|
|
2059
1346
|
class QalmaContextualToolbar {
|
|
2060
|
-
editor = input.required(...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
|
|
2061
1347
|
placement = input(null, ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
|
|
2062
|
-
|
|
1348
|
+
/** Accessible name for the toolbar region. */
|
|
1349
|
+
label = input('Selection formatting', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
2063
1350
|
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
1351
|
preserveSelection(event) {
|
|
2067
1352
|
event.preventDefault();
|
|
2068
1353
|
}
|
|
@@ -2071,397 +1356,57 @@ class QalmaContextualToolbar {
|
|
|
2071
1356
|
this.dismiss.emit();
|
|
2072
1357
|
}
|
|
2073
1358
|
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: `
|
|
1359
|
+
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
1360
|
@if (placement(); as placement) {
|
|
2084
1361
|
<qalma-toolbar
|
|
2085
|
-
label="
|
|
1362
|
+
[label]="label()"
|
|
2086
1363
|
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
1364
|
[style.transform]="placement.transform"
|
|
2088
1365
|
(mousedown)="preserveSelection($event)"
|
|
2089
1366
|
(keydown.escape)="dismissFromKeyboard($event)"
|
|
2090
1367
|
>
|
|
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>
|
|
1368
|
+
<ng-content />
|
|
2176
1369
|
</qalma-toolbar>
|
|
2177
1370
|
}
|
|
2178
|
-
`, isInline: true, dependencies: [{ kind: "component", type:
|
|
1371
|
+
`, isInline: true, dependencies: [{ kind: "component", type: QalmaToolbar, selector: "qalma-toolbar", inputs: ["label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2179
1372
|
}
|
|
2180
1373
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaContextualToolbar, decorators: [{
|
|
2181
1374
|
type: Component,
|
|
2182
1375
|
args: [{
|
|
2183
1376
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2184
|
-
imports: [
|
|
2185
|
-
providers: [
|
|
2186
|
-
provideIcons({
|
|
2187
|
-
lucideBold,
|
|
2188
|
-
lucideCode,
|
|
2189
|
-
lucideItalic,
|
|
2190
|
-
lucideLetterText,
|
|
2191
|
-
lucideLink,
|
|
2192
|
-
}),
|
|
2193
|
-
],
|
|
1377
|
+
imports: [QalmaToolbar],
|
|
2194
1378
|
selector: 'qalma-contextual-toolbar',
|
|
2195
1379
|
template: `
|
|
2196
1380
|
@if (placement(); as placement) {
|
|
2197
1381
|
<qalma-toolbar
|
|
2198
|
-
label="
|
|
1382
|
+
[label]="label()"
|
|
2199
1383
|
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
1384
|
[style.transform]="placement.transform"
|
|
2201
1385
|
(mousedown)="preserveSelection($event)"
|
|
2202
1386
|
(keydown.escape)="dismissFromKeyboard($event)"
|
|
2203
1387
|
>
|
|
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>
|
|
1388
|
+
<ng-content />
|
|
2289
1389
|
</qalma-toolbar>
|
|
2290
1390
|
}
|
|
2291
1391
|
`,
|
|
2292
1392
|
}]
|
|
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
|
-
}
|
|
1393
|
+
}], 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
1394
|
|
|
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 }] }] } });
|
|
1395
|
+
// @qalma/kit — Tailwind/shadcn-flavored UI for the Qalma editor.
|
|
1396
|
+
//
|
|
1397
|
+
// This entry point is the styled tier: components that pull `cva` + `@ng-icons`
|
|
1398
|
+
// and read the host's design tokens. It re-exports the entire dependency-free
|
|
1399
|
+
// `@qalma/kit/headless` surface (controllers, geometry, keyboard nav) so it
|
|
1400
|
+
// stays a superset — bring-your-own-design-system consumers can import the
|
|
1401
|
+
// leaner `@qalma/kit/headless` directly instead.
|
|
1402
|
+
//
|
|
1403
|
+
// The internal `QalmaButton` directive and `cn()` helper are intentionally NOT
|
|
1404
|
+
// exported: a host already has a button and a class-merge utility in its design
|
|
1405
|
+
// system. They remain private implementation details of the styled components.
|
|
2461
1406
|
|
|
2462
1407
|
/**
|
|
2463
1408
|
* Generated bundle index. Do not edit.
|
|
2464
1409
|
*/
|
|
2465
1410
|
|
|
2466
|
-
export {
|
|
1411
|
+
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
1412
|
//# sourceMappingURL=qalma-kit.mjs.map
|