@qalma/kit 0.1.0-beta.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/fesm2022/qalma-kit.mjs +2467 -0
- package/fesm2022/qalma-kit.mjs.map +1 -0
- package/package.json +55 -0
- package/types/qalma-kit.d.ts +530 -0
- package/types/qalma-kit.d.ts.map +1 -0
|
@@ -0,0 +1,2467 @@
|
|
|
1
|
+
import { clsx } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
3
|
+
import * as i0 from '@angular/core';
|
|
4
|
+
import { inject, ElementRef, input, computed, Directive, ChangeDetectionStrategy, Component, signal, DestroyRef, afterNextRender, output, effect } from '@angular/core';
|
|
5
|
+
import { cva } from 'class-variance-authority';
|
|
6
|
+
import { NgIcon, provideIcons } from '@ng-icons/core';
|
|
7
|
+
import { QalmaCommand, QalmaToolbar } from '@qalma/editor';
|
|
8
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
9
|
+
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
|
+
function cn(...inputs) {
|
|
12
|
+
return twMerge(clsx(inputs));
|
|
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 }] }] } });
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Default styling for a toolbar command button. Icon-only, square, and driven
|
|
62
|
+
* entirely by the host's shadcn-style design tokens (`text-muted-foreground`,
|
|
63
|
+
* `bg-secondary`, `bg-accent-subtle`…) so it themes with the surrounding app.
|
|
64
|
+
* The `qalma-command-active` class is toggled by the `QalmaCommand` directive
|
|
65
|
+
* when the underlying command is active.
|
|
66
|
+
*/
|
|
67
|
+
const TOOLBAR_BUTTON_CLASS = 'inline-flex h-[1.85rem] w-[1.85rem] cursor-pointer items-center justify-center rounded-[0.4rem] border border-transparent text-muted-foreground transition 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
|
+
/**
|
|
69
|
+
* Generic, declarative toolbar button for a single editor command.
|
|
70
|
+
*
|
|
71
|
+
* Renders a native `<button>` wired to the `QalmaCommand` directive (execute /
|
|
72
|
+
* active / disabled / aria) with an `<ng-icon>`, so a whole toolbar becomes a
|
|
73
|
+
* list of `<qalma-toolbar-button command icon label />` instead of the
|
|
74
|
+
* six-line `<button qalmaCommand><ng-icon></button>` block repeated per
|
|
75
|
+
* command. The host uses `display: contents` so the inner button participates
|
|
76
|
+
* directly in the surrounding toolbar's flex layout.
|
|
77
|
+
*
|
|
78
|
+
* Icons resolve from the `NgIcon` provider scope of the surrounding component
|
|
79
|
+
* (register them with `provideIcons({ … })` as usual).
|
|
80
|
+
*/
|
|
81
|
+
class QalmaToolbarButton {
|
|
82
|
+
/** Command name to execute, e.g. `toggleBold`. */
|
|
83
|
+
command = input.required(...(ngDevMode ? [{ debugName: "command" }] : /* istanbul ignore next */ []));
|
|
84
|
+
/** Optional command payload (forwarded to `qalmaCommandValue`). */
|
|
85
|
+
value = input(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
|
|
86
|
+
/** Lucide (or any registered) icon name, e.g. `lucideBold`. */
|
|
87
|
+
icon = input.required(...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
|
|
88
|
+
/** Accessible name — used for both `title` and `aria-label`. */
|
|
89
|
+
label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
90
|
+
/** Optional icon-size class override (default: `text-[0.9rem]`). */
|
|
91
|
+
iconClass = input('text-[0.9rem]', ...(ngDevMode ? [{ debugName: "iconClass" }] : /* istanbul ignore next */ []));
|
|
92
|
+
buttonClass = TOOLBAR_BUTTON_CLASS;
|
|
93
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaToolbarButton, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
94
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.17", type: QalmaToolbarButton, isStandalone: true, selector: "qalma-toolbar-button", inputs: { command: { classPropertyName: "command", publicName: "command", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, iconClass: { classPropertyName: "iconClass", publicName: "iconClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "contents" }, ngImport: i0, template: `
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
[qalmaCommand]="command()"
|
|
98
|
+
[qalmaCommandValue]="value()"
|
|
99
|
+
[class]="buttonClass"
|
|
100
|
+
[title]="label()"
|
|
101
|
+
[attr.aria-label]="label()"
|
|
102
|
+
>
|
|
103
|
+
<ng-icon [class]="iconClass()" [name]="icon()" aria-hidden="true" />
|
|
104
|
+
</button>
|
|
105
|
+
`, isInline: true, dependencies: [{ kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }, { kind: "directive", type: QalmaCommand, selector: "button[qalmaCommand]", inputs: ["qalmaCommand", "qalmaCommandValue"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
106
|
+
}
|
|
107
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaToolbarButton, decorators: [{
|
|
108
|
+
type: Component,
|
|
109
|
+
args: [{
|
|
110
|
+
selector: 'qalma-toolbar-button',
|
|
111
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
112
|
+
imports: [NgIcon, QalmaCommand],
|
|
113
|
+
host: {
|
|
114
|
+
class: 'contents',
|
|
115
|
+
},
|
|
116
|
+
template: `
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
[qalmaCommand]="command()"
|
|
120
|
+
[qalmaCommandValue]="value()"
|
|
121
|
+
[class]="buttonClass"
|
|
122
|
+
[title]="label()"
|
|
123
|
+
[attr.aria-label]="label()"
|
|
124
|
+
>
|
|
125
|
+
<ng-icon [class]="iconClass()" [name]="icon()" aria-hidden="true" />
|
|
126
|
+
</button>
|
|
127
|
+
`,
|
|
128
|
+
}]
|
|
129
|
+
}], propDecorators: { command: [{ type: i0.Input, args: [{ isSignal: true, alias: "command", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], iconClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconClass", required: false }] }] } });
|
|
130
|
+
|
|
131
|
+
function isToolbarCommandItem(item) {
|
|
132
|
+
return 'command' in item;
|
|
133
|
+
}
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Default command fragments — the declarative source of truth for the standard
|
|
136
|
+
// editor toolbar, reusable across consumers (playground, sandbox, downstream
|
|
137
|
+
// apps). Compose them into `ToolbarGroup[]` and hand them to
|
|
138
|
+
// `<qalma-toolbar-registry>`; interleave custom controls with `ToolbarTemplateItem`.
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
const QALMA_TOOLBAR_HEADINGS = [
|
|
141
|
+
{ command: 'setParagraph', icon: 'lucidePilcrow', label: 'Paragraph' },
|
|
142
|
+
{ command: 'toggleHeading1', icon: 'lucideHeading1', label: 'Heading 1' },
|
|
143
|
+
{ command: 'toggleHeading2', icon: 'lucideHeading2', label: 'Heading 2' },
|
|
144
|
+
{ command: 'toggleHeading3', icon: 'lucideHeading3', label: 'Heading 3' },
|
|
145
|
+
];
|
|
146
|
+
const QALMA_TOOLBAR_INLINE_MARKS = [
|
|
147
|
+
{ command: 'toggleBold', icon: 'lucideBold', label: 'Bold' },
|
|
148
|
+
{ command: 'toggleItalic', icon: 'lucideItalic', label: 'Italic' },
|
|
149
|
+
{ command: 'toggleUnderline', icon: 'lucideUnderline', label: 'Underline' },
|
|
150
|
+
{
|
|
151
|
+
command: 'toggleStrike',
|
|
152
|
+
icon: 'lucideStrikethrough',
|
|
153
|
+
label: 'Strikethrough',
|
|
154
|
+
},
|
|
155
|
+
{ command: 'toggleInlineCode', icon: 'lucideCode', label: 'Inline code' },
|
|
156
|
+
{ command: 'toggleMonospace', icon: 'lucideLetterText', label: 'Monospace' },
|
|
157
|
+
{ command: 'toggleSubscript', icon: 'lucideSubscript', label: 'Subscript' },
|
|
158
|
+
{
|
|
159
|
+
command: 'toggleSuperscript',
|
|
160
|
+
icon: 'lucideSuperscript',
|
|
161
|
+
label: 'Superscript',
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
const QALMA_TOOLBAR_ALIGN = [
|
|
165
|
+
{ command: 'setTextAlignLeft', icon: 'lucideAlignLeft', label: 'Align left' },
|
|
166
|
+
{
|
|
167
|
+
command: 'setTextAlignCenter',
|
|
168
|
+
icon: 'lucideAlignCenter',
|
|
169
|
+
label: 'Align center',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
command: 'setTextAlignRight',
|
|
173
|
+
icon: 'lucideAlignRight',
|
|
174
|
+
label: 'Align right',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
command: 'setTextAlignJustify',
|
|
178
|
+
icon: 'lucideAlignJustify',
|
|
179
|
+
label: 'Justify',
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
const QALMA_TOOLBAR_LISTS = [
|
|
183
|
+
{ command: 'toggleBulletList', icon: 'lucideList', label: 'Bullet list' },
|
|
184
|
+
{
|
|
185
|
+
command: 'toggleOrderedList',
|
|
186
|
+
icon: 'lucideListOrdered',
|
|
187
|
+
label: 'Ordered list',
|
|
188
|
+
},
|
|
189
|
+
{ command: 'toggleTaskList', icon: 'lucideListTodo', label: 'Task list' },
|
|
190
|
+
{ command: 'liftListItem', icon: 'lucideOutdent', label: 'Outdent' },
|
|
191
|
+
{ command: 'sinkListItem', icon: 'lucideIndent', label: 'Indent' },
|
|
192
|
+
{ command: 'toggleBlockquote', icon: 'lucideTextQuote', label: 'Blockquote' },
|
|
193
|
+
{ command: 'toggleCodeBlock', icon: 'lucideSquareCode', label: 'Code block' },
|
|
194
|
+
];
|
|
195
|
+
/** Always-available table entry point (inserting a new table). */
|
|
196
|
+
const QALMA_TOOLBAR_TABLE_INSERT = {
|
|
197
|
+
command: 'insertTable',
|
|
198
|
+
icon: 'lucideTable',
|
|
199
|
+
label: 'Insert table',
|
|
200
|
+
};
|
|
201
|
+
/** Table editing commands — only meaningful while the selection is in a table. */
|
|
202
|
+
const QALMA_TOOLBAR_TABLE_OPS = [
|
|
203
|
+
{
|
|
204
|
+
command: 'addRowAfter',
|
|
205
|
+
icon: 'lucideBetweenHorizontalEnd',
|
|
206
|
+
label: 'Add row',
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
command: 'addColumnAfter',
|
|
210
|
+
icon: 'lucideBetweenVerticalEnd',
|
|
211
|
+
label: 'Add column',
|
|
212
|
+
},
|
|
213
|
+
{ command: 'deleteRow', icon: 'lucideRows3', label: 'Delete row' },
|
|
214
|
+
{ command: 'deleteColumn', icon: 'lucideColumns3', label: 'Delete column' },
|
|
215
|
+
{
|
|
216
|
+
command: 'toggleHeaderRow',
|
|
217
|
+
icon: 'lucideHeading',
|
|
218
|
+
label: 'Toggle header row',
|
|
219
|
+
},
|
|
220
|
+
{ command: 'deleteTable', icon: 'lucideTrash2', label: 'Delete table' },
|
|
221
|
+
];
|
|
222
|
+
const QALMA_TOOLBAR_HISTORY = [
|
|
223
|
+
{ command: 'undo', icon: 'lucideUndo2', label: 'Undo' },
|
|
224
|
+
{ command: 'redo', icon: 'lucideRedo2', label: 'Redo' },
|
|
225
|
+
];
|
|
226
|
+
/** Standalone command that usually sits alongside the color controls. */
|
|
227
|
+
const QALMA_TOOLBAR_CLEAR_FORMATTING = {
|
|
228
|
+
command: 'clearFormatting',
|
|
229
|
+
icon: 'lucideEraser',
|
|
230
|
+
label: 'Clear formatting',
|
|
231
|
+
};
|
|
232
|
+
/** Standalone command that usually sits alongside the link/insert controls. */
|
|
233
|
+
const QALMA_TOOLBAR_UNSET_LINK = {
|
|
234
|
+
command: 'unsetLink',
|
|
235
|
+
icon: 'lucideUnlink',
|
|
236
|
+
label: 'Unlink',
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/** Vertical divider rendered between non-empty toolbar groups. */
|
|
240
|
+
const TOOLBAR_SEPARATOR_CLASS = 'mx-0.5 h-5 w-px shrink-0 self-center bg-border';
|
|
241
|
+
/**
|
|
242
|
+
* Renders a declarative toolbar registry: an ordered list of groups, each a
|
|
243
|
+
* list of command buttons and/or inline custom-control templates, with a
|
|
244
|
+
* separator drawn between non-empty groups.
|
|
245
|
+
*
|
|
246
|
+
* Meant to sit inside the editor's `<qalma-toolbar>` (which owns the
|
|
247
|
+
* `role="toolbar"` semantics); the host uses `display: contents` so the
|
|
248
|
+
* rendered buttons participate directly in that container's flex layout.
|
|
249
|
+
*
|
|
250
|
+
* ```html
|
|
251
|
+
* <qalma-toolbar class="flex …">
|
|
252
|
+
* <ng-template #colors> … custom controls … </ng-template>
|
|
253
|
+
* <qalma-toolbar-registry [groups]="[headings, marks, [{ template: colors }]]" />
|
|
254
|
+
* </qalma-toolbar>
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
class QalmaToolbarRegistry {
|
|
258
|
+
groups = input.required(...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
|
|
259
|
+
separatorClass = input(TOOLBAR_SEPARATOR_CLASS, ...(ngDevMode ? [{ debugName: "separatorClass" }] : /* istanbul ignore next */ []));
|
|
260
|
+
/** Drop empty groups so they neither render content nor a dangling separator. */
|
|
261
|
+
visibleGroups = computed(() => this.groups().filter((group) => group.length > 0), ...(ngDevMode ? [{ debugName: "visibleGroups" }] : /* istanbul ignore next */ []));
|
|
262
|
+
asCommand(item) {
|
|
263
|
+
return isToolbarCommandItem(item) ? item : null;
|
|
264
|
+
}
|
|
265
|
+
asTemplate(item) {
|
|
266
|
+
return item.template;
|
|
267
|
+
}
|
|
268
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaToolbarRegistry, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
269
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: QalmaToolbarRegistry, isStandalone: true, selector: "qalma-toolbar-registry", inputs: { groups: { classPropertyName: "groups", publicName: "groups", isSignal: true, isRequired: true, transformFunction: null }, separatorClass: { classPropertyName: "separatorClass", publicName: "separatorClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "contents" }, ngImport: i0, template: `
|
|
270
|
+
@for (group of visibleGroups(); track $index; let first = $first) {
|
|
271
|
+
@if (!first) {
|
|
272
|
+
<span [class]="separatorClass()" aria-hidden="true"></span>
|
|
273
|
+
}
|
|
274
|
+
@for (item of group; track $index) {
|
|
275
|
+
@if (asCommand(item); as command) {
|
|
276
|
+
<qalma-toolbar-button
|
|
277
|
+
[command]="command.command"
|
|
278
|
+
[value]="command.value"
|
|
279
|
+
[icon]="command.icon"
|
|
280
|
+
[label]="command.label"
|
|
281
|
+
/>
|
|
282
|
+
} @else {
|
|
283
|
+
<ng-container [ngTemplateOutlet]="asTemplate(item)" />
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: QalmaToolbarButton, selector: "qalma-toolbar-button", inputs: ["command", "value", "icon", "label", "iconClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
288
|
+
}
|
|
289
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaToolbarRegistry, decorators: [{
|
|
290
|
+
type: Component,
|
|
291
|
+
args: [{
|
|
292
|
+
selector: 'qalma-toolbar-registry',
|
|
293
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
294
|
+
imports: [NgTemplateOutlet, QalmaToolbarButton],
|
|
295
|
+
host: {
|
|
296
|
+
class: 'contents',
|
|
297
|
+
},
|
|
298
|
+
template: `
|
|
299
|
+
@for (group of visibleGroups(); track $index; let first = $first) {
|
|
300
|
+
@if (!first) {
|
|
301
|
+
<span [class]="separatorClass()" aria-hidden="true"></span>
|
|
302
|
+
}
|
|
303
|
+
@for (item of group; track $index) {
|
|
304
|
+
@if (asCommand(item); as command) {
|
|
305
|
+
<qalma-toolbar-button
|
|
306
|
+
[command]="command.command"
|
|
307
|
+
[value]="command.value"
|
|
308
|
+
[icon]="command.icon"
|
|
309
|
+
[label]="command.label"
|
|
310
|
+
/>
|
|
311
|
+
} @else {
|
|
312
|
+
<ng-container [ngTemplateOutlet]="asTemplate(item)" />
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
`,
|
|
317
|
+
}]
|
|
318
|
+
}], propDecorators: { groups: [{ type: i0.Input, args: [{ isSignal: true, alias: "groups", required: true }] }], separatorClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "separatorClass", required: false }] }] } });
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Registers the Lucide icons referenced by the default toolbar command
|
|
322
|
+
* fragments (`QALMA_TOOLBAR_*`), so consumers can wire the standard toolbar
|
|
323
|
+
* without re-declaring the icon set:
|
|
324
|
+
*
|
|
325
|
+
* ```ts
|
|
326
|
+
* providers: [provideQalmaToolbarIcons()]
|
|
327
|
+
* ```
|
|
328
|
+
*
|
|
329
|
+
* Add your own `provideIcons({ … })` alongside it for any custom controls
|
|
330
|
+
* (color pickers, upload buttons…) that live outside the command registry.
|
|
331
|
+
*/
|
|
332
|
+
function provideQalmaToolbarIcons() {
|
|
333
|
+
return provideIcons({
|
|
334
|
+
lucideAlignCenter,
|
|
335
|
+
lucideAlignJustify,
|
|
336
|
+
lucideAlignLeft,
|
|
337
|
+
lucideAlignRight,
|
|
338
|
+
lucideBetweenHorizontalEnd,
|
|
339
|
+
lucideBetweenVerticalEnd,
|
|
340
|
+
lucideBold,
|
|
341
|
+
lucideCode,
|
|
342
|
+
lucideColumns3,
|
|
343
|
+
lucideEraser,
|
|
344
|
+
lucideHeading,
|
|
345
|
+
lucideHeading1,
|
|
346
|
+
lucideHeading2,
|
|
347
|
+
lucideHeading3,
|
|
348
|
+
lucideIndent,
|
|
349
|
+
lucideItalic,
|
|
350
|
+
lucideLetterText,
|
|
351
|
+
lucideList,
|
|
352
|
+
lucideListOrdered,
|
|
353
|
+
lucideListTodo,
|
|
354
|
+
lucideOutdent,
|
|
355
|
+
lucidePilcrow,
|
|
356
|
+
lucideRedo2,
|
|
357
|
+
lucideRows3,
|
|
358
|
+
lucideSquareCode,
|
|
359
|
+
lucideStrikethrough,
|
|
360
|
+
lucideSubscript,
|
|
361
|
+
lucideSuperscript,
|
|
362
|
+
lucideTable,
|
|
363
|
+
lucideTextQuote,
|
|
364
|
+
lucideTrash2,
|
|
365
|
+
lucideUnderline,
|
|
366
|
+
lucideUndo2,
|
|
367
|
+
lucideUnlink,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const DEFAULT_GAP$1 = 8;
|
|
372
|
+
const DEFAULT_EDGE_MARGIN = 8;
|
|
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
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Arrow-key navigation + Enter-to-select for autocomplete-style popovers
|
|
456
|
+
* (mention menu, slash command menu). Shared so both features stop
|
|
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.
|
|
462
|
+
*/
|
|
463
|
+
class KeyboardNavigableList {
|
|
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);
|
|
839
|
+
host = inject(ElementRef);
|
|
840
|
+
controller = new QalmaDragHandleController(() => this.editor());
|
|
841
|
+
handle = this.controller.handle;
|
|
842
|
+
dropIndicator = this.controller.dropIndicator;
|
|
843
|
+
draggedBlockHighlight = this.controller.draggedBlockHighlight;
|
|
844
|
+
constructor() {
|
|
845
|
+
afterNextRender(() => {
|
|
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 });
|
|
857
|
+
}
|
|
858
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaDragHandleDirective, decorators: [{
|
|
859
|
+
type: Directive,
|
|
860
|
+
args: [{
|
|
861
|
+
exportAs: 'qalmaDragHandle',
|
|
862
|
+
selector: '[qalmaDragHandle]',
|
|
863
|
+
}]
|
|
864
|
+
}], ctorParameters: () => [], propDecorators: { editor: [{ type: i0.Input, args: [{ isSignal: true, alias: "qalmaDragHandle", required: true }] }] } });
|
|
865
|
+
|
|
866
|
+
class QalmaDragHandle {
|
|
867
|
+
editor = input.required(...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
|
|
868
|
+
handle = input(null, ...(ngDevMode ? [{ debugName: "handle" }] : /* istanbul ignore next */ []));
|
|
869
|
+
dropIndicator = input(null, ...(ngDevMode ? [{ debugName: "dropIndicator" }] : /* istanbul ignore next */ []));
|
|
870
|
+
draggedBlockHighlight = input(null, ...(ngDevMode ? [{ debugName: "draggedBlockHighlight" }] : /* istanbul ignore next */ []));
|
|
871
|
+
dismiss = output();
|
|
872
|
+
dragStart = output();
|
|
873
|
+
menuOpen = signal(false, ...(ngDevMode ? [{ debugName: "menuOpen" }] : /* istanbul ignore next */ []));
|
|
874
|
+
constructor() {
|
|
875
|
+
// Close the block-actions menu on an outside pointerdown or Escape. A click
|
|
876
|
+
// on the handle button or the menu itself (both under `[data-qalma-drag-handle]`)
|
|
877
|
+
// is treated as inside, so it doesn't self-dismiss. Connected for the
|
|
878
|
+
// component's lifetime; the callback is a no-op while the menu is closed.
|
|
879
|
+
new DismissibleOverlay({
|
|
880
|
+
isInside: (target) => target instanceof Element &&
|
|
881
|
+
!!target.closest('[data-qalma-drag-handle]'),
|
|
882
|
+
onDismiss: () => this.menuOpen.set(false),
|
|
883
|
+
}).connect(inject(DestroyRef));
|
|
884
|
+
}
|
|
885
|
+
toggleMenu() {
|
|
886
|
+
this.menuOpen.update((open) => !open);
|
|
887
|
+
}
|
|
888
|
+
startDrag(event, handle) {
|
|
889
|
+
this.menuOpen.set(false);
|
|
890
|
+
this.dragStart.emit({ event, handle });
|
|
891
|
+
}
|
|
892
|
+
canExecute(command, handle) {
|
|
893
|
+
return this.editor().canExecute(command, handle.target);
|
|
894
|
+
}
|
|
895
|
+
execute(command, handle) {
|
|
896
|
+
if (this.editor().execute(command, handle.target)) {
|
|
897
|
+
this.dismissHandle();
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
preserveSelection(event) {
|
|
901
|
+
event.preventDefault();
|
|
902
|
+
}
|
|
903
|
+
dismissFromKeyboard(event) {
|
|
904
|
+
event.preventDefault();
|
|
905
|
+
this.dismissHandle();
|
|
906
|
+
}
|
|
907
|
+
dismissHandle() {
|
|
908
|
+
this.menuOpen.set(false);
|
|
909
|
+
this.dismiss.emit();
|
|
910
|
+
}
|
|
911
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaDragHandle, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
912
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: QalmaDragHandle, isStandalone: true, selector: "qalma-drag-handle", inputs: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null }, handle: { classPropertyName: "handle", publicName: "handle", isSignal: true, isRequired: false, transformFunction: null }, dropIndicator: { classPropertyName: "dropIndicator", publicName: "dropIndicator", isSignal: true, isRequired: false, transformFunction: null }, draggedBlockHighlight: { classPropertyName: "draggedBlockHighlight", publicName: "draggedBlockHighlight", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismiss: "dismiss", dragStart: "dragStart" }, providers: [
|
|
913
|
+
provideIcons({
|
|
914
|
+
lucideArrowDown,
|
|
915
|
+
lucideArrowUp,
|
|
916
|
+
lucideCopy,
|
|
917
|
+
lucideGripVertical,
|
|
918
|
+
lucideMousePointer2,
|
|
919
|
+
lucideTrash2,
|
|
920
|
+
}),
|
|
921
|
+
], ngImport: i0, template: `
|
|
922
|
+
@if (draggedBlockHighlight(); as highlight) {
|
|
923
|
+
<div
|
|
924
|
+
data-qalma-dragging-block-highlight
|
|
925
|
+
class="pointer-events-none fixed left-0 top-0 z-20 rounded-md border border-accent bg-accent-subtle/80 opacity-70 shadow-md will-change-transform"
|
|
926
|
+
[style.transform]="highlight.transform"
|
|
927
|
+
[style.width.px]="highlight.width"
|
|
928
|
+
[style.height.px]="highlight.height"
|
|
929
|
+
></div>
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
@if (dropIndicator(); as indicator) {
|
|
933
|
+
<div
|
|
934
|
+
data-qalma-drag-drop-line
|
|
935
|
+
class="pointer-events-none fixed left-0 top-0 z-40 h-0.5 rounded-full bg-accent shadow-md will-change-transform"
|
|
936
|
+
[style.transform]="indicator.transform"
|
|
937
|
+
[style.width.px]="indicator.width"
|
|
938
|
+
></div>
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
@if (handle(); as handle) {
|
|
942
|
+
<div
|
|
943
|
+
data-qalma-drag-handle
|
|
944
|
+
class="fixed left-0 top-0 z-30 flex items-start gap-1 will-change-transform"
|
|
945
|
+
tabindex="-1"
|
|
946
|
+
[style.transform]="handle.transform"
|
|
947
|
+
(mousedown)="preserveSelection($event)"
|
|
948
|
+
(keydown.escape)="dismissFromKeyboard($event)"
|
|
949
|
+
>
|
|
950
|
+
<button
|
|
951
|
+
qalmaBtn
|
|
952
|
+
variant="ghost"
|
|
953
|
+
size="icon"
|
|
954
|
+
type="button"
|
|
955
|
+
class="!h-[1.85rem] !w-[1.85rem] cursor-grab rounded-[0.4rem] border border-transparent bg-card/95 text-muted-foreground shadow-sm active:cursor-grabbing hover:border-border hover:bg-secondary hover:text-foreground"
|
|
956
|
+
[class.border-accent]="menuOpen()"
|
|
957
|
+
[class.text-accent]="menuOpen()"
|
|
958
|
+
(pointerdown)="startDrag($event, handle)"
|
|
959
|
+
(click)="toggleMenu()"
|
|
960
|
+
title="Block actions"
|
|
961
|
+
aria-label="Block actions"
|
|
962
|
+
[attr.aria-expanded]="menuOpen()"
|
|
963
|
+
>
|
|
964
|
+
<ng-icon
|
|
965
|
+
class="text-[0.9rem]"
|
|
966
|
+
name="lucideGripVertical"
|
|
967
|
+
aria-hidden="true"
|
|
968
|
+
/>
|
|
969
|
+
</button>
|
|
970
|
+
|
|
971
|
+
@if (menuOpen()) {
|
|
972
|
+
<div
|
|
973
|
+
role="menu"
|
|
974
|
+
aria-label="Block actions"
|
|
975
|
+
class="w-44 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg"
|
|
976
|
+
>
|
|
977
|
+
<button
|
|
978
|
+
type="button"
|
|
979
|
+
class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-foreground transition hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-45"
|
|
980
|
+
[disabled]="!canExecute('selectBlock', handle)"
|
|
981
|
+
(mousedown)="preserveSelection($event)"
|
|
982
|
+
(click)="execute('selectBlock', handle)"
|
|
983
|
+
role="menuitem"
|
|
984
|
+
>
|
|
985
|
+
<ng-icon name="lucideMousePointer2" aria-hidden="true" />
|
|
986
|
+
Select block
|
|
987
|
+
</button>
|
|
988
|
+
<button
|
|
989
|
+
type="button"
|
|
990
|
+
class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-foreground transition hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-45"
|
|
991
|
+
[disabled]="!handle.canMoveUp"
|
|
992
|
+
(mousedown)="preserveSelection($event)"
|
|
993
|
+
(click)="execute('moveBlockUp', handle)"
|
|
994
|
+
role="menuitem"
|
|
995
|
+
>
|
|
996
|
+
<ng-icon name="lucideArrowUp" aria-hidden="true" />
|
|
997
|
+
Move up
|
|
998
|
+
</button>
|
|
999
|
+
<button
|
|
1000
|
+
type="button"
|
|
1001
|
+
class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-foreground transition hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-45"
|
|
1002
|
+
[disabled]="!handle.canMoveDown"
|
|
1003
|
+
(mousedown)="preserveSelection($event)"
|
|
1004
|
+
(click)="execute('moveBlockDown', handle)"
|
|
1005
|
+
role="menuitem"
|
|
1006
|
+
>
|
|
1007
|
+
<ng-icon name="lucideArrowDown" aria-hidden="true" />
|
|
1008
|
+
Move down
|
|
1009
|
+
</button>
|
|
1010
|
+
<button
|
|
1011
|
+
type="button"
|
|
1012
|
+
class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-foreground transition hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-45"
|
|
1013
|
+
[disabled]="!canExecute('duplicateBlock', handle)"
|
|
1014
|
+
(mousedown)="preserveSelection($event)"
|
|
1015
|
+
(click)="execute('duplicateBlock', handle)"
|
|
1016
|
+
role="menuitem"
|
|
1017
|
+
>
|
|
1018
|
+
<ng-icon name="lucideCopy" aria-hidden="true" />
|
|
1019
|
+
Duplicate
|
|
1020
|
+
</button>
|
|
1021
|
+
<button
|
|
1022
|
+
type="button"
|
|
1023
|
+
class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-destructive transition hover:bg-destructive/10 disabled:cursor-not-allowed disabled:opacity-45"
|
|
1024
|
+
[disabled]="!canExecute('deleteBlock', handle)"
|
|
1025
|
+
(mousedown)="preserveSelection($event)"
|
|
1026
|
+
(click)="execute('deleteBlock', handle)"
|
|
1027
|
+
role="menuitem"
|
|
1028
|
+
>
|
|
1029
|
+
<ng-icon name="lucideTrash2" aria-hidden="true" />
|
|
1030
|
+
Delete
|
|
1031
|
+
</button>
|
|
1032
|
+
</div>
|
|
1033
|
+
}
|
|
1034
|
+
</div>
|
|
1035
|
+
}
|
|
1036
|
+
`, isInline: true, dependencies: [{ kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }, { kind: "directive", type: QalmaButton, selector: "button[qalmaBtn], a[qalmaBtn]", inputs: ["variant", "size"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1037
|
+
}
|
|
1038
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaDragHandle, decorators: [{
|
|
1039
|
+
type: Component,
|
|
1040
|
+
args: [{
|
|
1041
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1042
|
+
imports: [NgIcon, QalmaButton],
|
|
1043
|
+
providers: [
|
|
1044
|
+
provideIcons({
|
|
1045
|
+
lucideArrowDown,
|
|
1046
|
+
lucideArrowUp,
|
|
1047
|
+
lucideCopy,
|
|
1048
|
+
lucideGripVertical,
|
|
1049
|
+
lucideMousePointer2,
|
|
1050
|
+
lucideTrash2,
|
|
1051
|
+
}),
|
|
1052
|
+
],
|
|
1053
|
+
selector: 'qalma-drag-handle',
|
|
1054
|
+
template: `
|
|
1055
|
+
@if (draggedBlockHighlight(); as highlight) {
|
|
1056
|
+
<div
|
|
1057
|
+
data-qalma-dragging-block-highlight
|
|
1058
|
+
class="pointer-events-none fixed left-0 top-0 z-20 rounded-md border border-accent bg-accent-subtle/80 opacity-70 shadow-md will-change-transform"
|
|
1059
|
+
[style.transform]="highlight.transform"
|
|
1060
|
+
[style.width.px]="highlight.width"
|
|
1061
|
+
[style.height.px]="highlight.height"
|
|
1062
|
+
></div>
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
@if (dropIndicator(); as indicator) {
|
|
1066
|
+
<div
|
|
1067
|
+
data-qalma-drag-drop-line
|
|
1068
|
+
class="pointer-events-none fixed left-0 top-0 z-40 h-0.5 rounded-full bg-accent shadow-md will-change-transform"
|
|
1069
|
+
[style.transform]="indicator.transform"
|
|
1070
|
+
[style.width.px]="indicator.width"
|
|
1071
|
+
></div>
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
@if (handle(); as handle) {
|
|
1075
|
+
<div
|
|
1076
|
+
data-qalma-drag-handle
|
|
1077
|
+
class="fixed left-0 top-0 z-30 flex items-start gap-1 will-change-transform"
|
|
1078
|
+
tabindex="-1"
|
|
1079
|
+
[style.transform]="handle.transform"
|
|
1080
|
+
(mousedown)="preserveSelection($event)"
|
|
1081
|
+
(keydown.escape)="dismissFromKeyboard($event)"
|
|
1082
|
+
>
|
|
1083
|
+
<button
|
|
1084
|
+
qalmaBtn
|
|
1085
|
+
variant="ghost"
|
|
1086
|
+
size="icon"
|
|
1087
|
+
type="button"
|
|
1088
|
+
class="!h-[1.85rem] !w-[1.85rem] cursor-grab rounded-[0.4rem] border border-transparent bg-card/95 text-muted-foreground shadow-sm active:cursor-grabbing hover:border-border hover:bg-secondary hover:text-foreground"
|
|
1089
|
+
[class.border-accent]="menuOpen()"
|
|
1090
|
+
[class.text-accent]="menuOpen()"
|
|
1091
|
+
(pointerdown)="startDrag($event, handle)"
|
|
1092
|
+
(click)="toggleMenu()"
|
|
1093
|
+
title="Block actions"
|
|
1094
|
+
aria-label="Block actions"
|
|
1095
|
+
[attr.aria-expanded]="menuOpen()"
|
|
1096
|
+
>
|
|
1097
|
+
<ng-icon
|
|
1098
|
+
class="text-[0.9rem]"
|
|
1099
|
+
name="lucideGripVertical"
|
|
1100
|
+
aria-hidden="true"
|
|
1101
|
+
/>
|
|
1102
|
+
</button>
|
|
1103
|
+
|
|
1104
|
+
@if (menuOpen()) {
|
|
1105
|
+
<div
|
|
1106
|
+
role="menu"
|
|
1107
|
+
aria-label="Block actions"
|
|
1108
|
+
class="w-44 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg"
|
|
1109
|
+
>
|
|
1110
|
+
<button
|
|
1111
|
+
type="button"
|
|
1112
|
+
class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-foreground transition hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-45"
|
|
1113
|
+
[disabled]="!canExecute('selectBlock', handle)"
|
|
1114
|
+
(mousedown)="preserveSelection($event)"
|
|
1115
|
+
(click)="execute('selectBlock', handle)"
|
|
1116
|
+
role="menuitem"
|
|
1117
|
+
>
|
|
1118
|
+
<ng-icon name="lucideMousePointer2" aria-hidden="true" />
|
|
1119
|
+
Select block
|
|
1120
|
+
</button>
|
|
1121
|
+
<button
|
|
1122
|
+
type="button"
|
|
1123
|
+
class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-foreground transition hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-45"
|
|
1124
|
+
[disabled]="!handle.canMoveUp"
|
|
1125
|
+
(mousedown)="preserveSelection($event)"
|
|
1126
|
+
(click)="execute('moveBlockUp', handle)"
|
|
1127
|
+
role="menuitem"
|
|
1128
|
+
>
|
|
1129
|
+
<ng-icon name="lucideArrowUp" aria-hidden="true" />
|
|
1130
|
+
Move up
|
|
1131
|
+
</button>
|
|
1132
|
+
<button
|
|
1133
|
+
type="button"
|
|
1134
|
+
class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-foreground transition hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-45"
|
|
1135
|
+
[disabled]="!handle.canMoveDown"
|
|
1136
|
+
(mousedown)="preserveSelection($event)"
|
|
1137
|
+
(click)="execute('moveBlockDown', handle)"
|
|
1138
|
+
role="menuitem"
|
|
1139
|
+
>
|
|
1140
|
+
<ng-icon name="lucideArrowDown" aria-hidden="true" />
|
|
1141
|
+
Move down
|
|
1142
|
+
</button>
|
|
1143
|
+
<button
|
|
1144
|
+
type="button"
|
|
1145
|
+
class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-foreground transition hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-45"
|
|
1146
|
+
[disabled]="!canExecute('duplicateBlock', handle)"
|
|
1147
|
+
(mousedown)="preserveSelection($event)"
|
|
1148
|
+
(click)="execute('duplicateBlock', handle)"
|
|
1149
|
+
role="menuitem"
|
|
1150
|
+
>
|
|
1151
|
+
<ng-icon name="lucideCopy" aria-hidden="true" />
|
|
1152
|
+
Duplicate
|
|
1153
|
+
</button>
|
|
1154
|
+
<button
|
|
1155
|
+
type="button"
|
|
1156
|
+
class="flex h-8 w-full cursor-pointer items-center gap-2 rounded-sm px-2 text-left text-xs font-medium text-destructive transition hover:bg-destructive/10 disabled:cursor-not-allowed disabled:opacity-45"
|
|
1157
|
+
[disabled]="!canExecute('deleteBlock', handle)"
|
|
1158
|
+
(mousedown)="preserveSelection($event)"
|
|
1159
|
+
(click)="execute('deleteBlock', handle)"
|
|
1160
|
+
role="menuitem"
|
|
1161
|
+
>
|
|
1162
|
+
<ng-icon name="lucideTrash2" aria-hidden="true" />
|
|
1163
|
+
Delete
|
|
1164
|
+
</button>
|
|
1165
|
+
</div>
|
|
1166
|
+
}
|
|
1167
|
+
</div>
|
|
1168
|
+
}
|
|
1169
|
+
`,
|
|
1170
|
+
}]
|
|
1171
|
+
}], 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
|
+
|
|
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
|
+
/**
|
|
1276
|
+
* Caret-anchored mention suggestion menu: an avatar-initial list with a loading
|
|
1277
|
+
* state. Positioning, keyboard handling and active highlighting come from
|
|
1278
|
+
* {@link QalmaSuggestionMenu}; feed it a `placement` (see `flipAbovePlacement`)
|
|
1279
|
+
* and the filtered `suggestions`.
|
|
1280
|
+
*/
|
|
1281
|
+
class QalmaMentionMenu extends QalmaSuggestionMenu {
|
|
1282
|
+
suggestions = input([], ...(ngDevMode ? [{ debugName: "suggestions" }] : /* istanbul ignore next */ []));
|
|
1283
|
+
loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
1284
|
+
optionList = this.suggestions;
|
|
1285
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaMentionMenu, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
1286
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: QalmaMentionMenu, isStandalone: true, selector: "qalma-mention-menu", inputs: { suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null } }, usesInheritance: true, ngImport: i0, template: `
|
|
1287
|
+
@if (placement(); as placement) {
|
|
1288
|
+
<div
|
|
1289
|
+
role="listbox"
|
|
1290
|
+
aria-label="Mention suggestions"
|
|
1291
|
+
class="fixed z-20 w-[min(280px,calc(100vw-24px))] overflow-hidden rounded-md border border-border bg-popover p-1 text-sm text-popover-foreground shadow-lg outline-none"
|
|
1292
|
+
[style.left.px]="placement.left"
|
|
1293
|
+
[style.top.px]="placement.top"
|
|
1294
|
+
[style.bottom.px]="placement.bottom"
|
|
1295
|
+
[style.max-height.px]="placement.maxHeight"
|
|
1296
|
+
>
|
|
1297
|
+
@if (loading()) {
|
|
1298
|
+
<div
|
|
1299
|
+
class="flex items-center gap-2 px-2 py-2 text-sm text-muted-foreground"
|
|
1300
|
+
>
|
|
1301
|
+
<span
|
|
1302
|
+
class="size-7 animate-pulse rounded-sm border border-border bg-secondary"
|
|
1303
|
+
aria-hidden="true"
|
|
1304
|
+
></span>
|
|
1305
|
+
<span>Loading…</span>
|
|
1306
|
+
</div>
|
|
1307
|
+
} @else {
|
|
1308
|
+
<div class="max-h-full overflow-y-auto">
|
|
1309
|
+
@for (
|
|
1310
|
+
option of suggestions();
|
|
1311
|
+
track option.id;
|
|
1312
|
+
let index = $index
|
|
1313
|
+
) {
|
|
1314
|
+
<button
|
|
1315
|
+
type="button"
|
|
1316
|
+
role="option"
|
|
1317
|
+
[attr.data-suggestion-index]="index"
|
|
1318
|
+
class="group grid h-12 w-full min-w-0 cursor-pointer grid-cols-[1.75rem_minmax(0,1fr)] items-center gap-2 rounded-sm px-2 text-left transition-colors hover:bg-accent-subtle hover:text-foreground focus:bg-accent-subtle focus:text-foreground focus:outline-none"
|
|
1319
|
+
[class.bg-accent-subtle]="index === activeIndex()"
|
|
1320
|
+
[class.text-foreground]="index === activeIndex()"
|
|
1321
|
+
[attr.aria-selected]="index === activeIndex()"
|
|
1322
|
+
[attr.tabindex]="index === activeIndex() ? 0 : -1"
|
|
1323
|
+
(mouseenter)="activate.emit(index)"
|
|
1324
|
+
(mousedown)="preserveSelection($event)"
|
|
1325
|
+
(keydown)="handleOptionKeydown($event, option, index)"
|
|
1326
|
+
(click)="pick.emit(option)"
|
|
1327
|
+
>
|
|
1328
|
+
<span
|
|
1329
|
+
class="flex size-7 items-center justify-center rounded-sm border border-border bg-card text-xs font-semibold text-foreground"
|
|
1330
|
+
[class.border-accent]="index === activeIndex()"
|
|
1331
|
+
[class.bg-background]="index === activeIndex()"
|
|
1332
|
+
[class.text-accent]="index === activeIndex()"
|
|
1333
|
+
aria-hidden="true"
|
|
1334
|
+
>
|
|
1335
|
+
{{ option.label.slice(0, 1) }}
|
|
1336
|
+
</span>
|
|
1337
|
+
<span class="min-w-0">
|
|
1338
|
+
<span class="block truncate font-medium leading-5">{{
|
|
1339
|
+
option.label
|
|
1340
|
+
}}</span>
|
|
1341
|
+
@if (index === activeIndex()) {
|
|
1342
|
+
<span
|
|
1343
|
+
class="block truncate text-xs leading-4 text-foreground"
|
|
1344
|
+
>
|
|
1345
|
+
{{ option.description }}
|
|
1346
|
+
</span>
|
|
1347
|
+
} @else {
|
|
1348
|
+
<span
|
|
1349
|
+
class="block truncate text-xs leading-4 text-muted-foreground group-hover:text-foreground group-focus:text-foreground"
|
|
1350
|
+
>
|
|
1351
|
+
{{ option.description }}
|
|
1352
|
+
</span>
|
|
1353
|
+
}
|
|
1354
|
+
</span>
|
|
1355
|
+
</button>
|
|
1356
|
+
}
|
|
1357
|
+
</div>
|
|
1358
|
+
}
|
|
1359
|
+
</div>
|
|
1360
|
+
}
|
|
1361
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1362
|
+
}
|
|
1363
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaMentionMenu, decorators: [{
|
|
1364
|
+
type: Component,
|
|
1365
|
+
args: [{
|
|
1366
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1367
|
+
selector: 'qalma-mention-menu',
|
|
1368
|
+
template: `
|
|
1369
|
+
@if (placement(); as placement) {
|
|
1370
|
+
<div
|
|
1371
|
+
role="listbox"
|
|
1372
|
+
aria-label="Mention suggestions"
|
|
1373
|
+
class="fixed z-20 w-[min(280px,calc(100vw-24px))] overflow-hidden rounded-md border border-border bg-popover p-1 text-sm text-popover-foreground shadow-lg outline-none"
|
|
1374
|
+
[style.left.px]="placement.left"
|
|
1375
|
+
[style.top.px]="placement.top"
|
|
1376
|
+
[style.bottom.px]="placement.bottom"
|
|
1377
|
+
[style.max-height.px]="placement.maxHeight"
|
|
1378
|
+
>
|
|
1379
|
+
@if (loading()) {
|
|
1380
|
+
<div
|
|
1381
|
+
class="flex items-center gap-2 px-2 py-2 text-sm text-muted-foreground"
|
|
1382
|
+
>
|
|
1383
|
+
<span
|
|
1384
|
+
class="size-7 animate-pulse rounded-sm border border-border bg-secondary"
|
|
1385
|
+
aria-hidden="true"
|
|
1386
|
+
></span>
|
|
1387
|
+
<span>Loading…</span>
|
|
1388
|
+
</div>
|
|
1389
|
+
} @else {
|
|
1390
|
+
<div class="max-h-full overflow-y-auto">
|
|
1391
|
+
@for (
|
|
1392
|
+
option of suggestions();
|
|
1393
|
+
track option.id;
|
|
1394
|
+
let index = $index
|
|
1395
|
+
) {
|
|
1396
|
+
<button
|
|
1397
|
+
type="button"
|
|
1398
|
+
role="option"
|
|
1399
|
+
[attr.data-suggestion-index]="index"
|
|
1400
|
+
class="group grid h-12 w-full min-w-0 cursor-pointer grid-cols-[1.75rem_minmax(0,1fr)] items-center gap-2 rounded-sm px-2 text-left transition-colors hover:bg-accent-subtle hover:text-foreground focus:bg-accent-subtle focus:text-foreground focus:outline-none"
|
|
1401
|
+
[class.bg-accent-subtle]="index === activeIndex()"
|
|
1402
|
+
[class.text-foreground]="index === activeIndex()"
|
|
1403
|
+
[attr.aria-selected]="index === activeIndex()"
|
|
1404
|
+
[attr.tabindex]="index === activeIndex() ? 0 : -1"
|
|
1405
|
+
(mouseenter)="activate.emit(index)"
|
|
1406
|
+
(mousedown)="preserveSelection($event)"
|
|
1407
|
+
(keydown)="handleOptionKeydown($event, option, index)"
|
|
1408
|
+
(click)="pick.emit(option)"
|
|
1409
|
+
>
|
|
1410
|
+
<span
|
|
1411
|
+
class="flex size-7 items-center justify-center rounded-sm border border-border bg-card text-xs font-semibold text-foreground"
|
|
1412
|
+
[class.border-accent]="index === activeIndex()"
|
|
1413
|
+
[class.bg-background]="index === activeIndex()"
|
|
1414
|
+
[class.text-accent]="index === activeIndex()"
|
|
1415
|
+
aria-hidden="true"
|
|
1416
|
+
>
|
|
1417
|
+
{{ option.label.slice(0, 1) }}
|
|
1418
|
+
</span>
|
|
1419
|
+
<span class="min-w-0">
|
|
1420
|
+
<span class="block truncate font-medium leading-5">{{
|
|
1421
|
+
option.label
|
|
1422
|
+
}}</span>
|
|
1423
|
+
@if (index === activeIndex()) {
|
|
1424
|
+
<span
|
|
1425
|
+
class="block truncate text-xs leading-4 text-foreground"
|
|
1426
|
+
>
|
|
1427
|
+
{{ option.description }}
|
|
1428
|
+
</span>
|
|
1429
|
+
} @else {
|
|
1430
|
+
<span
|
|
1431
|
+
class="block truncate text-xs leading-4 text-muted-foreground group-hover:text-foreground group-focus:text-foreground"
|
|
1432
|
+
>
|
|
1433
|
+
{{ option.description }}
|
|
1434
|
+
</span>
|
|
1435
|
+
}
|
|
1436
|
+
</span>
|
|
1437
|
+
</button>
|
|
1438
|
+
}
|
|
1439
|
+
</div>
|
|
1440
|
+
}
|
|
1441
|
+
</div>
|
|
1442
|
+
}
|
|
1443
|
+
`,
|
|
1444
|
+
}]
|
|
1445
|
+
}], propDecorators: { suggestions: [{ type: i0.Input, args: [{ isSignal: true, alias: "suggestions", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }] } });
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* Caret-anchored slash-command menu: an icon + shortcut list with a "Commands"
|
|
1449
|
+
* header and an Escape hint footer, scrolling the active option into view.
|
|
1450
|
+
* Positioning, keyboard handling and active highlighting come from
|
|
1451
|
+
* {@link QalmaSuggestionMenu}; feed it a `placement` (see `flipAbovePlacement`)
|
|
1452
|
+
* and the filtered `options`. Ships the default block-command icon set.
|
|
1453
|
+
*/
|
|
1454
|
+
class QalmaSlashCommandMenu extends QalmaSuggestionMenu {
|
|
1455
|
+
options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
1456
|
+
optionList = this.options;
|
|
1457
|
+
constructor() {
|
|
1458
|
+
super();
|
|
1459
|
+
effect(() => {
|
|
1460
|
+
const activeIndex = this.activeIndex();
|
|
1461
|
+
this.options();
|
|
1462
|
+
queueMicrotask(() => this.scrollOptionIntoView(activeIndex));
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaSlashCommandMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1466
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: QalmaSlashCommandMenu, isStandalone: true, selector: "qalma-slash-command-menu", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
|
|
1467
|
+
provideIcons({
|
|
1468
|
+
lucideCode,
|
|
1469
|
+
lucideHeading1,
|
|
1470
|
+
lucideHeading2,
|
|
1471
|
+
lucideHeading3,
|
|
1472
|
+
lucideList,
|
|
1473
|
+
lucideListOrdered,
|
|
1474
|
+
lucideListTodo,
|
|
1475
|
+
lucideMinus,
|
|
1476
|
+
lucidePilcrow,
|
|
1477
|
+
lucideSquareCode,
|
|
1478
|
+
lucideTable,
|
|
1479
|
+
lucideTextQuote,
|
|
1480
|
+
}),
|
|
1481
|
+
], usesInheritance: true, ngImport: i0, template: `
|
|
1482
|
+
@if (placement(); as placement) {
|
|
1483
|
+
<div
|
|
1484
|
+
role="listbox"
|
|
1485
|
+
aria-label="Slash command suggestions"
|
|
1486
|
+
class="fixed z-20 flex w-[min(320px,calc(100vw-24px))] flex-col overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg outline-none"
|
|
1487
|
+
[style.left.px]="placement.left"
|
|
1488
|
+
[style.top.px]="placement.top"
|
|
1489
|
+
[style.bottom.px]="placement.bottom"
|
|
1490
|
+
[style.max-height.px]="placement.maxHeight"
|
|
1491
|
+
>
|
|
1492
|
+
<div
|
|
1493
|
+
class="shrink-0 px-2 py-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"
|
|
1494
|
+
>
|
|
1495
|
+
Commands
|
|
1496
|
+
</div>
|
|
1497
|
+
<div data-suggestion-options class="min-h-0 flex-1 overflow-y-auto">
|
|
1498
|
+
@for (option of options(); track option.id; let index = $index) {
|
|
1499
|
+
<button
|
|
1500
|
+
type="button"
|
|
1501
|
+
role="option"
|
|
1502
|
+
[attr.data-suggestion-index]="index"
|
|
1503
|
+
class="group grid h-11 w-full min-w-0 cursor-pointer grid-cols-[1.75rem_minmax(0,1fr)_auto] items-center gap-2 rounded-sm px-2 text-left text-sm transition-colors hover:bg-accent-subtle hover:text-foreground focus:bg-accent-subtle focus:text-foreground focus:outline-none"
|
|
1504
|
+
[class.bg-accent-subtle]="index === activeIndex()"
|
|
1505
|
+
[class.text-foreground]="index === activeIndex()"
|
|
1506
|
+
[attr.aria-selected]="index === activeIndex()"
|
|
1507
|
+
[attr.tabindex]="index === activeIndex() ? 0 : -1"
|
|
1508
|
+
(mouseenter)="activate.emit(index)"
|
|
1509
|
+
(mousedown)="preserveSelection($event)"
|
|
1510
|
+
(keydown)="handleOptionKeydown($event, option, index)"
|
|
1511
|
+
(click)="pick.emit(option)"
|
|
1512
|
+
>
|
|
1513
|
+
<span
|
|
1514
|
+
class="flex size-7 items-center justify-center rounded-sm border border-border bg-card text-[14px] text-foreground"
|
|
1515
|
+
[class.border-accent]="index === activeIndex()"
|
|
1516
|
+
[class.bg-background]="index === activeIndex()"
|
|
1517
|
+
[class.text-accent]="index === activeIndex()"
|
|
1518
|
+
>
|
|
1519
|
+
<ng-icon [name]="option.icon" aria-hidden="true" />
|
|
1520
|
+
</span>
|
|
1521
|
+
<span class="min-w-0">
|
|
1522
|
+
<span class="block truncate font-medium leading-5">{{
|
|
1523
|
+
option.label
|
|
1524
|
+
}}</span>
|
|
1525
|
+
@if (index === activeIndex()) {
|
|
1526
|
+
<span
|
|
1527
|
+
class="block truncate text-xs leading-4 text-foreground"
|
|
1528
|
+
>
|
|
1529
|
+
{{ option.description }}
|
|
1530
|
+
</span>
|
|
1531
|
+
} @else {
|
|
1532
|
+
<span
|
|
1533
|
+
class="block truncate text-xs leading-4 text-muted-foreground group-hover:text-foreground group-focus:text-foreground"
|
|
1534
|
+
>
|
|
1535
|
+
{{ option.description }}
|
|
1536
|
+
</span>
|
|
1537
|
+
}
|
|
1538
|
+
</span>
|
|
1539
|
+
<kbd
|
|
1540
|
+
class="rounded-sm border border-border bg-card px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground"
|
|
1541
|
+
[class.border-accent]="index === activeIndex()"
|
|
1542
|
+
[class.bg-background]="index === activeIndex()"
|
|
1543
|
+
[class.text-accent]="index === activeIndex()"
|
|
1544
|
+
>
|
|
1545
|
+
{{ option.shortcut }}
|
|
1546
|
+
</kbd>
|
|
1547
|
+
</button>
|
|
1548
|
+
}
|
|
1549
|
+
</div>
|
|
1550
|
+
<div
|
|
1551
|
+
class="mt-1 flex h-8 shrink-0 items-center justify-between border-t border-border px-2 text-[11px] text-muted-foreground"
|
|
1552
|
+
>
|
|
1553
|
+
<span>Close menu</span>
|
|
1554
|
+
<kbd
|
|
1555
|
+
class="rounded-sm border border-border bg-card px-1.5 py-0.5 font-mono"
|
|
1556
|
+
>esc</kbd
|
|
1557
|
+
>
|
|
1558
|
+
</div>
|
|
1559
|
+
</div>
|
|
1560
|
+
}
|
|
1561
|
+
`, isInline: true, dependencies: [{ kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1562
|
+
}
|
|
1563
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaSlashCommandMenu, decorators: [{
|
|
1564
|
+
type: Component,
|
|
1565
|
+
args: [{
|
|
1566
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1567
|
+
imports: [NgIcon],
|
|
1568
|
+
providers: [
|
|
1569
|
+
provideIcons({
|
|
1570
|
+
lucideCode,
|
|
1571
|
+
lucideHeading1,
|
|
1572
|
+
lucideHeading2,
|
|
1573
|
+
lucideHeading3,
|
|
1574
|
+
lucideList,
|
|
1575
|
+
lucideListOrdered,
|
|
1576
|
+
lucideListTodo,
|
|
1577
|
+
lucideMinus,
|
|
1578
|
+
lucidePilcrow,
|
|
1579
|
+
lucideSquareCode,
|
|
1580
|
+
lucideTable,
|
|
1581
|
+
lucideTextQuote,
|
|
1582
|
+
}),
|
|
1583
|
+
],
|
|
1584
|
+
selector: 'qalma-slash-command-menu',
|
|
1585
|
+
template: `
|
|
1586
|
+
@if (placement(); as placement) {
|
|
1587
|
+
<div
|
|
1588
|
+
role="listbox"
|
|
1589
|
+
aria-label="Slash command suggestions"
|
|
1590
|
+
class="fixed z-20 flex w-[min(320px,calc(100vw-24px))] flex-col overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg outline-none"
|
|
1591
|
+
[style.left.px]="placement.left"
|
|
1592
|
+
[style.top.px]="placement.top"
|
|
1593
|
+
[style.bottom.px]="placement.bottom"
|
|
1594
|
+
[style.max-height.px]="placement.maxHeight"
|
|
1595
|
+
>
|
|
1596
|
+
<div
|
|
1597
|
+
class="shrink-0 px-2 py-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"
|
|
1598
|
+
>
|
|
1599
|
+
Commands
|
|
1600
|
+
</div>
|
|
1601
|
+
<div data-suggestion-options class="min-h-0 flex-1 overflow-y-auto">
|
|
1602
|
+
@for (option of options(); track option.id; let index = $index) {
|
|
1603
|
+
<button
|
|
1604
|
+
type="button"
|
|
1605
|
+
role="option"
|
|
1606
|
+
[attr.data-suggestion-index]="index"
|
|
1607
|
+
class="group grid h-11 w-full min-w-0 cursor-pointer grid-cols-[1.75rem_minmax(0,1fr)_auto] items-center gap-2 rounded-sm px-2 text-left text-sm transition-colors hover:bg-accent-subtle hover:text-foreground focus:bg-accent-subtle focus:text-foreground focus:outline-none"
|
|
1608
|
+
[class.bg-accent-subtle]="index === activeIndex()"
|
|
1609
|
+
[class.text-foreground]="index === activeIndex()"
|
|
1610
|
+
[attr.aria-selected]="index === activeIndex()"
|
|
1611
|
+
[attr.tabindex]="index === activeIndex() ? 0 : -1"
|
|
1612
|
+
(mouseenter)="activate.emit(index)"
|
|
1613
|
+
(mousedown)="preserveSelection($event)"
|
|
1614
|
+
(keydown)="handleOptionKeydown($event, option, index)"
|
|
1615
|
+
(click)="pick.emit(option)"
|
|
1616
|
+
>
|
|
1617
|
+
<span
|
|
1618
|
+
class="flex size-7 items-center justify-center rounded-sm border border-border bg-card text-[14px] text-foreground"
|
|
1619
|
+
[class.border-accent]="index === activeIndex()"
|
|
1620
|
+
[class.bg-background]="index === activeIndex()"
|
|
1621
|
+
[class.text-accent]="index === activeIndex()"
|
|
1622
|
+
>
|
|
1623
|
+
<ng-icon [name]="option.icon" aria-hidden="true" />
|
|
1624
|
+
</span>
|
|
1625
|
+
<span class="min-w-0">
|
|
1626
|
+
<span class="block truncate font-medium leading-5">{{
|
|
1627
|
+
option.label
|
|
1628
|
+
}}</span>
|
|
1629
|
+
@if (index === activeIndex()) {
|
|
1630
|
+
<span
|
|
1631
|
+
class="block truncate text-xs leading-4 text-foreground"
|
|
1632
|
+
>
|
|
1633
|
+
{{ option.description }}
|
|
1634
|
+
</span>
|
|
1635
|
+
} @else {
|
|
1636
|
+
<span
|
|
1637
|
+
class="block truncate text-xs leading-4 text-muted-foreground group-hover:text-foreground group-focus:text-foreground"
|
|
1638
|
+
>
|
|
1639
|
+
{{ option.description }}
|
|
1640
|
+
</span>
|
|
1641
|
+
}
|
|
1642
|
+
</span>
|
|
1643
|
+
<kbd
|
|
1644
|
+
class="rounded-sm border border-border bg-card px-1.5 py-0.5 font-mono text-[11px] text-muted-foreground"
|
|
1645
|
+
[class.border-accent]="index === activeIndex()"
|
|
1646
|
+
[class.bg-background]="index === activeIndex()"
|
|
1647
|
+
[class.text-accent]="index === activeIndex()"
|
|
1648
|
+
>
|
|
1649
|
+
{{ option.shortcut }}
|
|
1650
|
+
</kbd>
|
|
1651
|
+
</button>
|
|
1652
|
+
}
|
|
1653
|
+
</div>
|
|
1654
|
+
<div
|
|
1655
|
+
class="mt-1 flex h-8 shrink-0 items-center justify-between border-t border-border px-2 text-[11px] text-muted-foreground"
|
|
1656
|
+
>
|
|
1657
|
+
<span>Close menu</span>
|
|
1658
|
+
<kbd
|
|
1659
|
+
class="rounded-sm border border-border bg-card px-1.5 py-0.5 font-mono"
|
|
1660
|
+
>esc</kbd
|
|
1661
|
+
>
|
|
1662
|
+
</div>
|
|
1663
|
+
</div>
|
|
1664
|
+
}
|
|
1665
|
+
`,
|
|
1666
|
+
}]
|
|
1667
|
+
}], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }] } });
|
|
1668
|
+
|
|
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
|
+
class QalmaLinkPopover {
|
|
1812
|
+
popover = input(null, ...(ngDevMode ? [{ debugName: "popover" }] : /* istanbul ignore next */ []));
|
|
1813
|
+
href = input.required(...(ngDevMode ? [{ debugName: "href" }] : /* istanbul ignore next */ []));
|
|
1814
|
+
hrefChange = output();
|
|
1815
|
+
edit = output();
|
|
1816
|
+
save = output();
|
|
1817
|
+
remove = output();
|
|
1818
|
+
dismiss = output();
|
|
1819
|
+
keepOpen = output();
|
|
1820
|
+
scheduleHide = output();
|
|
1821
|
+
updateHref(event) {
|
|
1822
|
+
const target = event.target;
|
|
1823
|
+
if (target instanceof HTMLInputElement) {
|
|
1824
|
+
this.hrefChange.emit(target.value);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaLinkPopover, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1828
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: QalmaLinkPopover, isStandalone: true, selector: "qalma-link-popover", inputs: { popover: { classPropertyName: "popover", publicName: "popover", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { hrefChange: "hrefChange", edit: "edit", save: "save", remove: "remove", dismiss: "dismiss", keepOpen: "keepOpen", scheduleHide: "scheduleHide" }, providers: [
|
|
1829
|
+
provideIcons({
|
|
1830
|
+
lucideCheck,
|
|
1831
|
+
lucideExternalLink,
|
|
1832
|
+
lucideLink,
|
|
1833
|
+
lucidePencil,
|
|
1834
|
+
lucideUnlink,
|
|
1835
|
+
lucideX,
|
|
1836
|
+
}),
|
|
1837
|
+
], ngImport: i0, template: `
|
|
1838
|
+
@if (popover(); as popover) {
|
|
1839
|
+
<div
|
|
1840
|
+
data-qalma-link-popover
|
|
1841
|
+
role="dialog"
|
|
1842
|
+
aria-label="Link preview"
|
|
1843
|
+
class="fixed z-20 w-[min(360px,calc(100vw-32px))] rounded-md border border-border bg-popover p-1.5 text-sm text-popover-foreground shadow-lg outline-none"
|
|
1844
|
+
[style.left.px]="popover.left"
|
|
1845
|
+
[style.top.px]="popover.top"
|
|
1846
|
+
(mouseenter)="keepOpen.emit()"
|
|
1847
|
+
(mouseleave)="scheduleHide.emit()"
|
|
1848
|
+
(focusin)="keepOpen.emit()"
|
|
1849
|
+
(focusout)="scheduleHide.emit()"
|
|
1850
|
+
>
|
|
1851
|
+
@if (popover.editing) {
|
|
1852
|
+
<div class="flex items-center gap-1.5">
|
|
1853
|
+
<label
|
|
1854
|
+
class="flex h-9 min-w-0 flex-1 items-center gap-2 rounded-md border border-input bg-background px-2 text-sm transition focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/30"
|
|
1855
|
+
>
|
|
1856
|
+
<ng-icon
|
|
1857
|
+
class="shrink-0 text-muted-foreground"
|
|
1858
|
+
name="lucideLink"
|
|
1859
|
+
aria-hidden="true"
|
|
1860
|
+
/>
|
|
1861
|
+
<input
|
|
1862
|
+
class="min-w-0 flex-1 bg-transparent text-foreground outline-none placeholder:text-muted-foreground"
|
|
1863
|
+
type="url"
|
|
1864
|
+
aria-label="Edit link URL"
|
|
1865
|
+
[value]="href()"
|
|
1866
|
+
(input)="updateHref($event)"
|
|
1867
|
+
/>
|
|
1868
|
+
</label>
|
|
1869
|
+
<button
|
|
1870
|
+
qalmaBtn
|
|
1871
|
+
size="icon"
|
|
1872
|
+
type="button"
|
|
1873
|
+
[disabled]="!href().trim()"
|
|
1874
|
+
(click)="save.emit(popover)"
|
|
1875
|
+
title="Save link"
|
|
1876
|
+
aria-label="Save link"
|
|
1877
|
+
>
|
|
1878
|
+
<ng-icon name="lucideCheck" aria-hidden="true" />
|
|
1879
|
+
</button>
|
|
1880
|
+
<button
|
|
1881
|
+
qalmaBtn
|
|
1882
|
+
variant="outline"
|
|
1883
|
+
size="icon"
|
|
1884
|
+
type="button"
|
|
1885
|
+
(click)="dismiss.emit()"
|
|
1886
|
+
title="Cancel"
|
|
1887
|
+
aria-label="Cancel"
|
|
1888
|
+
>
|
|
1889
|
+
<ng-icon name="lucideX" aria-hidden="true" />
|
|
1890
|
+
</button>
|
|
1891
|
+
</div>
|
|
1892
|
+
} @else {
|
|
1893
|
+
<div class="flex items-center gap-1.5">
|
|
1894
|
+
<a
|
|
1895
|
+
class="inline-flex h-9 min-w-0 flex-1 items-center gap-1.5 rounded-md border border-border bg-card px-2.5 text-accent transition-colors hover:bg-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
1896
|
+
[href]="popover.href"
|
|
1897
|
+
[target]="popover.target ?? '_blank'"
|
|
1898
|
+
[rel]="popover.rel ?? 'noopener noreferrer'"
|
|
1899
|
+
title="Open link"
|
|
1900
|
+
>
|
|
1901
|
+
<span class="min-w-0 truncate font-medium">{{
|
|
1902
|
+
popover.href
|
|
1903
|
+
}}</span>
|
|
1904
|
+
<ng-icon
|
|
1905
|
+
class="shrink-0 text-base"
|
|
1906
|
+
name="lucideExternalLink"
|
|
1907
|
+
aria-hidden="true"
|
|
1908
|
+
/>
|
|
1909
|
+
</a>
|
|
1910
|
+
<button
|
|
1911
|
+
qalmaBtn
|
|
1912
|
+
variant="outline"
|
|
1913
|
+
size="icon"
|
|
1914
|
+
type="button"
|
|
1915
|
+
(click)="edit.emit(popover)"
|
|
1916
|
+
title="Edit link"
|
|
1917
|
+
aria-label="Edit link"
|
|
1918
|
+
>
|
|
1919
|
+
<ng-icon name="lucidePencil" aria-hidden="true" />
|
|
1920
|
+
</button>
|
|
1921
|
+
<button
|
|
1922
|
+
qalmaBtn
|
|
1923
|
+
variant="ghost"
|
|
1924
|
+
size="icon"
|
|
1925
|
+
type="button"
|
|
1926
|
+
class="text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
1927
|
+
(click)="remove.emit(popover)"
|
|
1928
|
+
title="Unlink"
|
|
1929
|
+
aria-label="Unlink"
|
|
1930
|
+
>
|
|
1931
|
+
<ng-icon name="lucideUnlink" aria-hidden="true" />
|
|
1932
|
+
</button>
|
|
1933
|
+
</div>
|
|
1934
|
+
}
|
|
1935
|
+
</div>
|
|
1936
|
+
}
|
|
1937
|
+
`, isInline: true, dependencies: [{ kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }, { kind: "directive", type: QalmaButton, selector: "button[qalmaBtn], a[qalmaBtn]", inputs: ["variant", "size"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1938
|
+
}
|
|
1939
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaLinkPopover, decorators: [{
|
|
1940
|
+
type: Component,
|
|
1941
|
+
args: [{
|
|
1942
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1943
|
+
imports: [NgIcon, QalmaButton],
|
|
1944
|
+
providers: [
|
|
1945
|
+
provideIcons({
|
|
1946
|
+
lucideCheck,
|
|
1947
|
+
lucideExternalLink,
|
|
1948
|
+
lucideLink,
|
|
1949
|
+
lucidePencil,
|
|
1950
|
+
lucideUnlink,
|
|
1951
|
+
lucideX,
|
|
1952
|
+
}),
|
|
1953
|
+
],
|
|
1954
|
+
selector: 'qalma-link-popover',
|
|
1955
|
+
template: `
|
|
1956
|
+
@if (popover(); as popover) {
|
|
1957
|
+
<div
|
|
1958
|
+
data-qalma-link-popover
|
|
1959
|
+
role="dialog"
|
|
1960
|
+
aria-label="Link preview"
|
|
1961
|
+
class="fixed z-20 w-[min(360px,calc(100vw-32px))] rounded-md border border-border bg-popover p-1.5 text-sm text-popover-foreground shadow-lg outline-none"
|
|
1962
|
+
[style.left.px]="popover.left"
|
|
1963
|
+
[style.top.px]="popover.top"
|
|
1964
|
+
(mouseenter)="keepOpen.emit()"
|
|
1965
|
+
(mouseleave)="scheduleHide.emit()"
|
|
1966
|
+
(focusin)="keepOpen.emit()"
|
|
1967
|
+
(focusout)="scheduleHide.emit()"
|
|
1968
|
+
>
|
|
1969
|
+
@if (popover.editing) {
|
|
1970
|
+
<div class="flex items-center gap-1.5">
|
|
1971
|
+
<label
|
|
1972
|
+
class="flex h-9 min-w-0 flex-1 items-center gap-2 rounded-md border border-input bg-background px-2 text-sm transition focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/30"
|
|
1973
|
+
>
|
|
1974
|
+
<ng-icon
|
|
1975
|
+
class="shrink-0 text-muted-foreground"
|
|
1976
|
+
name="lucideLink"
|
|
1977
|
+
aria-hidden="true"
|
|
1978
|
+
/>
|
|
1979
|
+
<input
|
|
1980
|
+
class="min-w-0 flex-1 bg-transparent text-foreground outline-none placeholder:text-muted-foreground"
|
|
1981
|
+
type="url"
|
|
1982
|
+
aria-label="Edit link URL"
|
|
1983
|
+
[value]="href()"
|
|
1984
|
+
(input)="updateHref($event)"
|
|
1985
|
+
/>
|
|
1986
|
+
</label>
|
|
1987
|
+
<button
|
|
1988
|
+
qalmaBtn
|
|
1989
|
+
size="icon"
|
|
1990
|
+
type="button"
|
|
1991
|
+
[disabled]="!href().trim()"
|
|
1992
|
+
(click)="save.emit(popover)"
|
|
1993
|
+
title="Save link"
|
|
1994
|
+
aria-label="Save link"
|
|
1995
|
+
>
|
|
1996
|
+
<ng-icon name="lucideCheck" aria-hidden="true" />
|
|
1997
|
+
</button>
|
|
1998
|
+
<button
|
|
1999
|
+
qalmaBtn
|
|
2000
|
+
variant="outline"
|
|
2001
|
+
size="icon"
|
|
2002
|
+
type="button"
|
|
2003
|
+
(click)="dismiss.emit()"
|
|
2004
|
+
title="Cancel"
|
|
2005
|
+
aria-label="Cancel"
|
|
2006
|
+
>
|
|
2007
|
+
<ng-icon name="lucideX" aria-hidden="true" />
|
|
2008
|
+
</button>
|
|
2009
|
+
</div>
|
|
2010
|
+
} @else {
|
|
2011
|
+
<div class="flex items-center gap-1.5">
|
|
2012
|
+
<a
|
|
2013
|
+
class="inline-flex h-9 min-w-0 flex-1 items-center gap-1.5 rounded-md border border-border bg-card px-2.5 text-accent transition-colors hover:bg-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
2014
|
+
[href]="popover.href"
|
|
2015
|
+
[target]="popover.target ?? '_blank'"
|
|
2016
|
+
[rel]="popover.rel ?? 'noopener noreferrer'"
|
|
2017
|
+
title="Open link"
|
|
2018
|
+
>
|
|
2019
|
+
<span class="min-w-0 truncate font-medium">{{
|
|
2020
|
+
popover.href
|
|
2021
|
+
}}</span>
|
|
2022
|
+
<ng-icon
|
|
2023
|
+
class="shrink-0 text-base"
|
|
2024
|
+
name="lucideExternalLink"
|
|
2025
|
+
aria-hidden="true"
|
|
2026
|
+
/>
|
|
2027
|
+
</a>
|
|
2028
|
+
<button
|
|
2029
|
+
qalmaBtn
|
|
2030
|
+
variant="outline"
|
|
2031
|
+
size="icon"
|
|
2032
|
+
type="button"
|
|
2033
|
+
(click)="edit.emit(popover)"
|
|
2034
|
+
title="Edit link"
|
|
2035
|
+
aria-label="Edit link"
|
|
2036
|
+
>
|
|
2037
|
+
<ng-icon name="lucidePencil" aria-hidden="true" />
|
|
2038
|
+
</button>
|
|
2039
|
+
<button
|
|
2040
|
+
qalmaBtn
|
|
2041
|
+
variant="ghost"
|
|
2042
|
+
size="icon"
|
|
2043
|
+
type="button"
|
|
2044
|
+
class="text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
2045
|
+
(click)="remove.emit(popover)"
|
|
2046
|
+
title="Unlink"
|
|
2047
|
+
aria-label="Unlink"
|
|
2048
|
+
>
|
|
2049
|
+
<ng-icon name="lucideUnlink" aria-hidden="true" />
|
|
2050
|
+
</button>
|
|
2051
|
+
</div>
|
|
2052
|
+
}
|
|
2053
|
+
</div>
|
|
2054
|
+
}
|
|
2055
|
+
`,
|
|
2056
|
+
}]
|
|
2057
|
+
}], 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
|
+
|
|
2059
|
+
class QalmaContextualToolbar {
|
|
2060
|
+
editor = input.required(...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
|
|
2061
|
+
placement = input(null, ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
|
|
2062
|
+
requestLink = output();
|
|
2063
|
+
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
|
+
preserveSelection(event) {
|
|
2067
|
+
event.preventDefault();
|
|
2068
|
+
}
|
|
2069
|
+
dismissFromKeyboard(event) {
|
|
2070
|
+
event.preventDefault();
|
|
2071
|
+
this.dismiss.emit();
|
|
2072
|
+
}
|
|
2073
|
+
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: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { requestLink: "requestLink", dismiss: "dismiss" }, providers: [
|
|
2075
|
+
provideIcons({
|
|
2076
|
+
lucideBold,
|
|
2077
|
+
lucideCode,
|
|
2078
|
+
lucideItalic,
|
|
2079
|
+
lucideLetterText,
|
|
2080
|
+
lucideLink,
|
|
2081
|
+
}),
|
|
2082
|
+
], ngImport: i0, template: `
|
|
2083
|
+
@if (placement(); as placement) {
|
|
2084
|
+
<qalma-toolbar
|
|
2085
|
+
label="Selection formatting"
|
|
2086
|
+
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
|
+
[style.transform]="placement.transform"
|
|
2088
|
+
(mousedown)="preserveSelection($event)"
|
|
2089
|
+
(keydown.escape)="dismissFromKeyboard($event)"
|
|
2090
|
+
>
|
|
2091
|
+
<button
|
|
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>
|
|
2176
|
+
</qalma-toolbar>
|
|
2177
|
+
}
|
|
2178
|
+
`, isInline: true, dependencies: [{ kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }, { kind: "directive", type: QalmaButton, selector: "button[qalmaBtn], a[qalmaBtn]", inputs: ["variant", "size"] }, { kind: "directive", type: QalmaCommand, selector: "button[qalmaCommand]", inputs: ["qalmaCommand", "qalmaCommandValue"] }, { kind: "component", type: QalmaToolbar, selector: "qalma-toolbar", inputs: ["label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2179
|
+
}
|
|
2180
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: QalmaContextualToolbar, decorators: [{
|
|
2181
|
+
type: Component,
|
|
2182
|
+
args: [{
|
|
2183
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2184
|
+
imports: [NgIcon, QalmaButton, QalmaCommand, QalmaToolbar],
|
|
2185
|
+
providers: [
|
|
2186
|
+
provideIcons({
|
|
2187
|
+
lucideBold,
|
|
2188
|
+
lucideCode,
|
|
2189
|
+
lucideItalic,
|
|
2190
|
+
lucideLetterText,
|
|
2191
|
+
lucideLink,
|
|
2192
|
+
}),
|
|
2193
|
+
],
|
|
2194
|
+
selector: 'qalma-contextual-toolbar',
|
|
2195
|
+
template: `
|
|
2196
|
+
@if (placement(); as placement) {
|
|
2197
|
+
<qalma-toolbar
|
|
2198
|
+
label="Selection formatting"
|
|
2199
|
+
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
|
+
[style.transform]="placement.transform"
|
|
2201
|
+
(mousedown)="preserveSelection($event)"
|
|
2202
|
+
(keydown.escape)="dismissFromKeyboard($event)"
|
|
2203
|
+
>
|
|
2204
|
+
<button
|
|
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>
|
|
2289
|
+
</qalma-toolbar>
|
|
2290
|
+
}
|
|
2291
|
+
`,
|
|
2292
|
+
}]
|
|
2293
|
+
}], propDecorators: { editor: [{ type: i0.Input, args: [{ isSignal: true, alias: "editor", required: true }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], requestLink: [{ type: i0.Output, args: ["requestLink"] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }] } });
|
|
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
|
+
}
|
|
2433
|
+
|
|
2434
|
+
class QalmaSelectionToolbarDirective {
|
|
2435
|
+
editor = input.required({ ...(ngDevMode ? { debugName: "editor" } : /* istanbul ignore next */ {}), alias: 'qalmaSelectionToolbar' });
|
|
2436
|
+
destroyRef = inject(DestroyRef);
|
|
2437
|
+
host = inject(ElementRef);
|
|
2438
|
+
controller = new QalmaSelectionToolbarController(() => this.editor());
|
|
2439
|
+
placement = this.controller.placement;
|
|
2440
|
+
constructor() {
|
|
2441
|
+
afterNextRender(() => {
|
|
2442
|
+
this.controller.connect(this.host.nativeElement, this.destroyRef);
|
|
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 }] }] } });
|
|
2461
|
+
|
|
2462
|
+
/**
|
|
2463
|
+
* Generated bundle index. Do not edit.
|
|
2464
|
+
*/
|
|
2465
|
+
|
|
2466
|
+
export { DismissibleOverlay, KeyboardNavigableList, LinkPopoverController, 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, QalmaButton, QalmaContextualToolbar, QalmaDragHandle, QalmaDragHandleController, QalmaDragHandleDirective, QalmaLinkPopover, QalmaMentionMenu, QalmaSelectionToolbarController, QalmaSelectionToolbarDirective, QalmaSlashCommandMenu, QalmaSuggestionMenu, QalmaToolbarButton, QalmaToolbarRegistry, SUGGESTION_OPTIONS_SCROLLER_ATTR, SUGGESTION_OPTION_INDEX_ATTR, TOOLBAR_BUTTON_CLASS, TOOLBAR_SEPARATOR_CLASS, anchorToRect, buttonVariants, cn, createLinkPopoverPlacement, findEditorLinkElement, flipAbovePlacement, isToolbarCommandItem, provideQalmaToolbarIcons };
|
|
2467
|
+
//# sourceMappingURL=qalma-kit.mjs.map
|