@h-k-dev/angular-email-editor 0.0.0-placeholder.0 → 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.
- package/README.md +127 -3
- package/fesm2022/h-k-dev-angular-email-editor-actions.mjs +179 -0
- package/fesm2022/h-k-dev-angular-email-editor-actions.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-address-chip.mjs +285 -0
- package/fesm2022/h-k-dev-angular-email-editor-address-chip.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-address-input.mjs +686 -0
- package/fesm2022/h-k-dev-angular-email-editor-address-input.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-address.mjs +489 -0
- package/fesm2022/h-k-dev-angular-email-editor-address.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-anchor.mjs +64 -0
- package/fesm2022/h-k-dev-angular-email-editor-anchor.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-attachment-chip.mjs +387 -0
- package/fesm2022/h-k-dev-angular-email-editor-attachment-chip.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-attachment-chips.mjs +80 -0
- package/fesm2022/h-k-dev-angular-email-editor-attachment-chips.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-chat-input.mjs +346 -0
- package/fesm2022/h-k-dev-angular-email-editor-chat-input.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-focus.mjs +68 -0
- package/fesm2022/h-k-dev-angular-email-editor-focus.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-image.mjs +132 -0
- package/fesm2022/h-k-dev-angular-email-editor-image.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-material.mjs +124 -0
- package/fesm2022/h-k-dev-angular-email-editor-material.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-metrics.mjs +243 -0
- package/fesm2022/h-k-dev-angular-email-editor-metrics.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-palette.mjs +87 -0
- package/fesm2022/h-k-dev-angular-email-editor-palette.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-proposal.mjs +307 -0
- package/fesm2022/h-k-dev-angular-email-editor-proposal.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-suggestion-menu.mjs +392 -0
- package/fesm2022/h-k-dev-angular-email-editor-suggestion-menu.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor.mjs +18818 -0
- package/fesm2022/h-k-dev-angular-email-editor.mjs.map +1 -0
- package/package.json +130 -6
- package/styles/_scroll.scss +101 -0
- package/types/h-k-dev-angular-email-editor-actions.d.ts +147 -0
- package/types/h-k-dev-angular-email-editor-address-chip.d.ts +193 -0
- package/types/h-k-dev-angular-email-editor-address-input.d.ts +328 -0
- package/types/h-k-dev-angular-email-editor-address.d.ts +125 -0
- package/types/h-k-dev-angular-email-editor-anchor.d.ts +44 -0
- package/types/h-k-dev-angular-email-editor-attachment-chip.d.ts +271 -0
- package/types/h-k-dev-angular-email-editor-attachment-chips.d.ts +48 -0
- package/types/h-k-dev-angular-email-editor-chat-input.d.ts +101 -0
- package/types/h-k-dev-angular-email-editor-focus.d.ts +40 -0
- package/types/h-k-dev-angular-email-editor-image.d.ts +66 -0
- package/types/h-k-dev-angular-email-editor-material.d.ts +65 -0
- package/types/h-k-dev-angular-email-editor-metrics.d.ts +110 -0
- package/types/h-k-dev-angular-email-editor-palette.d.ts +51 -0
- package/types/h-k-dev-angular-email-editor-proposal.d.ts +170 -0
- package/types/h-k-dev-angular-email-editor-suggestion-menu.d.ts +177 -0
- package/types/h-k-dev-angular-email-editor.d.ts +3959 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Node } from 'prosemirror-model';
|
|
2
|
+
import { PreparedText } from '@chenglou/pretext';
|
|
3
|
+
import { PreparedRichInline } from '@chenglou/pretext/rich-inline';
|
|
4
|
+
import { FunctionalExtension } from '@h-k-dev/angular-email-editor';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Math-based text measurement for the editor document, built on
|
|
8
|
+
* `@chenglou/pretext`. Block heights and line counts are computed from cached
|
|
9
|
+
* canvas glyph widths instead of DOM reads (`offsetHeight`,
|
|
10
|
+
* `getBoundingClientRect`), so measuring never triggers a layout reflow.
|
|
11
|
+
*/
|
|
12
|
+
/** Typography of the live editor surface, read from computed style once. */
|
|
13
|
+
interface EditorTypography {
|
|
14
|
+
fontFamily: string;
|
|
15
|
+
/** Body text size in px. */
|
|
16
|
+
fontSize: number;
|
|
17
|
+
fontWeight: number;
|
|
18
|
+
fontStyle: string;
|
|
19
|
+
/** Body line height in px. */
|
|
20
|
+
lineHeight: number;
|
|
21
|
+
/** Vertical gap between sibling top-level blocks (`--prose-gap`), px. */
|
|
22
|
+
blockGap: number;
|
|
23
|
+
/** Heading font-size factors for levels 1-6, relative to the body size. */
|
|
24
|
+
headingScale: readonly number[];
|
|
25
|
+
/** Horizontal space consumed by wrapper blocks, px. */
|
|
26
|
+
indents: {
|
|
27
|
+
blockquote: number;
|
|
28
|
+
list: number;
|
|
29
|
+
/** Vertical gap between adjacent list items, px. */
|
|
30
|
+
listItemGap: number;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Reads the typography pretext needs from the editor's computed style. One
|
|
35
|
+
* DOM read at startup (and again once webfonts settle) — never per keystroke.
|
|
36
|
+
*/
|
|
37
|
+
declare function readTypography(dom: HTMLElement): EditorTypography;
|
|
38
|
+
interface TextMetrics {
|
|
39
|
+
words: number;
|
|
40
|
+
characters: number;
|
|
41
|
+
/** Wrapped line count across all text blocks at the current width. */
|
|
42
|
+
lines: number;
|
|
43
|
+
/**
|
|
44
|
+
* Estimated rendered height of the document in px, block gaps included.
|
|
45
|
+
* Text-only: atom blocks (images) contribute no height since their
|
|
46
|
+
* rendered size isn't knowable without loading them.
|
|
47
|
+
*/
|
|
48
|
+
estimatedHeight: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Prepared measurements for one textblock. Single-font blocks (the common
|
|
52
|
+
* case in email) go through `prepare()` with `pre-wrap`, matching the editor
|
|
53
|
+
* CSS exactly — hard breaks become `\n` in one prepared handle. Blocks with
|
|
54
|
+
* mark-dependent fonts (bold/italic) use the rich-inline API, one prepared
|
|
55
|
+
* handle per hard-break segment — `pre-wrap` too, so a bold word's line
|
|
56
|
+
* breaks and spaces count exactly as the plain line's.
|
|
57
|
+
*/
|
|
58
|
+
type PreparedBlock = {
|
|
59
|
+
kind: 'plain';
|
|
60
|
+
prepared: PreparedText;
|
|
61
|
+
} | {
|
|
62
|
+
kind: 'rich';
|
|
63
|
+
segments: PreparedRichInline[];
|
|
64
|
+
};
|
|
65
|
+
interface CachedBlock {
|
|
66
|
+
block: PreparedBlock;
|
|
67
|
+
lineHeight: number;
|
|
68
|
+
words: number;
|
|
69
|
+
characters: number;
|
|
70
|
+
/** Line count memoised for the last width measured at. */
|
|
71
|
+
lastWidth: number;
|
|
72
|
+
lastLines: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Measurement cache keyed on ProseMirror's immutable nodes: blocks untouched
|
|
76
|
+
* by a transaction keep their node identity, so across keystrokes only the
|
|
77
|
+
* edited block is ever re-prepared. Create one per editor view; replace it
|
|
78
|
+
* wholesale to invalidate (e.g. after webfonts load).
|
|
79
|
+
*/
|
|
80
|
+
type BlockMeasureCache = WeakMap<Node, CachedBlock>;
|
|
81
|
+
declare const createBlockMeasureCache: () => BlockMeasureCache;
|
|
82
|
+
/**
|
|
83
|
+
* Measures the whole document at the given content width. Pure arithmetic
|
|
84
|
+
* over cached glyph widths — no DOM access.
|
|
85
|
+
*/
|
|
86
|
+
declare function measureDoc(doc: Node, contentWidth: number, typography: EditorTypography, cache: BlockMeasureCache): TextMetrics;
|
|
87
|
+
|
|
88
|
+
interface TextMetricsOptions {
|
|
89
|
+
/** Called with fresh metrics after every doc change and width resize. */
|
|
90
|
+
onMetrics: (metrics: TextMetrics) => void;
|
|
91
|
+
/** Called when a measurement fails — no canvas to measure with (a server
|
|
92
|
+
render, jsdom), or a font the browser cannot use. The editor goes on
|
|
93
|
+
unharmed; that measurement reports nothing, and the next change, or
|
|
94
|
+
the web fonts arriving, tries again. */
|
|
95
|
+
onError?: (error: unknown) => void;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Live document metrics (words, lines, estimated height) computed with
|
|
99
|
+
* `@chenglou/pretext` — pure arithmetic over cached glyph widths, no DOM
|
|
100
|
+
* measurement on the keystroke path. Unchanged blocks are never re-measured:
|
|
101
|
+
* the cache keys on ProseMirror's immutable nodes.
|
|
102
|
+
*
|
|
103
|
+
* Metrics are an extra, never a dependency of the editor: a measurement
|
|
104
|
+
* that throws is reported through `onError` and skipped, and without a
|
|
105
|
+
* `ResizeObserver` the width read at mount stands.
|
|
106
|
+
*/
|
|
107
|
+
declare const createTextMetrics: (options: TextMetricsOptions) => FunctionalExtension;
|
|
108
|
+
|
|
109
|
+
export { createBlockMeasureCache, createTextMetrics, measureDoc, readTypography };
|
|
110
|
+
export type { BlockMeasureCache, EditorTypography, TextMetrics, TextMetricsOptions };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { InjectionToken, Provider } from '@angular/core';
|
|
2
|
+
import { PaletteColor, ExtensionContext, SuggestionCommandItem } from '@h-k-dev/angular-email-editor';
|
|
3
|
+
|
|
4
|
+
/** The colours a host offers: for text, and for a background. */
|
|
5
|
+
interface EmailPalette {
|
|
6
|
+
readonly text: readonly PaletteColor[];
|
|
7
|
+
readonly background: readonly PaletteColor[];
|
|
8
|
+
}
|
|
9
|
+
/** The library's own palette — the swatches that pass on both a light and a
|
|
10
|
+
dark client (`emailTextPalette`, `emailBackgroundPalette`). */
|
|
11
|
+
declare const emailPalette: EmailPalette;
|
|
12
|
+
/** A host's palette: either list, the other staying the library's — or a
|
|
13
|
+
function of the library's palette, to mix: keep it and add a brand
|
|
14
|
+
colour, take a few out, replace one side. */
|
|
15
|
+
type EmailPaletteConfig = Partial<EmailPalette> | ((defaults: EmailPalette) => Partial<EmailPalette>);
|
|
16
|
+
/**
|
|
17
|
+
* The palette in use — the library's unless a host provides its own
|
|
18
|
+
* ({@link providePalette}). What the colour pickers offer and what the `/`
|
|
19
|
+
* menu's colour rows are made of ({@link colorSuggestions}).
|
|
20
|
+
*/
|
|
21
|
+
declare const EMAIL_PALETTE: InjectionToken<EmailPalette>;
|
|
22
|
+
/** The palette a config comes to, over the library's. */
|
|
23
|
+
declare function resolvePalette(config: EmailPaletteConfig): EmailPalette;
|
|
24
|
+
/**
|
|
25
|
+
* Makes a palette the app's — in `appConfig.providers`, or a component's:
|
|
26
|
+
*
|
|
27
|
+
* providePalette({ text: BRAND_TEXT_COLORS })
|
|
28
|
+
* providePalette((defaults) => ({
|
|
29
|
+
* text: [...defaults.text, { name: 'Brand', value: '#0a5c36' }],
|
|
30
|
+
* }))
|
|
31
|
+
*
|
|
32
|
+
* Every colour goes into an email as written: keep to what a client shows
|
|
33
|
+
* on both a light and a dark page (`passesDualContrast`).
|
|
34
|
+
*/
|
|
35
|
+
declare function providePalette(config: EmailPaletteConfig): Provider;
|
|
36
|
+
/** The palette in use, in an injection context. */
|
|
37
|
+
declare function injectPalette(): EmailPalette;
|
|
38
|
+
/**
|
|
39
|
+
* The palette as rows of a `/` menu, the way a chat's slash menu lists
|
|
40
|
+
* colours: "Red text", "Red background" — one row per colour of either
|
|
41
|
+
* side, in the section `color`, each carrying its `swatch`. Picked, a row
|
|
42
|
+
* colours the selection, or what is typed next at a bare caret (the
|
|
43
|
+
* `TextStyle` mark's `setColor` / `setBackgroundColor`); nothing, when the
|
|
44
|
+
* kit has no `TextStyle`.
|
|
45
|
+
*
|
|
46
|
+
* items: (ctx) => [...extensionSuggestions(ctx), ...colorSuggestions(ctx, palette)]
|
|
47
|
+
*/
|
|
48
|
+
declare function colorSuggestions(ctx: ExtensionContext, palette: EmailPalette): SuggestionCommandItem[];
|
|
49
|
+
|
|
50
|
+
export { EMAIL_PALETTE, colorSuggestions, emailPalette, injectPalette, providePalette, resolvePalette };
|
|
51
|
+
export type { EmailPalette, EmailPaletteConfig };
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { Signal, Injector } from '@angular/core';
|
|
3
|
+
import { ContentStreamRange, ContentStreamWriter, StreamContentOptions, ContentStreamRun, Editor } from '@h-k-dev/angular-email-editor';
|
|
4
|
+
import { AnchorRect } from '@h-k-dev/angular-email-editor/anchor';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A proposal in an editor, as a host's panel sees it: what is proposed and
|
|
8
|
+
* whether it is still coming, where it stands, and the three things to do
|
|
9
|
+
* about it. The reactive face of `proposeContent` / `acceptProposal` /
|
|
10
|
+
* `discardProposal` (the main entry, which needs `createContentProposal()`
|
|
11
|
+
* and `createContentStream()` in the kit).
|
|
12
|
+
*/
|
|
13
|
+
interface EditorProposal {
|
|
14
|
+
/** Something is proposed — written into the text, not the writer's yet. */
|
|
15
|
+
readonly active: Signal<boolean>;
|
|
16
|
+
/** The proposal is still being written. */
|
|
17
|
+
readonly streaming: Signal<boolean>;
|
|
18
|
+
/** Asked, and nothing has come yet: the model is thinking — for a host
|
|
19
|
+
to show it, since there is nothing in the text to see. */
|
|
20
|
+
readonly thinking: Signal<boolean>;
|
|
21
|
+
/** Where it stands, in the document. */
|
|
22
|
+
readonly range: Signal<ContentStreamRange | null>;
|
|
23
|
+
/** Its box in viewport coordinates — the last line's, for a panel to
|
|
24
|
+
stand under; a caret's when nothing is proposed yet. Measured anew on
|
|
25
|
+
every change of the editor, so it follows the writing. */
|
|
26
|
+
readonly box: Signal<AnchorRect | null>;
|
|
27
|
+
/** Proposes content at the selection (or at `target`), written by
|
|
28
|
+
`callback` — see `streamContent` for the writer and the options. A
|
|
29
|
+
proposal already there is discarded first. `null` without an editor. */
|
|
30
|
+
propose(callback: (writer: ContentStreamWriter) => Promise<void> | void, options?: Omit<StreamContentOptions, 'history'> & {
|
|
31
|
+
target?: number | ContentStreamRange;
|
|
32
|
+
}): ContentStreamRun | null;
|
|
33
|
+
/** Writes a part of the proposal again — `range`, inside it, or the
|
|
34
|
+
editor's selection when it lies inside — the rest standing. `null`
|
|
35
|
+
when there is no proposal holding it. */
|
|
36
|
+
revise(callback: (writer: ContentStreamWriter) => Promise<void> | void, options?: Omit<StreamContentOptions, 'history'> & {
|
|
37
|
+
range?: ContentStreamRange;
|
|
38
|
+
}): ContentStreamRun | null;
|
|
39
|
+
/** The editor's selection, when it is a range inside the proposal —
|
|
40
|
+
what {@link revise} would write again; null otherwise. */
|
|
41
|
+
readonly selectedPart: Signal<ContentStreamRange | null>;
|
|
42
|
+
/** Stops the writing where it is; what is proposed stays proposed. */
|
|
43
|
+
stop(): void;
|
|
44
|
+
/** Takes the proposal into the document: one change, one undo. */
|
|
45
|
+
accept(): boolean;
|
|
46
|
+
/** Takes it out, as if never written. */
|
|
47
|
+
discard(): boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The proposal of an editor, bound and live — for a host's own panel: an
|
|
51
|
+
* input of its choosing to ask with, and buttons to accept or discard:
|
|
52
|
+
*
|
|
53
|
+
* readonly proposal = injectProposal(() => this.editor());
|
|
54
|
+
*
|
|
55
|
+
* this.proposal.propose(async ({ write, signal }) => {
|
|
56
|
+
* for await (const piece of model.write(prompt, { signal })) write(piece);
|
|
57
|
+
* }, { format: 'html' });
|
|
58
|
+
*
|
|
59
|
+
* <button [emailProposalAccept]="proposal">Apply</button>
|
|
60
|
+
* <button [emailProposalDiscard]="proposal">Discard</button>
|
|
61
|
+
*
|
|
62
|
+
* `editor` is read reactively: the proposal follows the editor it is given.
|
|
63
|
+
* Call in an injection context, or hand it an injector.
|
|
64
|
+
*/
|
|
65
|
+
declare function injectProposal(editor: () => Editor | undefined, options?: {
|
|
66
|
+
injector?: Injector;
|
|
67
|
+
}): EditorProposal;
|
|
68
|
+
/**
|
|
69
|
+
* The proposal of an editor, as a directive: put it on the element that
|
|
70
|
+
* holds the host's panel — the chat, the buttons — and everything inside
|
|
71
|
+
* has it, by injection, with nothing to bind:
|
|
72
|
+
*
|
|
73
|
+
* <div [emailProposal]="editor" #p="emailProposal">
|
|
74
|
+
* <button emailProposalAccept [disabled]="!p.active()">Apply</button>
|
|
75
|
+
* <button emailProposalDiscard>Discard</button>
|
|
76
|
+
* </div>
|
|
77
|
+
*
|
|
78
|
+
* The directive *is* the {@link EditorProposal}: `p.active()`,
|
|
79
|
+
* `p.propose(…)`, `p.accept()` — the same face `injectProposal` gives a
|
|
80
|
+
* class, for a host that lives in its template. The two are one thing
|
|
81
|
+
* looked at from two sides; a trigger takes either, bound or injected.
|
|
82
|
+
*/
|
|
83
|
+
declare class Proposal implements EditorProposal {
|
|
84
|
+
#private;
|
|
85
|
+
/** The editor the proposal is in — read reactively, so it may come
|
|
86
|
+
later, or change. */
|
|
87
|
+
readonly editor: i0.InputSignal<Editor | undefined>;
|
|
88
|
+
readonly active: Signal<boolean>;
|
|
89
|
+
readonly streaming: Signal<boolean>;
|
|
90
|
+
readonly thinking: Signal<boolean>;
|
|
91
|
+
readonly range: Signal<ContentStreamRange | null>;
|
|
92
|
+
readonly box: Signal<AnchorRect | null>;
|
|
93
|
+
readonly selectedPart: Signal<ContentStreamRange | null>;
|
|
94
|
+
propose(...args: Parameters<EditorProposal['propose']>): ContentStreamRun | null;
|
|
95
|
+
revise(...args: Parameters<EditorProposal['revise']>): ContentStreamRun | null;
|
|
96
|
+
stop(): void;
|
|
97
|
+
accept(): boolean;
|
|
98
|
+
discard(): boolean;
|
|
99
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<Proposal, never>;
|
|
100
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<Proposal, "[emailProposal]", ["emailProposal"], { "editor": { "alias": "emailProposal"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Makes the host's own button the one that accepts the proposal: a click
|
|
104
|
+
* takes it into the document and hands the caret back; disabled — by the
|
|
105
|
+
* host's own means, bound from `disabled()` — while nothing is proposed or
|
|
106
|
+
* it is still being written. The proposal is bound, or, bare, the
|
|
107
|
+
* `[emailProposal]` ancestor's.
|
|
108
|
+
*
|
|
109
|
+
* <button mat-button [emailProposalAccept]="proposal" #apply="emailProposalAccept"
|
|
110
|
+
* [disabled]="apply.disabled()">Apply</button>
|
|
111
|
+
*/
|
|
112
|
+
declare class ProposalAccept {
|
|
113
|
+
#private;
|
|
114
|
+
readonly proposal: i0.InputSignalWithTransform<EditorProposal | undefined, "" | EditorProposal | undefined>;
|
|
115
|
+
readonly disabled: Signal<boolean>;
|
|
116
|
+
run(): boolean;
|
|
117
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<ProposalAccept, never>;
|
|
118
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<ProposalAccept, "[emailProposalAccept]", ["emailProposalAccept"], { "proposal": { "alias": "emailProposalAccept"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Makes the host's own button the one that discards the proposal: a click
|
|
122
|
+
* takes it out, wherever the writing has got to. The proposal is bound,
|
|
123
|
+
* or, bare, the `[emailProposal]` ancestor's.
|
|
124
|
+
*
|
|
125
|
+
* <button mat-button [emailProposalDiscard]="proposal">Discard</button>
|
|
126
|
+
*/
|
|
127
|
+
declare class ProposalDiscard {
|
|
128
|
+
#private;
|
|
129
|
+
readonly proposal: i0.InputSignalWithTransform<EditorProposal | undefined, "" | EditorProposal | undefined>;
|
|
130
|
+
readonly disabled: Signal<boolean>;
|
|
131
|
+
run(): boolean;
|
|
132
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<ProposalDiscard, never>;
|
|
133
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<ProposalDiscard, "[emailProposalDiscard]", ["emailProposalDiscard"], { "proposal": { "alias": "emailProposalDiscard"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The keys of a proposal, heard from anywhere on the page — the text, the
|
|
137
|
+
* host's input, a button — while one stands, before anything else hears
|
|
138
|
+
* them (the document, capture phase):
|
|
139
|
+
*
|
|
140
|
+
* - **Ctrl-Enter** (⌘-Enter on a Mac) accepts: a proposal on the table is
|
|
141
|
+
* what the key commits, and whatever the editor binds to it (a send,
|
|
142
|
+
* say) never sees it. While the proposal is still being written the key
|
|
143
|
+
* is swallowed — nothing is sent, nothing is applied; one thing at a
|
|
144
|
+
* time. `accepted` says it happened.
|
|
145
|
+
* - **Escape** is the host's: `escape` carries the event, and the host
|
|
146
|
+
* decides — close a menu over the text first, or let the proposal go —
|
|
147
|
+
* because what else is open is the host's to know. Nothing is prevented
|
|
148
|
+
* or stopped for it; the host does that on the event if it acts.
|
|
149
|
+
*
|
|
150
|
+
* Nothing is heard while nothing is proposed, and an IME's own keys stay
|
|
151
|
+
* the IME's. The proposal is bound, or, bare, the `[emailProposal]`
|
|
152
|
+
* ancestor's.
|
|
153
|
+
*
|
|
154
|
+
* <div [emailProposal]="editor" emailProposalKeys
|
|
155
|
+
* (accepted)="done()" (escape)="$event.preventDefault(); leave()">
|
|
156
|
+
*/
|
|
157
|
+
declare class ProposalKeys {
|
|
158
|
+
#private;
|
|
159
|
+
readonly proposal: i0.InputSignalWithTransform<EditorProposal | undefined, "" | EditorProposal | undefined>;
|
|
160
|
+
/** Ctrl-Enter (⌘-Enter) took the proposal into the document. */
|
|
161
|
+
readonly accepted: i0.OutputEmitterRef<void>;
|
|
162
|
+
/** Escape, with a proposal standing — for the host to act on. */
|
|
163
|
+
readonly escape: i0.OutputEmitterRef<KeyboardEvent>;
|
|
164
|
+
constructor();
|
|
165
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<ProposalKeys, never>;
|
|
166
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<ProposalKeys, "[emailProposalKeys]", ["emailProposalKeys"], { "proposal": { "alias": "emailProposalKeys"; "required": false; "isSignal": true; }; }, { "accepted": "accepted"; "escape": "escape"; }, never, never, true, never>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export { Proposal, ProposalAccept, ProposalDiscard, ProposalKeys, injectProposal };
|
|
170
|
+
export type { EditorProposal };
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import * as _h_k_dev_angular_email_editor from '@h-k-dev/angular-email-editor';
|
|
2
|
+
import { SuggestionGroup, SuggestionMenuState, SuggestionItem } from '@h-k-dev/angular-email-editor';
|
|
3
|
+
import * as _angular_core from '@angular/core';
|
|
4
|
+
import { TemplateRef, ElementRef } from '@angular/core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A suggestion menu's header slot, shown on level 2 above the group's
|
|
8
|
+
* children. The default is a back button and the group's title; a host
|
|
9
|
+
* that wants its own — a Material icon button, the group's icon, a
|
|
10
|
+
* breadcrumb — hands the menu a template:
|
|
11
|
+
*
|
|
12
|
+
* <div email-suggestion-menu [state]="state()">
|
|
13
|
+
* <ng-template emailSuggestionMenuHeader let-group let-back="back" let-label="backLabel">
|
|
14
|
+
* <button mat-icon-button tabindex="-1" [attr.aria-label]="label" (click)="back()">
|
|
15
|
+
* <mat-icon>arrow_back</mat-icon>
|
|
16
|
+
* </button>
|
|
17
|
+
* {{ group.title }}
|
|
18
|
+
* </ng-template>
|
|
19
|
+
* …
|
|
20
|
+
* </div>
|
|
21
|
+
*
|
|
22
|
+
* The template renders inside the menu's own `header` box.
|
|
23
|
+
*/
|
|
24
|
+
/** What the header template is given. */
|
|
25
|
+
interface SuggestionMenuHeaderContext {
|
|
26
|
+
/** The group whose children are listed — its title already translated. */
|
|
27
|
+
readonly $implicit: SuggestionGroup;
|
|
28
|
+
/** Back to level 1 with an empty query. */
|
|
29
|
+
readonly back: () => void;
|
|
30
|
+
/** The back control's accessible name, as the menu was given it. */
|
|
31
|
+
readonly backLabel: string;
|
|
32
|
+
}
|
|
33
|
+
/** Marks the `ng-template` the menu renders as its level-2 header. */
|
|
34
|
+
declare class SuggestionMenuHeader {
|
|
35
|
+
readonly template: TemplateRef<SuggestionMenuHeaderContext>;
|
|
36
|
+
static ngTemplateContextGuard(_directive: SuggestionMenuHeader, context: unknown): context is SuggestionMenuHeaderContext;
|
|
37
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<SuggestionMenuHeader, never>;
|
|
38
|
+
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SuggestionMenuHeader, "ng-template[emailSuggestionMenuHeader]", never, {}, {}, never, never, true, never>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A suggestion menu's floating box — the element `createSuggestionMenu` is
|
|
43
|
+
* given, rendered from the state it reports. The extension does the work
|
|
44
|
+
* (finding the trigger, filtering, paging, the keys, placing the box,
|
|
45
|
+
* applying); this gives the state its roles and ids, so the editor's
|
|
46
|
+
* `aria-controls` and `aria-activedescendant` land on real elements, keeps
|
|
47
|
+
* the next page coming as the list scrolls, and says what is going on.
|
|
48
|
+
*
|
|
49
|
+
* **The pair.** The menu is the container; the host renders one
|
|
50
|
+
* `[email-suggestion-menu-item]` per item into it and decides what a row
|
|
51
|
+
* shows (an icon, the title, a detail). Rows are options of the menu's
|
|
52
|
+
* listbox and never take focus: the caret stays in the editor, which is the
|
|
53
|
+
* combobox. One pair serves every trigger — `/`, `{{`, `||` — at once: they
|
|
54
|
+
* never open together, so a menu of several triggers is still one element
|
|
55
|
+
* and one state, which says whose list it is (`trigger`, `label`).
|
|
56
|
+
*
|
|
57
|
+
* <div email-suggestion-menu #menu [state]="state()">
|
|
58
|
+
* @for (item of state()?.items; track item.id) {
|
|
59
|
+
* <div email-suggestion-menu-item [item]="item">{{ item.title }}</div>
|
|
60
|
+
* }
|
|
61
|
+
* </div>
|
|
62
|
+
*
|
|
63
|
+
* createSuggestionMenu({ element: menu, onChange: (s) => state.set(s), triggers: [
|
|
64
|
+
* { trigger: '/', label: 'Insert block', items: extensionSuggestions },
|
|
65
|
+
* { trigger: '{{', label: 'Tokens', source: fetchMergeTags },
|
|
66
|
+
* ] })
|
|
67
|
+
*
|
|
68
|
+
* **Two levels.** Inside a group the menu shows a header above the list — a
|
|
69
|
+
* back button and the group's title by default, or the host's
|
|
70
|
+
* `ng-template[emailSuggestionMenuHeader]` — names the listbox after the
|
|
71
|
+
* group, and says "No results" rather than closing when nothing matches.
|
|
72
|
+
*
|
|
73
|
+
* **Said twice, differently.** What is going on shows as a line under the
|
|
74
|
+
* list, at once. A screen reader hears it from a separate live region that
|
|
75
|
+
* waits for the typing to settle (`announceDelay`) and also counts the
|
|
76
|
+
* results — the highlighted option's id does not change when the list is
|
|
77
|
+
* filtered under it, so nothing else would say the list is a new one.
|
|
78
|
+
*
|
|
79
|
+
* Every word it says is an input, English by default.
|
|
80
|
+
*/
|
|
81
|
+
declare class SuggestionMenu {
|
|
82
|
+
#private;
|
|
83
|
+
/** The extension's latest state — undefined before the editor mounts. */
|
|
84
|
+
readonly state: _angular_core.InputSignal<SuggestionMenuState | undefined>;
|
|
85
|
+
/** Names the listbox on level 1 when the open trigger has no `label` of
|
|
86
|
+
its own — a menu of several triggers names each list there ("Insert
|
|
87
|
+
block", "Personalization tokens"); level 2 is named by its group. */
|
|
88
|
+
readonly label: _angular_core.InputSignal<string>;
|
|
89
|
+
/** The level-2 back button's accessible name. */
|
|
90
|
+
readonly backLabel: _angular_core.InputSignal<string>;
|
|
91
|
+
/** Said while a source is still searching. */
|
|
92
|
+
readonly loadingLabel: _angular_core.InputSignal<string>;
|
|
93
|
+
/** Said on level 2 when nothing matches. */
|
|
94
|
+
readonly emptyLabel: _angular_core.InputSignal<string>;
|
|
95
|
+
/** Said while a further page is on its way. */
|
|
96
|
+
readonly loadingMoreLabel: _angular_core.InputSignal<string>;
|
|
97
|
+
/** Said when the source's answer failed. */
|
|
98
|
+
readonly errorLabel: _angular_core.InputSignal<string>;
|
|
99
|
+
/** Announced once a list is in: how many rows it has. */
|
|
100
|
+
readonly resultsLabel: _angular_core.InputSignal<(count: number) => string>;
|
|
101
|
+
/** Milliseconds the announcement waits for the typing to settle, so a
|
|
102
|
+
screen reader hears the list that stayed, not one per letter. Going
|
|
103
|
+
quiet (the menu closes) never waits. 0 announces at once. */
|
|
104
|
+
readonly announceDelay: _angular_core.InputSignal<number>;
|
|
105
|
+
/** Gap between the trigger's line and the menu, in px. */
|
|
106
|
+
readonly offset: _angular_core.InputSignal<number>;
|
|
107
|
+
/** The keyboard-highlighted row's index; -1 before there is a state. */
|
|
108
|
+
readonly activeIndex: _angular_core.Signal<number>;
|
|
109
|
+
/** The extension's id for the option at an index — the same function for
|
|
110
|
+
the menu's whole life. */
|
|
111
|
+
readonly optionId: _angular_core.Signal<((index: number) => string) | undefined>;
|
|
112
|
+
/** Where `item` sits in the current list; -1 when it is not in it. */
|
|
113
|
+
indexOf(item: SuggestionItem): number;
|
|
114
|
+
protected readonly header: _angular_core.Signal<SuggestionMenuHeader | undefined>;
|
|
115
|
+
protected readonly listbox: _angular_core.Signal<ElementRef<HTMLElement>>;
|
|
116
|
+
protected readonly end: _angular_core.Signal<ElementRef<HTMLElement>>;
|
|
117
|
+
/** The group being searched, on level 2. */
|
|
118
|
+
protected readonly parent: _angular_core.Signal<_h_k_dev_angular_email_editor.SuggestionGroup | null>;
|
|
119
|
+
protected readonly listLabel: _angular_core.Signal<string>;
|
|
120
|
+
/** The header template's context, while there is a group to head. */
|
|
121
|
+
protected readonly headerContext: _angular_core.Signal<SuggestionMenuHeaderContext | null>;
|
|
122
|
+
/** The line under the list: failed, searching, loading more, nothing
|
|
123
|
+
found, or nothing. Not "searching" over stale rows — dimmed, they say
|
|
124
|
+
it themselves, and a line that came and went with every keystroke
|
|
125
|
+
would resize the box (and have it placed again) twice per letter. */
|
|
126
|
+
protected readonly status: _angular_core.Signal<string>;
|
|
127
|
+
/** The announcement, once the typing has settled. */
|
|
128
|
+
protected readonly announced: _angular_core.Resource<string>;
|
|
129
|
+
constructor();
|
|
130
|
+
protected readonly back: () => void;
|
|
131
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<SuggestionMenu, never>;
|
|
132
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<SuggestionMenu, "[email-suggestion-menu]", never, { "state": { "alias": "state"; "required": false; "isSignal": true; }; "label": { "alias": "label"; "required": false; "isSignal": true; }; "backLabel": { "alias": "backLabel"; "required": false; "isSignal": true; }; "loadingLabel": { "alias": "loadingLabel"; "required": false; "isSignal": true; }; "emptyLabel": { "alias": "emptyLabel"; "required": false; "isSignal": true; }; "loadingMoreLabel": { "alias": "loadingMoreLabel"; "required": false; "isSignal": true; }; "errorLabel": { "alias": "errorLabel"; "required": false; "isSignal": true; }; "resultsLabel": { "alias": "resultsLabel"; "required": false; "isSignal": true; }; "announceDelay": { "alias": "announceDelay"; "required": false; "isSignal": true; }; "offset": { "alias": "offset"; "required": false; "isSignal": true; }; }, {}, ["header"], ["*"], true, never>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* One row of a suggestion menu: an option of its listbox, standing for one of
|
|
137
|
+
* the state's items. The host fills the row; the row takes its id (the one
|
|
138
|
+
* the editor's `aria-activedescendant` names), reflects the highlight as
|
|
139
|
+
* `aria-selected` and `data-active` — which is what the menu keeps scrolled
|
|
140
|
+
* into view — and applies its item on click: a group opens, a command runs.
|
|
141
|
+
*
|
|
142
|
+
* Never focusable, never a button: the caret stays in the editor, and the
|
|
143
|
+
* extension keeps a press on the menu from taking it away.
|
|
144
|
+
*/
|
|
145
|
+
declare class SuggestionMenuItem {
|
|
146
|
+
#private;
|
|
147
|
+
/** The item this row stands for — one of the menu state's `items`. */
|
|
148
|
+
readonly item: _angular_core.InputSignal<SuggestionItem>;
|
|
149
|
+
/** The keyboard highlight is on this row. */
|
|
150
|
+
readonly active: _angular_core.Signal<boolean>;
|
|
151
|
+
protected readonly id: _angular_core.Signal<string | null>;
|
|
152
|
+
protected select(): void;
|
|
153
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<SuggestionMenuItem, never>;
|
|
154
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<SuggestionMenuItem, "[email-suggestion-menu-item]", never, { "item": { "alias": "item"; "required": true; "isSignal": true; }; }, {}, never, ["*"], true, never>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A heading in a suggestion menu's list: the words of a section
|
|
159
|
+
* (`SuggestionItem.section`, worded by the state's `sectionTitle`), shown
|
|
160
|
+
* where the section changes from one row to the next. Never an option: it
|
|
161
|
+
* takes no highlight, no click, and no place in the count — presentation,
|
|
162
|
+
* the way a chat's slash menu heads its groups.
|
|
163
|
+
*
|
|
164
|
+
* @for (item of state.items; track item.id) {
|
|
165
|
+
* @if (item.section && item.section !== state.items[$index - 1]?.section) {
|
|
166
|
+
* <div email-suggestion-menu-section>{{ state.sectionTitle(item.section) }}</div>
|
|
167
|
+
* }
|
|
168
|
+
* <div email-suggestion-menu-item [item]="item">…</div>
|
|
169
|
+
* }
|
|
170
|
+
*/
|
|
171
|
+
declare class SuggestionMenuSection {
|
|
172
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<SuggestionMenuSection, never>;
|
|
173
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<SuggestionMenuSection, "[email-suggestion-menu-section]", never, {}, {}, never, ["*"], true, never>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export { SuggestionMenu, SuggestionMenuHeader, SuggestionMenuItem, SuggestionMenuSection };
|
|
177
|
+
export type { SuggestionMenuHeaderContext };
|