@h-k-dev/angular-email-editor 0.0.0-stage → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +383 -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 +1106 -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 +78 -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 +18820 -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 +234 -0
- package/types/h-k-dev-angular-email-editor-address-input.d.ts +449 -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 +46 -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,132 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { input, booleanAttribute, output, inject, ElementRef, computed, linkedSignal, afterNextRender, Directive } from '@angular/core';
|
|
3
|
+
import { selectedImage, selectedImageAlt } from '@h-k-dev/angular-email-editor';
|
|
4
|
+
import { editorState } from '@h-k-dev/angular-email-editor/actions';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Makes the host's own text field the alt text of the editor's selected
|
|
8
|
+
* image — the image a click selected, or a drag over it alone
|
|
9
|
+
* (`selectedImage`). Everything but the field and its words is done:
|
|
10
|
+
*
|
|
11
|
+
* <input dir="auto" [emailImageAlt]="editor" #alt="emailImageAlt"
|
|
12
|
+
* (emailImageAltClosed)="open.set(false)" />
|
|
13
|
+
* <button (click)="alt.apply()">Apply</button>
|
|
14
|
+
*
|
|
15
|
+
* - the field shows the image's alt, and again whenever another image is
|
|
16
|
+
* selected; it is focused and selected as it appears, so typing replaces
|
|
17
|
+
* it (`[emailImageAltAutoSelect]="false"` for a field that is always
|
|
18
|
+
* there, a side panel's);
|
|
19
|
+
* - Enter applies — trimmed, and an emptied field takes the alt off — and
|
|
20
|
+
* Escape puts the alt back as it was; both hand the caret back to the
|
|
21
|
+
* editor (`[emailImageAltFocus]="false"` keeps it) and say so through
|
|
22
|
+
* `emailImageAltClosed`: `true` applied, `false` not — the host's cue to
|
|
23
|
+
* close its popover. Neither fires mid-composition: an IME's own Enter
|
|
24
|
+
* and Escape stay the IME's;
|
|
25
|
+
* - `apply()` and `cancel()` are there for the host's buttons, `value()`
|
|
26
|
+
* and `active()` for its bindings (an Apply that waits for a change).
|
|
27
|
+
*
|
|
28
|
+
* The field's words and direction are the host's: its label, its
|
|
29
|
+
* placeholder, and `dir="auto"` so an alt in a right-to-left script reads
|
|
30
|
+
* the right way round. The image stays selected throughout — the editor
|
|
31
|
+
* keeps its selection while the field has focus — so the popover can sit
|
|
32
|
+
* where the host likes.
|
|
33
|
+
*/
|
|
34
|
+
class ImageAlt {
|
|
35
|
+
/** The editor whose selected image this field describes. */
|
|
36
|
+
editor = input.required({ ...(ngDevMode ? { debugName: "editor" } : /* istanbul ignore next */ {}), alias: 'emailImageAlt' });
|
|
37
|
+
/** Whether the field takes focus, its text selected, as it appears. */
|
|
38
|
+
autoSelect = input(true, { ...(ngDevMode ? { debugName: "autoSelect" } : /* istanbul ignore next */ {}), alias: 'emailImageAltAutoSelect',
|
|
39
|
+
transform: booleanAttribute });
|
|
40
|
+
/** Whether the caret returns to the editor after an apply or a cancel. */
|
|
41
|
+
returnFocus = input(true, { ...(ngDevMode ? { debugName: "returnFocus" } : /* istanbul ignore next */ {}), alias: 'emailImageAltFocus', transform: booleanAttribute });
|
|
42
|
+
/** Done with the field: `true` when the alt was applied, `false` when
|
|
43
|
+
not — cancelled, or no image to apply it to. */
|
|
44
|
+
closed = output({ alias: 'emailImageAltClosed' });
|
|
45
|
+
#element = inject(ElementRef)
|
|
46
|
+
.nativeElement;
|
|
47
|
+
#state = editorState(() => this.editor());
|
|
48
|
+
/** The selected image — where it is and what its alt says — or null. */
|
|
49
|
+
#image = computed(() => {
|
|
50
|
+
const state = this.#state();
|
|
51
|
+
const image = state && selectedImage(state);
|
|
52
|
+
return image ? { pos: image.pos, alt: selectedImageAlt(state) ?? '' } : null;
|
|
53
|
+
}, { ...(ngDevMode ? { debugName: "#image" } : /* istanbul ignore next */ {}), equal: (a, b) => a?.pos === b?.pos && a?.alt === b?.alt });
|
|
54
|
+
/** Whether an image is selected at all — there is nothing to describe
|
|
55
|
+
otherwise, and `apply()` does nothing. */
|
|
56
|
+
active = computed(() => this.#image() !== null, /* @ts-ignore */
|
|
57
|
+
...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
|
|
58
|
+
/**
|
|
59
|
+
* What the field says: the selected image's alt, until typing changes it
|
|
60
|
+
* — the draft. It starts over from the image whenever another one is
|
|
61
|
+
* selected or its alt changes elsewhere; typing changes no editor state,
|
|
62
|
+
* so it is never written over. Deliberately no write-back setter: the
|
|
63
|
+
* draft reaches the document once, on `apply()`, as one undo step — not
|
|
64
|
+
* a transaction per keystroke.
|
|
65
|
+
*/
|
|
66
|
+
value = linkedSignal(() => this.#image()?.alt ?? '', /* @ts-ignore */
|
|
67
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
68
|
+
constructor() {
|
|
69
|
+
// Focus is a DOM write, after the render that put the field (and its
|
|
70
|
+
// bound value) on the page.
|
|
71
|
+
afterNextRender({
|
|
72
|
+
write: () => {
|
|
73
|
+
if (!this.autoSelect())
|
|
74
|
+
return;
|
|
75
|
+
this.#element.focus();
|
|
76
|
+
this.#element.select();
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** Writes the draft to the selected image's alt. False when no image is
|
|
81
|
+
selected. */
|
|
82
|
+
apply() {
|
|
83
|
+
const applied = !!this.editor()?.commands['setImageAlt']?.(this.value());
|
|
84
|
+
this.#done(applied);
|
|
85
|
+
return applied;
|
|
86
|
+
}
|
|
87
|
+
/** Leaves the alt as it was, and the field with it. */
|
|
88
|
+
cancel() {
|
|
89
|
+
this.value.set(this.#image()?.alt ?? '');
|
|
90
|
+
this.#done(false);
|
|
91
|
+
}
|
|
92
|
+
onKey(event, apply) {
|
|
93
|
+
if (event.isComposing)
|
|
94
|
+
return;
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
if (apply)
|
|
97
|
+
this.apply();
|
|
98
|
+
else
|
|
99
|
+
this.cancel();
|
|
100
|
+
}
|
|
101
|
+
#done(applied) {
|
|
102
|
+
if (this.returnFocus())
|
|
103
|
+
this.editor()?.focus();
|
|
104
|
+
this.closed.emit(applied);
|
|
105
|
+
}
|
|
106
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ImageAlt, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
107
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: ImageAlt, isStandalone: true, selector: "input[emailImageAlt], textarea[emailImageAlt]", inputs: { editor: { classPropertyName: "editor", publicName: "emailImageAlt", isSignal: true, isRequired: true, transformFunction: null }, autoSelect: { classPropertyName: "autoSelect", publicName: "emailImageAltAutoSelect", isSignal: true, isRequired: false, transformFunction: null }, returnFocus: { classPropertyName: "returnFocus", publicName: "emailImageAltFocus", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closed: "emailImageAltClosed" }, host: { listeners: { "input": "value.set($any($event.target).value)", "keydown.enter": "onKey($event, true)", "keydown.escape": "onKey($event, false)" }, properties: { "value": "value()" } }, exportAs: ["emailImageAlt"], ngImport: i0 });
|
|
108
|
+
}
|
|
109
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: ImageAlt, decorators: [{
|
|
110
|
+
type: Directive,
|
|
111
|
+
args: [{
|
|
112
|
+
selector: 'input[emailImageAlt], textarea[emailImageAlt]',
|
|
113
|
+
exportAs: 'emailImageAlt',
|
|
114
|
+
host: {
|
|
115
|
+
'[value]': 'value()',
|
|
116
|
+
'(input)': 'value.set($any($event.target).value)',
|
|
117
|
+
'(keydown.enter)': 'onKey($event, true)',
|
|
118
|
+
'(keydown.escape)': 'onKey($event, false)',
|
|
119
|
+
},
|
|
120
|
+
}]
|
|
121
|
+
}], ctorParameters: () => [], propDecorators: { editor: [{ type: i0.Input, args: [{ isSignal: true, alias: "emailImageAlt", required: true }] }], autoSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "emailImageAltAutoSelect", required: false }] }], returnFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "emailImageAltFocus", required: false }] }], closed: [{ type: i0.Output, args: ["emailImageAltClosed"] }] } });
|
|
122
|
+
|
|
123
|
+
/*
|
|
124
|
+
* Public API Surface of @h-k-dev/angular-email-editor/image
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Generated bundle index. Do not edit.
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
export { ImageAlt };
|
|
132
|
+
//# sourceMappingURL=h-k-dev-angular-email-editor-image.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"h-k-dev-angular-email-editor-image.mjs","sources":["../../../projects/angular-email-editor/image/image-alt.ts","../../../projects/angular-email-editor/image/public-api.ts","../../../projects/angular-email-editor/image/h-k-dev-angular-email-editor-image.ts"],"sourcesContent":["import {\n Directive,\n ElementRef,\n afterNextRender,\n booleanAttribute,\n computed,\n inject,\n input,\n linkedSignal,\n output,\n} from '@angular/core';\nimport { Editor, selectedImage, selectedImageAlt } from '@h-k-dev/angular-email-editor';\nimport { editorState } from '@h-k-dev/angular-email-editor/actions';\n\n/**\n * Makes the host's own text field the alt text of the editor's selected\n * image — the image a click selected, or a drag over it alone\n * (`selectedImage`). Everything but the field and its words is done:\n *\n * <input dir=\"auto\" [emailImageAlt]=\"editor\" #alt=\"emailImageAlt\"\n * (emailImageAltClosed)=\"open.set(false)\" />\n * <button (click)=\"alt.apply()\">Apply</button>\n *\n * - the field shows the image's alt, and again whenever another image is\n * selected; it is focused and selected as it appears, so typing replaces\n * it (`[emailImageAltAutoSelect]=\"false\"` for a field that is always\n * there, a side panel's);\n * - Enter applies — trimmed, and an emptied field takes the alt off — and\n * Escape puts the alt back as it was; both hand the caret back to the\n * editor (`[emailImageAltFocus]=\"false\"` keeps it) and say so through\n * `emailImageAltClosed`: `true` applied, `false` not — the host's cue to\n * close its popover. Neither fires mid-composition: an IME's own Enter\n * and Escape stay the IME's;\n * - `apply()` and `cancel()` are there for the host's buttons, `value()`\n * and `active()` for its bindings (an Apply that waits for a change).\n *\n * The field's words and direction are the host's: its label, its\n * placeholder, and `dir=\"auto\"` so an alt in a right-to-left script reads\n * the right way round. The image stays selected throughout — the editor\n * keeps its selection while the field has focus — so the popover can sit\n * where the host likes.\n */\n@Directive({\n selector: 'input[emailImageAlt], textarea[emailImageAlt]',\n exportAs: 'emailImageAlt',\n host: {\n '[value]': 'value()',\n '(input)': 'value.set($any($event.target).value)',\n '(keydown.enter)': 'onKey($event, true)',\n '(keydown.escape)': 'onKey($event, false)',\n },\n})\nexport class ImageAlt {\n /** The editor whose selected image this field describes. */\n readonly editor = input.required<Editor | undefined>({ alias: 'emailImageAlt' });\n\n /** Whether the field takes focus, its text selected, as it appears. */\n readonly autoSelect = input(true, {\n alias: 'emailImageAltAutoSelect',\n transform: booleanAttribute,\n });\n\n /** Whether the caret returns to the editor after an apply or a cancel. */\n readonly returnFocus = input(true, { alias: 'emailImageAltFocus', transform: booleanAttribute });\n\n /** Done with the field: `true` when the alt was applied, `false` when\n not — cancelled, or no image to apply it to. */\n readonly closed = output<boolean>({ alias: 'emailImageAltClosed' });\n\n readonly #element = inject<ElementRef<HTMLInputElement | HTMLTextAreaElement>>(ElementRef)\n .nativeElement;\n\n readonly #state = editorState(() => this.editor());\n\n /** The selected image — where it is and what its alt says — or null. */\n readonly #image = computed(\n () => {\n const state = this.#state();\n const image = state && selectedImage(state);\n return image ? { pos: image.pos, alt: selectedImageAlt(state) ?? '' } : null;\n },\n { equal: (a, b) => a?.pos === b?.pos && a?.alt === b?.alt },\n );\n\n /** Whether an image is selected at all — there is nothing to describe\n otherwise, and `apply()` does nothing. */\n readonly active = computed(() => this.#image() !== null);\n\n /**\n * What the field says: the selected image's alt, until typing changes it\n * — the draft. It starts over from the image whenever another one is\n * selected or its alt changes elsewhere; typing changes no editor state,\n * so it is never written over. Deliberately no write-back setter: the\n * draft reaches the document once, on `apply()`, as one undo step — not\n * a transaction per keystroke.\n */\n readonly value = linkedSignal(() => this.#image()?.alt ?? '');\n\n constructor() {\n // Focus is a DOM write, after the render that put the field (and its\n // bound value) on the page.\n afterNextRender({\n write: () => {\n if (!this.autoSelect()) return;\n this.#element.focus();\n this.#element.select();\n },\n });\n }\n\n /** Writes the draft to the selected image's alt. False when no image is\n selected. */\n apply(): boolean {\n const applied = !!this.editor()?.commands['setImageAlt']?.(this.value());\n this.#done(applied);\n return applied;\n }\n\n /** Leaves the alt as it was, and the field with it. */\n cancel(): void {\n this.value.set(this.#image()?.alt ?? '');\n this.#done(false);\n }\n\n protected onKey(event: Event, apply: boolean): void {\n if ((event as KeyboardEvent).isComposing) return;\n event.preventDefault();\n if (apply) this.apply();\n else this.cancel();\n }\n\n #done(applied: boolean): void {\n if (this.returnFocus()) this.editor()?.focus();\n this.closed.emit(applied);\n }\n}\n","/*\n * Public API Surface of @h-k-dev/angular-email-editor/image\n */\n\nexport * from './image-alt';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, effect, DestroyRef, Directive } from '@angular/core';
|
|
3
|
+
import { MatFormFieldControl } from '@angular/material/form-field';
|
|
4
|
+
import { Subject } from 'rxjs';
|
|
5
|
+
import { AddressInput } from '@h-k-dev/angular-email-editor/address-input';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Makes an address input the control of an Angular Material form field —
|
|
9
|
+
* opt in per field, with the attribute:
|
|
10
|
+
*
|
|
11
|
+
* ```html
|
|
12
|
+
* <mat-form-field>
|
|
13
|
+
* <mat-label>To</mat-label>
|
|
14
|
+
* <div email-address-input emailMatFormField [formField]="envelope.to"></div>
|
|
15
|
+
* <mat-hint>Separate addresses with a comma</mat-hint>
|
|
16
|
+
* <mat-error>{{ envelope.to().errors()[0]?.message }}</mat-error>
|
|
17
|
+
* </mat-form-field>
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* The directive is its own object, provided as the field's
|
|
21
|
+
* `MatFormFieldControl` — so the input's `value`, `disabled` and `required`
|
|
22
|
+
* stay the signal-forms contract they are, and nothing of Material reaches
|
|
23
|
+
* the input itself. What the field asks, it reads off the input:
|
|
24
|
+
*
|
|
25
|
+
* - **Label** — `for` the input's text field (`id`); it floats while the
|
|
26
|
+
* field is focused or not `empty`: no chips *and* nothing typed.
|
|
27
|
+
* - **Errors** — `errorState` is invalid *and* touched: `mat-error` shows at
|
|
28
|
+
* the moment the input starts flagging itself, never before.
|
|
29
|
+
* - **Hints and errors are read out** — their ids join the text field's
|
|
30
|
+
* `aria-describedby`, beside any the host set itself.
|
|
31
|
+
* - **A click on the field** — anywhere in its box — puts the caret in.
|
|
32
|
+
*
|
|
33
|
+
* No `NgControl`: signal forms bind through `[formField]`, which the field
|
|
34
|
+
* does not need to know.
|
|
35
|
+
*
|
|
36
|
+
* The field's value is the input's — one header-form entry per recipient.
|
|
37
|
+
* A backend that keeps the header as one string gets it with
|
|
38
|
+
* `joinAddresses(list)` and hands it back through `splitAddresses(text)`
|
|
39
|
+
* (`@h-k-dev/angular-email-editor/address`); the pair round-trips entry for entry.
|
|
40
|
+
*/
|
|
41
|
+
class AddressFormField {
|
|
42
|
+
#input = inject(AddressInput);
|
|
43
|
+
/** Tells the field to look again — whenever anything it reads moves. */
|
|
44
|
+
stateChanges = new Subject();
|
|
45
|
+
controlType = 'email-address-input';
|
|
46
|
+
ngControl = null;
|
|
47
|
+
constructor() {
|
|
48
|
+
effect(() => {
|
|
49
|
+
// Everything the field reads, tracked: a change of any is news.
|
|
50
|
+
this.#input.value();
|
|
51
|
+
this.#input.text();
|
|
52
|
+
this.#input.focused();
|
|
53
|
+
this.#input.placeholder();
|
|
54
|
+
this.#input.required();
|
|
55
|
+
this.#input.disabled();
|
|
56
|
+
this.#input.invalid();
|
|
57
|
+
this.#input.touched();
|
|
58
|
+
this.stateChanges.next();
|
|
59
|
+
});
|
|
60
|
+
inject(DestroyRef).onDestroy(() => this.stateChanges.complete());
|
|
61
|
+
}
|
|
62
|
+
get value() {
|
|
63
|
+
return this.#input.value();
|
|
64
|
+
}
|
|
65
|
+
set value(value) {
|
|
66
|
+
this.#input.value.set(value ?? []);
|
|
67
|
+
}
|
|
68
|
+
get id() {
|
|
69
|
+
return this.#input.id;
|
|
70
|
+
}
|
|
71
|
+
get placeholder() {
|
|
72
|
+
return this.#input.placeholder();
|
|
73
|
+
}
|
|
74
|
+
get focused() {
|
|
75
|
+
return this.#input.focused();
|
|
76
|
+
}
|
|
77
|
+
get empty() {
|
|
78
|
+
return !this.#input.value().length && !this.#input.text();
|
|
79
|
+
}
|
|
80
|
+
get shouldLabelFloat() {
|
|
81
|
+
return this.focused || !this.empty;
|
|
82
|
+
}
|
|
83
|
+
get required() {
|
|
84
|
+
return this.#input.required();
|
|
85
|
+
}
|
|
86
|
+
get disabled() {
|
|
87
|
+
return this.#input.disabled();
|
|
88
|
+
}
|
|
89
|
+
get errorState() {
|
|
90
|
+
return this.#input.invalid() && this.#input.touched();
|
|
91
|
+
}
|
|
92
|
+
/** The host's own `aria-describedby`, which the field keeps in its list. */
|
|
93
|
+
get userAriaDescribedBy() {
|
|
94
|
+
return this.#input.ariaDescribedby() ?? undefined;
|
|
95
|
+
}
|
|
96
|
+
describedByIds = [];
|
|
97
|
+
setDescribedByIds(ids) {
|
|
98
|
+
this.describedByIds = ids;
|
|
99
|
+
this.#input.describe(ids);
|
|
100
|
+
}
|
|
101
|
+
onContainerClick() {
|
|
102
|
+
this.#input.focus();
|
|
103
|
+
}
|
|
104
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: AddressFormField, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
105
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.6", type: AddressFormField, isStandalone: true, selector: "[email-address-input][emailMatFormField]", providers: [{ provide: MatFormFieldControl, useExisting: AddressFormField }], ngImport: i0 });
|
|
106
|
+
}
|
|
107
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: AddressFormField, decorators: [{
|
|
108
|
+
type: Directive,
|
|
109
|
+
args: [{
|
|
110
|
+
selector: '[email-address-input][emailMatFormField]',
|
|
111
|
+
providers: [{ provide: MatFormFieldControl, useExisting: AddressFormField }],
|
|
112
|
+
}]
|
|
113
|
+
}], ctorParameters: () => [] });
|
|
114
|
+
|
|
115
|
+
/*
|
|
116
|
+
* Public API Surface of @h-k-dev/angular-email-editor/material
|
|
117
|
+
*/
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Generated bundle index. Do not edit.
|
|
121
|
+
*/
|
|
122
|
+
|
|
123
|
+
export { AddressFormField };
|
|
124
|
+
//# sourceMappingURL=h-k-dev-angular-email-editor-material.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"h-k-dev-angular-email-editor-material.mjs","sources":["../../../projects/angular-email-editor/material/address-form-field.ts","../../../projects/angular-email-editor/material/public-api.ts","../../../projects/angular-email-editor/material/h-k-dev-angular-email-editor-material.ts"],"sourcesContent":["import { DestroyRef, Directive, effect, inject } from '@angular/core';\nimport { MatFormFieldControl } from '@angular/material/form-field';\nimport { Subject } from 'rxjs';\nimport { AddressInput } from '@h-k-dev/angular-email-editor/address-input';\n\n/**\n * Makes an address input the control of an Angular Material form field —\n * opt in per field, with the attribute:\n *\n * ```html\n * <mat-form-field>\n * <mat-label>To</mat-label>\n * <div email-address-input emailMatFormField [formField]=\"envelope.to\"></div>\n * <mat-hint>Separate addresses with a comma</mat-hint>\n * <mat-error>{{ envelope.to().errors()[0]?.message }}</mat-error>\n * </mat-form-field>\n * ```\n *\n * The directive is its own object, provided as the field's\n * `MatFormFieldControl` — so the input's `value`, `disabled` and `required`\n * stay the signal-forms contract they are, and nothing of Material reaches\n * the input itself. What the field asks, it reads off the input:\n *\n * - **Label** — `for` the input's text field (`id`); it floats while the\n * field is focused or not `empty`: no chips *and* nothing typed.\n * - **Errors** — `errorState` is invalid *and* touched: `mat-error` shows at\n * the moment the input starts flagging itself, never before.\n * - **Hints and errors are read out** — their ids join the text field's\n * `aria-describedby`, beside any the host set itself.\n * - **A click on the field** — anywhere in its box — puts the caret in.\n *\n * No `NgControl`: signal forms bind through `[formField]`, which the field\n * does not need to know.\n *\n * The field's value is the input's — one header-form entry per recipient.\n * A backend that keeps the header as one string gets it with\n * `joinAddresses(list)` and hands it back through `splitAddresses(text)`\n * (`@h-k-dev/angular-email-editor/address`); the pair round-trips entry for entry.\n */\n@Directive({\n selector: '[email-address-input][emailMatFormField]',\n providers: [{ provide: MatFormFieldControl, useExisting: AddressFormField }],\n})\nexport class AddressFormField implements MatFormFieldControl<string[]> {\n readonly #input = inject(AddressInput);\n\n /** Tells the field to look again — whenever anything it reads moves. */\n readonly stateChanges = new Subject<void>();\n\n readonly controlType = 'email-address-input';\n readonly ngControl = null;\n\n constructor() {\n effect(() => {\n // Everything the field reads, tracked: a change of any is news.\n this.#input.value();\n this.#input.text();\n this.#input.focused();\n this.#input.placeholder();\n this.#input.required();\n this.#input.disabled();\n this.#input.invalid();\n this.#input.touched();\n this.stateChanges.next();\n });\n inject(DestroyRef).onDestroy(() => this.stateChanges.complete());\n }\n\n get value(): string[] {\n return this.#input.value();\n }\n\n set value(value: string[] | null) {\n this.#input.value.set(value ?? []);\n }\n\n get id(): string {\n return this.#input.id;\n }\n\n get placeholder(): string {\n return this.#input.placeholder();\n }\n\n get focused(): boolean {\n return this.#input.focused();\n }\n\n get empty(): boolean {\n return !this.#input.value().length && !this.#input.text();\n }\n\n get shouldLabelFloat(): boolean {\n return this.focused || !this.empty;\n }\n\n get required(): boolean {\n return this.#input.required();\n }\n\n get disabled(): boolean {\n return this.#input.disabled();\n }\n\n get errorState(): boolean {\n return this.#input.invalid() && this.#input.touched();\n }\n\n /** The host's own `aria-describedby`, which the field keeps in its list. */\n get userAriaDescribedBy(): string | undefined {\n return this.#input.ariaDescribedby() ?? undefined;\n }\n\n describedByIds: string[] = [];\n\n setDescribedByIds(ids: string[]): void {\n this.describedByIds = ids;\n this.#input.describe(ids);\n }\n\n onContainerClick(): void {\n this.#input.focus();\n }\n}\n","/*\n * Public API Surface of @h-k-dev/angular-email-editor/material\n */\n\nexport * from './address-form-field';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { layout, prepare } from '@chenglou/pretext';
|
|
2
|
+
import { measureRichInlineStats, prepareRichInline } from '@chenglou/pretext/rich-inline';
|
|
3
|
+
import { Plugin, PluginKey } from 'prosemirror-state';
|
|
4
|
+
import { defineExtension } from '@h-k-dev/angular-email-editor';
|
|
5
|
+
|
|
6
|
+
/** Browser default h1-h6 sizes; the editor stylesheet doesn't restyle them. */
|
|
7
|
+
const HEADING_SCALE = [2, 1.5, 1.17, 1, 0.83, 0.67];
|
|
8
|
+
const cssPx = (style, property, fallback) => {
|
|
9
|
+
const value = parseFloat(style.getPropertyValue(property));
|
|
10
|
+
return Number.isFinite(value) ? value : fallback;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Reads the typography pretext needs from the editor's computed style. One
|
|
14
|
+
* DOM read at startup (and again once webfonts settle) — never per keystroke.
|
|
15
|
+
*/
|
|
16
|
+
function readTypography(dom) {
|
|
17
|
+
const style = getComputedStyle(dom);
|
|
18
|
+
const fontSize = parseFloat(style.fontSize) || 16;
|
|
19
|
+
// `line-height: normal` parses as NaN; browsers render it at roughly 1.2.
|
|
20
|
+
const lineHeight = parseFloat(style.lineHeight) || fontSize * 1.2;
|
|
21
|
+
const innerSpacing = cssPx(style, '--mat-sys-inner-spacing', 16);
|
|
22
|
+
return {
|
|
23
|
+
// Pretext refuses a font string the canvas would ignore: an editor with
|
|
24
|
+
// no family in its computed style (detached, unstyled) measures in the
|
|
25
|
+
// browser's sans-serif rather than not at all.
|
|
26
|
+
fontFamily: style.fontFamily.trim() || 'sans-serif',
|
|
27
|
+
fontSize,
|
|
28
|
+
fontWeight: parseInt(style.fontWeight, 10) || 400,
|
|
29
|
+
fontStyle: style.fontStyle || 'normal',
|
|
30
|
+
lineHeight,
|
|
31
|
+
blockGap: cssPx(style, '--prose-gap', fontSize),
|
|
32
|
+
headingScale: HEADING_SCALE,
|
|
33
|
+
indents: {
|
|
34
|
+
blockquote: innerSpacing + 3, // padding-inline-start + border
|
|
35
|
+
list: 1.5 * fontSize, // ul/ol padding-inline-start
|
|
36
|
+
listItemGap: 0.25 * fontSize, // li + li margin-block-start
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const createBlockMeasureCache = () => new WeakMap();
|
|
41
|
+
/**
|
|
42
|
+
* Measures the whole document at the given content width. Pure arithmetic
|
|
43
|
+
* over cached glyph widths — no DOM access.
|
|
44
|
+
*/
|
|
45
|
+
function measureDoc(doc, contentWidth, typography, cache) {
|
|
46
|
+
const metrics = { words: 0, characters: 0, lines: 0, estimatedHeight: 0 };
|
|
47
|
+
const width = Math.max(contentWidth, 1);
|
|
48
|
+
const visitChildren = (parent, availableWidth, gap) => {
|
|
49
|
+
let index = 0;
|
|
50
|
+
parent.forEach((child) => {
|
|
51
|
+
if (index++ > 0)
|
|
52
|
+
metrics.estimatedHeight += gap;
|
|
53
|
+
visit(child, availableWidth);
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
const visit = (node, availableWidth) => {
|
|
57
|
+
if (node.isTextblock) {
|
|
58
|
+
const measured = measureTextblock(node, availableWidth, typography, cache);
|
|
59
|
+
metrics.words += measured.words;
|
|
60
|
+
metrics.characters += measured.characters;
|
|
61
|
+
metrics.lines += measured.lastLines;
|
|
62
|
+
metrics.estimatedHeight += measured.lastLines * measured.lineHeight;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
switch (node.type.name) {
|
|
66
|
+
case 'blockquote':
|
|
67
|
+
visitChildren(node, availableWidth - typography.indents.blockquote, 0);
|
|
68
|
+
break;
|
|
69
|
+
case 'bulletList':
|
|
70
|
+
case 'orderedList':
|
|
71
|
+
visitChildren(node, availableWidth - typography.indents.list, typography.indents.listItemGap);
|
|
72
|
+
break;
|
|
73
|
+
default:
|
|
74
|
+
// List items and future wrappers: measure content at full width.
|
|
75
|
+
if (node.childCount)
|
|
76
|
+
visitChildren(node, availableWidth, 0);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
visitChildren(doc, width, typography.blockGap);
|
|
81
|
+
metrics.estimatedHeight = Math.round(metrics.estimatedHeight);
|
|
82
|
+
return metrics;
|
|
83
|
+
}
|
|
84
|
+
function measureTextblock(node, width, typography, cache) {
|
|
85
|
+
const maxWidth = Math.max(width, 1);
|
|
86
|
+
let cached = cache.get(node);
|
|
87
|
+
if (!cached) {
|
|
88
|
+
cached = prepareBlock(node, typography);
|
|
89
|
+
cache.set(node, cached);
|
|
90
|
+
}
|
|
91
|
+
if (cached.lastWidth !== maxWidth) {
|
|
92
|
+
cached.lastLines = countLines(cached.block, maxWidth, cached.lineHeight);
|
|
93
|
+
cached.lastWidth = maxWidth;
|
|
94
|
+
}
|
|
95
|
+
return cached;
|
|
96
|
+
}
|
|
97
|
+
function countLines(block, maxWidth, lineHeight) {
|
|
98
|
+
if (block.kind === 'plain') {
|
|
99
|
+
// Browsers size an empty block to one line-height; pretext reports 0.
|
|
100
|
+
return Math.max(1, layout(block.prepared, maxWidth, lineHeight).lineCount);
|
|
101
|
+
}
|
|
102
|
+
return block.segments.reduce((lines, segment) => lines + Math.max(1, measureRichInlineStats(segment, maxWidth).lineCount), 0);
|
|
103
|
+
}
|
|
104
|
+
function blockFontFor(node, t) {
|
|
105
|
+
if (node.type.name === 'heading') {
|
|
106
|
+
const level = node.attrs['level'] ?? 1;
|
|
107
|
+
const size = t.fontSize * (t.headingScale[level - 1] ?? 1);
|
|
108
|
+
return {
|
|
109
|
+
size,
|
|
110
|
+
weight: 700,
|
|
111
|
+
style: t.fontStyle,
|
|
112
|
+
lineHeight: size * (t.lineHeight / t.fontSize),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return { size: t.fontSize, weight: t.fontWeight, style: t.fontStyle, lineHeight: t.lineHeight };
|
|
116
|
+
}
|
|
117
|
+
/** Canvas `font` shorthand, the format pretext's `prepare()` expects. */
|
|
118
|
+
const fontShorthand = (style, weight, size, family) => `${style === 'normal' ? '' : `${style} `}${weight} ${size}px ${family}`;
|
|
119
|
+
function prepareBlock(node, typography) {
|
|
120
|
+
const base = blockFontFor(node, typography);
|
|
121
|
+
const baseFont = fontShorthand(base.style, base.weight, base.size, typography.fontFamily);
|
|
122
|
+
// Inline runs split on hard breaks; each segment lays out as its own line(s).
|
|
123
|
+
const segments = [[]];
|
|
124
|
+
let words = 0;
|
|
125
|
+
let characters = 0;
|
|
126
|
+
let mixedFonts = false;
|
|
127
|
+
node.forEach((child) => {
|
|
128
|
+
if (child.type.name === 'hardBreak') {
|
|
129
|
+
segments.push([]);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!child.isText || !child.text)
|
|
133
|
+
return;
|
|
134
|
+
const bold = child.marks.some((mark) => mark.type.name === 'bold');
|
|
135
|
+
const italic = child.marks.some((mark) => mark.type.name === 'italic');
|
|
136
|
+
const font = bold || italic
|
|
137
|
+
? fontShorthand(italic ? 'italic' : base.style, bold ? 700 : base.weight, base.size, typography.fontFamily)
|
|
138
|
+
: baseFont;
|
|
139
|
+
if (font !== baseFont)
|
|
140
|
+
mixedFonts = true;
|
|
141
|
+
segments[segments.length - 1].push({ text: child.text, font });
|
|
142
|
+
words += child.text.split(/\s+/).filter(Boolean).length;
|
|
143
|
+
characters += child.text.length;
|
|
144
|
+
});
|
|
145
|
+
const block = mixedFonts
|
|
146
|
+
? {
|
|
147
|
+
kind: 'rich',
|
|
148
|
+
segments: segments.map((runs) => prepareRichInline(runs, { whiteSpace: 'pre-wrap' })),
|
|
149
|
+
}
|
|
150
|
+
: {
|
|
151
|
+
// Single font: `prepare()` with pre-wrap mirrors the editor CSS
|
|
152
|
+
// exactly, and hard breaks fold into the text as `\n`.
|
|
153
|
+
kind: 'plain',
|
|
154
|
+
prepared: prepare(segments.map((runs) => runs.map((run) => run.text).join('')).join('\n'), baseFont, { whiteSpace: 'pre-wrap' }),
|
|
155
|
+
};
|
|
156
|
+
return { block, lineHeight: base.lineHeight, words, characters, lastWidth: -1, lastLines: 0 };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Live document metrics (words, lines, estimated height) computed with
|
|
161
|
+
* `@chenglou/pretext` — pure arithmetic over cached glyph widths, no DOM
|
|
162
|
+
* measurement on the keystroke path. Unchanged blocks are never re-measured:
|
|
163
|
+
* the cache keys on ProseMirror's immutable nodes.
|
|
164
|
+
*
|
|
165
|
+
* Metrics are an extra, never a dependency of the editor: a measurement
|
|
166
|
+
* that throws is reported through `onError` and skipped, and without a
|
|
167
|
+
* `ResizeObserver` the width read at mount stands.
|
|
168
|
+
*/
|
|
169
|
+
const createTextMetrics = (options) => defineExtension({
|
|
170
|
+
name: 'textMetrics',
|
|
171
|
+
plugins: () => [
|
|
172
|
+
new Plugin({
|
|
173
|
+
key: new PluginKey('textMetrics'),
|
|
174
|
+
view: (view) => createTextMetricsView(view, options),
|
|
175
|
+
}),
|
|
176
|
+
],
|
|
177
|
+
});
|
|
178
|
+
function createTextMetricsView(view, { onMetrics, onError }) {
|
|
179
|
+
let typography = readTypography(view.dom);
|
|
180
|
+
let cache = createBlockMeasureCache();
|
|
181
|
+
// One startup read; afterwards the observer reports widths, so resizes
|
|
182
|
+
// and keystrokes alike never force a layout reflow.
|
|
183
|
+
let width = view.dom.clientWidth;
|
|
184
|
+
let destroyed = false;
|
|
185
|
+
const measure = (currentView = view) => {
|
|
186
|
+
let metrics;
|
|
187
|
+
try {
|
|
188
|
+
metrics = measureDoc(currentView.state.doc, width, typography, cache);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
onError?.(error);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
onMetrics(metrics);
|
|
195
|
+
};
|
|
196
|
+
const observer = typeof ResizeObserver === 'undefined'
|
|
197
|
+
? null
|
|
198
|
+
: new ResizeObserver((entries) => {
|
|
199
|
+
const next = entries[entries.length - 1].contentRect.width;
|
|
200
|
+
if (next === width || next === 0)
|
|
201
|
+
return;
|
|
202
|
+
width = next;
|
|
203
|
+
// Prepared glyph widths survive a resize; only the line arithmetic reruns.
|
|
204
|
+
measure();
|
|
205
|
+
});
|
|
206
|
+
observer?.observe(view.dom);
|
|
207
|
+
// Canvas measurements taken before webfonts finish loading are off;
|
|
208
|
+
// re-prepare everything once the real fonts are in.
|
|
209
|
+
document.fonts?.ready.then(() => {
|
|
210
|
+
if (destroyed)
|
|
211
|
+
return;
|
|
212
|
+
typography = readTypography(view.dom);
|
|
213
|
+
cache = createBlockMeasureCache();
|
|
214
|
+
measure();
|
|
215
|
+
});
|
|
216
|
+
measure();
|
|
217
|
+
return {
|
|
218
|
+
update(currentView, prevState) {
|
|
219
|
+
if (prevState && prevState.doc.eq(currentView.state.doc))
|
|
220
|
+
return;
|
|
221
|
+
measure(currentView);
|
|
222
|
+
},
|
|
223
|
+
destroy() {
|
|
224
|
+
destroyed = true;
|
|
225
|
+
observer?.disconnect();
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/*
|
|
231
|
+
* Public API Surface of @h-k-dev/angular-email-editor/metrics
|
|
232
|
+
*
|
|
233
|
+
* Its own entry because it is the only part of the library that needs
|
|
234
|
+
* `@chenglou/pretext` — an optional peer: a host without word counts never
|
|
235
|
+
* installs it.
|
|
236
|
+
*/
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Generated bundle index. Do not edit.
|
|
240
|
+
*/
|
|
241
|
+
|
|
242
|
+
export { createBlockMeasureCache, createTextMetrics, measureDoc, readTypography };
|
|
243
|
+
//# sourceMappingURL=h-k-dev-angular-email-editor-metrics.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"h-k-dev-angular-email-editor-metrics.mjs","sources":["../../../projects/angular-email-editor/metrics/measurement.ts","../../../projects/angular-email-editor/metrics/text-metrics.ts","../../../projects/angular-email-editor/metrics/public-api.ts","../../../projects/angular-email-editor/metrics/h-k-dev-angular-email-editor-metrics.ts"],"sourcesContent":["import { Node as PMNode } from 'prosemirror-model';\nimport { PreparedText, layout, prepare } from '@chenglou/pretext';\nimport {\n PreparedRichInline,\n RichInlineItem,\n measureRichInlineStats,\n prepareRichInline,\n} from '@chenglou/pretext/rich-inline';\n\n/**\n * Math-based text measurement for the editor document, built on\n * `@chenglou/pretext`. Block heights and line counts are computed from cached\n * canvas glyph widths instead of DOM reads (`offsetHeight`,\n * `getBoundingClientRect`), so measuring never triggers a layout reflow.\n */\n\n/** Typography of the live editor surface, read from computed style once. */\nexport interface EditorTypography {\n fontFamily: string;\n /** Body text size in px. */\n fontSize: number;\n fontWeight: number;\n fontStyle: string;\n /** Body line height in px. */\n lineHeight: number;\n /** Vertical gap between sibling top-level blocks (`--prose-gap`), px. */\n blockGap: number;\n /** Heading font-size factors for levels 1-6, relative to the body size. */\n headingScale: readonly number[];\n /** Horizontal space consumed by wrapper blocks, px. */\n indents: {\n blockquote: number;\n list: number;\n /** Vertical gap between adjacent list items, px. */\n listItemGap: number;\n };\n}\n\n/** Browser default h1-h6 sizes; the editor stylesheet doesn't restyle them. */\nconst HEADING_SCALE = [2, 1.5, 1.17, 1, 0.83, 0.67] as const;\n\nconst cssPx = (style: CSSStyleDeclaration, property: string, fallback: number): number => {\n const value = parseFloat(style.getPropertyValue(property));\n return Number.isFinite(value) ? value : fallback;\n};\n\n/**\n * Reads the typography pretext needs from the editor's computed style. One\n * DOM read at startup (and again once webfonts settle) — never per keystroke.\n */\nexport function readTypography(dom: HTMLElement): EditorTypography {\n const style = getComputedStyle(dom);\n const fontSize = parseFloat(style.fontSize) || 16;\n // `line-height: normal` parses as NaN; browsers render it at roughly 1.2.\n const lineHeight = parseFloat(style.lineHeight) || fontSize * 1.2;\n const innerSpacing = cssPx(style, '--mat-sys-inner-spacing', 16);\n\n return {\n // Pretext refuses a font string the canvas would ignore: an editor with\n // no family in its computed style (detached, unstyled) measures in the\n // browser's sans-serif rather than not at all.\n fontFamily: style.fontFamily.trim() || 'sans-serif',\n fontSize,\n fontWeight: parseInt(style.fontWeight, 10) || 400,\n fontStyle: style.fontStyle || 'normal',\n lineHeight,\n blockGap: cssPx(style, '--prose-gap', fontSize),\n headingScale: HEADING_SCALE,\n indents: {\n blockquote: innerSpacing + 3, // padding-inline-start + border\n list: 1.5 * fontSize, // ul/ol padding-inline-start\n listItemGap: 0.25 * fontSize, // li + li margin-block-start\n },\n };\n}\n\nexport interface TextMetrics {\n words: number;\n characters: number;\n /** Wrapped line count across all text blocks at the current width. */\n lines: number;\n /**\n * Estimated rendered height of the document in px, block gaps included.\n * Text-only: atom blocks (images) contribute no height since their\n * rendered size isn't knowable without loading them.\n */\n estimatedHeight: number;\n}\n\n/**\n * Prepared measurements for one textblock. Single-font blocks (the common\n * case in email) go through `prepare()` with `pre-wrap`, matching the editor\n * CSS exactly — hard breaks become `\\n` in one prepared handle. Blocks with\n * mark-dependent fonts (bold/italic) use the rich-inline API, one prepared\n * handle per hard-break segment — `pre-wrap` too, so a bold word's line\n * breaks and spaces count exactly as the plain line's.\n */\ntype PreparedBlock =\n { kind: 'plain'; prepared: PreparedText } | { kind: 'rich'; segments: PreparedRichInline[] };\n\ninterface CachedBlock {\n block: PreparedBlock;\n lineHeight: number;\n words: number;\n characters: number;\n /** Line count memoised for the last width measured at. */\n lastWidth: number;\n lastLines: number;\n}\n\n/**\n * Measurement cache keyed on ProseMirror's immutable nodes: blocks untouched\n * by a transaction keep their node identity, so across keystrokes only the\n * edited block is ever re-prepared. Create one per editor view; replace it\n * wholesale to invalidate (e.g. after webfonts load).\n */\nexport type BlockMeasureCache = WeakMap<PMNode, CachedBlock>;\n\nexport const createBlockMeasureCache = (): BlockMeasureCache => new WeakMap();\n\n/**\n * Measures the whole document at the given content width. Pure arithmetic\n * over cached glyph widths — no DOM access.\n */\nexport function measureDoc(\n doc: PMNode,\n contentWidth: number,\n typography: EditorTypography,\n cache: BlockMeasureCache,\n): TextMetrics {\n const metrics: TextMetrics = { words: 0, characters: 0, lines: 0, estimatedHeight: 0 };\n const width = Math.max(contentWidth, 1);\n\n const visitChildren = (parent: PMNode, availableWidth: number, gap: number) => {\n let index = 0;\n parent.forEach((child) => {\n if (index++ > 0) metrics.estimatedHeight += gap;\n visit(child, availableWidth);\n });\n };\n\n const visit = (node: PMNode, availableWidth: number) => {\n if (node.isTextblock) {\n const measured = measureTextblock(node, availableWidth, typography, cache);\n metrics.words += measured.words;\n metrics.characters += measured.characters;\n metrics.lines += measured.lastLines;\n metrics.estimatedHeight += measured.lastLines * measured.lineHeight;\n return;\n }\n\n switch (node.type.name) {\n case 'blockquote':\n visitChildren(node, availableWidth - typography.indents.blockquote, 0);\n break;\n case 'bulletList':\n case 'orderedList':\n visitChildren(\n node,\n availableWidth - typography.indents.list,\n typography.indents.listItemGap,\n );\n break;\n default:\n // List items and future wrappers: measure content at full width.\n if (node.childCount) visitChildren(node, availableWidth, 0);\n break;\n }\n };\n\n visitChildren(doc, width, typography.blockGap);\n metrics.estimatedHeight = Math.round(metrics.estimatedHeight);\n return metrics;\n}\n\nfunction measureTextblock(\n node: PMNode,\n width: number,\n typography: EditorTypography,\n cache: BlockMeasureCache,\n): CachedBlock {\n const maxWidth = Math.max(width, 1);\n\n let cached = cache.get(node);\n if (!cached) {\n cached = prepareBlock(node, typography);\n cache.set(node, cached);\n }\n\n if (cached.lastWidth !== maxWidth) {\n cached.lastLines = countLines(cached.block, maxWidth, cached.lineHeight);\n cached.lastWidth = maxWidth;\n }\n\n return cached;\n}\n\nfunction countLines(block: PreparedBlock, maxWidth: number, lineHeight: number): number {\n if (block.kind === 'plain') {\n // Browsers size an empty block to one line-height; pretext reports 0.\n return Math.max(1, layout(block.prepared, maxWidth, lineHeight).lineCount);\n }\n return block.segments.reduce(\n (lines, segment) => lines + Math.max(1, measureRichInlineStats(segment, maxWidth).lineCount),\n 0,\n );\n}\n\ninterface BlockFont {\n size: number;\n weight: number;\n style: string;\n lineHeight: number;\n}\n\nfunction blockFontFor(node: PMNode, t: EditorTypography): BlockFont {\n if (node.type.name === 'heading') {\n const level: number = node.attrs['level'] ?? 1;\n const size = t.fontSize * (t.headingScale[level - 1] ?? 1);\n return {\n size,\n weight: 700,\n style: t.fontStyle,\n lineHeight: size * (t.lineHeight / t.fontSize),\n };\n }\n return { size: t.fontSize, weight: t.fontWeight, style: t.fontStyle, lineHeight: t.lineHeight };\n}\n\n/** Canvas `font` shorthand, the format pretext's `prepare()` expects. */\nconst fontShorthand = (style: string, weight: number, size: number, family: string): string =>\n `${style === 'normal' ? '' : `${style} `}${weight} ${size}px ${family}`;\n\nfunction prepareBlock(node: PMNode, typography: EditorTypography): CachedBlock {\n const base = blockFontFor(node, typography);\n const baseFont = fontShorthand(base.style, base.weight, base.size, typography.fontFamily);\n\n // Inline runs split on hard breaks; each segment lays out as its own line(s).\n const segments: RichInlineItem[][] = [[]];\n let words = 0;\n let characters = 0;\n let mixedFonts = false;\n\n node.forEach((child) => {\n if (child.type.name === 'hardBreak') {\n segments.push([]);\n return;\n }\n if (!child.isText || !child.text) return;\n\n const bold = child.marks.some((mark) => mark.type.name === 'bold');\n const italic = child.marks.some((mark) => mark.type.name === 'italic');\n const font =\n bold || italic\n ? fontShorthand(\n italic ? 'italic' : base.style,\n bold ? 700 : base.weight,\n base.size,\n typography.fontFamily,\n )\n : baseFont;\n\n if (font !== baseFont) mixedFonts = true;\n segments[segments.length - 1].push({ text: child.text, font });\n words += child.text.split(/\\s+/).filter(Boolean).length;\n characters += child.text.length;\n });\n\n const block: PreparedBlock = mixedFonts\n ? {\n kind: 'rich',\n segments: segments.map((runs) => prepareRichInline(runs, { whiteSpace: 'pre-wrap' })),\n }\n : {\n // Single font: `prepare()` with pre-wrap mirrors the editor CSS\n // exactly, and hard breaks fold into the text as `\\n`.\n kind: 'plain',\n prepared: prepare(\n segments.map((runs) => runs.map((run) => run.text).join('')).join('\\n'),\n baseFont,\n { whiteSpace: 'pre-wrap' },\n ),\n };\n\n return { block, lineHeight: base.lineHeight, words, characters, lastWidth: -1, lastLines: 0 };\n}\n","import { EditorState, Plugin, PluginKey } from 'prosemirror-state';\nimport { EditorView } from 'prosemirror-view';\nimport { FunctionalExtension, defineExtension } from '@h-k-dev/angular-email-editor';\nimport { TextMetrics, createBlockMeasureCache, measureDoc, readTypography } from './measurement';\n\nexport interface TextMetricsOptions {\n /** Called with fresh metrics after every doc change and width resize. */\n onMetrics: (metrics: TextMetrics) => void;\n /** Called when a measurement fails — no canvas to measure with (a server\n render, jsdom), or a font the browser cannot use. The editor goes on\n unharmed; that measurement reports nothing, and the next change, or\n the web fonts arriving, tries again. */\n onError?: (error: unknown) => void;\n}\n\n/**\n * Live document metrics (words, lines, estimated height) computed with\n * `@chenglou/pretext` — pure arithmetic over cached glyph widths, no DOM\n * measurement on the keystroke path. Unchanged blocks are never re-measured:\n * the cache keys on ProseMirror's immutable nodes.\n *\n * Metrics are an extra, never a dependency of the editor: a measurement\n * that throws is reported through `onError` and skipped, and without a\n * `ResizeObserver` the width read at mount stands.\n */\nexport const createTextMetrics = (options: TextMetricsOptions): FunctionalExtension =>\n defineExtension({\n name: 'textMetrics',\n plugins: () => [\n new Plugin({\n key: new PluginKey('textMetrics'),\n view: (view) => createTextMetricsView(view, options),\n }),\n ],\n });\n\nfunction createTextMetricsView(view: EditorView, { onMetrics, onError }: TextMetricsOptions) {\n let typography = readTypography(view.dom);\n let cache = createBlockMeasureCache();\n // One startup read; afterwards the observer reports widths, so resizes\n // and keystrokes alike never force a layout reflow.\n let width = view.dom.clientWidth;\n let destroyed = false;\n\n const measure = (currentView: EditorView = view) => {\n let metrics: TextMetrics;\n try {\n metrics = measureDoc(currentView.state.doc, width, typography, cache);\n } catch (error) {\n onError?.(error);\n return;\n }\n onMetrics(metrics);\n };\n\n const observer =\n typeof ResizeObserver === 'undefined'\n ? null\n : new ResizeObserver((entries) => {\n const next = entries[entries.length - 1].contentRect.width;\n if (next === width || next === 0) return;\n width = next;\n // Prepared glyph widths survive a resize; only the line arithmetic reruns.\n measure();\n });\n observer?.observe(view.dom);\n\n // Canvas measurements taken before webfonts finish loading are off;\n // re-prepare everything once the real fonts are in.\n document.fonts?.ready.then(() => {\n if (destroyed) return;\n typography = readTypography(view.dom);\n cache = createBlockMeasureCache();\n measure();\n });\n\n measure();\n\n return {\n update(currentView: EditorView, prevState?: EditorState) {\n if (prevState && prevState.doc.eq(currentView.state.doc)) return;\n measure(currentView);\n },\n destroy() {\n destroyed = true;\n observer?.disconnect();\n },\n };\n}\n","/*\n * Public API Surface of @h-k-dev/angular-email-editor/metrics\n *\n * Its own entry because it is the only part of the library that needs\n * `@chenglou/pretext` — an optional peer: a host without word counts never\n * installs it.\n */\n\nexport * from './measurement';\nexport * from './text-metrics';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|