@mk-kit/ui 0.34.0

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -0
  3. package/block-editor/README.md +254 -0
  4. package/fesm2022/mk-kit-ui-block-editor.mjs +2158 -0
  5. package/fesm2022/mk-kit-ui-block-editor.mjs.map +1 -0
  6. package/fesm2022/mk-kit-ui-button.mjs +81 -0
  7. package/fesm2022/mk-kit-ui-button.mjs.map +1 -0
  8. package/fesm2022/mk-kit-ui-checkbox.mjs +136 -0
  9. package/fesm2022/mk-kit-ui-checkbox.mjs.map +1 -0
  10. package/fesm2022/mk-kit-ui-chip.mjs +122 -0
  11. package/fesm2022/mk-kit-ui-chip.mjs.map +1 -0
  12. package/fesm2022/mk-kit-ui-context-menu.mjs +144 -0
  13. package/fesm2022/mk-kit-ui-context-menu.mjs.map +1 -0
  14. package/fesm2022/mk-kit-ui-core.mjs +1576 -0
  15. package/fesm2022/mk-kit-ui-core.mjs.map +1 -0
  16. package/fesm2022/mk-kit-ui-data.mjs +6055 -0
  17. package/fesm2022/mk-kit-ui-data.mjs.map +1 -0
  18. package/fesm2022/mk-kit-ui-datetime.mjs +3409 -0
  19. package/fesm2022/mk-kit-ui-datetime.mjs.map +1 -0
  20. package/fesm2022/mk-kit-ui-directives.mjs +1779 -0
  21. package/fesm2022/mk-kit-ui-directives.mjs.map +1 -0
  22. package/fesm2022/mk-kit-ui-dnd.mjs +1073 -0
  23. package/fesm2022/mk-kit-ui-dnd.mjs.map +1 -0
  24. package/fesm2022/mk-kit-ui-feedback.mjs +2426 -0
  25. package/fesm2022/mk-kit-ui-feedback.mjs.map +1 -0
  26. package/fesm2022/mk-kit-ui-forms.mjs +9208 -0
  27. package/fesm2022/mk-kit-ui-forms.mjs.map +1 -0
  28. package/fesm2022/mk-kit-ui-icon.mjs +470 -0
  29. package/fesm2022/mk-kit-ui-icon.mjs.map +1 -0
  30. package/fesm2022/mk-kit-ui-media.mjs +896 -0
  31. package/fesm2022/mk-kit-ui-media.mjs.map +1 -0
  32. package/fesm2022/mk-kit-ui-navigation.mjs +2542 -0
  33. package/fesm2022/mk-kit-ui-navigation.mjs.map +1 -0
  34. package/fesm2022/mk-kit-ui-rich-text.mjs +565 -0
  35. package/fesm2022/mk-kit-ui-rich-text.mjs.map +1 -0
  36. package/fesm2022/mk-kit-ui-table.mjs +1378 -0
  37. package/fesm2022/mk-kit-ui-table.mjs.map +1 -0
  38. package/fesm2022/mk-kit-ui.mjs +32 -0
  39. package/fesm2022/mk-kit-ui.mjs.map +1 -0
  40. package/package.json +130 -0
  41. package/schematics/collection.json +10 -0
  42. package/schematics/ng-add/index.js +113 -0
  43. package/schematics/ng-add/schema.json +22 -0
  44. package/schematics/package.json +3 -0
  45. package/styles/mk-kit.css +750 -0
  46. package/types/mk-kit-ui-block-editor.d.ts +292 -0
  47. package/types/mk-kit-ui-button.d.ts +40 -0
  48. package/types/mk-kit-ui-checkbox.d.ts +62 -0
  49. package/types/mk-kit-ui-chip.d.ts +59 -0
  50. package/types/mk-kit-ui-context-menu.d.ts +57 -0
  51. package/types/mk-kit-ui-core.d.ts +1105 -0
  52. package/types/mk-kit-ui-data.d.ts +2580 -0
  53. package/types/mk-kit-ui-datetime.d.ts +1171 -0
  54. package/types/mk-kit-ui-directives.d.ts +807 -0
  55. package/types/mk-kit-ui-dnd.d.ts +423 -0
  56. package/types/mk-kit-ui-feedback.d.ts +1270 -0
  57. package/types/mk-kit-ui-forms.d.ts +3586 -0
  58. package/types/mk-kit-ui-icon.d.ts +108 -0
  59. package/types/mk-kit-ui-media.d.ts +549 -0
  60. package/types/mk-kit-ui-navigation.d.ts +1169 -0
  61. package/types/mk-kit-ui-rich-text.d.ts +187 -0
  62. package/types/mk-kit-ui-table.d.ts +739 -0
  63. package/types/mk-kit-ui.d.ts +17 -0
