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

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 +285 -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 +686 -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 +68 -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 +18818 -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 +193 -0
  38. package/types/h-k-dev-angular-email-editor-address-input.d.ts +328 -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 +40 -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,328 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { TemplateRef, ElementRef } from '@angular/core';
3
+ import { FormValueControl, PathKind, SchemaPath, SchemaPathRules } from '@angular/forms/signals';
4
+ import { AddressChipRemove, AddressChip } from '@h-k-dev/angular-email-editor/address-chip';
5
+ import { AddressRules } from '@h-k-dev/angular-email-editor/address';
6
+
7
+ /**
8
+ * The address input's label slot — opt in. By default the control owns no
9
+ * label: the template that holds the form draws the row and the label beside
10
+ * it, as it does for a plain `<input>`. A host that wants the label *inside*
11
+ * the control — at the start of the chips, the chips wrapping under it, the
12
+ * whole control one box (a Material form field around it, as iusta core
13
+ * writes its rows) — hands the control a template:
14
+ *
15
+ * <div email-address-input [formField]="envelope.to">
16
+ * <ng-template emailAddressLabel>To</ng-template>
17
+ * </div>
18
+ *
19
+ * The control renders it first in its row, in a box of its own
20
+ * (`[data-slot=label]`), and names its text input by it — unless the host
21
+ * named the input itself (`aria-labelledby` / `aria-label`).
22
+ */
23
+ declare class AddressInputLabel {
24
+ readonly template: TemplateRef<any>;
25
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<AddressInputLabel, never>;
26
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<AddressInputLabel, "ng-template[emailAddressLabel]", never, {}, {}, never, never, true, never>;
27
+ }
28
+
29
+ /**
30
+ * An address field the way Gmail's works: committed addresses as chips,
31
+ * free typing after them. Enter, comma, semicolon or a paste commits what
32
+ * was typed — a comma or semicolon inside a quoted name or angle brackets is
33
+ * just typed, and no key commits while an IME is composing; Backspace on the empty input takes the chip before the caret
34
+ * back for editing; `limit` caps the count (From takes one) and hides the
35
+ * input once reached.
36
+ *
37
+ * **Only an address becomes a chip while typing.** Enter, comma, semicolon,
38
+ * Tab and a paste commit the addresses in what was typed and leave the rest —
39
+ * what is not an address yet — in the input, flagged, for the user to
40
+ * finish. Leaving the field commits everything, so the form sees the typo
41
+ * and flags it; and focus coming back hands the malformed entries at the
42
+ * end of the list straight back to the input, as plain typing again.
43
+ *
44
+ * **The input is always at the end, and editing happens there.** A chip
45
+ * taken back for editing — a double click, Enter on a picked chip,
46
+ * Backspace, `edit()` — leaves its place: its address goes into the input,
47
+ * after the last chip, exactly like a typo the field handed back — and what
48
+ * is committed from it joins the end of the list. There is one place where
49
+ * text is typed, and it never moves.
50
+ *
51
+ * **One pending edit at a time.** There is only ever one thing being fixed,
52
+ * and it is what the caret is on. Starting another edit — `edit()`, Enter
53
+ * on a picked chip, Backspace taking the chip before the caret back — ends
54
+ * the one pending: a typo left in the input and any chip that is not an
55
+ * address go.
56
+ *
57
+ * **Chips while focused, a line of text otherwise.** With focus elsewhere
58
+ * the addresses read as a comma-separated line — no surface, no buttons;
59
+ * focus in the input turns them into chips.
60
+ *
61
+ * **The arrow keys rove the chips; the caret stays in the input.** With
62
+ * the caret at the start of the typing, ← puts a highlight on the last chip
63
+ * and walks back along the list; → walks forward, and past the last chip
64
+ * returns to the caret. Enter or F2 — or a double click on
65
+ * any chip — edits it,
66
+ * Backspace or Delete removes it, Escape drops the highlight, and so does
67
+ * typing. A click on a chip highlights it. Reaching a chip either way is
68
+ * being done with the typing: an address in the input becomes a chip first,
69
+ * as Enter would make it. The highlight is
70
+ * `aria-activedescendant` on the input and `aria-current` on the chip, and the
71
+ * chips' remove buttons stay out of the tab order — Tab lands on the input —
72
+ * except when the field is full and has no input to land on.
73
+ *
74
+ * A display name is kept — `Ada Lovelace <ada@example.com>` is one chip
75
+ * that shows "Ada Lovelace" — and the value stays `string[]` in header
76
+ * form, ready for a `To:` line. Typed bare, `Ada Lovelace ada@example.com`
77
+ * becomes that same chip. An address is in the list once: another entry
78
+ * with the same address — in any case, under any name — is dropped.
79
+ *
80
+ * **The control, not the row.** By default it owns no label and draws no row: the
81
+ * template that holds the form puts it in a row of its own making, beside a
82
+ * label of its own making, exactly as it does for a plain `<input>` — and
83
+ * names the control through `aria-labelledby` (the label's id) or
84
+ * `aria-label`, which land on the text input inside, the element assistive
85
+ * tech reaches. Clicking anywhere on the control puts the caret in; a row
86
+ * that wants its label to do the same calls `focus()`.
87
+ *
88
+ * **A signal-forms control, optionally.** It implements `FormValueControl`
89
+ * for `string[]`: bind a field with `[formField]="envelope.to"` and the
90
+ * directive drives `value` both ways, pushes `invalid`, `touched`,
91
+ * `disabled`, `readonly`, `required` and `name` in, and hears `touch` when
92
+ * focus leaves the input. Without a form, `[(value)]` alone is the whole
93
+ * contract; the state inputs are optional and the chips still flag a
94
+ * malformed address on their own. The rules that go with it — `addressList`
95
+ * — live beside it, because the built-in `required` and `email` know
96
+ * nothing of an array of mailboxes.
97
+ *
98
+ * **No messages of its own.** The control reflects the field's state —
99
+ * invalid *and* touched, so nothing shouts before the user has left it —
100
+ * and a malformed address is flagged on its chip; wording what is wrong is
101
+ * the job of the component that owns the form, which has the errors.
102
+ *
103
+ * **Parts are `data-slot`s** — `chips`, `entry`, `input` — plus the chips'
104
+ * own; that is how the control is styled from outside: theme it through
105
+ * the `--email-*` tokens, and reach a part for a structural override with
106
+ * two attributes.
107
+ *
108
+ * **The selection and its commands are public.** A host's own controls —
109
+ * a bar over the writer, the way a phone's action bar sits over the app
110
+ * bar — work the picked chip through `current()` and `select()` for the
111
+ * state and `edit()`, `remove()` and `copy()` for the commands, each on the
112
+ * current chip unless given another. Such a bar's buttons must decline
113
+ * their mousedown (`(mousedown)="$event.preventDefault()"`): focus stays in
114
+ * the input, and the selection with it — focus leaving the control clears
115
+ * the selection.
116
+ *
117
+ * **The chips' remove control is a template slot.** An
118
+ * `ng-template[emailAddressChipRemove]` in the input's content is handed to
119
+ * every chip it renders, in place of the default × button.
120
+ *
121
+ * **State on the host.** `aria-disabled` while disabled; `data-invalid`
122
+ * while invalid and touched; `data-full` once `limit` is reached;
123
+ * `data-focused` while focus is anywhere in the control. The text input
124
+ * carries `data-refused` while it holds text a commit left behind.
125
+ *
126
+ * No Material, no icon font, no headless library. It holds the list it
127
+ * shows and nothing else: contacts, lookups and sending are the host's.
128
+ */
129
+ declare class AddressInput implements FormValueControl<string[]> {
130
+ #private;
131
+ /** The committed addresses, in header form. Two-way; the form's value. */
132
+ readonly value: _angular_core.ModelSignal<string[]>;
133
+ readonly placeholder: _angular_core.InputSignal<string>;
134
+ /** The id of the row's label, passed through to the text input — the
135
+ host's label, wherever the host put it. */
136
+ readonly ariaLabelledby: _angular_core.InputSignal<string | null>;
137
+ /** A name for the text input when there is no visible label. */
138
+ readonly ariaLabel: _angular_core.InputSignal<string | null>;
139
+ /** The ids of whatever describes the field — a hint, an error line —
140
+ passed through to the text input, where assistive tech reads them. */
141
+ readonly ariaDescribedby: _angular_core.InputSignal<string | null>;
142
+ /** What the text input's `aria-describedby` says: the host's own ids and a
143
+ frame's, each once. */
144
+ protected readonly describedBy: _angular_core.Signal<string | null>;
145
+ /** Adds the ids of a frame's own lines — a form field's hint and error —
146
+ to what describes the text input, beside the host's `aria-describedby`;
147
+ each call replaces the frame's previous ids. */
148
+ describe(ids: readonly string[]): void;
149
+ /** Most addresses the field takes; the input hides once reached. Named
150
+ `limit`, not `max`: the form contract keeps that name for the `max` rule. */
151
+ readonly limit: _angular_core.InputSignalWithTransform<number, unknown>;
152
+ /** Prefix for each chip's remove button — "Remove ada@example.com". */
153
+ readonly removeLabel: _angular_core.InputSignal<string>;
154
+ /** What a malformed address is flagged as. */
155
+ readonly invalidLabel: _angular_core.InputSignal<string>;
156
+ /** The chips' note on a placeholder a template fills in later. */
157
+ readonly pendingLabel: _angular_core.InputSignal<string>;
158
+ /** The input's `name` attribute; the directive derives one from the path. */
159
+ readonly name: _angular_core.InputSignal<string>;
160
+ readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
161
+ readonly readonly: _angular_core.InputSignalWithTransform<boolean, unknown>;
162
+ readonly required: _angular_core.InputSignalWithTransform<boolean, unknown>;
163
+ readonly invalid: _angular_core.InputSignalWithTransform<boolean, unknown>;
164
+ readonly touched: _angular_core.InputSignalWithTransform<boolean, unknown>;
165
+ /** Focus left the input. The directive marks the field touched on it. */
166
+ readonly touch: _angular_core.OutputEmitterRef<void>;
167
+ /** The text input's id, for a host label's `for`. */
168
+ readonly id: string;
169
+ /** The host's label template, if it opted into one — rendered first in
170
+ the row; the text input is named by it. */
171
+ protected readonly labelSlot: _angular_core.Signal<AddressInputLabel | undefined>;
172
+ /** The label box's id, for the text input's `aria-labelledby`. */
173
+ protected readonly labelId: string;
174
+ /** What names the text input: the host's own `aria-labelledby`; else the
175
+ label slot — unless the host gave an `aria-label` instead. */
176
+ protected readonly labelledBy: _angular_core.Signal<string | null>;
177
+ /** The host's remove template, passed on to every chip. */
178
+ protected readonly removeSlot: _angular_core.Signal<AddressChipRemove | undefined>;
179
+ protected readonly field: _angular_core.Signal<ElementRef<HTMLInputElement>>;
180
+ /** Focus is somewhere in the control — the input or a chip's remove
181
+ button. The chips are chips only then. Read-only; for a host's own
182
+ frame (a form field whose label floats on it). */
183
+ readonly focused: _angular_core.Signal<boolean>;
184
+ /** What is typed in the input and not committed yet — for a host's own
185
+ frame, where a field with typing in it is not empty. */
186
+ readonly text: _angular_core.Signal<string>;
187
+ /** The input holds text a commit left behind: not an address yet. */
188
+ protected readonly refused: _angular_core.WritableSignal<boolean>;
189
+ /** The chip the arrow keys or a click picked, by index in the value — the
190
+ one Enter, Backspace and Delete act on while the caret stays in the
191
+ input. */
192
+ protected readonly active: _angular_core.WritableSignal<number | null>;
193
+ /** The picked chip's address — the one the arrow keys or a click reached
194
+ and the commands act on — or null. Public, for a host's own controls. */
195
+ readonly current: _angular_core.Signal<string | null>;
196
+ /** Picks a chip by address, or none. */
197
+ select(address: string | null): void;
198
+ /** The chips as rendered, for the commands that are the chip's own. */
199
+ protected readonly chips: _angular_core.Signal<readonly AddressChip[]>;
200
+ /** The active chip's id, for the input's `aria-activedescendant`. */
201
+ protected readonly activeId: _angular_core.Signal<string | null>;
202
+ protected chipId(index: number): string;
203
+ /** The cap is reached: a further address is refused. */
204
+ readonly full: _angular_core.Signal<boolean>;
205
+ /** Full, and nothing in it to fix: the input hides — the chips say
206
+ everything. A full list ending in a typo keeps it, so focus can hand
207
+ the typo back. */
208
+ protected readonly closed: _angular_core.Signal<boolean>;
209
+ /** Nothing may change: disabled or read-only. Chips lose their remove
210
+ button, the input is out of play. */
211
+ protected readonly locked: _angular_core.Signal<boolean>;
212
+ /** Invalid *and* touched — the moment the state becomes the user's business. */
213
+ protected readonly flagged: _angular_core.Signal<boolean>;
214
+ /** Splits a run into addresses and adds the new ones to the end of the
215
+ list, up to `limit`. Duplicates — by address, case-insensitively —
216
+ are dropped; a malformed one is kept and
217
+ flagged — this is the host's way in, and the host decides what it
218
+ holds. */
219
+ commit(raw: string): void;
220
+ /** Drops one address — the picked chip's by default. Identity by string:
221
+ the list never holds the same header form twice, so this takes exactly
222
+ the chip that asked. */
223
+ remove(address?: string | null): void;
224
+ /** A chip's remove control was used: the address goes, and the caret
225
+ comes back to the input — the focused button went with its chip, and
226
+ focus left on nothing would strand a keyboard user and read to a host
227
+ as the field being left. */
228
+ protected removeChip(address: string): void;
229
+ /** Takes a chip — the picked one by default — back for editing: it
230
+ leaves the list and the input, at the end, holds its address, caret at
231
+ the end. The edit pending until now ends first. A locked list edits
232
+ nothing. */
233
+ edit(address?: string | null, input?: HTMLInputElement): void;
234
+ /** Puts the caret in the input — the whole control is the field, and a
235
+ host's label row may forward its own clicks here. A full field has no
236
+ input to put it in: focus goes to the last chip's remove control. */
237
+ focus(): void;
238
+ /** A press anywhere on the control is the input's from that moment: it
239
+ takes focus at the press, not at the click after the release — so a
240
+ chip pressed in a line of text turns the line to chips at once, and a
241
+ hold on a chip runs with the document focused, which the clipboard
242
+ insists on. `keepCaret` then declines the press's own default, which
243
+ would move focus off again. */
244
+ protected onPointerdown(event: PointerEvent): void;
245
+ /** A press anywhere on the control but the text input itself — a chip,
246
+ its button, the gap between — leaves the caret where it is. The
247
+ browser would otherwise move focus to the body for the press and back
248
+ on the click, and in that instant the row would drop to text and
249
+ reflow: a flash, and a blur that commits and touches for nothing. */
250
+ protected keepCaret(event: MouseEvent): void;
251
+ /** A click on a chip — not on its remove control — picks it, and only
252
+ that: a click on the picked chip picks it again. The caret is in the
253
+ input either way. */
254
+ protected onChipClick(event: MouseEvent, index: number, input: HTMLInputElement): void;
255
+ /** A double click on a chip — not on its remove control — takes it back
256
+ for editing. */
257
+ protected onChipDblclick(event: MouseEvent, address: string, input: HTMLInputElement): void;
258
+ /** Copies a chip's address — the picked one by default — to the
259
+ clipboard, as a hold on the chip does; the chip shows and says so.
260
+ Resolves to whether the copy went. */
261
+ copy(address?: string | null): Promise<boolean>;
262
+ protected onFocusin(): void;
263
+ protected onInput(input: HTMLInputElement): void;
264
+ protected onKeydown(event: KeyboardEvent, input: HTMLInputElement): void;
265
+ /** A paste of several addresses: the addresses become chips, the rest
266
+ joins what the input already holds. A single token is left to the
267
+ input, so a name with spaces can still be edited before it commits. */
268
+ protected onPaste(event: ClipboardEvent, input: HTMLInputElement): void;
269
+ /** Focus arrived in the input: the typos at the end of the list come back
270
+ into it, to be fixed — unless the user is already typing there. They
271
+ come back as plain typing, not flagged: the caret is on them, and the
272
+ next commit judges them again. */
273
+ protected onFocus(input: HTMLInputElement): void;
274
+ /** Leaving the field commits everything typed — the addresses, then a
275
+ typo at the end of the list, so the form sees it and the next focus
276
+ hands it back — and is the touch. Moving to a chip's remove button is
277
+ not leaving. */
278
+ protected onBlur(event: FocusEvent, input: HTMLInputElement): void;
279
+ protected onFocusout(event: FocusEvent): void;
280
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<AddressInput, never>;
281
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<AddressInput, "[email-address-input]", never, { "value": { "alias": "value"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "ariaLabelledby": { "alias": "aria-labelledby"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "aria-label"; "required": false; "isSignal": true; }; "ariaDescribedby": { "alias": "aria-describedby"; "required": false; "isSignal": true; }; "limit": { "alias": "limit"; "required": false; "isSignal": true; }; "removeLabel": { "alias": "removeLabel"; "required": false; "isSignal": true; }; "invalidLabel": { "alias": "invalidLabel"; "required": false; "isSignal": true; }; "pendingLabel": { "alias": "pendingLabel"; "required": false; "isSignal": true; }; "name": { "alias": "name"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "readonly": { "alias": "readonly"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "invalid": { "alias": "invalid"; "required": false; "isSignal": true; }; "touched": { "alias": "touched"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "touch": "touch"; }, ["labelSlot", "removeSlot"], never, true, never>;
282
+ }
283
+
284
+ /** The error kinds `addressList` reports, for a host that switches on them. */
285
+ declare const ADDRESS_LIST_EMPTY = "addressList.empty";
286
+ declare const ADDRESS_LIST_INVALID = "addressList.invalid";
287
+ declare const ADDRESS_LIST_TOO_MANY = "addressList.tooMany";
288
+ interface AddressListOptions {
289
+ /** Fewest addresses the list must hold. 1 by default; 0 for Cc and Bcc. */
290
+ readonly min?: number;
291
+ /** Most addresses the list may hold. Unbounded by default; 1 for From. */
292
+ readonly max?: number;
293
+ /** The address rule to judge by. By default the one in force where the
294
+ form is made (`provideAddressRules`), else ours. */
295
+ readonly rules?: AddressRules;
296
+ /** The messages, for a host that words them itself or translates them. */
297
+ readonly messages?: {
298
+ readonly empty?: (min: number) => string;
299
+ readonly invalid?: (offenders: readonly string[]) => string;
300
+ readonly tooMany?: (max: number) => string;
301
+ };
302
+ }
303
+ /**
304
+ * The rule an address field needs and the built-ins cannot give it:
305
+ * `required` takes an empty array for a value, and `email` validates one
306
+ * string. This validates a `string[]` of mailboxes in header form — the
307
+ * address input's value — as a list: at least `min` of them (one, unless
308
+ * told otherwise), at most `max`, and every one a well-formed address, with
309
+ * the offenders named in the message so a chip can be found and fixed. A
310
+ * placeholder a template fills in later (`{{contact.email}}`, by the rule's
311
+ * `isPlaceholder`) is no offender — it counts toward `min` and `max`;
312
+ * whether it may still be sent unresolved is the host's call.
313
+ *
314
+ * ```ts
315
+ * const envelope = form(model, (p) => {
316
+ * addressList(p.from, { max: 1 });
317
+ * addressList(p.to);
318
+ * addressList(p.cc, { min: 0 });
319
+ * });
320
+ * ```
321
+ *
322
+ * Errors carry a `kind` from the `ADDRESS_LIST_*` constants and a `message`
323
+ * the input shows; pass `messages` to word or translate them.
324
+ */
325
+ declare function addressList<TPathKind extends PathKind = PathKind.Root>(path: SchemaPath<string[], SchemaPathRules.Supported, TPathKind>, options?: AddressListOptions): void;
326
+
327
+ export { ADDRESS_LIST_EMPTY, ADDRESS_LIST_INVALID, ADDRESS_LIST_TOO_MANY, AddressInput, AddressInputLabel, addressList };
328
+ export type { AddressListOptions };
@@ -0,0 +1,125 @@
1
+ import { InjectionToken, Provider } from '@angular/core';
2
+
3
+ /**
4
+ * A mailbox the way a header spells it: an address, and the display name a
5
+ * client shows in front of it — `Ada Lovelace <ada@example.com>`,
6
+ * `"Lovelace, Ada" <ada@example.com>`, or the bare `ada@example.com`.
7
+ *
8
+ * The chips hold that header form as one string, so a host's `string[]`
9
+ * carries names without a second type and goes into a `To:` header as it
10
+ * is.
11
+ */
12
+ interface Mailbox {
13
+ /** The display name, if the mailbox has one; quotes already removed. */
14
+ readonly name?: string;
15
+ readonly address: string;
16
+ }
17
+ /**
18
+ * The address rule — what an address is, how a mailbox is read and written,
19
+ * how typed text splits into mailboxes. The library's own is `rules.ts`
20
+ * ({@link defaultAddressRules}); a host with a rule of its own — a stricter
21
+ * validator, an established parser, the rule its backend already enforces —
22
+ * provides it with {@link provideAddressRules}, whole or in part, and every
23
+ * address piece asks it instead: the input, the chip, the `addressList`
24
+ * form rule, and (passed as an option) `replyEnvelope` and
25
+ * `toInboundMessage`.
26
+ */
27
+ interface AddressRules {
28
+ /** A typed, pasted or header run as mailboxes in header form. */
29
+ split(raw: string): string[];
30
+ /** A header-form mailbox as its name and address. */
31
+ parse(raw: string): Mailbox;
32
+ /** A mailbox in header form, the name quoted where it has to be. */
33
+ format(mailbox: Mailbox): string;
34
+ /** Whether a bare address — no name, no brackets — is well-formed. */
35
+ isValid(address: string): boolean;
36
+ /** Whether an address is a placeholder a template fills in later
37
+ (`{{contact.email}}`) — neither valid nor invalid until then. A host
38
+ that sends what is typed, unresolved, turns it off with
39
+ `provideAddressRules({ isPlaceholder: () => false })`. */
40
+ isPlaceholder(address: string): boolean;
41
+ /** What makes two addresses one recipient: equal identities are. */
42
+ identity(address: string): string;
43
+ }
44
+ /** The address rule in force — ours unless a host provides its own. */
45
+ declare const ADDRESS_RULES: InjectionToken<AddressRules>;
46
+ /**
47
+ * Puts a host's address rule in force: the parts given replace ours, the
48
+ * rest stay ours. Keeping an existing validator is one line:
49
+ *
50
+ * ```ts
51
+ * providers: [provideAddressRules({ isValid: isValidEmail })]
52
+ * ```
53
+ */
54
+ declare function provideAddressRules(rules: Partial<AddressRules>): Provider;
55
+ /** Whether a header-form string is a mailbox whose address is well-formed. */
56
+ declare function isMailbox(raw: string, rules?: AddressRules): boolean;
57
+ /** Whether a header-form entry's address is a placeholder — pending: a
58
+ template fills it in later, so it is no mistake to flag. */
59
+ declare function isPending(raw: string, rules?: AddressRules): boolean;
60
+ /** What makes two header-form entries one recipient — the name in front of
61
+ the address makes no second one. For a list that holds each recipient
62
+ once. */
63
+ declare function addressKey(raw: string, rules?: AddressRules): string;
64
+ /**
65
+ * A list of header-form entries as one header value — `"Müller, Hans"
66
+ * <h@x.de>, b@c.de` — for a backend or a template that keeps To, Cc and Bcc
67
+ * as text. The way back is the rule's own `split`: `rules.split(joined)` is
68
+ * the list again, entry for entry. A well-formed mailbox is written in its
69
+ * canonical form (a name with a comma quoted); anything else — a typo, a
70
+ * `{{placeholder}}` — goes in as it is, for the backend to resolve or refuse.
71
+ *
72
+ * ```ts
73
+ * to: joinAddresses(form.to().value()), // out to the backend
74
+ * form.to().value.set(splitAddresses(template.to)) // back in
75
+ * ```
76
+ */
77
+ declare function joinAddresses(list: readonly string[], rules?: AddressRules): string;
78
+
79
+ /** Whether a bare address — no name, no brackets — is well-formed by the
80
+ rule above. */
81
+ declare function isEmailAddress(address: string): boolean;
82
+ /** Whether a separator typed at `caret` would separate — false inside a
83
+ quoted name, a comment or angle brackets, where a comma is part of the
84
+ mailbox: `"Lovelace, Ada"`. */
85
+ declare function separatesAt(raw: string, caret: number): boolean;
86
+ /**
87
+ * Splits a header-form mailbox into name and address: quoted strings
88
+ * unquoted and unescaped, encoded words decoded, comments skipped — or, after
89
+ * a bare address, taken as its name — and an obsolete route dropped.
90
+ * Anything that is no mailbox comes back as its trimmed text, as the address,
91
+ * for the rule to refuse.
92
+ */
93
+ declare function parseMailbox(raw: string): Mailbox;
94
+ /** The header form of a mailbox — the name quoted (and escaped) when it has
95
+ to be. UTF-8 stays as it is (RFC 6532); a mailer encodes for the wire. */
96
+ declare function formatMailbox(mailbox: Mailbox): string;
97
+ /**
98
+ * Splits a typed, pasted or header run into mailboxes — see the leniencies
99
+ * above. A well-formed mailbox comes back in its canonical header form
100
+ * (`<a@x.io>` → `a@x.io`, `Dr. Ada <a@x.io>` → `"Dr. Ada" <a@x.io>`, a
101
+ * comment or an encoded word turned into the name); anything else comes back
102
+ * as typed, for the user to fix. Empty tokens are dropped; duplicates are
103
+ * the caller's.
104
+ */
105
+ declare function splitAddresses(raw: string): string[];
106
+ /**
107
+ * What makes two entries one recipient. The domain is case-insensitive and
108
+ * compared in its ASCII form (`müller.de` is `xn--mller-kva.de`); the local
109
+ * part is lowercased too — RFC 5321 lets a server tell `Ada` from `ada`, no
110
+ * real one does, and one recipient twice is the mistake that matters. Both
111
+ * NFC-normalized, so a composed and a decomposed `ü` are one.
112
+ */
113
+ declare function addressIdentity(address: string): string;
114
+ /** Decodes the RFC 2047 encoded words in a header text; one that cannot be
115
+ decoded stays as it is. Two encoded words only whitespace separates are
116
+ joined with none. */
117
+ declare function decodeEncodedWords(text: string): string;
118
+ /** Whether an address holds a `{{…}}` placeholder — one a template fills
119
+ in later, so neither valid nor invalid yet. */
120
+ declare function isAddressPlaceholder(address: string): boolean;
121
+ /** Our rule, as the contract every part of the library asks through. */
122
+ declare const defaultAddressRules: AddressRules;
123
+
124
+ export { ADDRESS_RULES, addressIdentity, addressKey, decodeEncodedWords, defaultAddressRules, formatMailbox, isAddressPlaceholder, isEmailAddress, isMailbox, isPending, joinAddresses, parseMailbox, provideAddressRules, separatesAt, splitAddresses };
125
+ export type { AddressRules, Mailbox };
@@ -0,0 +1,44 @@
1
+ import * as _angular_core from '@angular/core';
2
+
3
+ /** A box in viewport coordinates — a `DOMRect`, a menu state's
4
+ `boundingBox`, a caret's `coordsAtPos`. A point when it has no size. */
5
+ interface AnchorRect {
6
+ left: number;
7
+ top: number;
8
+ width?: number;
9
+ height?: number;
10
+ }
11
+ /**
12
+ * A real element kept over a box the editor reports — a selection, a caret,
13
+ * a block. The editor knows rectangles; everything that floats wants an
14
+ * *element* to hold on to: the CDK's `cdkOverlayOrigin`, Material's
15
+ * `matMenuTriggerFor`, Aria's menu trigger, CSS anchor positioning,
16
+ * Floating UI. This is that element, so each of them works as it is:
17
+ *
18
+ * <span [emailAnchor]="state().boundingBox" cdkOverlayOrigin #origin="cdkOverlayOrigin"></span>
19
+ * <ng-template cdkConnectedOverlay [cdkConnectedOverlayOrigin]="origin" …>
20
+ *
21
+ * Behaviour only: it places its element — fixed, at the box, out of the
22
+ * pointer's way — and renders nothing. While the box is `null` it stays
23
+ * where it last was, so what is anchored to it can close (and animate out)
24
+ * in place rather than jump to the corner.
25
+ *
26
+ * The box is in viewport coordinates, so the element is `position: fixed`;
27
+ * it must not sit inside a transformed ancestor, which would become its
28
+ * frame. It does not follow a scroll by itself: it is as current as the box
29
+ * it is given.
30
+ */
31
+ declare class Anchor {
32
+ /** The box to stand over; `null` while there is none. */
33
+ readonly rect: _angular_core.InputSignal<AnchorRect | null | undefined>;
34
+ /** The element itself — for what takes an element rather than a template
35
+ reference (`#anchor="emailAnchor"`, then `anchor.element`). */
36
+ readonly element: HTMLElement;
37
+ /** The box being stood over: the last one given, held through `null`. */
38
+ protected readonly box: _angular_core.WritableSignal<Required<AnchorRect>>;
39
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<Anchor, never>;
40
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<Anchor, "[emailAnchor]", ["emailAnchor"], { "rect": { "alias": "emailAnchor"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
41
+ }
42
+
43
+ export { Anchor };
44
+ export type { AnchorRect };