@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.
- package/README.md +127 -2
- 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 +133 -3
- 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
package/README.md
CHANGED
|
@@ -1,3 +1,128 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Angular Email Editor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@h-k-dev/angular-email-editor)
|
|
4
|
+
[](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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"}
|