@@ -0,0 +1,565 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, DOCUMENT, ElementRef, viewChild, input, booleanAttribute, output, signal, computed, afterRenderEffect, ChangeDetectionStrategy, Component, model, forwardRef } from '@angular/core';
3
+ import { NG_VALUE_ACCESSOR } from '@angular/forms';
4
+ import { MK_I18N, MkFieldContext } from '@mk-kit/ui/core';
5
+
6
+ /**
7
+ * Rich-text engine — an accessible contenteditable surface with a floating
8
+ * inline formatting toolbar (Bold, Italic, Underline, Strikethrough, Inline
9
+ * code, Link, Clear). Built on the native Selection API +
10
+ * `document.execCommand` (deprecated but universally supported and
11
+ * dependency-free — the only practical way to do inline formatting without a
12
+ * heavyweight editor engine).
13
+ *
14
+ * This is the low-level building block: it emits HTML via `contentChange`, and
15
+ * structural intents (`splitAt`, `removeEmpty`, `arrowOut`) so a block-editor
16
+ * host can create, merge or navigate blocks. For a standalone form control use
17
+ * {@link MkRichText} (`<mk-rich-text>`), which wraps this engine in a CVA over
18
+ * a sanitised HTML string.
19
+ */
20
+ class MkRichTextEngine {
21
+ document = inject(DOCUMENT);
22
+ host = inject(ElementRef);
23
+ editableRef = viewChild.required('editable', /* @ts-ignore */
24
+ ...(ngDevMode ? [{ debugName: "editableRef" }] : /* istanbul ignore next */ []));
25
+ i18n = inject(MK_I18N);
26
+ /** Pending toolbar-hide delay from `onBlur`; cleared on destroy. */
27
+ blurTimer = null;
28
+ /** Current HTML content. */
29
+ html = input('', /* @ts-ignore */
30
+ ...(ngDevMode ? [{ debugName: "html" }] : /* istanbul ignore next */ []));
31
+ /** Placeholder shown when empty. */
32
+ placeholder = input('', /* @ts-ignore */
33
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
34
+ /** Disable editing. */
35
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
36
+ /** Allow soft line breaks (Shift+Enter). Plain Enter always splits. */
37
+ multiline = input(true, { ...(ngDevMode ? { debugName: "multiline" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
38
+ /**
39
+ * Emit `splitAt` on Enter (block-editor hosts). When `false` — standalone
40
+ * field usage — Enter inserts a line break inside this surface instead.
41
+ */
42
+ splitOnEnter = input(true, { ...(ngDevMode ? { debugName: "splitOnEnter" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
43
+ /** Minimum height of the editable surface (any CSS length). */
44
+ minHeight = input(null, /* @ts-ignore */
45
+ ...(ngDevMode ? [{ debugName: "minHeight" }] : /* istanbul ignore next */ []));
46
+ /** Accessible label for the editable region. */
47
+ ariaLabel = input(this.i18n.blockEditor.editableText, /* @ts-ignore */
48
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
49
+ /** Id of an external label element, for `aria-labelledby`. */
50
+ labelledBy = input(null, /* @ts-ignore */
51
+ ...(ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []));
52
+ /** Space-separated id list for `aria-describedby`. */
53
+ describedBy = input(null, /* @ts-ignore */
54
+ ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
55
+ /** Reflect an invalid state via `aria-invalid`. */
56
+ invalid = input(false, { ...(ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
57
+ /** Fired on every edit with the sanitised-by-construction HTML. */
58
+ contentChange = output();
59
+ /** Enter pressed: `before` stays in this block, `after` starts a new one. */
60
+ splitAt = output();
61
+ /** Backspace at the very start of an empty block. */
62
+ removeEmpty = output();
63
+ /** Caret tried to leave the top/bottom edge with an arrow key. */
64
+ arrowOut = output();
65
+ /** Focus/blur bubbled so the wrapper can show selection chrome. */
66
+ focusChange = output();
67
+ isEmpty = signal(true, /* @ts-ignore */
68
+ ...(ngDevMode ? [{ debugName: "isEmpty" }] : /* istanbul ignore next */ []));
69
+ toolbar = signal({
70
+ visible: false,
71
+ top: 0,
72
+ left: 0,
73
+ }, /* @ts-ignore */
74
+ ...(ngDevMode ? [{ debugName: "toolbar" }] : /* istanbul ignore next */ []));
75
+ activeFormats = signal(new Set(), /* @ts-ignore */
76
+ ...(ngDevMode ? [{ debugName: "activeFormats" }] : /* istanbul ignore next */ []));
77
+ tools = [
78
+ { id: 'bold', label: this.i18n.blockEditor.bold, icon: 'B', shortcut: 'Ctrl+B' },
79
+ { id: 'italic', label: this.i18n.blockEditor.italic, icon: 'I', shortcut: 'Ctrl+I' },
80
+ { id: 'underline', label: this.i18n.blockEditor.underline, icon: 'U', shortcut: 'Ctrl+U' },
81
+ { id: 'strikeThrough', label: this.i18n.blockEditor.strikethrough, icon: 'S' },
82
+ { id: 'code', label: this.i18n.blockEditor.inlineCode, icon: '</>' },
83
+ { id: 'link', label: this.i18n.blockEditor.link, icon: '🔗' },
84
+ { id: 'clear', label: this.i18n.blockEditor.clearFormatting, icon: '⌫' },
85
+ ];
86
+ showPlaceholder = computed(() => this.isEmpty(), /* @ts-ignore */
87
+ ...(ngDevMode ? [{ debugName: "showPlaceholder" }] : /* istanbul ignore next */ []));
88
+ constructor() {
89
+ // Sync the DOM from the `html` input only when the user is not actively
90
+ // editing this element — otherwise setting innerHTML would reset the caret.
91
+ afterRenderEffect(() => {
92
+ const value = this.html() ?? '';
93
+ const el = this.editableRef().nativeElement;
94
+ if (this.document.activeElement === el)
95
+ return;
96
+ if (el.innerHTML !== value)
97
+ el.innerHTML = value;
98
+ this.isEmpty.set(this.computeEmpty(el));
99
+ });
100
+ }
101
+ /** Places the caret and focuses (called by the host for navigation). */
102
+ focus(position = 'end') {
103
+ const el = this.editableRef().nativeElement;
104
+ el.focus();
105
+ this.setCaret(el, position);
106
+ }
107
+ onInput() {
108
+ const el = this.editableRef().nativeElement;
109
+ this.isEmpty.set(this.computeEmpty(el));
110
+ this.contentChange.emit(el.innerHTML);
111
+ }
112
+ onFocus() {
113
+ this.focusChange.emit(true);
114
+ }
115
+ ngOnDestroy() {
116
+ if (this.blurTimer !== null) {
117
+ clearTimeout(this.blurTimer);
118
+ this.blurTimer = null;
119
+ }
120
+ }
121
+ onBlur() {
122
+ this.focusChange.emit(false);
123
+ // Hide the toolbar on a delay, but only if focus really left the
124
+ // component — Tabbing into the toolbar itself must not dismiss it.
125
+ if (this.blurTimer !== null)
126
+ clearTimeout(this.blurTimer);
127
+ this.blurTimer = setTimeout(() => {
128
+ this.blurTimer = null;
129
+ const active = this.document.activeElement;
130
+ if (active && this.host.nativeElement.contains(active))
131
+ return;
132
+ this.toolbar.update((t) => (t.visible ? { ...t, visible: false } : t));
133
+ }, 150);
134
+ }
135
+ onSelectionChange() {
136
+ this.updateToolbar();
137
+ }
138
+ onKeydown(event) {
139
+ if (this.disabled())
140
+ return;
141
+ const el = this.editableRef().nativeElement;
142
+ if (event.key === 'Enter' && !event.shiftKey) {
143
+ event.preventDefault();
144
+ if (this.splitOnEnter())
145
+ this.emitSplit(el);
146
+ else
147
+ this.insertLineBreak();
148
+ return;
149
+ }
150
+ // Shift+Enter in single-line mode is a hard split too.
151
+ if (event.key === 'Enter' && event.shiftKey && !this.multiline()) {
152
+ event.preventDefault();
153
+ if (this.splitOnEnter())
154
+ this.emitSplit(el);
155
+ else
156
+ this.insertLineBreak();
157
+ return;
158
+ }
159
+ if (event.key === 'Backspace' && this.isCaretAtStart(el) && this.isSelectionCollapsed()) {
160
+ if (this.computeEmpty(el)) {
161
+ event.preventDefault();
162
+ this.removeEmpty.emit();
163
+ }
164
+ return;
165
+ }
166
+ if (event.key === 'ArrowUp' && this.isCaretAtStart(el) && this.isSelectionCollapsed()) {
167
+ event.preventDefault();
168
+ this.arrowOut.emit('up');
169
+ return;
170
+ }
171
+ if (event.key === 'ArrowDown' && this.isCaretAtEnd(el) && this.isSelectionCollapsed()) {
172
+ event.preventDefault();
173
+ this.arrowOut.emit('down');
174
+ return;
175
+ }
176
+ }
177
+ /** Escape inside the toolbar returns focus to the text; arrows move tools. */
178
+ onToolbarKeydown(event) {
179
+ if (event.key === 'Escape') {
180
+ event.preventDefault();
181
+ event.stopPropagation();
182
+ this.toolbar.update((t) => ({ ...t, visible: false }));
183
+ this.editableRef().nativeElement.focus();
184
+ return;
185
+ }
186
+ if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
187
+ const bar = event.currentTarget;
188
+ const buttons = Array.from(bar.querySelectorAll('.mk-rich-text__tool'));
189
+ const current = buttons.indexOf(this.document.activeElement);
190
+ if (current === -1 || buttons.length === 0)
191
+ return;
192
+ event.preventDefault();
193
+ const delta = event.key === 'ArrowRight' ? 1 : -1;
194
+ buttons[(current + delta + buttons.length) % buttons.length].focus();
195
+ }
196
+ }
197
+ /** Runs a toolbar command, keeping the model + toolbar state in sync. */
198
+ runTool(id) {
199
+ const el = this.editableRef().nativeElement;
200
+ el.focus();
201
+ switch (id) {
202
+ case 'bold':
203
+ case 'italic':
204
+ case 'underline':
205
+ case 'strikeThrough':
206
+ this.document.execCommand(id);
207
+ break;
208
+ case 'code':
209
+ this.wrapInlineCode();
210
+ break;
211
+ case 'link':
212
+ this.applyLink();
213
+ break;
214
+ case 'clear':
215
+ this.document.execCommand('removeFormat');
216
+ this.document.execCommand('unlink');
217
+ break;
218
+ }
219
+ this.onInput();
220
+ this.updateToolbar();
221
+ }
222
+ isActive(id) {
223
+ return this.activeFormats().has(id);
224
+ }
225
+ // --- internals ------------------------------------------------------------
226
+ emitSplit(el) {
227
+ const { before, after } = this.splitHtmlAtCaret(el);
228
+ this.splitAt.emit({ before, after });
229
+ }
230
+ /** Inserts a `<br>` at the caret (Enter in `splitOnEnter=false` mode). */
231
+ insertLineBreak() {
232
+ // insertHTML keeps the markup to the sanitiser's allow-list (`<br>`),
233
+ // unlike the browser's default Enter handling which wraps lines in divs.
234
+ this.document.execCommand('insertHTML', false, '<br>');
235
+ this.onInput();
236
+ }
237
+ /** Extracts markup after the caret so the host can start a new block with it. */
238
+ splitHtmlAtCaret(el) {
239
+ const selection = this.document.defaultView?.getSelection();
240
+ if (!selection || selection.rangeCount === 0) {
241
+ return { before: el.innerHTML, after: '' };
242
+ }
243
+ const range = selection.getRangeAt(0);
244
+ const tail = range.cloneRange();
245
+ tail.selectNodeContents(el);
246
+ tail.setStart(range.endContainer, range.endOffset);
247
+ const fragment = tail.cloneContents();
248
+ const holder = this.document.createElement('div');
249
+ holder.appendChild(fragment);
250
+ const after = holder.innerHTML;
251
+ const head = range.cloneRange();
252
+ head.selectNodeContents(el);
253
+ head.setEnd(range.startContainer, range.startOffset);
254
+ const headHolder = this.document.createElement('div');
255
+ headHolder.appendChild(head.cloneContents());
256
+ const before = headHolder.innerHTML;
257
+ return { before, after };
258
+ }
259
+ wrapInlineCode() {
260
+ const selection = this.document.defaultView?.getSelection();
261
+ if (!selection || selection.rangeCount === 0 || selection.isCollapsed)
262
+ return;
263
+ const range = selection.getRangeAt(0);
264
+ const code = this.document.createElement('code');
265
+ try {
266
+ range.surroundContents(code);
267
+ }
268
+ catch {
269
+ // Selection crosses element boundaries — fall back to insertHTML.
270
+ const html = range.toString();
271
+ this.document.execCommand('insertHTML', false, `<code>${escapeText(html)}</code>`);
272
+ }
273
+ }
274
+ applyLink() {
275
+ const selection = this.document.defaultView?.getSelection();
276
+ if (!selection || selection.isCollapsed)
277
+ return;
278
+ const url = this.document.defaultView?.prompt(this.i18n.blockEditor.linkUrlPrompt, 'https://');
279
+ if (url == null)
280
+ return;
281
+ const trimmed = url.trim();
282
+ if (trimmed === '') {
283
+ this.document.execCommand('unlink');
284
+ return;
285
+ }
286
+ if (!/^(https?:|mailto:|tel:|\/|#)/i.test(trimmed))
287
+ return; // block javascript: etc.
288
+ this.document.execCommand('createLink', false, trimmed);
289
+ }
290
+ updateToolbar() {
291
+ const el = this.editableRef().nativeElement;
292
+ const selection = this.document.defaultView?.getSelection();
293
+ if (this.disabled() ||
294
+ !selection ||
295
+ selection.rangeCount === 0 ||
296
+ selection.isCollapsed ||
297
+ !el.contains(selection.anchorNode)) {
298
+ this.toolbar.update((t) => (t.visible ? { ...t, visible: false } : t));
299
+ return;
300
+ }
301
+ const rect = selection.getRangeAt(0).getBoundingClientRect();
302
+ const host = el.getBoundingClientRect();
303
+ this.toolbar.set({
304
+ visible: true,
305
+ top: rect.top - host.top - 8,
306
+ left: rect.left - host.left + rect.width / 2,
307
+ });
308
+ const active = new Set();
309
+ for (const id of ['bold', 'italic', 'underline', 'strikeThrough']) {
310
+ try {
311
+ if (this.document.queryCommandState(id))
312
+ active.add(id);
313
+ }
314
+ catch {
315
+ /* queryCommandState can throw in some engines */
316
+ }
317
+ }
318
+ this.activeFormats.set(active);
319
+ }
320
+ computeEmpty(el) {
321
+ const text = el.textContent?.trim() ?? '';
322
+ return text === '' && !el.querySelector('img, hr, iframe');
323
+ }
324
+ setCaret(el, position) {
325
+ const selection = this.document.defaultView?.getSelection();
326
+ if (!selection)
327
+ return;
328
+ const range = this.document.createRange();
329
+ range.selectNodeContents(el);
330
+ range.collapse(position === 'start');
331
+ selection.removeAllRanges();
332
+ selection.addRange(range);
333
+ }
334
+ isSelectionCollapsed() {
335
+ return this.document.defaultView?.getSelection()?.isCollapsed ?? true;
336
+ }
337
+ isCaretAtStart(el) {
338
+ const selection = this.document.defaultView?.getSelection();
339
+ if (!selection || selection.rangeCount === 0)
340
+ return false;
341
+ const range = selection.getRangeAt(0).cloneRange();
342
+ range.selectNodeContents(el);
343
+ range.setEnd(selection.anchorNode, selection.anchorOffset);
344
+ return range.toString().length === 0;
345
+ }
346
+ isCaretAtEnd(el) {
347
+ const selection = this.document.defaultView?.getSelection();
348
+ if (!selection || selection.rangeCount === 0)
349
+ return false;
350
+ const range = selection.getRangeAt(0).cloneRange();
351
+ range.selectNodeContents(el);
352
+ range.setStart(selection.anchorNode, selection.anchorOffset);
353
+ return range.toString().length === 0;
354
+ }
355
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkRichTextEngine, deps: [], target: i0.ɵɵFactoryTarget.Component });
356
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkRichTextEngine, isStandalone: true, selector: "mk-rich-text-engine", inputs: { html: { classPropertyName: "html", publicName: "html", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, multiline: { classPropertyName: "multiline", publicName: "multiline", isSignal: true, isRequired: false, transformFunction: null }, splitOnEnter: { classPropertyName: "splitOnEnter", publicName: "splitOnEnter", isSignal: true, isRequired: false, transformFunction: null }, minHeight: { classPropertyName: "minHeight", publicName: "minHeight", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, labelledBy: { classPropertyName: "labelledBy", publicName: "labelledBy", isSignal: true, isRequired: false, transformFunction: null }, describedBy: { classPropertyName: "describedBy", publicName: "describedBy", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { contentChange: "contentChange", splitAt: "splitAt", removeEmpty: "removeEmpty", arrowOut: "arrowOut", focusChange: "focusChange" }, host: { classAttribute: "mk-rich-text" }, viewQueries: [{ propertyName: "editableRef", first: true, predicate: ["editable"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (toolbar().visible && !disabled()) {\n <div\n class=\"mk-rich-text__toolbar\"\n role=\"toolbar\"\n [attr.aria-label]=\"i18n.blockEditor.textFormatting\"\n [style.top.px]=\"toolbar().top\"\n [style.left.px]=\"toolbar().left\"\n (keydown)=\"onToolbarKeydown($event)\"\n >\n @for (tool of tools; track tool.id) {\n <button\n type=\"button\"\n class=\"mk-rich-text__tool\"\n [class.mk-rich-text__tool--active]=\"isActive(tool.id)\"\n [class.mk-rich-text__tool--code]=\"tool.id === 'code'\"\n [attr.aria-pressed]=\"isActive(tool.id)\"\n [attr.aria-label]=\"tool.label + (tool.shortcut ? ' (' + tool.shortcut + ')' : '')\"\n [title]=\"tool.label\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"runTool(tool.id)\"\n >\n {{ tool.icon }}\n </button>\n }\n </div>\n}\n\n<div\n #editable\n class=\"mk-rich-text__editable\"\n data-mk-editable\n role=\"textbox\"\n aria-multiline=\"true\"\n [attr.aria-label]=\"labelledBy() ? null : ariaLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"invalid() ? 'true' : null\"\n [attr.contenteditable]=\"disabled() ? null : 'true'\"\n [attr.data-placeholder]=\"placeholder()\"\n [style.min-height]=\"minHeight()\"\n [class.mk-rich-text__editable--empty]=\"showPlaceholder()\"\n (input)=\"onInput()\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\"\n (keyup)=\"onSelectionChange()\"\n (mouseup)=\"onSelectionChange()\"\n></div>\n", styles: [":host{display:block;position:relative}.mk-rich-text__editable{display:block;width:100%;min-height:1.5em;outline:none;color:var(--mk-text);font-family:inherit;font-size:inherit;line-height:var(--mk-line-height-relaxed);white-space:pre-wrap;word-break:break-word}.mk-rich-text__editable:focus-visible{outline:none}.mk-rich-text__editable--empty:before{content:attr(data-placeholder);color:var(--mk-text-subtle);pointer-events:none}.mk-rich-text__editable a{color:var(--mk-primary);text-decoration:underline}.mk-rich-text__editable code{font-family:var(--mk-font-mono);font-size:.9em;padding:.1em .3em;border-radius:var(--mk-radius-xs);background:var(--mk-code-bg)}.mk-rich-text__toolbar{position:absolute;z-index:var(--mk-z-menu);display:flex;gap:2px;padding:var(--mk-space-1);transform:translate(-50%,-100%);background:var(--mk-surface-inverse);border-radius:var(--mk-radius-md);box-shadow:var(--mk-shadow-lg);animation:mk-rich-text-pop var(--mk-duration-fast) var(--mk-ease-out)}@keyframes mk-rich-text-pop{0%{opacity:0;transform:translate(-50%,-90%)}to{opacity:1;transform:translate(-50%,-100%)}}@media(prefers-reduced-motion:reduce){.mk-rich-text__toolbar{animation:none}}.mk-rich-text__tool{display:inline-flex;align-items:center;justify-content:center;min-width:28px;height:28px;padding:0 var(--mk-space-1);border:none;border-radius:var(--mk-radius-sm);background:transparent;color:var(--mk-text-inverse);font-family:inherit;font-size:var(--mk-font-size-sm);font-weight:var(--mk-font-weight-bold);cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-rich-text__tool--code{font-family:var(--mk-font-mono);font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-normal)}.mk-rich-text__tool:hover{background:var(--mk-hover-overlay)}.mk-rich-text__tool--active{background:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-rich-text__tool:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
357
+ }
358
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkRichTextEngine, decorators: [{
359
+ type: Component,
360
+ args: [{ selector: 'mk-rich-text-engine', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'mk-rich-text' }, template: "@if (toolbar().visible && !disabled()) {\n <div\n class=\"mk-rich-text__toolbar\"\n role=\"toolbar\"\n [attr.aria-label]=\"i18n.blockEditor.textFormatting\"\n [style.top.px]=\"toolbar().top\"\n [style.left.px]=\"toolbar().left\"\n (keydown)=\"onToolbarKeydown($event)\"\n >\n @for (tool of tools; track tool.id) {\n <button\n type=\"button\"\n class=\"mk-rich-text__tool\"\n [class.mk-rich-text__tool--active]=\"isActive(tool.id)\"\n [class.mk-rich-text__tool--code]=\"tool.id === 'code'\"\n [attr.aria-pressed]=\"isActive(tool.id)\"\n [attr.aria-label]=\"tool.label + (tool.shortcut ? ' (' + tool.shortcut + ')' : '')\"\n [title]=\"tool.label\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"runTool(tool.id)\"\n >\n {{ tool.icon }}\n </button>\n }\n </div>\n}\n\n<div\n #editable\n class=\"mk-rich-text__editable\"\n data-mk-editable\n role=\"textbox\"\n aria-multiline=\"true\"\n [attr.aria-label]=\"labelledBy() ? null : ariaLabel()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"invalid() ? 'true' : null\"\n [attr.contenteditable]=\"disabled() ? null : 'true'\"\n [attr.data-placeholder]=\"placeholder()\"\n [style.min-height]=\"minHeight()\"\n [class.mk-rich-text__editable--empty]=\"showPlaceholder()\"\n (input)=\"onInput()\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\"\n (keyup)=\"onSelectionChange()\"\n (mouseup)=\"onSelectionChange()\"\n></div>\n", styles: [":host{display:block;position:relative}.mk-rich-text__editable{display:block;width:100%;min-height:1.5em;outline:none;color:var(--mk-text);font-family:inherit;font-size:inherit;line-height:var(--mk-line-height-relaxed);white-space:pre-wrap;word-break:break-word}.mk-rich-text__editable:focus-visible{outline:none}.mk-rich-text__editable--empty:before{content:attr(data-placeholder);color:var(--mk-text-subtle);pointer-events:none}.mk-rich-text__editable a{color:var(--mk-primary);text-decoration:underline}.mk-rich-text__editable code{font-family:var(--mk-font-mono);font-size:.9em;padding:.1em .3em;border-radius:var(--mk-radius-xs);background:var(--mk-code-bg)}.mk-rich-text__toolbar{position:absolute;z-index:var(--mk-z-menu);display:flex;gap:2px;padding:var(--mk-space-1);transform:translate(-50%,-100%);background:var(--mk-surface-inverse);border-radius:var(--mk-radius-md);box-shadow:var(--mk-shadow-lg);animation:mk-rich-text-pop var(--mk-duration-fast) var(--mk-ease-out)}@keyframes mk-rich-text-pop{0%{opacity:0;transform:translate(-50%,-90%)}to{opacity:1;transform:translate(-50%,-100%)}}@media(prefers-reduced-motion:reduce){.mk-rich-text__toolbar{animation:none}}.mk-rich-text__tool{display:inline-flex;align-items:center;justify-content:center;min-width:28px;height:28px;padding:0 var(--mk-space-1);border:none;border-radius:var(--mk-radius-sm);background:transparent;color:var(--mk-text-inverse);font-family:inherit;font-size:var(--mk-font-size-sm);font-weight:var(--mk-font-weight-bold);cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-rich-text__tool--code{font-family:var(--mk-font-mono);font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-normal)}.mk-rich-text__tool:hover{background:var(--mk-hover-overlay)}.mk-rich-text__tool--active{background:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-rich-text__tool:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}\n"] }]
361
+ }], ctorParameters: () => [], propDecorators: { editableRef: [{ type: i0.ViewChild, args: ['editable', { isSignal: true }] }], html: [{ type: i0.Input, args: [{ isSignal: true, alias: "html", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], multiline: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiline", required: false }] }], splitOnEnter: [{ type: i0.Input, args: [{ isSignal: true, alias: "splitOnEnter", required: false }] }], minHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "minHeight", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], labelledBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelledBy", required: false }] }], describedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "describedBy", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], contentChange: [{ type: i0.Output, args: ["contentChange"] }], splitAt: [{ type: i0.Output, args: ["splitAt"] }], removeEmpty: [{ type: i0.Output, args: ["removeEmpty"] }], arrowOut: [{ type: i0.Output, args: ["arrowOut"] }], focusChange: [{ type: i0.Output, args: ["focusChange"] }] } });
362
+ function escapeText(value) {
363
+ return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
364
+ }
365
+
366
+ /**
367
+ * Allow-list sanitiser for inline rich-text HTML — the security backbone of
368
+ * both `<mk-rich-text>` and the block editor's text blocks.
369
+ *
370
+ * SECURITY MODEL: rich-text surfaces store HTML authored through the editor's
371
+ * restricted formatting commands. Before that markup is emitted, persisted or
372
+ * re-rendered it is passed through {@link sanitizeInlineHtml}, an allow-list
373
+ * cleaner that strips scripts, event handlers and dangerous URLs. Angular's
374
+ * `DomSanitizer` re-sanitises at display time where applicable. This module is
375
+ * dependency-free so it can run on a server (SSG/SSR) — never feed its output
376
+ * back into `innerHTML` without sanitising again.
377
+ */
378
+ const ALLOWED_INLINE_TAGS = new Set([
379
+ 'b', 'strong', 'i', 'em', 'u', 's', 'strike', 'code', 'a', 'br', 'span', 'mark', 'sub', 'sup',
380
+ ]);
381
+ /** Escapes a value for safe placement in a double-quoted attribute. */
382
+ function escapeAttr(value) {
383
+ return String(value ?? '')
384
+ .replace(/&/g, '&amp;')
385
+ .replace(/</g, '&lt;')
386
+ .replace(/>/g, '&gt;')
387
+ .replace(/"/g, '&quot;');
388
+ }
389
+ /** True for URL schemes safe to place in `href`/`src`. */
390
+ function isSafeUrl(url) {
391
+ const trimmed = String(url ?? '').trim();
392
+ if (trimmed === '')
393
+ return false;
394
+ // Allow relative, http(s), mailto, tel and data:image.
395
+ if (/^(https?:|mailto:|tel:)/i.test(trimmed))
396
+ return true;
397
+ if (/^data:image\//i.test(trimmed))
398
+ return true;
399
+ if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed))
400
+ return false; // any other scheme
401
+ return true; // relative / anchor / protocol-relative-ish
402
+ }
403
+ /**
404
+ * Allow-list cleaner for inline rich text. Keeps a small set of formatting
405
+ * tags, strips everything else's tags (preserving their text), removes event
406
+ * handler attributes and unsafe `href`s. Not a substitute for `DomSanitizer`,
407
+ * but makes the serialised string safe by construction.
408
+ */
409
+ function sanitizeInlineHtml(html) {
410
+ if (!html)
411
+ return '';
412
+ let out = String(html);
413
+ // Drop script/style blocks entirely.
414
+ out = out.replace(/<\s*(script|style)[\s\S]*?<\s*\/\s*\1\s*>/gi, '');
415
+ // Rewrite each tag: keep allow-listed ones with a scrubbed attribute set.
416
+ out = out.replace(/<\/?[a-z][a-z0-9]*\b[^>]*>/gi, (tag) => {
417
+ const closing = /^<\s*\//.test(tag);
418
+ const name = (tag.match(/^<\s*\/?\s*([a-z0-9]+)/i)?.[1] ?? '').toLowerCase();
419
+ if (!ALLOWED_INLINE_TAGS.has(name))
420
+ return '';
421
+ if (closing)
422
+ return `</${name}>`;
423
+ if (name === 'a') {
424
+ const href = tag.match(/\shref\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
425
+ const url = href?.[1] ?? href?.[2] ?? '';
426
+ if (url && isSafeUrl(url)) {
427
+ return `<a href="${escapeAttr(url)}" rel="noopener noreferrer nofollow" target="_blank">`;
428
+ }
429
+ return '<a>';
430
+ }
431
+ return `<${name}>`;
432
+ });
433
+ return out;
434
+ }
435
+
436
+ /**
437
+ * Rich text — a small WYSIWYG form control over a sanitised HTML string, for
438
+ * single "description"-style fields where the full block editor is overkill.
439
+ * Renders a textarea-like surface with the floating inline formatting toolbar
440
+ * (Bold, Italic, Underline, Strikethrough, Inline code, Link, Clear) on text
441
+ * selection; Enter inserts a line break.
442
+ *
443
+ * Implements `ControlValueAccessor` with a two-way `value` model, so it works
444
+ * with `[(ngModel)]`, reactive forms and `[(value)]`. Every value — written or
445
+ * emitted — passes through {@link sanitizeInlineHtml}, and a visually empty
446
+ * surface (whitespace / lone `<br>`s) normalises to `''` so `required`
447
+ * validation behaves.
448
+ *
449
+ * ```html
450
+ * <mk-rich-text [(ngModel)]="description" placeholder="Describe it…" />
451
+ * <mk-rich-text [(value)]="html" minHeight="8rem" />
452
+ * ```
453
+ */
454
+ class MkRichText {
455
+ field = inject(MkFieldContext, { optional: true });
456
+ i18n = inject(MK_I18N);
457
+ /** Two-way HTML string value (sanitised; `''` when visually empty). */
458
+ value = model('', /* @ts-ignore */
459
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
460
+ /** Placeholder shown when empty. */
461
+ placeholder = input('', /* @ts-ignore */
462
+ ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
463
+ /** Disable the control. */
464
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
465
+ /** Force the invalid visual + `aria-invalid` when used standalone. */
466
+ invalid = input(false, { ...(ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
467
+ /** Minimum height of the editable area (any CSS length). */
468
+ minHeight = input('4.5em', /* @ts-ignore */
469
+ ...(ngDevMode ? [{ debugName: "minHeight" }] : /* istanbul ignore next */ []));
470
+ /** Accessible label when not labelled by an `mk-form-field`. */
471
+ ariaLabel = input('', { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
472
+ cvaDisabled = signal(false, /* @ts-ignore */
473
+ ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
474
+ onChange = () => { };
475
+ onTouched = () => { };
476
+ isDisabled = computed(() => this.disabled() || this.cvaDisabled(), /* @ts-ignore */
477
+ ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
478
+ isInvalid = computed(() => this.invalid() || (this.field?.hasError() ?? false), /* @ts-ignore */
479
+ ...(ngDevMode ? [{ debugName: "isInvalid" }] : /* istanbul ignore next */ []));
480
+ labelledBy = this.field?.labelId ?? null;
481
+ describedBy = computed(() => this.field?.describedBy() ?? null, /* @ts-ignore */
482
+ ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
483
+ effectiveAriaLabel = computed(() => this.ariaLabel() || this.i18n.blockEditor.editableText, /* @ts-ignore */
484
+ ...(ngDevMode ? [{ debugName: "effectiveAriaLabel" }] : /* istanbul ignore next */ []));
485
+ /** Sanitised view of the model, rendered into the engine. */
486
+ safeHtml = computed(() => sanitizeInlineHtml(this.value()), /* @ts-ignore */
487
+ ...(ngDevMode ? [{ debugName: "safeHtml" }] : /* istanbul ignore next */ []));
488
+ /** User edit: sanitise, normalise the empty state, propagate. */
489
+ onContentChange(html) {
490
+ const value = normalizeRichTextValue(html);
491
+ this.value.set(value);
492
+ this.onChange(value);
493
+ }
494
+ onFocusChange(focused) {
495
+ if (!focused)
496
+ this.onTouched();
497
+ }
498
+ // --- ControlValueAccessor -------------------------------------------------
499
+ writeValue(value) {
500
+ this.value.set(normalizeRichTextValue(value == null ? '' : String(value)));
501
+ }
502
+ registerOnChange(fn) {
503
+ this.onChange = fn;
504
+ }
505
+ registerOnTouched(fn) {
506
+ this.onTouched = fn;
507
+ }
508
+ setDisabledState(isDisabled) {
509
+ this.cvaDisabled.set(isDisabled);
510
+ }
511
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkRichText, deps: [], target: i0.ɵɵFactoryTarget.Component });
512
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.7", type: MkRichText, isStandalone: true, selector: "mk-rich-text", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, minHeight: { classPropertyName: "minHeight", publicName: "minHeight", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { properties: { "class.mk-rich-text-field--disabled": "isDisabled()", "class.mk-rich-text-field--invalid": "isInvalid()" }, classAttribute: "mk-rich-text-field" }, providers: [
513
+ {
514
+ provide: NG_VALUE_ACCESSOR,
515
+ useExisting: forwardRef(() => MkRichText),
516
+ multi: true,
517
+ },
518
+ ], ngImport: i0, template: "<mk-rich-text-engine\n [html]=\"safeHtml()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [splitOnEnter]=\"false\"\n [minHeight]=\"minHeight()\"\n [ariaLabel]=\"effectiveAriaLabel()\"\n [labelledBy]=\"labelledBy\"\n [describedBy]=\"describedBy()\"\n [invalid]=\"isInvalid()\"\n (contentChange)=\"onContentChange($event)\"\n (focusChange)=\"onFocusChange($event)\"\n/>\n", styles: [":host{display:block;width:100%;padding:var(--mk-space-2) var(--mk-space-3);font-family:inherit;font-size:var(--mk-font-size-md);color:var(--mk-text);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),box-shadow var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}:host(:hover:not(.mk-rich-text-field--disabled)){border-color:var(--mk-border-strong)}:host(:focus-within){outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:-2px;border-color:var(--mk-primary)}:host(.mk-rich-text-field--invalid){border-color:var(--mk-danger)}:host(.mk-rich-text-field--invalid:focus-within){outline-color:var(--mk-danger);border-color:var(--mk-danger)}:host(.mk-rich-text-field--disabled){background-color:var(--mk-surface-2);color:var(--mk-text-disabled);border-color:var(--mk-border-subtle);cursor:not-allowed;opacity:.7}@media(pointer:coarse){:host{font-size:max(var(--mk-font-size-md),16px)}}\n"], dependencies: [{ kind: "component", type: MkRichTextEngine, selector: "mk-rich-text-engine", inputs: ["html", "placeholder", "disabled", "multiline", "splitOnEnter", "minHeight", "ariaLabel", "labelledBy", "describedBy", "invalid"], outputs: ["contentChange", "splitAt", "removeEmpty", "arrowOut", "focusChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
519
+ }
520
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkRichText, decorators: [{
521
+ type: Component,
522
+ args: [{ selector: 'mk-rich-text', imports: [MkRichTextEngine], changeDetection: ChangeDetectionStrategy.OnPush, host: {
523
+ class: 'mk-rich-text-field',
524
+ '[class.mk-rich-text-field--disabled]': 'isDisabled()',
525
+ '[class.mk-rich-text-field--invalid]': 'isInvalid()',
526
+ }, providers: [
527
+ {
528
+ provide: NG_VALUE_ACCESSOR,
529
+ useExisting: forwardRef(() => MkRichText),
530
+ multi: true,
531
+ },
532
+ ], template: "<mk-rich-text-engine\n [html]=\"safeHtml()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [splitOnEnter]=\"false\"\n [minHeight]=\"minHeight()\"\n [ariaLabel]=\"effectiveAriaLabel()\"\n [labelledBy]=\"labelledBy\"\n [describedBy]=\"describedBy()\"\n [invalid]=\"isInvalid()\"\n (contentChange)=\"onContentChange($event)\"\n (focusChange)=\"onFocusChange($event)\"\n/>\n", styles: [":host{display:block;width:100%;padding:var(--mk-space-2) var(--mk-space-3);font-family:inherit;font-size:var(--mk-font-size-md);color:var(--mk-text);background-color:var(--mk-surface);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),box-shadow var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}:host(:hover:not(.mk-rich-text-field--disabled)){border-color:var(--mk-border-strong)}:host(:focus-within){outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:-2px;border-color:var(--mk-primary)}:host(.mk-rich-text-field--invalid){border-color:var(--mk-danger)}:host(.mk-rich-text-field--invalid:focus-within){outline-color:var(--mk-danger);border-color:var(--mk-danger)}:host(.mk-rich-text-field--disabled){background-color:var(--mk-surface-2);color:var(--mk-text-disabled);border-color:var(--mk-border-subtle);cursor:not-allowed;opacity:.7}@media(pointer:coarse){:host{font-size:max(var(--mk-font-size-md),16px)}}\n"] }]
533
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], minHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "minHeight", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }] } });
534
+ /**
535
+ * Sanitises an HTML string and collapses visually empty markup (whitespace,
536
+ * `&nbsp;`s, lone `<br>`s and empty formatting tags) to `''`.
537
+ */
538
+ function normalizeRichTextValue(html) {
539
+ const sanitized = sanitizeInlineHtml(html);
540
+ const text = sanitized
541
+ .replace(/<[^>]*>/g, '')
542
+ .replace(/&nbsp;/gi, ' ')
543
+ .trim();
544
+ return text === '' ? '' : sanitized;
545
+ }
546
+
547
+ /**
548
+ * RICH TEXT entry for @mk-kit/ui.
549
+ *
550
+ * A small WYSIWYG form control (`<mk-rich-text>`) over a sanitised HTML
551
+ * string, plus the low-level contenteditable engine and the allow-list
552
+ * sanitiser it is built on. The block editor composes the same engine for its
553
+ * paragraph, heading, quote and list blocks.
554
+ *
555
+ * NOTE: this barrel deliberately does not export an HTML-escape helper — the
556
+ * block-editor entry exports `mkEscapeHtml` and the root umbrella re-exports
557
+ * both entries.
558
+ */
559
+
560
+ /**
561
+ * Generated bundle index. Do not edit.
562
+ */
563
+
564
+ export { MkRichText, MkRichTextEngine, sanitizeInlineHtml };
565
+ //# sourceMappingURL=mk-kit-ui-rich-text.mjs.map