@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
package/README.md CHANGED
@@ -1,3 +1,128 @@
1
- # Temporary Holding Version
1
+ # Angular Email Editor
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/@h-k-dev/angular-email-editor.svg)](https://www.npmjs.com/package/@h-k-dev/angular-email-editor)
4
+ [![license](https://img.shields.io/npm/l/@h-k-dev/angular-email-editor.svg)](https://github.com/h-k-dev/angular-email-editor/blob/main/LICENSE)
5
+
6
+ An email editor for Angular, built on [ProseMirror](https://prosemirror.net): a visual
7
+ composer and an HTML source editor over **one schema that is the vocabulary of email
8
+ HTML** — what Gmail, Outlook and Apple Mail apply survives, nothing else is emitted.
9
+ Headless where it can be: you own the markup and the pixels; the library brings the
10
+ engine, the behaviour and accessible building blocks.
11
+
12
+ **[Try the live demo →](https://h-k-dev.github.io/angular-email-editor/)**
13
+
14
+ > **Pre-1.0.** The API still moves between `0.x` versions; releases are deliberate, not
15
+ > per commit. Pin a version and read the release notes before upgrading.
16
+
17
+ ## What you get
18
+
19
+ - **Email-safe by construction** — markup is parsed into the email vocabulary and
20
+ re-emitted deterministically: inline styles, `<div>` lines, tables that render in
21
+ Outlook. Builder exports (MJML) keep their structure; pasted Word/Docs markup is cleaned.
22
+ - **Corrects what other editors stored** — opening HTML saved by WYSIWYG editors such as
23
+ Summernote repairs `mailto://` links, `http://{{placeholder}}` links, Word's `<o:p>`,
24
+ Bootstrap-only table borders and formatting wrapped around block lines, without losing
25
+ the author's intent.
26
+ - **Two editors, one document** — the visual composer and an HTML source pane
27
+ (highlighting, lint, formatter) run the same commands over the same schema.
28
+ - **Placeholders** — `{{ expression }}` tokens as merge-tag pills, an optional AngularJS
29
+ expression dialect with live syntax diagnostics, and **placeholder filling**
30
+ (`createMergeTagResolution`, <kbd>Ctrl</kbd>/<kbd>⌘</kbd>+<kbd>Shift</kbd>+<kbd>Enter</kbd>):
31
+ your evaluator gets each expression raw, the values go back in as text, in one undo step.
32
+ - **Compose workflow** — reply/forward seeds with localized labels, quoted history kept
33
+ beside the body, inline images (`cid:`), a send intent with `text/plain` alternative.
34
+ - **Address fields that implement the RFCs** — RFC 5322/5321/6531 parsing and
35
+ formatting behind a pluggable rule, chips, signal-forms validation, placeholders as
36
+ pending (not invalid) addresses, and an Angular Material form-field adapter.
37
+ - **Attachment chips** with upload stages (Uppy/tus vocabulary) and progress.
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ npm install @h-k-dev/angular-email-editor prosemirror-commands prosemirror-gapcursor prosemirror-history prosemirror-inputrules prosemirror-keymap prosemirror-model prosemirror-schema-list prosemirror-state prosemirror-tables prosemirror-transform prosemirror-view
43
+ ```
44
+
45
+ Requires Angular `22`. The ProseMirror packages are peers so your app holds exactly one
46
+ copy of each.
47
+
48
+ | Optional peer | Needed for |
49
+ | --- | --- |
50
+ | `@angular/forms` | `/address-input`, `/attachment-chips` (signal-forms controls) |
51
+ | `@angular/material`, `rxjs` | `/material` (the `mat-form-field` adapter) |
52
+ | `@chenglou/pretext` `^1.0.0` | `/metrics` (word, line and height metrics) |
53
+
54
+ ## Quick start
55
+
56
+ ```ts
57
+ import { Component, DestroyRef, ElementRef, afterNextRender, inject, signal, viewChild } from '@angular/core';
58
+ import { Editor, createEditor, emailExtensions } from '@h-k-dev/angular-email-editor';
59
+
60
+ @Component({
61
+ selector: 'app-mail-body',
62
+ template: `<div #host></div>`,
63
+ })
64
+ export class MailBody {
65
+ readonly html = signal('<div>Hello</div>');
66
+ private readonly host = viewChild.required<ElementRef<HTMLElement>>('host');
67
+ #editor?: Editor;
68
+
69
+ constructor() {
70
+ afterNextRender(() => {
71
+ this.#editor = createEditor({
72
+ parent: this.host().nativeElement,
73
+ extensions: emailExtensions,
74
+ content: this.html(),
75
+ onUpdate: (editor) => this.html.set(editor.getHTML()),
76
+ });
77
+ });
78
+ inject(DestroyRef).onDestroy(() => this.#editor?.destroy());
79
+ }
80
+ }
81
+ ```
82
+
83
+ `emailExtensions` is the full email kit; add extensions next to it (`createSendIntent`,
84
+ `createMergeTagResolution`, `createSuggestionMenu`, …) or build a kit of your own.
85
+
86
+ ## Entry points
87
+
88
+ Every UI piece is its own secondary entry point, so you only bundle what you import.
89
+
90
+ | Import | What it is |
91
+ | --- | --- |
92
+ | `@h-k-dev/angular-email-editor` | The engine: `createEditor`, kits, schema, HTML parse/serialize, extensions |
93
+ | `…/actions` | Editor actions as signals — `injectActions`, `[emailAction]` for your own buttons |
94
+ | `…/address` | The address rule (RFC 5322/5321/6531): parse, format, split, join, `ADDRESS_RULES` |
95
+ | `…/address-input` | The address field: chips, `FormValueControl<string[]>`, `addressList` validator |
96
+ | `…/address-chip` | One address chip |
97
+ | `…/attachment-chips` | The attachment strip, a `FormValueControl` over the list |
98
+ | `…/attachment-chip` | One attachment chip with upload stages and progress |
99
+ | `…/suggestion-menu` | The `/` and `{{` menus' floating box, with roles and ids |
100
+ | `…/palette` | The colour palette (readable on light and dark) |
101
+ | `…/proposal` | A streamed proposal's state for your panel (accept / discard) |
102
+ | `…/chat-input` | A chat-style prompt input on the same engine |
103
+ | `…/image` | `[emailImageAlt]` — your text field as the selected image's alt text |
104
+ | `…/anchor` | `[emailAnchor]` — position an overlay at a rect |
105
+ | `…/focus` | `[emailKeepFocus]` — controls that keep the caret in the editor |
106
+ | `…/material` | `[emailMatFormField]` — an address input inside `mat-form-field` |
107
+ | `…/metrics` | Live text metrics (`createTextMetrics`), measured without DOM reads |
108
+
109
+ ## Styling
110
+
111
+ The components theme through a token cascade: their own `--email-*` tokens first, then
112
+ Material's `--mat-sys-*` system tokens, then a built-in light/dark fallback — a Material 3
113
+ app is themed out of the box.
114
+
115
+ The editable surface is yours to style; it carries the `aee-editor` class. Include
116
+ ProseMirror's required CSS (`prosemirror-view/style/prosemirror.css`, and
117
+ `prosemirror-tables/style/tables.css` for tables). The shared scrollbar look is a Sass mixin:
118
+
119
+ ```scss
120
+ @use '@h-k-dev/angular-email-editor/styles/scroll';
121
+ ```
122
+
123
+ The [demo app](https://github.com/h-k-dev/angular-email-editor/tree/main/projects/app) is
124
+ the reference composer — toolbar, menus, compose windows — built only on this public API.
125
+
126
+ ## License
127
+
128
+ [MIT](https://github.com/h-k-dev/angular-email-editor/blob/main/LICENSE)
@@ -0,0 +1,179 @@
1
+ import * as i0 from '@angular/core';
2
+ import { signal, effect, inject, Injector, computed, untracked, input, booleanAttribute, Directive } from '@angular/core';
3
+ import { isActionEnabled } from '@h-k-dev/angular-email-editor';
4
+
5
+ /**
6
+ * An editor's state as a signal: the text, the selection, the stored marks —
7
+ * whatever changed, the signal has the new one. It follows the editor it is
8
+ * given, so handing it "the source pane in code view, the email editor
9
+ * otherwise" is one `computed`; `undefined` while there is none.
10
+ *
11
+ * It listens through `editor.subscribe`, so the editor can already exist:
12
+ * nothing has to be in the kit before the editor is made.
13
+ *
14
+ * Call in an injection context, or hand it an injector.
15
+ */
16
+ function editorState(editor, options = {}) {
17
+ const state = signal(undefined, /* @ts-ignore */
18
+ ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
19
+ effect((onCleanup) => {
20
+ const current = editor();
21
+ state.set(current?.state);
22
+ if (current)
23
+ onCleanup(current.subscribe(() => state.set(current.state)));
24
+ }, { injector: options.injector });
25
+ return state.asReadonly();
26
+ }
27
+ /**
28
+ * The actions of an editor, bound and live — for a toolbar, a bubble menu,
29
+ * a menu:
30
+ *
31
+ * readonly actions = injectActions(() => this.editor());
32
+ *
33
+ * <button [emailAction]="actions.get('bold')">…</button>
34
+ *
35
+ * `editor` is read reactively: when it changes — a composer switching to its
36
+ * code view — the actions are the new editor's. Its kit decides what exists:
37
+ * the source pane offers the marks, so there a list button has no action
38
+ * and shows disabled, with no routing written anywhere.
39
+ *
40
+ * `host` and `override` bring in the host's own — see {@link HostAction} — so
41
+ * that one list serves a surface whose buttons are not all the editor's.
42
+ *
43
+ * Call in an injection context, or hand it an injector.
44
+ */
45
+ function injectActions(editor, options = {}) {
46
+ const injector = options.injector ?? inject(Injector);
47
+ const state = editorState(editor, { injector });
48
+ const bind = (current, action, over) => {
49
+ const isActive = over?.isActive ?? action?.isActive;
50
+ const isEnabled = (now) => over?.isEnabled ? over.isEnabled(now) : action ? isActionEnabled(action, now) : true;
51
+ return {
52
+ action,
53
+ id: (over ?? action).id,
54
+ external: !!over,
55
+ toggles: !!isActive,
56
+ pressed: computed(() => {
57
+ const now = state();
58
+ return !!now && !!isActive?.(now);
59
+ }),
60
+ disabled: computed(() => {
61
+ const now = state();
62
+ return !now || !isEnabled(now);
63
+ }),
64
+ run: () => {
65
+ if (!over)
66
+ return current.exec(action.command);
67
+ over.run();
68
+ return true;
69
+ },
70
+ focus: () => current.focus(),
71
+ };
72
+ };
73
+ const bound = computed(() => {
74
+ const current = editor();
75
+ const byId = new Map();
76
+ // Binding reads no state: only the editor decides which actions exist.
77
+ if (!current)
78
+ return byId;
79
+ const hosted = new Map((options.host ?? []).map((over) => [over.id, over]));
80
+ for (const action of current.actions) {
81
+ const given = options.override?.[action.id];
82
+ // A host action under a kit's id is an override by another name.
83
+ const over = given ? { id: action.id, ...given } : hosted.get(action.id);
84
+ hosted.delete(action.id);
85
+ byId.set(action.id, untracked(() => bind(current, action, over)));
86
+ }
87
+ // What stands alone comes after the kit's own, in the order given.
88
+ for (const over of hosted.values()) {
89
+ byId.set(over.id, untracked(() => bind(current, undefined, over)));
90
+ }
91
+ return byId;
92
+ }, /* @ts-ignore */
93
+ ...(ngDevMode ? [{ debugName: "bound" }] : /* istanbul ignore next */ []));
94
+ return {
95
+ get: (id) => bound().get(id),
96
+ all: computed(() => [...bound().values()]),
97
+ state,
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Makes the host's own control the trigger of an editor action: a click runs
103
+ * it, and the control says what the action's state is.
104
+ *
105
+ * <button mat-icon-button [emailAction]="actions.get('bold')" #bold="emailAction"
106
+ * [disabled]="bold.disabled()">
107
+ * <mat-icon>format_bold</mat-icon>
108
+ * </button>
109
+ *
110
+ * Behaviour only, and ARIA only where the host's widget has not spoken:
111
+ * - `aria-pressed` for a toggle, and `aria-disabled` when it cannot run —
112
+ * both off with `[emailActionAria]="false"` for a widget that writes its own
113
+ * (a Material button toggle);
114
+ * - never the native `disabled`, a class or a style: how disabled *looks* is
115
+ * the host's, bound from `disabled()` as above. A control that is only
116
+ * `aria-disabled` stays reachable by keyboard, which is what a toolbar wants;
117
+ * - a click on a disabled trigger runs nothing.
118
+ *
119
+ * After running, the caret goes back to the editor the action ran on — a
120
+ * no-op for a pointer press under `emailKeepFocus`, the way back for a key
121
+ * press in a toolbar. `[emailActionFocus]="false"` leaves focus on the
122
+ * control, as the ARIA toolbar pattern has it. A host's own action (a link
123
+ * dialog, a picker) never has focus taken from what it opened.
124
+ */
125
+ class ActionTrigger {
126
+ /** The action to trigger — `actions.get(id)`. `undefined` (no editor yet,
127
+ or a kit without it) is a trigger with nothing to do: disabled. */
128
+ action = input.required({ ...(ngDevMode ? { debugName: "action" } : /* istanbul ignore next */ {}), alias: 'emailAction' });
129
+ /** Whether this directive writes `aria-pressed` and `aria-disabled`. */
130
+ aria = input(true, { ...(ngDevMode ? { debugName: "aria" } : /* istanbul ignore next */ {}), alias: 'emailActionAria', transform: booleanAttribute });
131
+ /** Whether the caret returns to the editor after the action ran. */
132
+ returnFocus = input(true, { ...(ngDevMode ? { debugName: "returnFocus" } : /* istanbul ignore next */ {}), alias: 'emailActionFocus', transform: booleanAttribute });
133
+ /** On at the selection. */
134
+ pressed = computed(() => this.action()?.pressed() ?? false, /* @ts-ignore */
135
+ ...(ngDevMode ? [{ debugName: "pressed" }] : /* istanbul ignore next */ []));
136
+ /** Cannot run right now — or has no action at all. */
137
+ disabled = computed(() => this.action()?.disabled() ?? true, /* @ts-ignore */
138
+ ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
139
+ ariaPressed = computed(() => this.aria() && this.action()?.toggles ? this.pressed() : null, /* @ts-ignore */
140
+ ...(ngDevMode ? [{ debugName: "ariaPressed" }] : /* istanbul ignore next */ []));
141
+ ariaDisabled = computed(() => (this.aria() && this.disabled() ? true : null), /* @ts-ignore */
142
+ ...(ngDevMode ? [{ debugName: "ariaDisabled" }] : /* istanbul ignore next */ []));
143
+ /** Runs the action, as a click does. False when it did not apply. */
144
+ run() {
145
+ const action = this.action();
146
+ if (!action || this.disabled())
147
+ return false;
148
+ const applied = action.run();
149
+ // A host's own action opened something of the host's: focus is its now.
150
+ if (this.returnFocus() && !action.external)
151
+ action.focus();
152
+ return applied;
153
+ }
154
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ActionTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
155
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: ActionTrigger, isStandalone: true, selector: "[emailAction]", inputs: { action: { classPropertyName: "action", publicName: "emailAction", isSignal: true, isRequired: true, transformFunction: null }, aria: { classPropertyName: "aria", publicName: "emailActionAria", isSignal: true, isRequired: false, transformFunction: null }, returnFocus: { classPropertyName: "returnFocus", publicName: "emailActionFocus", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "run()" }, properties: { "attr.aria-pressed": "ariaPressed()", "attr.aria-disabled": "ariaDisabled()" } }, exportAs: ["emailAction"], ngImport: i0 });
156
+ }
157
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ActionTrigger, decorators: [{
158
+ type: Directive,
159
+ args: [{
160
+ selector: '[emailAction]',
161
+ exportAs: 'emailAction',
162
+ host: {
163
+ '[attr.aria-pressed]': 'ariaPressed()',
164
+ '[attr.aria-disabled]': 'ariaDisabled()',
165
+ '(click)': 'run()',
166
+ },
167
+ }]
168
+ }], propDecorators: { action: [{ type: i0.Input, args: [{ isSignal: true, alias: "emailAction", required: true }] }], aria: [{ type: i0.Input, args: [{ isSignal: true, alias: "emailActionAria", required: false }] }], returnFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "emailActionFocus", required: false }] }] } });
169
+
170
+ /*
171
+ * Public API Surface of @h-k-dev/angular-email-editor/actions
172
+ */
173
+
174
+ /**
175
+ * Generated bundle index. Do not edit.
176
+ */
177
+
178
+ export { ActionTrigger, editorState, injectActions };
179
+ //# sourceMappingURL=h-k-dev-angular-email-editor-actions.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"h-k-dev-angular-email-editor-actions.mjs","sources":["../../../projects/angular-email-editor/actions/actions.ts","../../../projects/angular-email-editor/actions/action-trigger.ts","../../../projects/angular-email-editor/actions/public-api.ts","../../../projects/angular-email-editor/actions/h-k-dev-angular-email-editor-actions.ts"],"sourcesContent":["import { Injector, Signal, computed, effect, inject, signal, untracked } from '@angular/core';\nimport { EditorState } from 'prosemirror-state';\nimport { Editor, EditorAction, isActionEnabled } from '@h-k-dev/angular-email-editor';\n\n/**\n * An editor's state as a signal: the text, the selection, the stored marks —\n * whatever changed, the signal has the new one. It follows the editor it is\n * given, so handing it \"the source pane in code view, the email editor\n * otherwise\" is one `computed`; `undefined` while there is none.\n *\n * It listens through `editor.subscribe`, so the editor can already exist:\n * nothing has to be in the kit before the editor is made.\n *\n * Call in an injection context, or hand it an injector.\n */\nexport function editorState(\n editor: () => Editor | undefined,\n options: { injector?: Injector } = {},\n): Signal<EditorState | undefined> {\n const state = signal<EditorState | undefined>(undefined);\n effect(\n (onCleanup) => {\n const current = editor();\n state.set(current?.state);\n if (current) onCleanup(current.subscribe(() => state.set(current.state)));\n },\n { injector: options.injector },\n );\n return state.asReadonly();\n}\n\n/**\n * An action of the host's own: something that opens its UI — a link dialog,\n * a colour or table picker — rather than running a command. It has a `run`,\n * not a `command`, on purpose: a command is *asked* whether it would apply\n * (called without a dispatch), and a function that opens a dialog must never\n * be called to ask.\n *\n * Two ways in, by what it is:\n * - `host` — an action of its own, there for every editor: \"Link\", which\n * no extension can offer because it needs the host's dialog;\n * - `override` — the host's `run` *over* an action the kit declares, by its\n * id: a toolbar's \"Insert table\" that opens a size picker instead of\n * inserting one. It exists only where the kit's action does (so not in a\n * code view), and what it leaves out — when it is on, whether it can run —\n * is still the extension's answer.\n */\nexport interface HostAction {\n id: string;\n /** What triggering it does. Focus is the host's from here on: the caret\n is not sent back to the editor after it. */\n run: () => void;\n /** On at the selection — makes it a toggle. */\n isActive?: (state: EditorState) => boolean;\n /** Can run right now. Absent: the extension action's answer when it\n stands over one, else yes. */\n isEnabled?: (state: EditorState) => boolean;\n}\n\n/** An action bound to a live editor: what a button needs. */\nexport interface BoundAction {\n /** The action as its extension declared it — id, title, icon, keywords;\n `undefined` for a {@link HostAction} that stands alone. */\n readonly action: EditorAction | undefined;\n readonly id: string;\n /** It is the host's `run`: what happens after — where focus goes — is the\n host's too. */\n readonly external: boolean;\n /** On at the selection. Always false for an action with no \"on\". */\n readonly pressed: Signal<boolean>;\n /** Whether it is a toggle at all — whether `pressed` means anything. */\n readonly toggles: boolean;\n /** Cannot run right now. */\n readonly disabled: Signal<boolean>;\n /** Runs it on the editor; false when it did not apply. */\n run(): boolean;\n /** Puts the caret back in the editor it acts on. */\n focus(): void;\n}\n\n/** The actions of the editor being watched. */\nexport interface EditorActions {\n /** The action of that id — `undefined` while there is no editor, and when\n the editor's kit has no such action: a button for it has nothing to do.\n Reactive: it follows the editor. */\n get(id: string): BoundAction | undefined;\n /** Every action of the current editor, in kit order. */\n readonly all: Signal<readonly BoundAction[]>;\n /** The watched editor's state. */\n readonly state: Signal<EditorState | undefined>;\n}\n\n/**\n * The actions of an editor, bound and live — for a toolbar, a bubble menu,\n * a menu:\n *\n * readonly actions = injectActions(() => this.editor());\n *\n * <button [emailAction]=\"actions.get('bold')\">…</button>\n *\n * `editor` is read reactively: when it changes — a composer switching to its\n * code view — the actions are the new editor's. Its kit decides what exists:\n * the source pane offers the marks, so there a list button has no action\n * and shows disabled, with no routing written anywhere.\n *\n * `host` and `override` bring in the host's own — see {@link HostAction} — so\n * that one list serves a surface whose buttons are not all the editor's.\n *\n * Call in an injection context, or hand it an injector.\n */\nexport function injectActions(\n editor: () => Editor | undefined,\n options: {\n host?: readonly HostAction[];\n override?: Readonly<Record<string, Omit<HostAction, 'id'>>>;\n injector?: Injector;\n } = {},\n): EditorActions {\n const injector = options.injector ?? inject(Injector);\n const state = editorState(editor, { injector });\n\n const bind = (current: Editor, action?: EditorAction, over?: HostAction): BoundAction => {\n const isActive = over?.isActive ?? action?.isActive;\n const isEnabled = (now: EditorState) =>\n over?.isEnabled ? over.isEnabled(now) : action ? isActionEnabled(action, now) : true;\n return {\n action,\n id: (over ?? action)!.id,\n external: !!over,\n toggles: !!isActive,\n pressed: computed(() => {\n const now = state();\n return !!now && !!isActive?.(now);\n }),\n disabled: computed(() => {\n const now = state();\n return !now || !isEnabled(now);\n }),\n run: () => {\n if (!over) return current.exec(action!.command);\n over.run();\n return true;\n },\n focus: () => current.focus(),\n };\n };\n\n const bound = computed(() => {\n const current = editor();\n const byId = new Map<string, BoundAction>();\n // Binding reads no state: only the editor decides which actions exist.\n if (!current) return byId;\n const hosted = new Map((options.host ?? []).map((over) => [over.id, over]));\n for (const action of current.actions) {\n const given = options.override?.[action.id];\n // A host action under a kit's id is an override by another name.\n const over = given ? { id: action.id, ...given } : hosted.get(action.id);\n hosted.delete(action.id);\n byId.set(\n action.id,\n untracked(() => bind(current, action, over)),\n );\n }\n // What stands alone comes after the kit's own, in the order given.\n for (const over of hosted.values()) {\n byId.set(\n over.id,\n untracked(() => bind(current, undefined, over)),\n );\n }\n return byId;\n });\n\n return {\n get: (id) => bound().get(id),\n all: computed(() => [...bound().values()]),\n state,\n };\n}\n","import { Directive, booleanAttribute, computed, input } from '@angular/core';\nimport { BoundAction } from './actions';\n\n/**\n * Makes the host's own control the trigger of an editor action: a click runs\n * it, and the control says what the action's state is.\n *\n * <button mat-icon-button [emailAction]=\"actions.get('bold')\" #bold=\"emailAction\"\n * [disabled]=\"bold.disabled()\">\n * <mat-icon>format_bold</mat-icon>\n * </button>\n *\n * Behaviour only, and ARIA only where the host's widget has not spoken:\n * - `aria-pressed` for a toggle, and `aria-disabled` when it cannot run —\n * both off with `[emailActionAria]=\"false\"` for a widget that writes its own\n * (a Material button toggle);\n * - never the native `disabled`, a class or a style: how disabled *looks* is\n * the host's, bound from `disabled()` as above. A control that is only\n * `aria-disabled` stays reachable by keyboard, which is what a toolbar wants;\n * - a click on a disabled trigger runs nothing.\n *\n * After running, the caret goes back to the editor the action ran on — a\n * no-op for a pointer press under `emailKeepFocus`, the way back for a key\n * press in a toolbar. `[emailActionFocus]=\"false\"` leaves focus on the\n * control, as the ARIA toolbar pattern has it. A host's own action (a link\n * dialog, a picker) never has focus taken from what it opened.\n */\n@Directive({\n selector: '[emailAction]',\n exportAs: 'emailAction',\n host: {\n '[attr.aria-pressed]': 'ariaPressed()',\n '[attr.aria-disabled]': 'ariaDisabled()',\n '(click)': 'run()',\n },\n})\nexport class ActionTrigger {\n /** The action to trigger — `actions.get(id)`. `undefined` (no editor yet,\n or a kit without it) is a trigger with nothing to do: disabled. */\n readonly action = input.required<BoundAction | undefined>({ alias: 'emailAction' });\n\n /** Whether this directive writes `aria-pressed` and `aria-disabled`. */\n readonly aria = input(true, { alias: 'emailActionAria', transform: booleanAttribute });\n\n /** Whether the caret returns to the editor after the action ran. */\n readonly returnFocus = input(true, { alias: 'emailActionFocus', transform: booleanAttribute });\n\n /** On at the selection. */\n readonly pressed = computed(() => this.action()?.pressed() ?? false);\n\n /** Cannot run right now — or has no action at all. */\n readonly disabled = computed(() => this.action()?.disabled() ?? true);\n\n protected readonly ariaPressed = computed(() =>\n this.aria() && this.action()?.toggles ? this.pressed() : null,\n );\n\n protected readonly ariaDisabled = computed(() => (this.aria() && this.disabled() ? true : null));\n\n /** Runs the action, as a click does. False when it did not apply. */\n run(): boolean {\n const action = this.action();\n if (!action || this.disabled()) return false;\n const applied = action.run();\n // A host's own action opened something of the host's: focus is its now.\n if (this.returnFocus() && !action.external) action.focus();\n return applied;\n }\n}\n","/*\n * Public API Surface of @h-k-dev/angular-email-editor/actions\n */\n\nexport * from './actions';\nexport * from './action-trigger';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}