@h-k-dev/angular-email-editor 0.0.0-stage → 0.0.2

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.
Files changed (51) hide show
  1. package/README.md +127 -2
  2. package/fesm2022/h-k-dev-angular-email-editor-actions.mjs +179 -0
  3. package/fesm2022/h-k-dev-angular-email-editor-actions.mjs.map +1 -0
  4. package/fesm2022/h-k-dev-angular-email-editor-address-chip.mjs +383 -0
  5. package/fesm2022/h-k-dev-angular-email-editor-address-chip.mjs.map +1 -0
  6. package/fesm2022/h-k-dev-angular-email-editor-address-input.mjs +1106 -0
  7. package/fesm2022/h-k-dev-angular-email-editor-address-input.mjs.map +1 -0
  8. package/fesm2022/h-k-dev-angular-email-editor-address.mjs +489 -0
  9. package/fesm2022/h-k-dev-angular-email-editor-address.mjs.map +1 -0
  10. package/fesm2022/h-k-dev-angular-email-editor-anchor.mjs +64 -0
  11. package/fesm2022/h-k-dev-angular-email-editor-anchor.mjs.map +1 -0
  12. package/fesm2022/h-k-dev-angular-email-editor-attachment-chip.mjs +387 -0
  13. package/fesm2022/h-k-dev-angular-email-editor-attachment-chip.mjs.map +1 -0
  14. package/fesm2022/h-k-dev-angular-email-editor-attachment-chips.mjs +80 -0
  15. package/fesm2022/h-k-dev-angular-email-editor-attachment-chips.mjs.map +1 -0
  16. package/fesm2022/h-k-dev-angular-email-editor-chat-input.mjs +346 -0
  17. package/fesm2022/h-k-dev-angular-email-editor-chat-input.mjs.map +1 -0
  18. package/fesm2022/h-k-dev-angular-email-editor-focus.mjs +78 -0
  19. package/fesm2022/h-k-dev-angular-email-editor-focus.mjs.map +1 -0
  20. package/fesm2022/h-k-dev-angular-email-editor-image.mjs +132 -0
  21. package/fesm2022/h-k-dev-angular-email-editor-image.mjs.map +1 -0
  22. package/fesm2022/h-k-dev-angular-email-editor-material.mjs +124 -0
  23. package/fesm2022/h-k-dev-angular-email-editor-material.mjs.map +1 -0
  24. package/fesm2022/h-k-dev-angular-email-editor-metrics.mjs +243 -0
  25. package/fesm2022/h-k-dev-angular-email-editor-metrics.mjs.map +1 -0
  26. package/fesm2022/h-k-dev-angular-email-editor-palette.mjs +87 -0
  27. package/fesm2022/h-k-dev-angular-email-editor-palette.mjs.map +1 -0
  28. package/fesm2022/h-k-dev-angular-email-editor-proposal.mjs +307 -0
  29. package/fesm2022/h-k-dev-angular-email-editor-proposal.mjs.map +1 -0
  30. package/fesm2022/h-k-dev-angular-email-editor-suggestion-menu.mjs +392 -0
  31. package/fesm2022/h-k-dev-angular-email-editor-suggestion-menu.mjs.map +1 -0
  32. package/fesm2022/h-k-dev-angular-email-editor.mjs +18820 -0
  33. package/fesm2022/h-k-dev-angular-email-editor.mjs.map +1 -0
  34. package/package.json +133 -3
  35. package/styles/_scroll.scss +101 -0
  36. package/types/h-k-dev-angular-email-editor-actions.d.ts +147 -0
  37. package/types/h-k-dev-angular-email-editor-address-chip.d.ts +234 -0
  38. package/types/h-k-dev-angular-email-editor-address-input.d.ts +449 -0
  39. package/types/h-k-dev-angular-email-editor-address.d.ts +125 -0
  40. package/types/h-k-dev-angular-email-editor-anchor.d.ts +44 -0
  41. package/types/h-k-dev-angular-email-editor-attachment-chip.d.ts +271 -0
  42. package/types/h-k-dev-angular-email-editor-attachment-chips.d.ts +48 -0
  43. package/types/h-k-dev-angular-email-editor-chat-input.d.ts +101 -0
  44. package/types/h-k-dev-angular-email-editor-focus.d.ts +46 -0
  45. package/types/h-k-dev-angular-email-editor-image.d.ts +66 -0
  46. package/types/h-k-dev-angular-email-editor-material.d.ts +65 -0
  47. package/types/h-k-dev-angular-email-editor-metrics.d.ts +110 -0
  48. package/types/h-k-dev-angular-email-editor-palette.d.ts +51 -0
  49. package/types/h-k-dev-angular-email-editor-proposal.d.ts +170 -0
  50. package/types/h-k-dev-angular-email-editor-suggestion-menu.d.ts +177 -0
  51. package/types/h-k-dev-angular-email-editor.d.ts +3959 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"h-k-dev-angular-email-editor-attachment-chips.mjs","sources":["../../../projects/angular-email-editor/attachment-chips/attachment-chips.ts","../../../projects/angular-email-editor/attachment-chips/public-api.ts","../../../projects/angular-email-editor/attachment-chips/h-k-dev-angular-email-editor-attachment-chips.ts"],"sourcesContent":["import {\n Component,\n booleanAttribute,\n computed,\n contentChildren,\n effect,\n input,\n model,\n} from '@angular/core';\nimport type { FormValueControl } from '@angular/forms/signals';\nimport { Attachment, AttachmentChip } from '@h-k-dev/angular-email-editor/attachment-chip';\n\n/**\n * The row a message's attachment chips sit in — under the body, in Gmail's\n * spot: the strip belongs to the message, not to the envelope.\n *\n * **The strip is the form's control.** It holds the list as its `value` —\n * a `FormValueControl` over the attachments, bound with `[formField]` or\n * two-way as `[(value)]` — and nothing else: the host renders one\n * `<li email-attachment-chip>` per item into it and decides, chip by chip\n * in its own template, what each one shows (real progress, a simulated one,\n * none). That is why the pair is two components. The chips register by\n * being content: each one's `removed` is relayed into the value for as long\n * as it is in the strip, so a chip's × takes its item out of the message\n * with no wiring on the host's side. Identity, not name — the host's items\n * are the value's, so the chip that asked is exactly the item that goes.\n * `disabled` (the form's, or the host's) holds the value still.\n *\n * `aria-busy` is reflected while any chip is busy, so a host can style or\n * gate on the strip as a whole (`[email-attachment-chips][aria-busy='true']`,\n * or a disabled Send).\n *\n * Meant for a `<ul>`. `role=\"list\"` is set anyway, because `list-style: none`\n * costs a list its semantics in Safari. Hidden while it holds no chips, so a\n * host can place it unconditionally.\n */\n@Component({\n selector: '[email-attachment-chips]',\n template: '<ng-content />',\n styleUrl: './attachment-chips.scss',\n host: {\n role: 'list',\n '[attr.aria-label]': 'label()',\n '[attr.aria-busy]': 'busy() || null',\n '[attr.aria-disabled]': 'disabled() || null',\n '[hidden]': '!chips().length',\n },\n})\nexport class AttachmentChips<T extends Attachment = Attachment> implements FormValueControl<T[]> {\n /** Names the list for assistive tech. */\n readonly label = input('Attachments');\n\n /** The attachments the strip stands for — the form's value. The host\n renders a chip per item; a chip's removal takes its item out of here. */\n readonly value = model<T[]>([]);\n\n /** Nothing may leave the value — e.g. while the message sends. */\n readonly disabled = input(false, { transform: booleanAttribute });\n\n protected readonly chips = contentChildren(AttachmentChip);\n\n /** Any chip in the strip is busy — uploading, downloading, simulating. */\n readonly busy = computed(() => this.chips().some((chip) => chip.busy()));\n\n constructor() {\n effect((onCleanup) => {\n const subscriptions = this.chips().map((chip) =>\n chip.removed.subscribe(() => this.remove(chip.attachment() as T)),\n );\n onCleanup(() => subscriptions.forEach((subscription) => subscription.unsubscribe()));\n });\n }\n\n /** Takes one attachment out of the value, by identity. A no-op while\n disabled. */\n remove(attachment: T): void {\n if (this.disabled()) return;\n this.value.update((list) => list.filter((item) => item !== attachment));\n }\n}\n","/*\n * Public API Surface of @h-k-dev/angular-email-editor/attachment-chips\n */\n\nexport * from './attachment-chips';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,346 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, ElementRef, effect, input, booleanAttribute, model, output, signal, afterNextRender, DestroyRef, untracked, Directive, Component } from '@angular/core';
3
+ import { Plugin, PluginKey } from 'prosemirror-state';
4
+ import { DecorationSet, Decoration } from 'prosemirror-view';
5
+ import { defineExtension, createEditor, Document, Paragraph, Text, HardBreak, BulletList, OrderedList, ListItem, History, NoTextDrag, BaseKeymap } from '@h-k-dev/angular-email-editor';
6
+
7
+ /**
8
+ * The chat input's motion — the part of it CSS alone cannot carry. The
9
+ * stylesheet owns the picture (the `height` transition, its duration and
10
+ * easing on `--email-chat-input-motion`, reduced motion turning it off);
11
+ * this file owns the number it runs to.
12
+ *
13
+ * A height only transitions between lengths, never to or from `auto` — so
14
+ * the text's own height is measured and the box's written in pixels, the
15
+ * way ChatGPT's composer does it. A layout transition: run on the main
16
+ * thread, smooth with GPU acceleration off.
17
+ */
18
+ /**
19
+ * Keeps the host's `--email-chat-input-height` on the text's height plus
20
+ * the host's padding and border, from a `ResizeObserver` on the editor.
21
+ * Written straight to the host in the observer — before the frame paints,
22
+ * so the transition starts with the line, not a frame behind it. The text
23
+ * sizes itself, capped and scrolling on its own (the stylesheet), so the
24
+ * box never scrolls: no scrollbar flashing while it catches up. Nothing on
25
+ * the server, nor where there is no `ResizeObserver` — the box is `auto`.
26
+ *
27
+ * Call in an injection context, on the chat input's host.
28
+ */
29
+ function followTextHeight(editor) {
30
+ const host = inject(ElementRef).nativeElement;
31
+ effect((onCleanup) => {
32
+ const current = editor();
33
+ if (!current || typeof ResizeObserver !== 'function')
34
+ return;
35
+ const observer = new ResizeObserver(([entry]) => {
36
+ const text = entry.borderBoxSize?.[0]?.blockSize ?? entry.target.getBoundingClientRect().height;
37
+ const style = getComputedStyle(host);
38
+ const frame = parseFloat(style.paddingTop) +
39
+ parseFloat(style.paddingBottom) +
40
+ parseFloat(style.borderTopWidth) +
41
+ parseFloat(style.borderBottomWidth);
42
+ host.style.setProperty('--email-chat-input-height', `${text + frame}px`);
43
+ });
44
+ observer.observe(current.view.dom);
45
+ onCleanup(() => observer.disconnect());
46
+ });
47
+ }
48
+
49
+ /** The text as it is sent: a line per block, a list item as a dash line —
50
+ the writer's points, kept as points. */
51
+ function chatInputText(doc) {
52
+ const lines = [];
53
+ doc.descendants((node, _pos, parent) => {
54
+ if (!node.isTextblock)
55
+ return true;
56
+ const text = node.textBetween(0, node.content.size, '\n', ' ').trim();
57
+ if (text)
58
+ lines.push(parent?.type.name === 'listItem' ? `- ${text}` : text);
59
+ return false;
60
+ });
61
+ return lines.join('\n');
62
+ }
63
+ /** Says what the field is for while it is empty: a class and the words on
64
+ its one paragraph, for the stylesheet (`data-placeholder`). */
65
+ const placeholder = (text) => defineExtension({
66
+ name: 'chatInputPlaceholder',
67
+ plugins: () => [
68
+ new Plugin({
69
+ key: new PluginKey('chatInputPlaceholder'),
70
+ props: {
71
+ decorations: (state) => {
72
+ const { doc } = state;
73
+ const first = doc.firstChild;
74
+ const empty = doc.childCount === 1 && !!first?.isTextblock && first.content.size === 0;
75
+ if (!empty || !text())
76
+ return null;
77
+ return DecorationSet.create(doc, [
78
+ Decoration.node(0, first.nodeSize, {
79
+ class: 'email-chat-input__empty',
80
+ 'data-placeholder': text(),
81
+ }),
82
+ ]);
83
+ },
84
+ },
85
+ }),
86
+ ],
87
+ });
88
+ /**
89
+ * The behaviour of a chat input, on an element of the host's own: a field
90
+ * to ask an assistant with, the way a chat's message box works — Enter
91
+ * sends, Shift-Enter breaks the line — that also takes **lists**, so a list
92
+ * of points reaches the assistant as points: `- ` or `1. ` at a line's
93
+ * start begins one, Enter goes on to the next item, Enter on an empty item
94
+ * leaves the list, Tab and Shift-Tab nest and lift. Ctrl-Enter sends from
95
+ * anywhere, a list included. Nothing else: no headings, no bold — an
96
+ * assistant reads words. No look of its own: the host's element, the
97
+ * host's styles (`ChatInput` is the styled one).
98
+ *
99
+ * <div emailChatInput placeholder="Tell the assistant what to change…"
100
+ * [(value)]="instructions" (sent)="ask($event)" (escaped)="close()"></div>
101
+ *
102
+ * `value` is the text as it is sent — a line per block, list items as
103
+ * dash lines (`chatInputText`) — two-way: set it and the field shows it
104
+ * (a dash line becomes an item again). `sent` carries the same text;
105
+ * `clear()` empties the field, `focus()` puts the caret in it. A
106
+ * ProseMirror editor is mounted inside the element (the library's own,
107
+ * `.aee-editor`), so a host's editor styles reach it.
108
+ *
109
+ * **A signal-forms control, optionally.** It is a `FormValueControl` for
110
+ * `string`: bind a field with `[formField]="chat.message"` and the
111
+ * directive drives `value` both ways, pushes `disabled` and `readonly`
112
+ * in, hears `touch` when focus leaves the field, and `reset()` empties
113
+ * it. The form holds the *message*; the ask is still `sent` — an action
114
+ * with a stream behind it is the host's, not the form's submit.
115
+ */
116
+ class ChatInputField {
117
+ /** What the field is for, while it is empty. */
118
+ placeholder = input('', /* @ts-ignore */
119
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
120
+ /** The field's name for assistive technology. */
121
+ label = input('Message', /* @ts-ignore */
122
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
123
+ /** Whether typing is refused. */
124
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
125
+ /** Whether the text is shown but not edited. */
126
+ readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
127
+ /** The text, as it is sent: lines, and `- ` dash lines for list items. */
128
+ value = model('', /* @ts-ignore */
129
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
130
+ /** Enter (outside a list) or Ctrl-Enter: the text, as `value` has it.
131
+ Nothing is sent empty. The field keeps its text — the host clears it
132
+ (`clear()`) once the ask is on its way. */
133
+ sent = output();
134
+ /** Escape, in the field. */
135
+ escaped = output();
136
+ /** Focus left the field — a form marks it touched on this. */
137
+ touch = output();
138
+ #host = inject(ElementRef).nativeElement;
139
+ #editor = signal(null, /* @ts-ignore */
140
+ ...(ngDevMode ? [{ debugName: "#editor" }] : /* istanbul ignore next */ []));
141
+ /** The editor inside, once mounted — for a host that wants more of it
142
+ than the text: its selection, a command, its `view`. */
143
+ editor = this.#editor.asReadonly();
144
+ constructor() {
145
+ afterNextRender({ write: () => this.#mount() });
146
+ inject(DestroyRef).onDestroy(() => this.#editor()?.destroy());
147
+ // A value written from outside shows in the field — unless it is what
148
+ // the field just said itself, or the field is being typed in.
149
+ effect(() => {
150
+ const value = this.value();
151
+ const editor = this.#editor();
152
+ if (!editor || editor.view.hasFocus())
153
+ return;
154
+ untracked(() => {
155
+ if (chatInputText(editor.state.doc) !== value)
156
+ this.#show(value);
157
+ });
158
+ });
159
+ }
160
+ /** Empties the field. */
161
+ clear() {
162
+ this.#editor()?.setText('');
163
+ this.value.set('');
164
+ }
165
+ /** A form's reset: the field, empty. */
166
+ reset() {
167
+ this.clear();
168
+ }
169
+ focus() {
170
+ this.#editor()?.focus();
171
+ }
172
+ #mount() {
173
+ const send = (state, dispatch) => {
174
+ if (!dispatch)
175
+ return true;
176
+ this.#send();
177
+ return true;
178
+ };
179
+ const sendOutsideLists = (state, dispatch) => {
180
+ const { $from } = state.selection;
181
+ for (let depth = $from.depth; depth > 0; depth--) {
182
+ if ($from.node(depth).type.name === 'listItem')
183
+ return false;
184
+ }
185
+ return send(state, dispatch);
186
+ };
187
+ const escape = (_state, dispatch) => {
188
+ if (dispatch)
189
+ this.escaped.emit();
190
+ return true;
191
+ };
192
+ const editor = createEditor({
193
+ parent: this.#host,
194
+ extensions: [
195
+ Document,
196
+ Paragraph,
197
+ Text,
198
+ HardBreak,
199
+ // The lists carry their own keys (Enter goes on to the next item,
200
+ // Tab nests) and their own `- ` / `1. ` input rules.
201
+ BulletList,
202
+ OrderedList,
203
+ ListItem,
204
+ History,
205
+ NoTextDrag,
206
+ defineExtension({
207
+ name: 'chatInputKeys',
208
+ keymap: () => ({
209
+ Enter: sendOutsideLists,
210
+ 'Mod-Enter': send,
211
+ 'Shift-Enter': (state, dispatch) => {
212
+ const hardBreak = state.schema.nodes['hardBreak'];
213
+ dispatch?.(state.tr.replaceSelectionWith(hardBreak.create()).scrollIntoView());
214
+ return true;
215
+ },
216
+ Escape: escape,
217
+ }),
218
+ }),
219
+ BaseKeymap,
220
+ placeholder(() => this.placeholder()),
221
+ ],
222
+ attributes: {
223
+ class: 'email-chat-input__editor',
224
+ role: 'textbox',
225
+ 'aria-multiline': 'true',
226
+ 'aria-label': this.label(),
227
+ },
228
+ onUpdate: (current) => this.value.set(chatInputText(current.state.doc)),
229
+ });
230
+ editor.view.setProps({
231
+ editable: () => !this.disabled() && !this.readonly(),
232
+ handleDOMEvents: {
233
+ blur: () => {
234
+ this.touch.emit();
235
+ return false;
236
+ },
237
+ },
238
+ });
239
+ this.#editor.set(editor);
240
+ if (this.value())
241
+ this.#show(this.value());
242
+ }
243
+ /** Puts a text in the field: dash lines become list items again. */
244
+ #show(text) {
245
+ const editor = this.#editor();
246
+ if (!editor)
247
+ return;
248
+ const lines = text.split('\n');
249
+ let html = '';
250
+ let inList = false;
251
+ for (const line of lines) {
252
+ const item = /^\s*[-*]\s+(.*)$/.exec(line);
253
+ if (item) {
254
+ if (!inList)
255
+ html += '<ul>';
256
+ html += `<li>${escapeHtml(item[1])}</li>`;
257
+ inList = true;
258
+ }
259
+ else {
260
+ if (inList)
261
+ html += '</ul>';
262
+ inList = false;
263
+ html += `<p>${escapeHtml(line)}</p>`;
264
+ }
265
+ }
266
+ if (inList)
267
+ html += '</ul>';
268
+ editor.setContent(html);
269
+ }
270
+ #send() {
271
+ const editor = this.#editor();
272
+ if (!editor)
273
+ return;
274
+ const text = chatInputText(editor.state.doc);
275
+ if (!text.trim())
276
+ return;
277
+ this.sent.emit(text);
278
+ }
279
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ChatInputField, deps: [], target: i0.ɵɵFactoryTarget.Directive });
280
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: ChatInputField, isStandalone: true, selector: "[emailChatInput]", inputs: { placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", sent: "sent", escaped: "escaped", touch: "touch" }, exportAs: ["emailChatInput"], ngImport: i0 });
281
+ }
282
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ChatInputField, decorators: [{
283
+ type: Directive,
284
+ args: [{ selector: '[emailChatInput]', exportAs: 'emailChatInput' }]
285
+ }], ctorParameters: () => [], propDecorators: { placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], sent: [{ type: i0.Output, args: ["sent"] }], escaped: [{ type: i0.Output, args: ["escaped"] }], touch: [{ type: i0.Output, args: ["touch"] }] } });
286
+ /**
287
+ * The chat input, styled: `ChatInputField`'s behaviour in a box of its
288
+ * own — outlined on focus, growing and shrinking with its text in a short
289
+ * transition, scrolling past a height, disabled dimmed — the look
290
+ * tokenized (`--email-chat-input-*`, Material's system tokens beneath).
291
+ * The same inputs and outputs, forwarded; the same signal-forms contract.
292
+ *
293
+ * <div email-chat-input placeholder="Tell the assistant what to change…"
294
+ * [(value)]="instructions" (sent)="ask($event)" (escaped)="close()"></div>
295
+ *
296
+ * `field` is the directive underneath; `editor`, `clear()` and `focus()`
297
+ * are its, at hand.
298
+ *
299
+ * **The growing** (chat-input.animation.ts): the box follows the text's
300
+ * measured height through a `height` transition — `--email-chat-input-motion`
301
+ * is its duration and easing (`150ms ease`); reduced motion turns it off. The
302
+ * text grows to `--email-chat-input-max-height` (`5lh`, five lines) and
303
+ * then scrolls inside the box.
304
+ */
305
+ class ChatInput {
306
+ /** The behaviour underneath: the field itself. */
307
+ field = inject(ChatInputField);
308
+ /** The editor inside, once mounted. */
309
+ editor = this.field.editor;
310
+ constructor() {
311
+ followTextHeight(this.editor);
312
+ }
313
+ /** Empties the field. */
314
+ clear() {
315
+ this.field.clear();
316
+ }
317
+ focus() {
318
+ this.field.focus();
319
+ }
320
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ChatInput, deps: [], target: i0.ɵɵFactoryTarget.Component });
321
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.1.6", type: ChatInput, isStandalone: true, selector: "div[email-chat-input]", host: { properties: { "class.email-chat-input--disabled": "field.disabled()" } }, hostDirectives: [{ directive: ChatInputField, inputs: ["placeholder", "placeholder", "label", "label", "disabled", "disabled", "readonly", "readonly", "value", "value"], outputs: ["valueChange", "valueChange", "sent", "sent", "escaped", "escaped", "touch", "touch"] }], ngImport: i0, template: '', isInline: true, styles: [":host{display:block;box-sizing:border-box;min-width:0;height:var(--email-chat-input-height, auto);overflow:hidden;transition:height var(--email-chat-input-motion, .15s ease);padding:var(--email-chat-input-padding, 6px 10px);color:var(--email-chat-input-color, var(--mat-sys-on-surface, light-dark(#1f1f1f, #e3e3e3)));background-color:var(--email-chat-input-background, var(--mat-sys-surface-bright, light-dark(#ffffff, #2b2c2e)));border-radius:var(--email-chat-input-corner, var(--mat-sys-corner-small, 4px));font:var(--email-chat-input-font, var(--mat-sys-body-medium, inherit))}:host:focus-within{outline:2px solid var(--email-chat-input-focus, var(--mat-sys-primary, #0056b3));outline-offset:-2px}@media(prefers-reduced-motion:reduce){:host{transition:none}}:host(.email-chat-input--disabled){opacity:.6}:host ::ng-deep .email-chat-input__editor{outline:none;height:auto;max-width:none;max-height:var(--email-chat-input-max-height, 5lh);overflow-y:auto;--_email-scroll-size: var(--email-scroll-size, 6px);--_email-scroll-thumb: var( --email-scroll-thumb, color-mix(in srgb, var(--mat-sys-outline-variant, light-dark(#c4c7c5, #444746)) 50%, transparent) );--_email-scroll-thumb-hover: var( --email-scroll-thumb-hover, color-mix(in srgb, var(--mat-sys-outline-variant, light-dark(#c4c7c5, #444746)) 75%, transparent) );--_email-scroll-current: transparent;scrollbar-gutter:stable;overscroll-behavior-x:contain}@supports (-moz-appearance: none){:host ::ng-deep .email-chat-input__editor{scrollbar-width:thin;scrollbar-color:var(--_email-scroll-current) var(--email-scroll-track, transparent)}}:host ::ng-deep .email-chat-input__editor::-webkit-scrollbar{width:var(--_email-scroll-size);height:var(--_email-scroll-size)}:host ::ng-deep .email-chat-input__editor::-webkit-scrollbar-track{background:var(--email-scroll-track, transparent)}:host ::ng-deep .email-chat-input__editor::-webkit-scrollbar-corner{background:transparent}:host ::ng-deep .email-chat-input__editor::-webkit-scrollbar-thumb{background-color:var(--_email-scroll-current);border-radius:1000px}:host ::ng-deep .email-chat-input__editor:hover,:host ::ng-deep .email-chat-input__editor:focus-within{--_email-scroll-current: var(--_email-scroll-thumb)}:host ::ng-deep .email-chat-input__editor:hover::-webkit-scrollbar-thumb:hover,:host ::ng-deep .email-chat-input__editor:focus-within::-webkit-scrollbar-thumb:hover{background-color:var(--_email-scroll-thumb-hover)}@media(hover:none){:host ::ng-deep .email-chat-input__editor{--_email-scroll-current: var(--_email-scroll-thumb)}}:host ::ng-deep .email-chat-input__editor p,:host ::ng-deep .email-chat-input__editor ul,:host ::ng-deep .email-chat-input__editor ol{margin:0}:host ::ng-deep .email-chat-input__editor ul,:host ::ng-deep .email-chat-input__editor ol{padding-inline-start:20px}:host ::ng-deep .email-chat-input__editor .email-chat-input__empty:before{content:attr(data-placeholder);position:absolute;pointer-events:none;color:var(--email-chat-input-placeholder-color, var(--mat-sys-on-surface-variant, light-dark(#777777, #9a9a9a)))}\n"] });
322
+ }
323
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ChatInput, decorators: [{
324
+ type: Component,
325
+ args: [{ selector: 'div[email-chat-input]', template: '', hostDirectives: [
326
+ {
327
+ directive: ChatInputField,
328
+ inputs: ['placeholder', 'label', 'disabled', 'readonly', 'value'],
329
+ outputs: ['valueChange', 'sent', 'escaped', 'touch'],
330
+ },
331
+ ], host: { '[class.email-chat-input--disabled]': 'field.disabled()' }, styles: [":host{display:block;box-sizing:border-box;min-width:0;height:var(--email-chat-input-height, auto);overflow:hidden;transition:height var(--email-chat-input-motion, .15s ease);padding:var(--email-chat-input-padding, 6px 10px);color:var(--email-chat-input-color, var(--mat-sys-on-surface, light-dark(#1f1f1f, #e3e3e3)));background-color:var(--email-chat-input-background, var(--mat-sys-surface-bright, light-dark(#ffffff, #2b2c2e)));border-radius:var(--email-chat-input-corner, var(--mat-sys-corner-small, 4px));font:var(--email-chat-input-font, var(--mat-sys-body-medium, inherit))}:host:focus-within{outline:2px solid var(--email-chat-input-focus, var(--mat-sys-primary, #0056b3));outline-offset:-2px}@media(prefers-reduced-motion:reduce){:host{transition:none}}:host(.email-chat-input--disabled){opacity:.6}:host ::ng-deep .email-chat-input__editor{outline:none;height:auto;max-width:none;max-height:var(--email-chat-input-max-height, 5lh);overflow-y:auto;--_email-scroll-size: var(--email-scroll-size, 6px);--_email-scroll-thumb: var( --email-scroll-thumb, color-mix(in srgb, var(--mat-sys-outline-variant, light-dark(#c4c7c5, #444746)) 50%, transparent) );--_email-scroll-thumb-hover: var( --email-scroll-thumb-hover, color-mix(in srgb, var(--mat-sys-outline-variant, light-dark(#c4c7c5, #444746)) 75%, transparent) );--_email-scroll-current: transparent;scrollbar-gutter:stable;overscroll-behavior-x:contain}@supports (-moz-appearance: none){:host ::ng-deep .email-chat-input__editor{scrollbar-width:thin;scrollbar-color:var(--_email-scroll-current) var(--email-scroll-track, transparent)}}:host ::ng-deep .email-chat-input__editor::-webkit-scrollbar{width:var(--_email-scroll-size);height:var(--_email-scroll-size)}:host ::ng-deep .email-chat-input__editor::-webkit-scrollbar-track{background:var(--email-scroll-track, transparent)}:host ::ng-deep .email-chat-input__editor::-webkit-scrollbar-corner{background:transparent}:host ::ng-deep .email-chat-input__editor::-webkit-scrollbar-thumb{background-color:var(--_email-scroll-current);border-radius:1000px}:host ::ng-deep .email-chat-input__editor:hover,:host ::ng-deep .email-chat-input__editor:focus-within{--_email-scroll-current: var(--_email-scroll-thumb)}:host ::ng-deep .email-chat-input__editor:hover::-webkit-scrollbar-thumb:hover,:host ::ng-deep .email-chat-input__editor:focus-within::-webkit-scrollbar-thumb:hover{background-color:var(--_email-scroll-thumb-hover)}@media(hover:none){:host ::ng-deep .email-chat-input__editor{--_email-scroll-current: var(--_email-scroll-thumb)}}:host ::ng-deep .email-chat-input__editor p,:host ::ng-deep .email-chat-input__editor ul,:host ::ng-deep .email-chat-input__editor ol{margin:0}:host ::ng-deep .email-chat-input__editor ul,:host ::ng-deep .email-chat-input__editor ol{padding-inline-start:20px}:host ::ng-deep .email-chat-input__editor .email-chat-input__empty:before{content:attr(data-placeholder);position:absolute;pointer-events:none;color:var(--email-chat-input-placeholder-color, var(--mat-sys-on-surface-variant, light-dark(#777777, #9a9a9a)))}\n"] }]
332
+ }], ctorParameters: () => [] });
333
+ function escapeHtml(text) {
334
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
335
+ }
336
+
337
+ /*
338
+ * Public API Surface of @h-k-dev/angular-email-editor/chat-input
339
+ */
340
+
341
+ /**
342
+ * Generated bundle index. Do not edit.
343
+ */
344
+
345
+ export { ChatInput, ChatInputField, chatInputText };
346
+ //# sourceMappingURL=h-k-dev-angular-email-editor-chat-input.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"h-k-dev-angular-email-editor-chat-input.mjs","sources":["../../../projects/angular-email-editor/chat-input/chat-input.animation.ts","../../../projects/angular-email-editor/chat-input/chat-input.ts","../../../projects/angular-email-editor/chat-input/public-api.ts","../../../projects/angular-email-editor/chat-input/h-k-dev-angular-email-editor-chat-input.ts"],"sourcesContent":["import { ElementRef, Signal, effect, inject } from '@angular/core';\nimport type { Editor } from '@h-k-dev/angular-email-editor';\n\n/**\n * The chat input's motion — the part of it CSS alone cannot carry. The\n * stylesheet owns the picture (the `height` transition, its duration and\n * easing on `--email-chat-input-motion`, reduced motion turning it off);\n * this file owns the number it runs to.\n *\n * A height only transitions between lengths, never to or from `auto` — so\n * the text's own height is measured and the box's written in pixels, the\n * way ChatGPT's composer does it. A layout transition: run on the main\n * thread, smooth with GPU acceleration off.\n */\n\n/**\n * Keeps the host's `--email-chat-input-height` on the text's height plus\n * the host's padding and border, from a `ResizeObserver` on the editor.\n * Written straight to the host in the observer — before the frame paints,\n * so the transition starts with the line, not a frame behind it. The text\n * sizes itself, capped and scrolling on its own (the stylesheet), so the\n * box never scrolls: no scrollbar flashing while it catches up. Nothing on\n * the server, nor where there is no `ResizeObserver` — the box is `auto`.\n *\n * Call in an injection context, on the chat input's host.\n */\nexport function followTextHeight(editor: Signal<Editor | null>): void {\n const host = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;\n effect((onCleanup) => {\n const current = editor();\n if (!current || typeof ResizeObserver !== 'function') return;\n const observer = new ResizeObserver(([entry]) => {\n const text = entry.borderBoxSize?.[0]?.blockSize ?? entry.target.getBoundingClientRect().height;\n const style = getComputedStyle(host);\n const frame =\n parseFloat(style.paddingTop) +\n parseFloat(style.paddingBottom) +\n parseFloat(style.borderTopWidth) +\n parseFloat(style.borderBottomWidth);\n host.style.setProperty('--email-chat-input-height', `${text + frame}px`);\n });\n observer.observe(current.view.dom);\n onCleanup(() => observer.disconnect());\n });\n}\n","import {\n Component,\n DestroyRef,\n Directive,\n ElementRef,\n afterNextRender,\n booleanAttribute,\n effect,\n inject,\n input,\n model,\n output,\n signal,\n untracked,\n} from '@angular/core';\nimport type { FormValueControl } from '@angular/forms/signals';\nimport { Command, Plugin, PluginKey } from 'prosemirror-state';\nimport { Node as ProseMirrorNode } from 'prosemirror-model';\nimport { Decoration, DecorationSet } from 'prosemirror-view';\nimport {\n BaseKeymap,\n BulletList,\n Document,\n Editor,\n Extension,\n HardBreak,\n History,\n ListItem,\n NoTextDrag,\n OrderedList,\n Paragraph,\n Text,\n createEditor,\n defineExtension,\n} from '@h-k-dev/angular-email-editor';\nimport { followTextHeight } from './chat-input.animation';\n\n/** The text as it is sent: a line per block, a list item as a dash line —\n the writer's points, kept as points. */\nexport function chatInputText(doc: ProseMirrorNode): string {\n const lines: string[] = [];\n doc.descendants((node, _pos, parent) => {\n if (!node.isTextblock) return true;\n const text = node.textBetween(0, node.content.size, '\\n', ' ').trim();\n if (text) lines.push(parent?.type.name === 'listItem' ? `- ${text}` : text);\n return false;\n });\n return lines.join('\\n');\n}\n\n/** Says what the field is for while it is empty: a class and the words on\n its one paragraph, for the stylesheet (`data-placeholder`). */\nconst placeholder = (text: () => string): Extension =>\n defineExtension({\n name: 'chatInputPlaceholder',\n plugins: () => [\n new Plugin({\n key: new PluginKey('chatInputPlaceholder'),\n props: {\n decorations: (state) => {\n const { doc } = state;\n const first = doc.firstChild;\n const empty = doc.childCount === 1 && !!first?.isTextblock && first.content.size === 0;\n if (!empty || !text()) return null;\n return DecorationSet.create(doc, [\n Decoration.node(0, first!.nodeSize, {\n class: 'email-chat-input__empty',\n 'data-placeholder': text(),\n }),\n ]);\n },\n },\n }),\n ],\n });\n\n/**\n * The behaviour of a chat input, on an element of the host's own: a field\n * to ask an assistant with, the way a chat's message box works — Enter\n * sends, Shift-Enter breaks the line — that also takes **lists**, so a list\n * of points reaches the assistant as points: `- ` or `1. ` at a line's\n * start begins one, Enter goes on to the next item, Enter on an empty item\n * leaves the list, Tab and Shift-Tab nest and lift. Ctrl-Enter sends from\n * anywhere, a list included. Nothing else: no headings, no bold — an\n * assistant reads words. No look of its own: the host's element, the\n * host's styles (`ChatInput` is the styled one).\n *\n * <div emailChatInput placeholder=\"Tell the assistant what to change…\"\n * [(value)]=\"instructions\" (sent)=\"ask($event)\" (escaped)=\"close()\"></div>\n *\n * `value` is the text as it is sent — a line per block, list items as\n * dash lines (`chatInputText`) — two-way: set it and the field shows it\n * (a dash line becomes an item again). `sent` carries the same text;\n * `clear()` empties the field, `focus()` puts the caret in it. A\n * ProseMirror editor is mounted inside the element (the library's own,\n * `.aee-editor`), so a host's editor styles reach it.\n *\n * **A signal-forms control, optionally.** It is a `FormValueControl` for\n * `string`: bind a field with `[formField]=\"chat.message\"` and the\n * directive drives `value` both ways, pushes `disabled` and `readonly`\n * in, hears `touch` when focus leaves the field, and `reset()` empties\n * it. The form holds the *message*; the ask is still `sent` — an action\n * with a stream behind it is the host's, not the form's submit.\n */\n@Directive({ selector: '[emailChatInput]', exportAs: 'emailChatInput' })\nexport class ChatInputField implements FormValueControl<string> {\n /** What the field is for, while it is empty. */\n readonly placeholder = input('');\n\n /** The field's name for assistive technology. */\n readonly label = input('Message');\n\n /** Whether typing is refused. */\n readonly disabled = input(false, { transform: booleanAttribute });\n\n /** Whether the text is shown but not edited. */\n readonly readonly = input(false, { transform: booleanAttribute });\n\n /** The text, as it is sent: lines, and `- ` dash lines for list items. */\n readonly value = model('');\n\n /** Enter (outside a list) or Ctrl-Enter: the text, as `value` has it.\n Nothing is sent empty. The field keeps its text — the host clears it\n (`clear()`) once the ask is on its way. */\n readonly sent = output<string>();\n\n /** Escape, in the field. */\n readonly escaped = output<void>();\n\n /** Focus left the field — a form marks it touched on this. */\n readonly touch = output<void>();\n\n readonly #host = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;\n\n readonly #editor = signal<Editor | null>(null);\n\n /** The editor inside, once mounted — for a host that wants more of it\n than the text: its selection, a command, its `view`. */\n readonly editor = this.#editor.asReadonly();\n\n constructor() {\n afterNextRender({ write: () => this.#mount() });\n inject(DestroyRef).onDestroy(() => this.#editor()?.destroy());\n // A value written from outside shows in the field — unless it is what\n // the field just said itself, or the field is being typed in.\n effect(() => {\n const value = this.value();\n const editor = this.#editor();\n if (!editor || editor.view.hasFocus()) return;\n untracked(() => {\n if (chatInputText(editor.state.doc) !== value) this.#show(value);\n });\n });\n }\n\n /** Empties the field. */\n clear(): void {\n this.#editor()?.setText('');\n this.value.set('');\n }\n\n /** A form's reset: the field, empty. */\n reset(): void {\n this.clear();\n }\n\n focus(): void {\n this.#editor()?.focus();\n }\n\n #mount(): void {\n const send: Command = (state, dispatch) => {\n if (!dispatch) return true;\n this.#send();\n return true;\n };\n const sendOutsideLists: Command = (state, dispatch) => {\n const { $from } = state.selection;\n for (let depth = $from.depth; depth > 0; depth--) {\n if ($from.node(depth).type.name === 'listItem') return false;\n }\n return send(state, dispatch);\n };\n const escape: Command = (_state, dispatch) => {\n if (dispatch) this.escaped.emit();\n return true;\n };\n const editor = createEditor({\n parent: this.#host,\n extensions: [\n Document,\n Paragraph,\n Text,\n HardBreak,\n // The lists carry their own keys (Enter goes on to the next item,\n // Tab nests) and their own `- ` / `1. ` input rules.\n BulletList,\n OrderedList,\n ListItem,\n History,\n NoTextDrag,\n defineExtension({\n name: 'chatInputKeys',\n keymap: () => ({\n Enter: sendOutsideLists,\n 'Mod-Enter': send,\n 'Shift-Enter': (state, dispatch) => {\n const hardBreak = state.schema.nodes['hardBreak'];\n dispatch?.(state.tr.replaceSelectionWith(hardBreak.create()).scrollIntoView());\n return true;\n },\n Escape: escape,\n }),\n }),\n BaseKeymap,\n placeholder(() => this.placeholder()),\n ],\n attributes: {\n class: 'email-chat-input__editor',\n role: 'textbox',\n 'aria-multiline': 'true',\n 'aria-label': this.label(),\n },\n onUpdate: (current) => this.value.set(chatInputText(current.state.doc)),\n });\n editor.view.setProps({\n editable: () => !this.disabled() && !this.readonly(),\n handleDOMEvents: {\n blur: () => {\n this.touch.emit();\n return false;\n },\n },\n });\n this.#editor.set(editor);\n if (this.value()) this.#show(this.value());\n }\n\n /** Puts a text in the field: dash lines become list items again. */\n #show(text: string): void {\n const editor = this.#editor();\n if (!editor) return;\n const lines = text.split('\\n');\n let html = '';\n let inList = false;\n for (const line of lines) {\n const item = /^\\s*[-*]\\s+(.*)$/.exec(line);\n if (item) {\n if (!inList) html += '<ul>';\n html += `<li>${escapeHtml(item[1])}</li>`;\n inList = true;\n } else {\n if (inList) html += '</ul>';\n inList = false;\n html += `<p>${escapeHtml(line)}</p>`;\n }\n }\n if (inList) html += '</ul>';\n editor.setContent(html);\n }\n\n #send(): void {\n const editor = this.#editor();\n if (!editor) return;\n const text = chatInputText(editor.state.doc);\n if (!text.trim()) return;\n this.sent.emit(text);\n }\n}\n\n/**\n * The chat input, styled: `ChatInputField`'s behaviour in a box of its\n * own — outlined on focus, growing and shrinking with its text in a short\n * transition, scrolling past a height, disabled dimmed — the look\n * tokenized (`--email-chat-input-*`, Material's system tokens beneath).\n * The same inputs and outputs, forwarded; the same signal-forms contract.\n *\n * <div email-chat-input placeholder=\"Tell the assistant what to change…\"\n * [(value)]=\"instructions\" (sent)=\"ask($event)\" (escaped)=\"close()\"></div>\n *\n * `field` is the directive underneath; `editor`, `clear()` and `focus()`\n * are its, at hand.\n *\n * **The growing** (chat-input.animation.ts): the box follows the text's\n * measured height through a `height` transition — `--email-chat-input-motion`\n * is its duration and easing (`150ms ease`); reduced motion turns it off. The\n * text grows to `--email-chat-input-max-height` (`5lh`, five lines) and\n * then scrolls inside the box.\n */\n@Component({\n selector: 'div[email-chat-input]',\n template: '',\n styleUrl: './chat-input.scss',\n hostDirectives: [\n {\n directive: ChatInputField,\n inputs: ['placeholder', 'label', 'disabled', 'readonly', 'value'],\n outputs: ['valueChange', 'sent', 'escaped', 'touch'],\n },\n ],\n host: { '[class.email-chat-input--disabled]': 'field.disabled()' },\n})\nexport class ChatInput {\n /** The behaviour underneath: the field itself. */\n readonly field = inject(ChatInputField);\n\n /** The editor inside, once mounted. */\n readonly editor = this.field.editor;\n\n constructor() {\n followTextHeight(this.editor);\n }\n\n /** Empties the field. */\n clear(): void {\n this.field.clear();\n }\n\n focus(): void {\n this.field.focus();\n }\n}\n\nfunction escapeHtml(text: string): string {\n return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n}\n","/*\n * Public API Surface of @h-k-dev/angular-email-editor/chat-input\n */\n\nexport * from './chat-input';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, booleanAttribute, inject, ElementRef, Directive } from '@angular/core';
3
+
4
+ /** Where a press may still take focus by default: real text entry. A field
5
+ inside a toolbar has to be typed in; everything else there is a tool. */
6
+ const KEEP_FOCUS_EXCEPT = 'input, textarea, select, [contenteditable]:not([contenteditable="false"])';
7
+ /**
8
+ * A press on this element never takes focus from wherever it is — the
9
+ * editor keeps its caret and its selection while a toolbar button, a menu
10
+ * row or a swatch is clicked, and whatever depends on the editor's focus (a
11
+ * bubble menu, a suggestion list) stays open through the click.
12
+ *
13
+ * Behaviour only: it renders nothing and sets no attribute, so it goes on a
14
+ * host's own elements — a Material button, an Aria toolbar, a CDK overlay's
15
+ * panel — or into a host's own component through `hostDirectives`:
16
+ *
17
+ * <div role="toolbar" emailKeepFocus> … <button mat-icon-button>…</button> … </div>
18
+ *
19
+ * @Component({ …, hostDirectives: [KeepFocus] })
20
+ *
21
+ * One on a container covers everything in it: the press bubbles. Only the
22
+ * pointer is kept out — Tab and the arrow keys still reach the tools, which
23
+ * is what makes them usable from the keyboard — and only the press: the
24
+ * click that follows runs as ever.
25
+ *
26
+ * It needs no editor: what keeps focus is whichever element has it, so the
27
+ * same directive serves a composer's address rows.
28
+ *
29
+ * A press on something marked `draggable="true"` — an address chip that
30
+ * can be dragged to another row — is always left alone, exceptions or not:
31
+ * no engine starts a drag from a press whose default was declined. Such an
32
+ * element keeps focus in its own way (the address input parks it on its
33
+ * chip list).
34
+ */
35
+ class KeepFocus {
36
+ /** Off with `[emailKeepFocus]="false"`; on by its presence. */
37
+ enabled = input(true, { ...(ngDevMode ? { debugName: "enabled" } : /* istanbul ignore next */ {}), alias: 'emailKeepFocus', transform: booleanAttribute });
38
+ /** What inside this element may still take focus on a press — a selector.
39
+ Text entry by default ({@link KEEP_FOCUS_EXCEPT}); `''` for nothing. */
40
+ except = input(KEEP_FOCUS_EXCEPT, { ...(ngDevMode ? { debugName: "except" } : /* istanbul ignore next */ {}), alias: 'emailKeepFocusExcept' });
41
+ #host = inject(ElementRef).nativeElement;
42
+ onMousedown(event) {
43
+ if (!this.enabled())
44
+ return;
45
+ const except = this.except();
46
+ const target = event.target;
47
+ // A drag starts only from a press that keeps its default.
48
+ const draggable = target?.closest?.('[draggable="true"]');
49
+ if (draggable && this.#host.contains(draggable))
50
+ return;
51
+ // An exception counts inside this element only: standing in an editor's
52
+ // contenteditable must not make every press here an exception.
53
+ const excepted = except ? target?.closest?.(except) : null;
54
+ if (excepted && this.#host.contains(excepted))
55
+ return;
56
+ event.preventDefault();
57
+ }
58
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: KeepFocus, deps: [], target: i0.ɵɵFactoryTarget.Directive });
59
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: KeepFocus, isStandalone: true, selector: "[emailKeepFocus]", inputs: { enabled: { classPropertyName: "enabled", publicName: "emailKeepFocus", isSignal: true, isRequired: false, transformFunction: null }, except: { classPropertyName: "except", publicName: "emailKeepFocusExcept", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "mousedown": "onMousedown($event)" } }, ngImport: i0 });
60
+ }
61
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: KeepFocus, decorators: [{
62
+ type: Directive,
63
+ args: [{
64
+ selector: '[emailKeepFocus]',
65
+ host: { '(mousedown)': 'onMousedown($event)' },
66
+ }]
67
+ }], propDecorators: { enabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "emailKeepFocus", required: false }] }], except: [{ type: i0.Input, args: [{ isSignal: true, alias: "emailKeepFocusExcept", required: false }] }] } });
68
+
69
+ /*
70
+ * Public API Surface of @h-k-dev/angular-email-editor/focus
71
+ */
72
+
73
+ /**
74
+ * Generated bundle index. Do not edit.
75
+ */
76
+
77
+ export { KEEP_FOCUS_EXCEPT, KeepFocus };
78
+ //# sourceMappingURL=h-k-dev-angular-email-editor-focus.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"h-k-dev-angular-email-editor-focus.mjs","sources":["../../../projects/angular-email-editor/focus/keep-focus.ts","../../../projects/angular-email-editor/focus/public-api.ts","../../../projects/angular-email-editor/focus/h-k-dev-angular-email-editor-focus.ts"],"sourcesContent":["import { Directive, ElementRef, booleanAttribute, inject, input } from '@angular/core';\n\n/** Where a press may still take focus by default: real text entry. A field\n inside a toolbar has to be typed in; everything else there is a tool. */\nexport const KEEP_FOCUS_EXCEPT =\n 'input, textarea, select, [contenteditable]:not([contenteditable=\"false\"])';\n\n/**\n * A press on this element never takes focus from wherever it is — the\n * editor keeps its caret and its selection while a toolbar button, a menu\n * row or a swatch is clicked, and whatever depends on the editor's focus (a\n * bubble menu, a suggestion list) stays open through the click.\n *\n * Behaviour only: it renders nothing and sets no attribute, so it goes on a\n * host's own elements — a Material button, an Aria toolbar, a CDK overlay's\n * panel — or into a host's own component through `hostDirectives`:\n *\n * <div role=\"toolbar\" emailKeepFocus> … <button mat-icon-button>…</button> … </div>\n *\n * @Component({ …, hostDirectives: [KeepFocus] })\n *\n * One on a container covers everything in it: the press bubbles. Only the\n * pointer is kept out — Tab and the arrow keys still reach the tools, which\n * is what makes them usable from the keyboard — and only the press: the\n * click that follows runs as ever.\n *\n * It needs no editor: what keeps focus is whichever element has it, so the\n * same directive serves a composer's address rows.\n *\n * A press on something marked `draggable=\"true\"` — an address chip that\n * can be dragged to another row — is always left alone, exceptions or not:\n * no engine starts a drag from a press whose default was declined. Such an\n * element keeps focus in its own way (the address input parks it on its\n * chip list).\n */\n@Directive({\n selector: '[emailKeepFocus]',\n host: { '(mousedown)': 'onMousedown($event)' },\n})\nexport class KeepFocus {\n /** Off with `[emailKeepFocus]=\"false\"`; on by its presence. */\n readonly enabled = input(true, { alias: 'emailKeepFocus', transform: booleanAttribute });\n\n /** What inside this element may still take focus on a press — a selector.\n Text entry by default ({@link KEEP_FOCUS_EXCEPT}); `''` for nothing. */\n readonly except = input(KEEP_FOCUS_EXCEPT, { alias: 'emailKeepFocusExcept' });\n\n readonly #host = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;\n\n protected onMousedown(event: MouseEvent): void {\n if (!this.enabled()) return;\n const except = this.except();\n const target = event.target as Element | null;\n // A drag starts only from a press that keeps its default.\n const draggable = target?.closest?.('[draggable=\"true\"]');\n if (draggable && this.#host.contains(draggable)) return;\n // An exception counts inside this element only: standing in an editor's\n // contenteditable must not make every press here an exception.\n const excepted = except ? target?.closest?.(except) : null;\n if (excepted && this.#host.contains(excepted)) return;\n event.preventDefault();\n }\n}\n","/*\n * Public API Surface of @h-k-dev/angular-email-editor/focus\n */\n\nexport * from './keep-focus';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;;AAEA;AAC4E;AACrE,MAAM,iBAAiB,GAC5B;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BG;MAKU,SAAS,CAAA;;AAEX,IAAA,OAAO,GAAG,KAAK,CAAC,IAAI,EAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,SAAA,EAAA,8BAAA,EAAA,CAAA,EAAI,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,gBAAgB,GAAG;AAExF;AAC2E;IAClE,MAAM,GAAG,KAAK,CAAC,iBAAiB,8EAAI,KAAK,EAAE,sBAAsB,EAAA,CAAG;AAEpE,IAAA,KAAK,GAAG,MAAM,CAA0B,UAAU,CAAC,CAAC,aAAa;AAEhE,IAAA,WAAW,CAAC,KAAiB,EAAA;AACrC,QAAA,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE;YAAE;AACrB,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE;AAC5B,QAAA,MAAM,MAAM,GAAG,KAAK,CAAC,MAAwB;;QAE7C,MAAM,SAAS,GAAG,MAAM,EAAE,OAAO,GAAG,oBAAoB,CAAC;QACzD,IAAI,SAAS,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC;YAAE;;;AAGjD,QAAA,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC,GAAG,IAAI;QAC1D,IAAI,QAAQ,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAAE;QAC/C,KAAK,CAAC,cAAc,EAAE;IACxB;uGAtBW,SAAS,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;2FAAT,SAAS,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,kBAAA,EAAA,MAAA,EAAA,EAAA,OAAA,EAAA,EAAA,iBAAA,EAAA,SAAA,EAAA,UAAA,EAAA,gBAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,MAAA,EAAA,EAAA,iBAAA,EAAA,QAAA,EAAA,UAAA,EAAA,sBAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,IAAA,EAAA,EAAA,SAAA,EAAA,EAAA,WAAA,EAAA,qBAAA,EAAA,EAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA;;2FAAT,SAAS,EAAA,UAAA,EAAA,CAAA;kBAJrB,SAAS;AAAC,YAAA,IAAA,EAAA,CAAA;AACT,oBAAA,QAAQ,EAAE,kBAAkB;AAC5B,oBAAA,IAAI,EAAE,EAAE,aAAa,EAAE,qBAAqB,EAAE;AAC/C,iBAAA;;;ACtCD;;AAEG;;ACFH;;AAEG;;;;"}