@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.
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/block-editor/README.md +254 -0
- package/fesm2022/mk-kit-ui-block-editor.mjs +2158 -0
- package/fesm2022/mk-kit-ui-block-editor.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-button.mjs +81 -0
- package/fesm2022/mk-kit-ui-button.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-checkbox.mjs +136 -0
- package/fesm2022/mk-kit-ui-checkbox.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-chip.mjs +122 -0
- package/fesm2022/mk-kit-ui-chip.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-context-menu.mjs +144 -0
- package/fesm2022/mk-kit-ui-context-menu.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-core.mjs +1576 -0
- package/fesm2022/mk-kit-ui-core.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-data.mjs +6055 -0
- package/fesm2022/mk-kit-ui-data.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-datetime.mjs +3409 -0
- package/fesm2022/mk-kit-ui-datetime.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-directives.mjs +1779 -0
- package/fesm2022/mk-kit-ui-directives.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-dnd.mjs +1073 -0
- package/fesm2022/mk-kit-ui-dnd.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-feedback.mjs +2426 -0
- package/fesm2022/mk-kit-ui-feedback.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-forms.mjs +9208 -0
- package/fesm2022/mk-kit-ui-forms.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-icon.mjs +470 -0
- package/fesm2022/mk-kit-ui-icon.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-media.mjs +896 -0
- package/fesm2022/mk-kit-ui-media.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-navigation.mjs +2542 -0
- package/fesm2022/mk-kit-ui-navigation.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-rich-text.mjs +565 -0
- package/fesm2022/mk-kit-ui-rich-text.mjs.map +1 -0
- package/fesm2022/mk-kit-ui-table.mjs +1378 -0
- package/fesm2022/mk-kit-ui-table.mjs.map +1 -0
- package/fesm2022/mk-kit-ui.mjs +32 -0
- package/fesm2022/mk-kit-ui.mjs.map +1 -0
- package/package.json +130 -0
- package/schematics/collection.json +10 -0
- package/schematics/ng-add/index.js +113 -0
- package/schematics/ng-add/schema.json +22 -0
- package/schematics/package.json +3 -0
- package/styles/mk-kit.css +750 -0
- package/types/mk-kit-ui-block-editor.d.ts +292 -0
- package/types/mk-kit-ui-button.d.ts +40 -0
- package/types/mk-kit-ui-checkbox.d.ts +62 -0
- package/types/mk-kit-ui-chip.d.ts +59 -0
- package/types/mk-kit-ui-context-menu.d.ts +57 -0
- package/types/mk-kit-ui-core.d.ts +1105 -0
- package/types/mk-kit-ui-data.d.ts +2580 -0
- package/types/mk-kit-ui-datetime.d.ts +1171 -0
- package/types/mk-kit-ui-directives.d.ts +807 -0
- package/types/mk-kit-ui-dnd.d.ts +423 -0
- package/types/mk-kit-ui-feedback.d.ts +1270 -0
- package/types/mk-kit-ui-forms.d.ts +3586 -0
- package/types/mk-kit-ui-icon.d.ts +108 -0
- package/types/mk-kit-ui-media.d.ts +549 -0
- package/types/mk-kit-ui-navigation.d.ts +1169 -0
- package/types/mk-kit-ui-rich-text.d.ts +187 -0
- package/types/mk-kit-ui-table.d.ts +739 -0
- package/types/mk-kit-ui.d.ts +17 -0
|
@@ -0,0 +1,2158 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, signal, computed, Injectable, input, booleanAttribute, output, ChangeDetectionStrategy, Component, viewChild, DOCUMENT, ElementRef, DestroyRef, model, forwardRef } from '@angular/core';
|
|
3
|
+
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
4
|
+
import * as i1 from '@mk-kit/ui/core';
|
|
5
|
+
import { MK_I18N, mkUniqueId } from '@mk-kit/ui/core';
|
|
6
|
+
import { MkRichTextEngine, sanitizeInlineHtml } from '@mk-kit/ui/rich-text';
|
|
7
|
+
export { sanitizeInlineHtml } from '@mk-kit/ui/rich-text';
|
|
8
|
+
import { DomSanitizer } from '@angular/platform-browser';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Block document model for the mk-kit block editor.
|
|
12
|
+
*
|
|
13
|
+
* The model is deliberately small and 100% JSON-serialisable so a document can
|
|
14
|
+
* be persisted to a database, sent over the wire, and re-hydrated without any
|
|
15
|
+
* class instances or functions. All editor state lives in these plain objects.
|
|
16
|
+
*/
|
|
17
|
+
/** Current document schema version. */
|
|
18
|
+
const MK_BLOCK_DOCUMENT_VERSION = 1;
|
|
19
|
+
/** An empty, valid document. */
|
|
20
|
+
function mkEmptyDocument() {
|
|
21
|
+
return { version: MK_BLOCK_DOCUMENT_VERSION, blocks: [] };
|
|
22
|
+
}
|
|
23
|
+
let blockCounter = 0;
|
|
24
|
+
/**
|
|
25
|
+
* Generates a reasonably unique, JSON-safe block id. Combines a monotonic
|
|
26
|
+
* counter with a short random suffix so ids are unique even across documents
|
|
27
|
+
* merged at runtime.
|
|
28
|
+
*/
|
|
29
|
+
function mkBlockId(prefix = 'blk') {
|
|
30
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
31
|
+
return `${prefix}-${++blockCounter}-${rand}`;
|
|
32
|
+
}
|
|
33
|
+
/** Deep-clones a block (and its subtree), assigning fresh ids. */
|
|
34
|
+
function mkCloneBlock(block) {
|
|
35
|
+
return {
|
|
36
|
+
id: mkBlockId(block.type),
|
|
37
|
+
type: block.type,
|
|
38
|
+
data: structuredCloneSafe(block.data),
|
|
39
|
+
children: block.children?.map(mkCloneBlock),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Structured clone with a JSON fallback for older runtimes. */
|
|
43
|
+
function structuredCloneSafe(value) {
|
|
44
|
+
try {
|
|
45
|
+
return structuredClone(value);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return JSON.parse(JSON.stringify(value ?? null));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Convenience factory: builds a `create()`-ready block with fresh id. */
|
|
53
|
+
function block$1(type, data = {}, children) {
|
|
54
|
+
return { id: mkBlockId(type), type, data, ...(children ? { children } : {}) };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The default palette of built-in block definitions. Grouped for the inserter.
|
|
58
|
+
* Consumers can spread this and append custom definitions, or replace it.
|
|
59
|
+
*
|
|
60
|
+
* The English `label`/`group`/`description` text here is only a base: the
|
|
61
|
+
* editor resolves the built-in types' display text from the `blockEditor`
|
|
62
|
+
* i18n strings (see `provideMkI18n`) before merging custom definitions.
|
|
63
|
+
*/
|
|
64
|
+
const MK_DEFAULT_BLOCKS = [
|
|
65
|
+
{
|
|
66
|
+
type: 'paragraph',
|
|
67
|
+
label: 'Paragraph',
|
|
68
|
+
icon: '¶',
|
|
69
|
+
group: 'Text',
|
|
70
|
+
description: 'Rich text with inline formatting.',
|
|
71
|
+
keywords: ['text', 'body', 'p'],
|
|
72
|
+
transforms: ['heading', 'quote', 'code'],
|
|
73
|
+
create: () => block$1('paragraph', { html: '' }),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: 'heading',
|
|
77
|
+
label: 'Heading',
|
|
78
|
+
icon: 'H',
|
|
79
|
+
group: 'Text',
|
|
80
|
+
description: 'A section title (H1–H4).',
|
|
81
|
+
keywords: ['title', 'h1', 'h2', 'h3'],
|
|
82
|
+
transforms: ['paragraph'],
|
|
83
|
+
create: () => block$1('heading', { html: '', level: 2 }),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: 'list',
|
|
87
|
+
label: 'List',
|
|
88
|
+
icon: '•',
|
|
89
|
+
group: 'Text',
|
|
90
|
+
description: 'Bulleted or numbered list.',
|
|
91
|
+
keywords: ['ul', 'ol', 'bullet', 'numbered'],
|
|
92
|
+
create: () => block$1('list', { ordered: false, items: [''] }),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
type: 'quote',
|
|
96
|
+
label: 'Quote',
|
|
97
|
+
icon: '“',
|
|
98
|
+
group: 'Text',
|
|
99
|
+
description: 'A blockquote with optional citation.',
|
|
100
|
+
keywords: ['blockquote', 'citation'],
|
|
101
|
+
transforms: ['paragraph'],
|
|
102
|
+
create: () => block$1('quote', { html: '', citation: '' }),
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
type: 'code',
|
|
106
|
+
label: 'Code',
|
|
107
|
+
icon: '</>',
|
|
108
|
+
group: 'Text',
|
|
109
|
+
description: 'Preformatted, monospace code.',
|
|
110
|
+
keywords: ['pre', 'snippet', 'monospace'],
|
|
111
|
+
create: () => block$1('code', { code: '', language: '' }),
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
type: 'image',
|
|
115
|
+
label: 'Image',
|
|
116
|
+
icon: '🖼',
|
|
117
|
+
group: 'Media',
|
|
118
|
+
description: 'Upload or link an image.',
|
|
119
|
+
keywords: ['photo', 'picture', 'img', 'upload'],
|
|
120
|
+
create: () => block$1('image', { src: '', alt: '', caption: '', align: 'center', width: 100 }),
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
type: 'embed',
|
|
124
|
+
label: 'Embed',
|
|
125
|
+
icon: '▶',
|
|
126
|
+
group: 'Media',
|
|
127
|
+
description: 'YouTube, Vimeo or any URL.',
|
|
128
|
+
keywords: ['youtube', 'vimeo', 'video', 'iframe', 'url'],
|
|
129
|
+
create: () => block$1('embed', { url: '' }),
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
type: 'button',
|
|
133
|
+
label: 'Button',
|
|
134
|
+
icon: '⬚',
|
|
135
|
+
group: 'Media',
|
|
136
|
+
description: 'A call-to-action link button.',
|
|
137
|
+
keywords: ['cta', 'link', 'action'],
|
|
138
|
+
create: () => block$1('button', { label: 'Click me', href: '', tone: 'primary', variant: 'solid', align: 'left' }),
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
type: 'divider',
|
|
142
|
+
label: 'Divider',
|
|
143
|
+
icon: '—',
|
|
144
|
+
group: 'Layout',
|
|
145
|
+
description: 'A horizontal separator.',
|
|
146
|
+
keywords: ['hr', 'separator', 'rule'],
|
|
147
|
+
create: () => block$1('divider', {}),
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
type: 'columns',
|
|
151
|
+
label: 'Columns',
|
|
152
|
+
icon: '▥',
|
|
153
|
+
group: 'Layout',
|
|
154
|
+
description: 'A responsive multi-column layout.',
|
|
155
|
+
keywords: ['grid', 'layout', 'row', 'column'],
|
|
156
|
+
allowedInColumns: false,
|
|
157
|
+
create: () => block$1('columns', { count: 2, ratio: '50-50', gap: 4, align: 'stretch', justify: 'start' }, [block$1('column', {}, []), block$1('column', {}, [])]),
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
/**
|
|
161
|
+
* Multi-provider injection token for adding block definitions app-wide. Each
|
|
162
|
+
* provided array is merged over {@link MK_DEFAULT_BLOCKS} (by `type`). The
|
|
163
|
+
* editor's `blocks` input takes final precedence.
|
|
164
|
+
*
|
|
165
|
+
* ```ts
|
|
166
|
+
* providers: [
|
|
167
|
+
* { provide: MK_BLOCK_DEFINITIONS, multi: true, useValue: [myCalloutBlock] },
|
|
168
|
+
* ]
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
const MK_BLOCK_DEFINITIONS = new InjectionToken('MK_BLOCK_DEFINITIONS');
|
|
172
|
+
/** App-wide upload handler token (overridable by the editor's input). */
|
|
173
|
+
const MK_BLOCK_UPLOAD_HANDLER = new InjectionToken('MK_BLOCK_UPLOAD_HANDLER');
|
|
174
|
+
/** Default embed providers: YouTube + Vimeo. */
|
|
175
|
+
const MK_DEFAULT_EMBED_PROVIDERS = [
|
|
176
|
+
{
|
|
177
|
+
name: 'YouTube',
|
|
178
|
+
test: /(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{11})/i,
|
|
179
|
+
aspectRatio: 16 / 9,
|
|
180
|
+
toEmbedUrl(url) {
|
|
181
|
+
const m = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{11})/i);
|
|
182
|
+
return m ? `https://www.youtube-nocookie.com/embed/${m[1]}` : null;
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'Vimeo',
|
|
187
|
+
test: /vimeo\.com\/(?:video\/)?(\d+)/i,
|
|
188
|
+
aspectRatio: 16 / 9,
|
|
189
|
+
toEmbedUrl(url) {
|
|
190
|
+
const m = url.match(/vimeo\.com\/(?:video\/)?(\d+)/i);
|
|
191
|
+
return m ? `https://player.vimeo.com/video/${m[1]}` : null;
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
/** App-wide embed providers token (merged with the editor's input). */
|
|
196
|
+
const MK_BLOCK_EMBED_PROVIDERS = new InjectionToken('MK_BLOCK_EMBED_PROVIDERS');
|
|
197
|
+
/**
|
|
198
|
+
* Merges default definitions with app-token definitions and per-editor input,
|
|
199
|
+
* de-duplicating by `type` (later sources win).
|
|
200
|
+
*/
|
|
201
|
+
function mkMergeBlockDefinitions(defaults, ...sources) {
|
|
202
|
+
const byType = new Map();
|
|
203
|
+
for (const def of defaults)
|
|
204
|
+
byType.set(def.type, def);
|
|
205
|
+
for (const source of sources) {
|
|
206
|
+
if (!source)
|
|
207
|
+
continue;
|
|
208
|
+
for (const def of source)
|
|
209
|
+
byType.set(def.type, def);
|
|
210
|
+
}
|
|
211
|
+
return [...byType.values()];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Shared, editor-scoped context threaded to every nested `mk-block-list` and
|
|
216
|
+
* block-editor component. Provided by {@link MkBlockEditor}, so deeply nested
|
|
217
|
+
* blocks (e.g. inside columns) read config and coordinate focus without every
|
|
218
|
+
* level re-declaring inputs.
|
|
219
|
+
*
|
|
220
|
+
* The config fields are plain `Signal` references that {@link MkBlockEditor}
|
|
221
|
+
* connects to its input-derived signals in its constructor — i.e. BEFORE any
|
|
222
|
+
* child component can inject this context — so consumers always track the live
|
|
223
|
+
* source with no mirroring effects. The initial values here only serve
|
|
224
|
+
* contexts created without an editor (e.g. component tests).
|
|
225
|
+
*/
|
|
226
|
+
class MkBlockEditorContext {
|
|
227
|
+
announcer;
|
|
228
|
+
i18n = inject(MK_I18N);
|
|
229
|
+
/** Active block palette (defaults merged with app/editor definitions). */
|
|
230
|
+
definitions = signal([], /* @ts-ignore */
|
|
231
|
+
...(ngDevMode ? [{ debugName: "definitions" }] : /* istanbul ignore next */ []));
|
|
232
|
+
/** Read-only mode: content visible, editing chrome hidden. */
|
|
233
|
+
readonly = signal(false, /* @ts-ignore */
|
|
234
|
+
...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
|
|
235
|
+
/** Disabled mode (form-level). */
|
|
236
|
+
disabled = signal(false, /* @ts-ignore */
|
|
237
|
+
...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
238
|
+
/** Placeholder for empty text blocks. */
|
|
239
|
+
placeholder = signal(this.i18n.blockEditor.emptyBlockPlaceholder, /* @ts-ignore */
|
|
240
|
+
...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
241
|
+
/** Effective upload handler (input beats token beats data-URL fallback). */
|
|
242
|
+
uploadHandler = signal(null, /* @ts-ignore */
|
|
243
|
+
...(ngDevMode ? [{ debugName: "uploadHandler" }] : /* istanbul ignore next */ []));
|
|
244
|
+
/** Effective embed providers (defaults + token + input). */
|
|
245
|
+
embedProviders = signal([], /* @ts-ignore */
|
|
246
|
+
...(ngDevMode ? [{ debugName: "embedProviders" }] : /* istanbul ignore next */ []));
|
|
247
|
+
/** Root editor host, used to locate editables for focus coordination. */
|
|
248
|
+
hostRef = null;
|
|
249
|
+
/**
|
|
250
|
+
* `type → definition` lookup, rebuilt only when the registry array changes.
|
|
251
|
+
* `definitionFor` is called several times per block per change-detection
|
|
252
|
+
* pass, so an `array.find` scan here was O(blocks × definitions) per CD.
|
|
253
|
+
*/
|
|
254
|
+
definitionMap = computed(() => {
|
|
255
|
+
const map = new Map();
|
|
256
|
+
for (const def of this.definitions())
|
|
257
|
+
map.set(def.type, def);
|
|
258
|
+
return map;
|
|
259
|
+
}, /* @ts-ignore */
|
|
260
|
+
...(ngDevMode ? [{ debugName: "definitionMap" }] : /* istanbul ignore next */ []));
|
|
261
|
+
constructor(announcer) {
|
|
262
|
+
this.announcer = announcer;
|
|
263
|
+
}
|
|
264
|
+
/** Returns the definition for a block type, if registered. */
|
|
265
|
+
definitionFor(type) {
|
|
266
|
+
return this.definitionMap().get(type);
|
|
267
|
+
}
|
|
268
|
+
/** Human label for a block type (falls back to the raw type). */
|
|
269
|
+
labelFor(type) {
|
|
270
|
+
return this.definitionFor(type)?.label ?? type;
|
|
271
|
+
}
|
|
272
|
+
/** Announce a status change to assistive tech. */
|
|
273
|
+
announce(message) {
|
|
274
|
+
this.announcer.announce(message, 'polite');
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Move focus to a block's primary editable after the next render. Queries the
|
|
278
|
+
* live DOM by `data-block-id` so it works regardless of nesting depth.
|
|
279
|
+
*/
|
|
280
|
+
focusBlock(id, position = 'end') {
|
|
281
|
+
const root = this.hostRef?.nativeElement;
|
|
282
|
+
if (!root || typeof window === 'undefined')
|
|
283
|
+
return;
|
|
284
|
+
setTimeout(() => {
|
|
285
|
+
const wrapper = root.querySelector(`[data-block-id="${cssEscape(id)}"]`);
|
|
286
|
+
const editable = wrapper?.querySelector('[data-mk-editable]');
|
|
287
|
+
const target = editable ?? wrapper?.querySelector('[data-mk-focus]');
|
|
288
|
+
if (!target)
|
|
289
|
+
return;
|
|
290
|
+
target.focus();
|
|
291
|
+
if (editable)
|
|
292
|
+
placeCaret(editable, position);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockEditorContext, deps: [{ token: i1.MkLiveAnnouncer }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
296
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockEditorContext });
|
|
297
|
+
}
|
|
298
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockEditorContext, decorators: [{
|
|
299
|
+
type: Injectable
|
|
300
|
+
}], ctorParameters: () => [{ type: i1.MkLiveAnnouncer }] });
|
|
301
|
+
/** Escapes an id for use in a CSS attribute selector. */
|
|
302
|
+
function cssEscape(value) {
|
|
303
|
+
return value.replace(/["\\]/g, '\\$&');
|
|
304
|
+
}
|
|
305
|
+
/** Places the caret at the start or end of a contenteditable element. */
|
|
306
|
+
function placeCaret(el, position) {
|
|
307
|
+
const win = el.ownerDocument.defaultView;
|
|
308
|
+
const selection = win?.getSelection();
|
|
309
|
+
if (!selection)
|
|
310
|
+
return;
|
|
311
|
+
const range = el.ownerDocument.createRange();
|
|
312
|
+
range.selectNodeContents(el);
|
|
313
|
+
range.collapse(position === 'start');
|
|
314
|
+
selection.removeAllRanges();
|
|
315
|
+
selection.addRange(range);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Paragraph block — rich text stored as sanitised HTML in `block.data.html`,
|
|
320
|
+
* with the floating inline toolbar provided by {@link MkRichTextEngine}.
|
|
321
|
+
*/
|
|
322
|
+
class MkParagraphBlock {
|
|
323
|
+
i18n = inject(MK_I18N);
|
|
324
|
+
block = input.required(/* @ts-ignore */
|
|
325
|
+
...(ngDevMode ? [{ debugName: "block" }] : /* istanbul ignore next */ []));
|
|
326
|
+
readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
327
|
+
placeholder = input(this.i18n.blockEditor.emptyBlockPlaceholder, /* @ts-ignore */
|
|
328
|
+
...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
329
|
+
blockChange = output();
|
|
330
|
+
splitAt = output();
|
|
331
|
+
removeEmpty = output();
|
|
332
|
+
arrowOut = output();
|
|
333
|
+
onContent(html) {
|
|
334
|
+
const b = this.block();
|
|
335
|
+
this.blockChange.emit({ ...b, data: { ...b.data, html } });
|
|
336
|
+
}
|
|
337
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkParagraphBlock, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
338
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.7", type: MkParagraphBlock, isStandalone: true, selector: "mk-paragraph-block", inputs: { block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blockChange: "blockChange", splitAt: "splitAt", removeEmpty: "removeEmpty", arrowOut: "arrowOut" }, ngImport: i0, template: `
|
|
339
|
+
<mk-rich-text-engine
|
|
340
|
+
[html]="block().data['html'] ?? ''"
|
|
341
|
+
[placeholder]="placeholder()"
|
|
342
|
+
[disabled]="readonly()"
|
|
343
|
+
[ariaLabel]="i18n.blockEditor.blockParagraph"
|
|
344
|
+
(contentChange)="onContent($event)"
|
|
345
|
+
(splitAt)="splitAt.emit($event)"
|
|
346
|
+
(removeEmpty)="removeEmpty.emit()"
|
|
347
|
+
(arrowOut)="arrowOut.emit($event)"
|
|
348
|
+
/>
|
|
349
|
+
`, isInline: true, 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 });
|
|
350
|
+
}
|
|
351
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkParagraphBlock, decorators: [{
|
|
352
|
+
type: Component,
|
|
353
|
+
args: [{
|
|
354
|
+
selector: 'mk-paragraph-block',
|
|
355
|
+
imports: [MkRichTextEngine],
|
|
356
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
357
|
+
template: `
|
|
358
|
+
<mk-rich-text-engine
|
|
359
|
+
[html]="block().data['html'] ?? ''"
|
|
360
|
+
[placeholder]="placeholder()"
|
|
361
|
+
[disabled]="readonly()"
|
|
362
|
+
[ariaLabel]="i18n.blockEditor.blockParagraph"
|
|
363
|
+
(contentChange)="onContent($event)"
|
|
364
|
+
(splitAt)="splitAt.emit($event)"
|
|
365
|
+
(removeEmpty)="removeEmpty.emit()"
|
|
366
|
+
(arrowOut)="arrowOut.emit($event)"
|
|
367
|
+
/>
|
|
368
|
+
`,
|
|
369
|
+
}]
|
|
370
|
+
}], propDecorators: { block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: true }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], blockChange: [{ type: i0.Output, args: ["blockChange"] }], splitAt: [{ type: i0.Output, args: ["splitAt"] }], removeEmpty: [{ type: i0.Output, args: ["removeEmpty"] }], arrowOut: [{ type: i0.Output, args: ["arrowOut"] }] } });
|
|
371
|
+
|
|
372
|
+
const LEVELS = [1, 2, 3, 4];
|
|
373
|
+
/**
|
|
374
|
+
* Heading block — the same rich text as a paragraph but rendered at a
|
|
375
|
+
* configurable level (H1–H4), chosen from an inline settings row.
|
|
376
|
+
*/
|
|
377
|
+
class MkHeadingBlock {
|
|
378
|
+
i18n = inject(MK_I18N);
|
|
379
|
+
block = input.required(/* @ts-ignore */
|
|
380
|
+
...(ngDevMode ? [{ debugName: "block" }] : /* istanbul ignore next */ []));
|
|
381
|
+
readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
382
|
+
blockChange = output();
|
|
383
|
+
splitAt = output();
|
|
384
|
+
removeEmpty = output();
|
|
385
|
+
arrowOut = output();
|
|
386
|
+
levels = LEVELS;
|
|
387
|
+
level = computed(() => {
|
|
388
|
+
const l = this.block().data['level'];
|
|
389
|
+
return LEVELS.includes(l) ? l : 2;
|
|
390
|
+
}, /* @ts-ignore */
|
|
391
|
+
...(ngDevMode ? [{ debugName: "level" }] : /* istanbul ignore next */ []));
|
|
392
|
+
setLevel(level) {
|
|
393
|
+
const b = this.block();
|
|
394
|
+
this.blockChange.emit({ ...b, data: { ...b.data, level } });
|
|
395
|
+
}
|
|
396
|
+
onContent(html) {
|
|
397
|
+
const b = this.block();
|
|
398
|
+
this.blockChange.emit({ ...b, data: { ...b.data, html } });
|
|
399
|
+
}
|
|
400
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkHeadingBlock, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
401
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkHeadingBlock, isStandalone: true, selector: "mk-heading-block", inputs: { block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blockChange: "blockChange", splitAt: "splitAt", removeEmpty: "removeEmpty", arrowOut: "arrowOut" }, ngImport: i0, template: `
|
|
402
|
+
@if (!readonly()) {
|
|
403
|
+
<div class="mk-heading-block__levels" role="group" [attr.aria-label]="i18n.blockEditor.headingLevelGroup">
|
|
404
|
+
@for (lvl of levels; track lvl) {
|
|
405
|
+
<button
|
|
406
|
+
type="button"
|
|
407
|
+
class="mk-heading-block__level"
|
|
408
|
+
[class.mk-heading-block__level--active]="level() === lvl"
|
|
409
|
+
[attr.aria-pressed]="level() === lvl"
|
|
410
|
+
(click)="setLevel(lvl)"
|
|
411
|
+
>
|
|
412
|
+
H{{ lvl }}
|
|
413
|
+
</button>
|
|
414
|
+
}
|
|
415
|
+
</div>
|
|
416
|
+
}
|
|
417
|
+
<mk-rich-text-engine
|
|
418
|
+
class="mk-heading-block__text"
|
|
419
|
+
[class]="'mk-heading-block__text--h' + level()"
|
|
420
|
+
[html]="block().data['html'] ?? ''"
|
|
421
|
+
[placeholder]="i18n.blockEditor.headingPlaceholder(level())"
|
|
422
|
+
[disabled]="readonly()"
|
|
423
|
+
[multiline]="false"
|
|
424
|
+
[ariaLabel]="i18n.blockEditor.headingLevel(level())"
|
|
425
|
+
(contentChange)="onContent($event)"
|
|
426
|
+
(splitAt)="splitAt.emit($event)"
|
|
427
|
+
(removeEmpty)="removeEmpty.emit()"
|
|
428
|
+
(arrowOut)="arrowOut.emit($event)"
|
|
429
|
+
/>
|
|
430
|
+
`, isInline: true, styles: [":host{display:block}.mk-heading-block__levels{display:inline-flex;gap:2px;margin-bottom:var(--mk-space-1);padding:2px;border-radius:var(--mk-radius-sm);background:var(--mk-surface-2)}.mk-heading-block__level{min-width:30px;height:24px;border:none;border-radius:var(--mk-radius-xs);background:transparent;color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-semibold);cursor:pointer}.mk-heading-block__level:hover{background:var(--mk-hover-overlay)}.mk-heading-block__level--active{background:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-heading-block__level:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-heading-block__text--h1{font-size:var(--mk-font-size-3xl);font-weight:var(--mk-font-weight-bold)}.mk-heading-block__text--h2{font-size:var(--mk-font-size-2xl);font-weight:var(--mk-font-weight-bold)}.mk-heading-block__text--h3{font-size:var(--mk-font-size-xl);font-weight:var(--mk-font-weight-semibold)}.mk-heading-block__text--h4{font-size:var(--mk-font-size-lg);font-weight:var(--mk-font-weight-semibold)}\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 });
|
|
431
|
+
}
|
|
432
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkHeadingBlock, decorators: [{
|
|
433
|
+
type: Component,
|
|
434
|
+
args: [{ selector: 'mk-heading-block', imports: [MkRichTextEngine], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
435
|
+
@if (!readonly()) {
|
|
436
|
+
<div class="mk-heading-block__levels" role="group" [attr.aria-label]="i18n.blockEditor.headingLevelGroup">
|
|
437
|
+
@for (lvl of levels; track lvl) {
|
|
438
|
+
<button
|
|
439
|
+
type="button"
|
|
440
|
+
class="mk-heading-block__level"
|
|
441
|
+
[class.mk-heading-block__level--active]="level() === lvl"
|
|
442
|
+
[attr.aria-pressed]="level() === lvl"
|
|
443
|
+
(click)="setLevel(lvl)"
|
|
444
|
+
>
|
|
445
|
+
H{{ lvl }}
|
|
446
|
+
</button>
|
|
447
|
+
}
|
|
448
|
+
</div>
|
|
449
|
+
}
|
|
450
|
+
<mk-rich-text-engine
|
|
451
|
+
class="mk-heading-block__text"
|
|
452
|
+
[class]="'mk-heading-block__text--h' + level()"
|
|
453
|
+
[html]="block().data['html'] ?? ''"
|
|
454
|
+
[placeholder]="i18n.blockEditor.headingPlaceholder(level())"
|
|
455
|
+
[disabled]="readonly()"
|
|
456
|
+
[multiline]="false"
|
|
457
|
+
[ariaLabel]="i18n.blockEditor.headingLevel(level())"
|
|
458
|
+
(contentChange)="onContent($event)"
|
|
459
|
+
(splitAt)="splitAt.emit($event)"
|
|
460
|
+
(removeEmpty)="removeEmpty.emit()"
|
|
461
|
+
(arrowOut)="arrowOut.emit($event)"
|
|
462
|
+
/>
|
|
463
|
+
`, styles: [":host{display:block}.mk-heading-block__levels{display:inline-flex;gap:2px;margin-bottom:var(--mk-space-1);padding:2px;border-radius:var(--mk-radius-sm);background:var(--mk-surface-2)}.mk-heading-block__level{min-width:30px;height:24px;border:none;border-radius:var(--mk-radius-xs);background:transparent;color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-semibold);cursor:pointer}.mk-heading-block__level:hover{background:var(--mk-hover-overlay)}.mk-heading-block__level--active{background:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-heading-block__level:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-heading-block__text--h1{font-size:var(--mk-font-size-3xl);font-weight:var(--mk-font-weight-bold)}.mk-heading-block__text--h2{font-size:var(--mk-font-size-2xl);font-weight:var(--mk-font-weight-bold)}.mk-heading-block__text--h3{font-size:var(--mk-font-size-xl);font-weight:var(--mk-font-weight-semibold)}.mk-heading-block__text--h4{font-size:var(--mk-font-size-lg);font-weight:var(--mk-font-weight-semibold)}\n"] }]
|
|
464
|
+
}], propDecorators: { block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: true }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], blockChange: [{ type: i0.Output, args: ["blockChange"] }], splitAt: [{ type: i0.Output, args: ["splitAt"] }], removeEmpty: [{ type: i0.Output, args: ["removeEmpty"] }], arrowOut: [{ type: i0.Output, args: ["arrowOut"] }] } });
|
|
465
|
+
|
|
466
|
+
/** Quote block — a blockquote with rich text and an optional citation line. */
|
|
467
|
+
class MkQuoteBlock {
|
|
468
|
+
i18n = inject(MK_I18N);
|
|
469
|
+
block = input.required(/* @ts-ignore */
|
|
470
|
+
...(ngDevMode ? [{ debugName: "block" }] : /* istanbul ignore next */ []));
|
|
471
|
+
readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
472
|
+
blockChange = output();
|
|
473
|
+
splitAt = output();
|
|
474
|
+
removeEmpty = output();
|
|
475
|
+
arrowOut = output();
|
|
476
|
+
onContent(html) {
|
|
477
|
+
const b = this.block();
|
|
478
|
+
this.blockChange.emit({ ...b, data: { ...b.data, html } });
|
|
479
|
+
}
|
|
480
|
+
onCite(event) {
|
|
481
|
+
const b = this.block();
|
|
482
|
+
const citation = event.target.value;
|
|
483
|
+
this.blockChange.emit({ ...b, data: { ...b.data, citation } });
|
|
484
|
+
}
|
|
485
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkQuoteBlock, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
486
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkQuoteBlock, isStandalone: true, selector: "mk-quote-block", inputs: { block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blockChange: "blockChange", splitAt: "splitAt", removeEmpty: "removeEmpty", arrowOut: "arrowOut" }, ngImport: i0, template: `
|
|
487
|
+
<blockquote class="mk-quote-block__quote">
|
|
488
|
+
<mk-rich-text-engine
|
|
489
|
+
[html]="block().data['html'] ?? ''"
|
|
490
|
+
[placeholder]="i18n.blockEditor.blockQuote"
|
|
491
|
+
[disabled]="readonly()"
|
|
492
|
+
[ariaLabel]="i18n.blockEditor.quoteText"
|
|
493
|
+
(contentChange)="onContent($event)"
|
|
494
|
+
(splitAt)="splitAt.emit($event)"
|
|
495
|
+
(removeEmpty)="removeEmpty.emit()"
|
|
496
|
+
(arrowOut)="arrowOut.emit($event)"
|
|
497
|
+
/>
|
|
498
|
+
</blockquote>
|
|
499
|
+
@if (!readonly() || block().data['citation']) {
|
|
500
|
+
<input
|
|
501
|
+
class="mk-quote-block__cite"
|
|
502
|
+
type="text"
|
|
503
|
+
[value]="block().data['citation'] ?? ''"
|
|
504
|
+
[disabled]="readonly()"
|
|
505
|
+
[placeholder]="i18n.blockEditor.addCitation"
|
|
506
|
+
[attr.aria-label]="i18n.blockEditor.citation"
|
|
507
|
+
(input)="onCite($event)"
|
|
508
|
+
/>
|
|
509
|
+
}
|
|
510
|
+
`, isInline: true, styles: [":host{display:block}.mk-quote-block__quote{margin:0;padding-inline-start:var(--mk-space-4);border-inline-start:4px solid var(--mk-primary);font-size:var(--mk-font-size-lg);font-style:italic;color:var(--mk-text)}.mk-quote-block__cite{margin-top:var(--mk-space-2);margin-inline-start:var(--mk-space-4);width:calc(100% - var(--mk-space-4));border:none;background:transparent;color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-sm)}.mk-quote-block__cite:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset);border-radius:var(--mk-radius-xs)}\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 });
|
|
511
|
+
}
|
|
512
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkQuoteBlock, decorators: [{
|
|
513
|
+
type: Component,
|
|
514
|
+
args: [{ selector: 'mk-quote-block', imports: [MkRichTextEngine], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
515
|
+
<blockquote class="mk-quote-block__quote">
|
|
516
|
+
<mk-rich-text-engine
|
|
517
|
+
[html]="block().data['html'] ?? ''"
|
|
518
|
+
[placeholder]="i18n.blockEditor.blockQuote"
|
|
519
|
+
[disabled]="readonly()"
|
|
520
|
+
[ariaLabel]="i18n.blockEditor.quoteText"
|
|
521
|
+
(contentChange)="onContent($event)"
|
|
522
|
+
(splitAt)="splitAt.emit($event)"
|
|
523
|
+
(removeEmpty)="removeEmpty.emit()"
|
|
524
|
+
(arrowOut)="arrowOut.emit($event)"
|
|
525
|
+
/>
|
|
526
|
+
</blockquote>
|
|
527
|
+
@if (!readonly() || block().data['citation']) {
|
|
528
|
+
<input
|
|
529
|
+
class="mk-quote-block__cite"
|
|
530
|
+
type="text"
|
|
531
|
+
[value]="block().data['citation'] ?? ''"
|
|
532
|
+
[disabled]="readonly()"
|
|
533
|
+
[placeholder]="i18n.blockEditor.addCitation"
|
|
534
|
+
[attr.aria-label]="i18n.blockEditor.citation"
|
|
535
|
+
(input)="onCite($event)"
|
|
536
|
+
/>
|
|
537
|
+
}
|
|
538
|
+
`, styles: [":host{display:block}.mk-quote-block__quote{margin:0;padding-inline-start:var(--mk-space-4);border-inline-start:4px solid var(--mk-primary);font-size:var(--mk-font-size-lg);font-style:italic;color:var(--mk-text)}.mk-quote-block__cite{margin-top:var(--mk-space-2);margin-inline-start:var(--mk-space-4);width:calc(100% - var(--mk-space-4));border:none;background:transparent;color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-sm)}.mk-quote-block__cite:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset);border-radius:var(--mk-radius-xs)}\n"] }]
|
|
539
|
+
}], propDecorators: { block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: true }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], blockChange: [{ type: i0.Output, args: ["blockChange"] }], splitAt: [{ type: i0.Output, args: ["splitAt"] }], removeEmpty: [{ type: i0.Output, args: ["removeEmpty"] }], arrowOut: [{ type: i0.Output, args: ["arrowOut"] }] } });
|
|
540
|
+
|
|
541
|
+
/** List block — a bulleted or numbered list of rich-text items. */
|
|
542
|
+
class MkListBlock {
|
|
543
|
+
i18n = inject(MK_I18N);
|
|
544
|
+
block = input.required(/* @ts-ignore */
|
|
545
|
+
...(ngDevMode ? [{ debugName: "block" }] : /* istanbul ignore next */ []));
|
|
546
|
+
readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
547
|
+
blockChange = output();
|
|
548
|
+
ordered = computed(() => !!this.block().data['ordered'], /* @ts-ignore */
|
|
549
|
+
...(ngDevMode ? [{ debugName: "ordered" }] : /* istanbul ignore next */ []));
|
|
550
|
+
items = computed(() => {
|
|
551
|
+
const items = this.block().data['items'];
|
|
552
|
+
return Array.isArray(items) && items.length ? items : [''];
|
|
553
|
+
}, /* @ts-ignore */
|
|
554
|
+
...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
555
|
+
setOrdered(ordered) {
|
|
556
|
+
const b = this.block();
|
|
557
|
+
this.blockChange.emit({ ...b, data: { ...b.data, ordered } });
|
|
558
|
+
}
|
|
559
|
+
onItem(index, html) {
|
|
560
|
+
const items = [...this.items()];
|
|
561
|
+
items[index] = html;
|
|
562
|
+
this.emitItems(items);
|
|
563
|
+
}
|
|
564
|
+
onSplit(index, split) {
|
|
565
|
+
const items = [...this.items()];
|
|
566
|
+
items[index] = split.before;
|
|
567
|
+
items.splice(index + 1, 0, split.after);
|
|
568
|
+
this.emitItems(items);
|
|
569
|
+
}
|
|
570
|
+
onRemove(index) {
|
|
571
|
+
const items = [...this.items()];
|
|
572
|
+
if (items.length <= 1)
|
|
573
|
+
return;
|
|
574
|
+
items.splice(index, 1);
|
|
575
|
+
this.emitItems(items);
|
|
576
|
+
}
|
|
577
|
+
emitItems(items) {
|
|
578
|
+
const b = this.block();
|
|
579
|
+
this.blockChange.emit({ ...b, data: { ...b.data, items } });
|
|
580
|
+
}
|
|
581
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkListBlock, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
582
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkListBlock, isStandalone: true, selector: "mk-list-block", inputs: { block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blockChange: "blockChange" }, ngImport: i0, template: `
|
|
583
|
+
@if (!readonly()) {
|
|
584
|
+
<div class="mk-list-block__opts" role="group" [attr.aria-label]="i18n.blockEditor.listStyle">
|
|
585
|
+
<button
|
|
586
|
+
type="button"
|
|
587
|
+
class="mk-list-block__opt"
|
|
588
|
+
[class.mk-list-block__opt--active]="!ordered()"
|
|
589
|
+
[attr.aria-pressed]="!ordered()"
|
|
590
|
+
(click)="setOrdered(false)"
|
|
591
|
+
>
|
|
592
|
+
• {{ i18n.blockEditor.bulleted }}
|
|
593
|
+
</button>
|
|
594
|
+
<button
|
|
595
|
+
type="button"
|
|
596
|
+
class="mk-list-block__opt"
|
|
597
|
+
[class.mk-list-block__opt--active]="ordered()"
|
|
598
|
+
[attr.aria-pressed]="ordered()"
|
|
599
|
+
(click)="setOrdered(true)"
|
|
600
|
+
>
|
|
601
|
+
1. {{ i18n.blockEditor.numbered }}
|
|
602
|
+
</button>
|
|
603
|
+
</div>
|
|
604
|
+
}
|
|
605
|
+
@if (ordered()) {
|
|
606
|
+
<ol class="mk-list-block__list">
|
|
607
|
+
@for (item of items(); track $index) {
|
|
608
|
+
<li class="mk-list-block__item">
|
|
609
|
+
<mk-rich-text-engine
|
|
610
|
+
[html]="item"
|
|
611
|
+
[placeholder]="i18n.blockEditor.listItem"
|
|
612
|
+
[disabled]="readonly()"
|
|
613
|
+
[multiline]="false"
|
|
614
|
+
[ariaLabel]="i18n.blockEditor.listItem"
|
|
615
|
+
(contentChange)="onItem($index, $event)"
|
|
616
|
+
(splitAt)="onSplit($index, $event)"
|
|
617
|
+
(removeEmpty)="onRemove($index)"
|
|
618
|
+
/>
|
|
619
|
+
</li>
|
|
620
|
+
}
|
|
621
|
+
</ol>
|
|
622
|
+
} @else {
|
|
623
|
+
<ul class="mk-list-block__list">
|
|
624
|
+
@for (item of items(); track $index) {
|
|
625
|
+
<li class="mk-list-block__item">
|
|
626
|
+
<mk-rich-text-engine
|
|
627
|
+
[html]="item"
|
|
628
|
+
[placeholder]="i18n.blockEditor.listItem"
|
|
629
|
+
[disabled]="readonly()"
|
|
630
|
+
[multiline]="false"
|
|
631
|
+
[ariaLabel]="i18n.blockEditor.listItem"
|
|
632
|
+
(contentChange)="onItem($index, $event)"
|
|
633
|
+
(splitAt)="onSplit($index, $event)"
|
|
634
|
+
(removeEmpty)="onRemove($index)"
|
|
635
|
+
/>
|
|
636
|
+
</li>
|
|
637
|
+
}
|
|
638
|
+
</ul>
|
|
639
|
+
}
|
|
640
|
+
`, isInline: true, styles: [":host{display:block}.mk-list-block__opts{display:inline-flex;gap:2px;margin-bottom:var(--mk-space-2);padding:2px;border-radius:var(--mk-radius-sm);background:var(--mk-surface-2)}.mk-list-block__opt{height:24px;padding:0 var(--mk-space-2);border:none;border-radius:var(--mk-radius-xs);background:transparent;color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-medium);cursor:pointer}.mk-list-block__opt:hover{background:var(--mk-hover-overlay)}.mk-list-block__opt--active{background:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-list-block__opt:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-list-block__list{margin:0;padding-inline-start:var(--mk-space-6);color:var(--mk-text)}.mk-list-block__item{margin:var(--mk-space-1) 0}\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 });
|
|
641
|
+
}
|
|
642
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkListBlock, decorators: [{
|
|
643
|
+
type: Component,
|
|
644
|
+
args: [{ selector: 'mk-list-block', imports: [MkRichTextEngine], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
645
|
+
@if (!readonly()) {
|
|
646
|
+
<div class="mk-list-block__opts" role="group" [attr.aria-label]="i18n.blockEditor.listStyle">
|
|
647
|
+
<button
|
|
648
|
+
type="button"
|
|
649
|
+
class="mk-list-block__opt"
|
|
650
|
+
[class.mk-list-block__opt--active]="!ordered()"
|
|
651
|
+
[attr.aria-pressed]="!ordered()"
|
|
652
|
+
(click)="setOrdered(false)"
|
|
653
|
+
>
|
|
654
|
+
• {{ i18n.blockEditor.bulleted }}
|
|
655
|
+
</button>
|
|
656
|
+
<button
|
|
657
|
+
type="button"
|
|
658
|
+
class="mk-list-block__opt"
|
|
659
|
+
[class.mk-list-block__opt--active]="ordered()"
|
|
660
|
+
[attr.aria-pressed]="ordered()"
|
|
661
|
+
(click)="setOrdered(true)"
|
|
662
|
+
>
|
|
663
|
+
1. {{ i18n.blockEditor.numbered }}
|
|
664
|
+
</button>
|
|
665
|
+
</div>
|
|
666
|
+
}
|
|
667
|
+
@if (ordered()) {
|
|
668
|
+
<ol class="mk-list-block__list">
|
|
669
|
+
@for (item of items(); track $index) {
|
|
670
|
+
<li class="mk-list-block__item">
|
|
671
|
+
<mk-rich-text-engine
|
|
672
|
+
[html]="item"
|
|
673
|
+
[placeholder]="i18n.blockEditor.listItem"
|
|
674
|
+
[disabled]="readonly()"
|
|
675
|
+
[multiline]="false"
|
|
676
|
+
[ariaLabel]="i18n.blockEditor.listItem"
|
|
677
|
+
(contentChange)="onItem($index, $event)"
|
|
678
|
+
(splitAt)="onSplit($index, $event)"
|
|
679
|
+
(removeEmpty)="onRemove($index)"
|
|
680
|
+
/>
|
|
681
|
+
</li>
|
|
682
|
+
}
|
|
683
|
+
</ol>
|
|
684
|
+
} @else {
|
|
685
|
+
<ul class="mk-list-block__list">
|
|
686
|
+
@for (item of items(); track $index) {
|
|
687
|
+
<li class="mk-list-block__item">
|
|
688
|
+
<mk-rich-text-engine
|
|
689
|
+
[html]="item"
|
|
690
|
+
[placeholder]="i18n.blockEditor.listItem"
|
|
691
|
+
[disabled]="readonly()"
|
|
692
|
+
[multiline]="false"
|
|
693
|
+
[ariaLabel]="i18n.blockEditor.listItem"
|
|
694
|
+
(contentChange)="onItem($index, $event)"
|
|
695
|
+
(splitAt)="onSplit($index, $event)"
|
|
696
|
+
(removeEmpty)="onRemove($index)"
|
|
697
|
+
/>
|
|
698
|
+
</li>
|
|
699
|
+
}
|
|
700
|
+
</ul>
|
|
701
|
+
}
|
|
702
|
+
`, styles: [":host{display:block}.mk-list-block__opts{display:inline-flex;gap:2px;margin-bottom:var(--mk-space-2);padding:2px;border-radius:var(--mk-radius-sm);background:var(--mk-surface-2)}.mk-list-block__opt{height:24px;padding:0 var(--mk-space-2);border:none;border-radius:var(--mk-radius-xs);background:transparent;color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-medium);cursor:pointer}.mk-list-block__opt:hover{background:var(--mk-hover-overlay)}.mk-list-block__opt--active{background:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-list-block__opt:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-list-block__list{margin:0;padding-inline-start:var(--mk-space-6);color:var(--mk-text)}.mk-list-block__item{margin:var(--mk-space-1) 0}\n"] }]
|
|
703
|
+
}], propDecorators: { block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: true }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], blockChange: [{ type: i0.Output, args: ["blockChange"] }] } });
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Code block — a plain, monospace, preformatted text area. Content is stored as
|
|
707
|
+
* raw text (never executed, never interpreted as HTML) in `block.data.code`.
|
|
708
|
+
*/
|
|
709
|
+
class MkCodeBlock {
|
|
710
|
+
i18n = inject(MK_I18N);
|
|
711
|
+
block = input.required(/* @ts-ignore */
|
|
712
|
+
...(ngDevMode ? [{ debugName: "block" }] : /* istanbul ignore next */ []));
|
|
713
|
+
readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
714
|
+
blockChange = output();
|
|
715
|
+
rows() {
|
|
716
|
+
const lines = String(this.block().data['code'] ?? '').split('\n').length;
|
|
717
|
+
return Math.max(3, Math.min(30, lines + 1));
|
|
718
|
+
}
|
|
719
|
+
onCode(event) {
|
|
720
|
+
const b = this.block();
|
|
721
|
+
const code = event.target.value;
|
|
722
|
+
this.blockChange.emit({ ...b, data: { ...b.data, code } });
|
|
723
|
+
}
|
|
724
|
+
onLang(event) {
|
|
725
|
+
const b = this.block();
|
|
726
|
+
const language = event.target.value;
|
|
727
|
+
this.blockChange.emit({ ...b, data: { ...b.data, language } });
|
|
728
|
+
}
|
|
729
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkCodeBlock, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
730
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkCodeBlock, isStandalone: true, selector: "mk-code-block", inputs: { block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blockChange: "blockChange" }, ngImport: i0, template: `
|
|
731
|
+
@if (!readonly()) {
|
|
732
|
+
<input
|
|
733
|
+
class="mk-code-block__lang"
|
|
734
|
+
type="text"
|
|
735
|
+
[value]="block().data['language'] ?? ''"
|
|
736
|
+
[placeholder]="i18n.blockEditor.codeLanguagePlaceholder"
|
|
737
|
+
[attr.aria-label]="i18n.blockEditor.codeLanguage"
|
|
738
|
+
(input)="onLang($event)"
|
|
739
|
+
/>
|
|
740
|
+
}
|
|
741
|
+
<textarea
|
|
742
|
+
class="mk-code-block__area"
|
|
743
|
+
data-mk-focus
|
|
744
|
+
spellcheck="false"
|
|
745
|
+
autocapitalize="off"
|
|
746
|
+
autocomplete="off"
|
|
747
|
+
[value]="block().data['code'] ?? ''"
|
|
748
|
+
[readOnly]="readonly()"
|
|
749
|
+
[rows]="rows()"
|
|
750
|
+
[placeholder]="i18n.blockEditor.enterCode"
|
|
751
|
+
[attr.aria-label]="i18n.blockEditor.blockCode"
|
|
752
|
+
(input)="onCode($event)"
|
|
753
|
+
></textarea>
|
|
754
|
+
`, isInline: true, styles: [":host{display:block}.mk-code-block__lang{display:block;margin-bottom:var(--mk-space-1);padding:var(--mk-space-1) var(--mk-space-2);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text-muted);font-family:var(--mk-font-mono);font-size:var(--mk-font-size-xs)}.mk-code-block__area{display:block;width:100%;min-height:4em;padding:var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-code-bg);color:var(--mk-text);font-family:var(--mk-font-mono);font-size:var(--mk-font-size-sm);line-height:var(--mk-line-height-normal);resize:vertical;tab-size:2}.mk-code-block__area:focus-visible,.mk-code-block__lang: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 });
|
|
755
|
+
}
|
|
756
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkCodeBlock, decorators: [{
|
|
757
|
+
type: Component,
|
|
758
|
+
args: [{ selector: 'mk-code-block', changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
759
|
+
@if (!readonly()) {
|
|
760
|
+
<input
|
|
761
|
+
class="mk-code-block__lang"
|
|
762
|
+
type="text"
|
|
763
|
+
[value]="block().data['language'] ?? ''"
|
|
764
|
+
[placeholder]="i18n.blockEditor.codeLanguagePlaceholder"
|
|
765
|
+
[attr.aria-label]="i18n.blockEditor.codeLanguage"
|
|
766
|
+
(input)="onLang($event)"
|
|
767
|
+
/>
|
|
768
|
+
}
|
|
769
|
+
<textarea
|
|
770
|
+
class="mk-code-block__area"
|
|
771
|
+
data-mk-focus
|
|
772
|
+
spellcheck="false"
|
|
773
|
+
autocapitalize="off"
|
|
774
|
+
autocomplete="off"
|
|
775
|
+
[value]="block().data['code'] ?? ''"
|
|
776
|
+
[readOnly]="readonly()"
|
|
777
|
+
[rows]="rows()"
|
|
778
|
+
[placeholder]="i18n.blockEditor.enterCode"
|
|
779
|
+
[attr.aria-label]="i18n.blockEditor.blockCode"
|
|
780
|
+
(input)="onCode($event)"
|
|
781
|
+
></textarea>
|
|
782
|
+
`, styles: [":host{display:block}.mk-code-block__lang{display:block;margin-bottom:var(--mk-space-1);padding:var(--mk-space-1) var(--mk-space-2);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text-muted);font-family:var(--mk-font-mono);font-size:var(--mk-font-size-xs)}.mk-code-block__area{display:block;width:100%;min-height:4em;padding:var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-code-bg);color:var(--mk-text);font-family:var(--mk-font-mono);font-size:var(--mk-font-size-sm);line-height:var(--mk-line-height-normal);resize:vertical;tab-size:2}.mk-code-block__area:focus-visible,.mk-code-block__lang:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}\n"] }]
|
|
783
|
+
}], propDecorators: { block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: true }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], blockChange: [{ type: i0.Output, args: ["blockChange"] }] } });
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Image block — upload (file picker or drag-drop) or paste a URL. If an
|
|
787
|
+
* `uploadHandler` is configured (editor input or {@link MK_BLOCK_UPLOAD_HANDLER}
|
|
788
|
+
* token) it is used to persist the file and get back a URL; otherwise the file
|
|
789
|
+
* is read as an inline `data:` URL via `FileReader`. Supports alt text,
|
|
790
|
+
* caption, width and alignment.
|
|
791
|
+
*/
|
|
792
|
+
class MkImageBlock {
|
|
793
|
+
ctx = inject(MkBlockEditorContext);
|
|
794
|
+
fileInput = viewChild('fileInput', /* @ts-ignore */
|
|
795
|
+
...(ngDevMode ? [{ debugName: "fileInput" }] : /* istanbul ignore next */ []));
|
|
796
|
+
i18n = inject(MK_I18N);
|
|
797
|
+
block = input.required(/* @ts-ignore */
|
|
798
|
+
...(ngDevMode ? [{ debugName: "block" }] : /* istanbul ignore next */ []));
|
|
799
|
+
readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
800
|
+
blockChange = output();
|
|
801
|
+
aligns = ['left', 'center', 'right'];
|
|
802
|
+
uploading = signal(false, /* @ts-ignore */
|
|
803
|
+
...(ngDevMode ? [{ debugName: "uploading" }] : /* istanbul ignore next */ []));
|
|
804
|
+
error = signal(null, /* @ts-ignore */
|
|
805
|
+
...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
806
|
+
dragActive = signal(false, /* @ts-ignore */
|
|
807
|
+
...(ngDevMode ? [{ debugName: "dragActive" }] : /* istanbul ignore next */ []));
|
|
808
|
+
src() {
|
|
809
|
+
return this.block().data['src'] ?? '';
|
|
810
|
+
}
|
|
811
|
+
/** Localised caption for an alignment option. */
|
|
812
|
+
alignLabel(align) {
|
|
813
|
+
const s = this.i18n.blockEditor;
|
|
814
|
+
return { left: s.alignLeft, center: s.alignCenter, right: s.alignRight }[align];
|
|
815
|
+
}
|
|
816
|
+
openPicker() {
|
|
817
|
+
this.fileInput()?.nativeElement.click();
|
|
818
|
+
}
|
|
819
|
+
onFileSelected(event) {
|
|
820
|
+
const input = event.target;
|
|
821
|
+
const file = input.files?.[0];
|
|
822
|
+
if (file)
|
|
823
|
+
this.handleFile(file);
|
|
824
|
+
input.value = '';
|
|
825
|
+
}
|
|
826
|
+
onDrop(event) {
|
|
827
|
+
event.preventDefault();
|
|
828
|
+
this.dragActive.set(false);
|
|
829
|
+
if (this.readonly())
|
|
830
|
+
return;
|
|
831
|
+
const file = event.dataTransfer?.files?.[0];
|
|
832
|
+
if (file)
|
|
833
|
+
this.handleFile(file);
|
|
834
|
+
}
|
|
835
|
+
onDragOver(event) {
|
|
836
|
+
if (this.readonly())
|
|
837
|
+
return;
|
|
838
|
+
event.preventDefault();
|
|
839
|
+
this.dragActive.set(true);
|
|
840
|
+
}
|
|
841
|
+
onDragLeave() {
|
|
842
|
+
this.dragActive.set(false);
|
|
843
|
+
}
|
|
844
|
+
onUrl(event) {
|
|
845
|
+
const value = event.target.value.trim();
|
|
846
|
+
this.patch({ src: value });
|
|
847
|
+
}
|
|
848
|
+
onAlt(event) {
|
|
849
|
+
this.patch({ alt: event.target.value });
|
|
850
|
+
}
|
|
851
|
+
onCaption(event) {
|
|
852
|
+
this.patch({ caption: event.target.value });
|
|
853
|
+
}
|
|
854
|
+
setAlign(align) {
|
|
855
|
+
this.patch({ align });
|
|
856
|
+
}
|
|
857
|
+
onWidth(event) {
|
|
858
|
+
this.patch({ width: Number(event.target.value) });
|
|
859
|
+
}
|
|
860
|
+
clear() {
|
|
861
|
+
this.patch({ src: '' });
|
|
862
|
+
}
|
|
863
|
+
async handleFile(file) {
|
|
864
|
+
if (!file.type.startsWith('image/')) {
|
|
865
|
+
this.error.set(this.i18n.blockEditor.notAnImage);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
this.error.set(null);
|
|
869
|
+
this.uploading.set(true);
|
|
870
|
+
try {
|
|
871
|
+
const handler = this.ctx.uploadHandler();
|
|
872
|
+
const url = handler ? await handler(file) : await this.readAsDataUrl(file);
|
|
873
|
+
this.patch({ src: url, alt: this.block().data['alt'] || file.name });
|
|
874
|
+
this.ctx.announce(this.i18n.blockEditor.imageAdded);
|
|
875
|
+
}
|
|
876
|
+
catch {
|
|
877
|
+
this.error.set(this.i18n.blockEditor.uploadFailed);
|
|
878
|
+
}
|
|
879
|
+
finally {
|
|
880
|
+
this.uploading.set(false);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
readAsDataUrl(file) {
|
|
884
|
+
return new Promise((resolve, reject) => {
|
|
885
|
+
const reader = new FileReader();
|
|
886
|
+
reader.onload = () => resolve(String(reader.result));
|
|
887
|
+
reader.onerror = () => reject(reader.error);
|
|
888
|
+
reader.readAsDataURL(file);
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
patch(data) {
|
|
892
|
+
const b = this.block();
|
|
893
|
+
this.blockChange.emit({ ...b, data: { ...b.data, ...data } });
|
|
894
|
+
}
|
|
895
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkImageBlock, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
896
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkImageBlock, isStandalone: true, selector: "mk-image-block", inputs: { block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blockChange: "blockChange" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<input\n #fileInput\n type=\"file\"\n accept=\"image/*\"\n class=\"mk-image-block__file\"\n hidden\n (change)=\"onFileSelected($event)\"\n/>\n\n@if (src()) {\n <figure\n class=\"mk-image-block__figure\"\n [class.mk-image-block__figure--left]=\"block().data['align'] === 'left'\"\n [class.mk-image-block__figure--right]=\"block().data['align'] === 'right'\"\n >\n <img\n class=\"mk-image-block__img\"\n [src]=\"src()\"\n [alt]=\"block().data['alt'] ?? ''\"\n [style.width.%]=\"block().data['width'] || 100\"\n />\n @if (block().data['caption'] || !readonly()) {\n <figcaption class=\"mk-image-block__caption\">{{ block().data['caption'] }}</figcaption>\n }\n </figure>\n\n @if (!readonly()) {\n <div class=\"mk-image-block__settings\">\n <label class=\"mk-image-block__field\">\n <span>{{ i18n.blockEditor.altText }}</span>\n <input type=\"text\" [value]=\"block().data['alt'] ?? ''\" (input)=\"onAlt($event)\" />\n </label>\n <label class=\"mk-image-block__field\">\n <span>{{ i18n.blockEditor.caption }}</span>\n <input type=\"text\" [value]=\"block().data['caption'] ?? ''\" (input)=\"onCaption($event)\" />\n </label>\n <div class=\"mk-image-block__field\">\n <span id=\"mk-img-align\">{{ i18n.blockEditor.alignment }}</span>\n <div class=\"mk-image-block__aligns\" role=\"group\" aria-labelledby=\"mk-img-align\">\n @for (a of aligns; track a) {\n <button\n type=\"button\"\n class=\"mk-image-block__align\"\n [class.mk-image-block__align--active]=\"(block().data['align'] ?? 'center') === a\"\n [attr.aria-pressed]=\"(block().data['align'] ?? 'center') === a\"\n (click)=\"setAlign(a)\"\n >\n {{ alignLabel(a) }}\n </button>\n }\n </div>\n </div>\n <label class=\"mk-image-block__field\">\n <span>{{ i18n.blockEditor.imageWidth(block().data['width'] || 100) }}</span>\n <input\n type=\"range\"\n min=\"20\"\n max=\"100\"\n step=\"5\"\n [value]=\"block().data['width'] || 100\"\n (input)=\"onWidth($event)\"\n />\n </label>\n <button type=\"button\" class=\"mk-image-block__replace\" (click)=\"clear()\">\n {{ i18n.blockEditor.replaceImage }}\n </button>\n </div>\n }\n} @else {\n <div\n class=\"mk-image-block__drop\"\n [class.mk-image-block__drop--active]=\"dragActive()\"\n (drop)=\"onDrop($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave()\"\n >\n @if (uploading()) {\n <p class=\"mk-image-block__hint\">{{ i18n.blockEditor.uploading }}</p>\n } @else {\n <p class=\"mk-image-block__icon\" aria-hidden=\"true\">\uD83D\uDDBC</p>\n <p class=\"mk-image-block__hint\">{{ i18n.blockEditor.dropImagePrompt }}</p>\n <button\n type=\"button\"\n class=\"mk-image-block__upload\"\n data-mk-focus\n [disabled]=\"readonly()\"\n (click)=\"openPicker()\"\n >\n {{ i18n.blockEditor.chooseFile }}\n </button>\n <label class=\"mk-image-block__url\">\n <span class=\"mk-visually-hidden\">{{ i18n.blockEditor.imageUrl }}</span>\n <input\n type=\"url\"\n [placeholder]=\"i18n.blockEditor.pasteImageUrl\"\n [disabled]=\"readonly()\"\n (change)=\"onUrl($event)\"\n />\n </label>\n }\n @if (error()) {\n <p class=\"mk-image-block__error\" role=\"alert\">{{ error() }}</p>\n }\n </div>\n}\n", styles: [":host{display:block}.mk-image-block__figure{margin:0;text-align:center}.mk-image-block__figure--left{text-align:left}.mk-image-block__figure--right{text-align:right}.mk-image-block__img{max-width:100%;height:auto;border-radius:var(--mk-radius-md);vertical-align:middle}.mk-image-block__caption{margin-top:var(--mk-space-2);color:var(--mk-text-muted);font-size:var(--mk-font-size-sm)}.mk-image-block__drop{display:flex;flex-direction:column;align-items:center;gap:var(--mk-space-2);padding:var(--mk-space-8) var(--mk-space-4);border:var(--mk-border-width-strong) dashed var(--mk-border-strong);border-radius:var(--mk-radius-lg);background:var(--mk-surface-2);text-align:center;transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-image-block__drop--active{border-color:var(--mk-primary);background:var(--mk-primary-subtle)}.mk-image-block__icon{margin:0;font-size:var(--mk-font-size-3xl)}.mk-image-block__hint{margin:0;color:var(--mk-text-muted);font-size:var(--mk-font-size-sm)}.mk-image-block__upload{height:var(--mk-control-height-sm);padding:0 var(--mk-space-4);border:var(--mk-border-width) solid var(--mk-border-strong);border-radius:var(--mk-radius-md);background:var(--mk-surface);color:var(--mk-text);font:inherit;font-weight:var(--mk-font-weight-medium);cursor:pointer}.mk-image-block__upload:hover{background:var(--mk-surface-3)}.mk-image-block__url input{width:min(320px,80vw);height:var(--mk-control-height-sm);padding:0 var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface);color:var(--mk-text);font:inherit}.mk-image-block__error{margin:0;color:var(--mk-danger-text);font-size:var(--mk-font-size-sm)}.mk-image-block__settings{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:var(--mk-space-3);margin-top:var(--mk-space-3);padding:var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border-subtle);border-radius:var(--mk-radius-md);background:var(--mk-surface-2)}.mk-image-block__field{display:flex;flex-direction:column;gap:var(--mk-space-1);font-size:var(--mk-font-size-xs);color:var(--mk-text-muted)}.mk-image-block__field input[type=text],.mk-image-block__field input[type=url]{height:var(--mk-control-height-sm);padding:0 var(--mk-space-2);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text);font:inherit}.mk-image-block__aligns{display:inline-flex;gap:2px}.mk-image-block__align{flex:1;height:var(--mk-control-height-sm);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-xs);text-transform:capitalize;cursor:pointer}.mk-image-block__align--active{background:var(--mk-primary);border-color:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-image-block__replace{align-self:end;height:var(--mk-control-height-sm);border:none;border-radius:var(--mk-radius-sm);background:transparent;color:var(--mk-danger-text);font:inherit;font-size:var(--mk-font-size-sm);cursor:pointer}button:focus-visible,input:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
897
|
+
}
|
|
898
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkImageBlock, decorators: [{
|
|
899
|
+
type: Component,
|
|
900
|
+
args: [{ selector: 'mk-image-block', changeDetection: ChangeDetectionStrategy.OnPush, template: "<input\n #fileInput\n type=\"file\"\n accept=\"image/*\"\n class=\"mk-image-block__file\"\n hidden\n (change)=\"onFileSelected($event)\"\n/>\n\n@if (src()) {\n <figure\n class=\"mk-image-block__figure\"\n [class.mk-image-block__figure--left]=\"block().data['align'] === 'left'\"\n [class.mk-image-block__figure--right]=\"block().data['align'] === 'right'\"\n >\n <img\n class=\"mk-image-block__img\"\n [src]=\"src()\"\n [alt]=\"block().data['alt'] ?? ''\"\n [style.width.%]=\"block().data['width'] || 100\"\n />\n @if (block().data['caption'] || !readonly()) {\n <figcaption class=\"mk-image-block__caption\">{{ block().data['caption'] }}</figcaption>\n }\n </figure>\n\n @if (!readonly()) {\n <div class=\"mk-image-block__settings\">\n <label class=\"mk-image-block__field\">\n <span>{{ i18n.blockEditor.altText }}</span>\n <input type=\"text\" [value]=\"block().data['alt'] ?? ''\" (input)=\"onAlt($event)\" />\n </label>\n <label class=\"mk-image-block__field\">\n <span>{{ i18n.blockEditor.caption }}</span>\n <input type=\"text\" [value]=\"block().data['caption'] ?? ''\" (input)=\"onCaption($event)\" />\n </label>\n <div class=\"mk-image-block__field\">\n <span id=\"mk-img-align\">{{ i18n.blockEditor.alignment }}</span>\n <div class=\"mk-image-block__aligns\" role=\"group\" aria-labelledby=\"mk-img-align\">\n @for (a of aligns; track a) {\n <button\n type=\"button\"\n class=\"mk-image-block__align\"\n [class.mk-image-block__align--active]=\"(block().data['align'] ?? 'center') === a\"\n [attr.aria-pressed]=\"(block().data['align'] ?? 'center') === a\"\n (click)=\"setAlign(a)\"\n >\n {{ alignLabel(a) }}\n </button>\n }\n </div>\n </div>\n <label class=\"mk-image-block__field\">\n <span>{{ i18n.blockEditor.imageWidth(block().data['width'] || 100) }}</span>\n <input\n type=\"range\"\n min=\"20\"\n max=\"100\"\n step=\"5\"\n [value]=\"block().data['width'] || 100\"\n (input)=\"onWidth($event)\"\n />\n </label>\n <button type=\"button\" class=\"mk-image-block__replace\" (click)=\"clear()\">\n {{ i18n.blockEditor.replaceImage }}\n </button>\n </div>\n }\n} @else {\n <div\n class=\"mk-image-block__drop\"\n [class.mk-image-block__drop--active]=\"dragActive()\"\n (drop)=\"onDrop($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave()\"\n >\n @if (uploading()) {\n <p class=\"mk-image-block__hint\">{{ i18n.blockEditor.uploading }}</p>\n } @else {\n <p class=\"mk-image-block__icon\" aria-hidden=\"true\">\uD83D\uDDBC</p>\n <p class=\"mk-image-block__hint\">{{ i18n.blockEditor.dropImagePrompt }}</p>\n <button\n type=\"button\"\n class=\"mk-image-block__upload\"\n data-mk-focus\n [disabled]=\"readonly()\"\n (click)=\"openPicker()\"\n >\n {{ i18n.blockEditor.chooseFile }}\n </button>\n <label class=\"mk-image-block__url\">\n <span class=\"mk-visually-hidden\">{{ i18n.blockEditor.imageUrl }}</span>\n <input\n type=\"url\"\n [placeholder]=\"i18n.blockEditor.pasteImageUrl\"\n [disabled]=\"readonly()\"\n (change)=\"onUrl($event)\"\n />\n </label>\n }\n @if (error()) {\n <p class=\"mk-image-block__error\" role=\"alert\">{{ error() }}</p>\n }\n </div>\n}\n", styles: [":host{display:block}.mk-image-block__figure{margin:0;text-align:center}.mk-image-block__figure--left{text-align:left}.mk-image-block__figure--right{text-align:right}.mk-image-block__img{max-width:100%;height:auto;border-radius:var(--mk-radius-md);vertical-align:middle}.mk-image-block__caption{margin-top:var(--mk-space-2);color:var(--mk-text-muted);font-size:var(--mk-font-size-sm)}.mk-image-block__drop{display:flex;flex-direction:column;align-items:center;gap:var(--mk-space-2);padding:var(--mk-space-8) var(--mk-space-4);border:var(--mk-border-width-strong) dashed var(--mk-border-strong);border-radius:var(--mk-radius-lg);background:var(--mk-surface-2);text-align:center;transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-image-block__drop--active{border-color:var(--mk-primary);background:var(--mk-primary-subtle)}.mk-image-block__icon{margin:0;font-size:var(--mk-font-size-3xl)}.mk-image-block__hint{margin:0;color:var(--mk-text-muted);font-size:var(--mk-font-size-sm)}.mk-image-block__upload{height:var(--mk-control-height-sm);padding:0 var(--mk-space-4);border:var(--mk-border-width) solid var(--mk-border-strong);border-radius:var(--mk-radius-md);background:var(--mk-surface);color:var(--mk-text);font:inherit;font-weight:var(--mk-font-weight-medium);cursor:pointer}.mk-image-block__upload:hover{background:var(--mk-surface-3)}.mk-image-block__url input{width:min(320px,80vw);height:var(--mk-control-height-sm);padding:0 var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface);color:var(--mk-text);font:inherit}.mk-image-block__error{margin:0;color:var(--mk-danger-text);font-size:var(--mk-font-size-sm)}.mk-image-block__settings{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:var(--mk-space-3);margin-top:var(--mk-space-3);padding:var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border-subtle);border-radius:var(--mk-radius-md);background:var(--mk-surface-2)}.mk-image-block__field{display:flex;flex-direction:column;gap:var(--mk-space-1);font-size:var(--mk-font-size-xs);color:var(--mk-text-muted)}.mk-image-block__field input[type=text],.mk-image-block__field input[type=url]{height:var(--mk-control-height-sm);padding:0 var(--mk-space-2);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text);font:inherit}.mk-image-block__aligns{display:inline-flex;gap:2px}.mk-image-block__align{flex:1;height:var(--mk-control-height-sm);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-xs);text-transform:capitalize;cursor:pointer}.mk-image-block__align--active{background:var(--mk-primary);border-color:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-image-block__replace{align-self:end;height:var(--mk-control-height-sm);border:none;border-radius:var(--mk-radius-sm);background:transparent;color:var(--mk-danger-text);font:inherit;font-size:var(--mk-font-size-sm);cursor:pointer}button:focus-visible,input:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}\n"] }]
|
|
901
|
+
}], propDecorators: { fileInput: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }], block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: true }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], blockChange: [{ type: i0.Output, args: ["blockChange"] }] } });
|
|
902
|
+
|
|
903
|
+
/** Escapes text for safe placement in element content. */
|
|
904
|
+
function mkEscapeHtml(value) {
|
|
905
|
+
return String(value ?? '')
|
|
906
|
+
.replace(/&/g, '&')
|
|
907
|
+
.replace(/</g, '<')
|
|
908
|
+
.replace(/>/g, '>');
|
|
909
|
+
}
|
|
910
|
+
/** Escapes a value for safe placement in a double-quoted attribute. */
|
|
911
|
+
function mkEscapeAttr(value) {
|
|
912
|
+
return mkEscapeHtml(value).replace(/"/g, '"');
|
|
913
|
+
}
|
|
914
|
+
/** True for URL schemes safe to place in `href`/`src`. */
|
|
915
|
+
function mkIsSafeUrl(url) {
|
|
916
|
+
const trimmed = String(url ?? '').trim();
|
|
917
|
+
if (trimmed === '')
|
|
918
|
+
return false;
|
|
919
|
+
// Allow relative, http(s), mailto, tel and data:image.
|
|
920
|
+
if (/^(https?:|mailto:|tel:)/i.test(trimmed))
|
|
921
|
+
return true;
|
|
922
|
+
if (/^data:image\//i.test(trimmed))
|
|
923
|
+
return true;
|
|
924
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed))
|
|
925
|
+
return false; // any other scheme
|
|
926
|
+
return true; // relative / anchor / protocol-relative-ish
|
|
927
|
+
}
|
|
928
|
+
/** Maps a ratio preset like `66-33` to a CSS `grid-template-columns` value. */
|
|
929
|
+
function mkRatioToColumns(ratio, count) {
|
|
930
|
+
const parts = String(ratio ?? '')
|
|
931
|
+
.split('-')
|
|
932
|
+
.map((n) => parseFloat(n))
|
|
933
|
+
.filter((n) => !isNaN(n) && n > 0);
|
|
934
|
+
if (parts.length >= 2)
|
|
935
|
+
return parts.map((n) => `${n}fr`).join(' ');
|
|
936
|
+
return `repeat(${Math.max(1, count || 1)}, 1fr)`;
|
|
937
|
+
}
|
|
938
|
+
const HEADING_LEVELS = new Set([1, 2, 3, 4]);
|
|
939
|
+
/** Serialises one block to a semantic HTML string. */
|
|
940
|
+
function blockToHtml(b) {
|
|
941
|
+
switch (b.type) {
|
|
942
|
+
case 'paragraph':
|
|
943
|
+
return `<p>${sanitizeInlineHtml(b.data['html'] ?? '')}</p>`;
|
|
944
|
+
case 'heading': {
|
|
945
|
+
const level = HEADING_LEVELS.has(b.data['level']) ? b.data['level'] : 2;
|
|
946
|
+
return `<h${level}>${sanitizeInlineHtml(b.data['html'] ?? '')}</h${level}>`;
|
|
947
|
+
}
|
|
948
|
+
case 'list': {
|
|
949
|
+
const ordered = !!b.data['ordered'];
|
|
950
|
+
const tag = ordered ? 'ol' : 'ul';
|
|
951
|
+
const items = Array.isArray(b.data['items']) ? b.data['items'] : [];
|
|
952
|
+
const li = items.map((it) => `<li>${sanitizeInlineHtml(it ?? '')}</li>`).join('');
|
|
953
|
+
return `<${tag}>${li}</${tag}>`;
|
|
954
|
+
}
|
|
955
|
+
case 'quote': {
|
|
956
|
+
const cite = String(b.data['citation'] ?? '').trim();
|
|
957
|
+
const citeHtml = cite ? `<cite>${mkEscapeHtml(cite)}</cite>` : '';
|
|
958
|
+
return `<blockquote><p>${sanitizeInlineHtml(b.data['html'] ?? '')}</p>${citeHtml}</blockquote>`;
|
|
959
|
+
}
|
|
960
|
+
case 'code': {
|
|
961
|
+
const code = mkEscapeHtml(b.data['code'] ?? '');
|
|
962
|
+
const lang = String(b.data['language'] ?? '').trim();
|
|
963
|
+
const cls = lang ? ` class="language-${mkEscapeAttr(lang)}"` : '';
|
|
964
|
+
return `<pre><code${cls}>${code}</code></pre>`;
|
|
965
|
+
}
|
|
966
|
+
case 'image': {
|
|
967
|
+
const src = String(b.data['src'] ?? '');
|
|
968
|
+
if (!mkIsSafeUrl(src) || !src)
|
|
969
|
+
return '';
|
|
970
|
+
const align = String(b.data['align'] ?? 'center');
|
|
971
|
+
const width = Number(b.data['width']) || 100;
|
|
972
|
+
const alt = mkEscapeAttr(b.data['alt'] ?? '');
|
|
973
|
+
const caption = String(b.data['caption'] ?? '').trim();
|
|
974
|
+
const figStyle = `text-align:${align === 'left' ? 'left' : align === 'right' ? 'right' : 'center'};margin:1rem 0;`;
|
|
975
|
+
const img = `<img src="${mkEscapeAttr(src)}" alt="${alt}" style="max-width:${Math.min(100, Math.max(10, width))}%;height:auto;border-radius:.5rem;" loading="lazy">`;
|
|
976
|
+
const cap = caption ? `<figcaption style="font-size:.875rem;opacity:.75;margin-top:.5rem;">${mkEscapeHtml(caption)}</figcaption>` : '';
|
|
977
|
+
return `<figure style="${figStyle}">${img}${cap}</figure>`;
|
|
978
|
+
}
|
|
979
|
+
case 'button': {
|
|
980
|
+
const href = String(b.data['href'] ?? '');
|
|
981
|
+
const label = mkEscapeHtml(b.data['label'] ?? 'Button');
|
|
982
|
+
const align = String(b.data['align'] ?? 'left');
|
|
983
|
+
const safe = mkIsSafeUrl(href) && href ? mkEscapeAttr(href) : '#';
|
|
984
|
+
const wrapStyle = `text-align:${align};margin:1rem 0;`;
|
|
985
|
+
const btnStyle = 'display:inline-block;padding:.6rem 1.1rem;border-radius:.5rem;text-decoration:none;font-weight:600;';
|
|
986
|
+
return `<div style="${wrapStyle}"><a href="${safe}" class="mk-cta mk-cta--${mkEscapeAttr(b.data['tone'] ?? 'primary')}" style="${btnStyle}" rel="noopener noreferrer">${label}</a></div>`;
|
|
987
|
+
}
|
|
988
|
+
case 'divider':
|
|
989
|
+
return '<hr>';
|
|
990
|
+
case 'embed': {
|
|
991
|
+
const url = String(b.data['url'] ?? '');
|
|
992
|
+
const embedUrl = String(b.data['embedUrl'] ?? '');
|
|
993
|
+
const ratio = Number(b.data['aspectRatio']) || 16 / 9;
|
|
994
|
+
if (embedUrl && mkIsSafeUrl(embedUrl)) {
|
|
995
|
+
const pad = (100 / ratio).toFixed(3);
|
|
996
|
+
return (`<div class="mk-embed" style="position:relative;width:100%;padding-top:${pad}%;margin:1rem 0;">` +
|
|
997
|
+
`<iframe src="${mkEscapeAttr(embedUrl)}" title="Embedded content" loading="lazy" ` +
|
|
998
|
+
`sandbox="allow-scripts allow-same-origin allow-presentation allow-popups" ` +
|
|
999
|
+
`referrerpolicy="strict-origin-when-cross-origin" allowfullscreen ` +
|
|
1000
|
+
`style="position:absolute;inset:0;width:100%;height:100%;border:0;border-radius:.5rem;"></iframe></div>`);
|
|
1001
|
+
}
|
|
1002
|
+
if (mkIsSafeUrl(url) && url) {
|
|
1003
|
+
return `<p class="mk-embed-fallback"><a href="${mkEscapeAttr(url)}" rel="noopener noreferrer" target="_blank">${mkEscapeHtml(url)}</a></p>`;
|
|
1004
|
+
}
|
|
1005
|
+
return '';
|
|
1006
|
+
}
|
|
1007
|
+
case 'columns': {
|
|
1008
|
+
const count = Number(b.data['count']) || (b.children?.length ?? 2);
|
|
1009
|
+
const cols = mkRatioToColumns(b.data['ratio'] ?? '', count);
|
|
1010
|
+
const gap = (Number(b.data['gap']) || 4) * 0.25;
|
|
1011
|
+
const align = String(b.data['align'] ?? 'stretch');
|
|
1012
|
+
const justify = String(b.data['justify'] ?? 'start');
|
|
1013
|
+
const justifyMap = {
|
|
1014
|
+
start: 'flex-start', center: 'center', end: 'flex-end', between: 'space-between',
|
|
1015
|
+
};
|
|
1016
|
+
const style = `display:grid;grid-template-columns:${cols};gap:${gap}rem;align-items:${align};` +
|
|
1017
|
+
`justify-content:${justifyMap[justify] ?? 'flex-start'};margin:1rem 0;`;
|
|
1018
|
+
const columns = (b.children ?? [])
|
|
1019
|
+
.map((col) => `<div class="mk-column">${(col.children ?? []).map(blockToHtml).join('')}</div>`)
|
|
1020
|
+
.join('');
|
|
1021
|
+
return `<div class="mk-columns" style="${style}">${columns}</div>`;
|
|
1022
|
+
}
|
|
1023
|
+
case 'column':
|
|
1024
|
+
return (b.children ?? []).map(blockToHtml).join('');
|
|
1025
|
+
default:
|
|
1026
|
+
return '';
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Serialises a document to clean, semantic, self-contained HTML — the "rich
|
|
1031
|
+
* HTML content" you persist and publish for a blog post or content page.
|
|
1032
|
+
* Layout blocks become responsive grid wrappers with inline styles.
|
|
1033
|
+
*/
|
|
1034
|
+
function mkBlocksToHtml(doc) {
|
|
1035
|
+
if (!doc?.blocks?.length)
|
|
1036
|
+
return '';
|
|
1037
|
+
return doc.blocks.map(blockToHtml).join('\n');
|
|
1038
|
+
}
|
|
1039
|
+
/** Strips all tags, collapsing a block to its text content. */
|
|
1040
|
+
function stripTags(html) {
|
|
1041
|
+
return String(html ?? '')
|
|
1042
|
+
.replace(/<[^>]*>/g, ' ')
|
|
1043
|
+
.replace(/ /g, ' ')
|
|
1044
|
+
.replace(/&/g, '&')
|
|
1045
|
+
.replace(/</g, '<')
|
|
1046
|
+
.replace(/>/g, '>')
|
|
1047
|
+
.replace(/\s+/g, ' ')
|
|
1048
|
+
.trim();
|
|
1049
|
+
}
|
|
1050
|
+
/** Collects the plain-text content of a block subtree. */
|
|
1051
|
+
function blockToText(b) {
|
|
1052
|
+
switch (b.type) {
|
|
1053
|
+
case 'paragraph':
|
|
1054
|
+
case 'heading':
|
|
1055
|
+
case 'quote':
|
|
1056
|
+
return stripTags(b.data['html'] ?? '');
|
|
1057
|
+
case 'list':
|
|
1058
|
+
return (Array.isArray(b.data['items']) ? b.data['items'] : [])
|
|
1059
|
+
.map((i) => stripTags(i))
|
|
1060
|
+
.filter(Boolean)
|
|
1061
|
+
.join(' · ');
|
|
1062
|
+
case 'code':
|
|
1063
|
+
return String(b.data['code'] ?? '').trim();
|
|
1064
|
+
case 'image':
|
|
1065
|
+
return String(b.data['caption'] ?? b.data['alt'] ?? '').trim();
|
|
1066
|
+
case 'button':
|
|
1067
|
+
return String(b.data['label'] ?? '').trim();
|
|
1068
|
+
case 'embed':
|
|
1069
|
+
return String(b.data['url'] ?? '').trim();
|
|
1070
|
+
case 'columns':
|
|
1071
|
+
case 'column':
|
|
1072
|
+
return (b.children ?? [])
|
|
1073
|
+
.map((c) => (c.type === 'column' ? blockToText(c) : blockToText(c)))
|
|
1074
|
+
.filter(Boolean)
|
|
1075
|
+
.join(' ');
|
|
1076
|
+
default:
|
|
1077
|
+
return '';
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Serialises a document to plain text — handy for previews, excerpts, search
|
|
1082
|
+
* indexes and meta descriptions.
|
|
1083
|
+
*/
|
|
1084
|
+
function mkBlocksToText(doc) {
|
|
1085
|
+
if (!doc?.blocks?.length)
|
|
1086
|
+
return '';
|
|
1087
|
+
return doc.blocks
|
|
1088
|
+
.map(blockToText)
|
|
1089
|
+
.filter((t) => t.length > 0)
|
|
1090
|
+
.join('\n\n');
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Embed block — paste a URL; matching allow-listed providers (YouTube, Vimeo,
|
|
1095
|
+
* plus any supplied via `embedProviders`) render a SANDBOXED iframe. The only
|
|
1096
|
+
* value passed to `DomSanitizer.bypassSecurityTrustResourceUrl` is the
|
|
1097
|
+
* provider-transformed embed URL (never the raw pasted string). Non-matching
|
|
1098
|
+
* URLs fall back to a safe link card — raw HTML is never injected.
|
|
1099
|
+
*/
|
|
1100
|
+
class MkEmbedBlock {
|
|
1101
|
+
ctx = inject(MkBlockEditorContext);
|
|
1102
|
+
sanitizer = inject(DomSanitizer);
|
|
1103
|
+
i18n = inject(MK_I18N);
|
|
1104
|
+
block = input.required(/* @ts-ignore */
|
|
1105
|
+
...(ngDevMode ? [{ debugName: "block" }] : /* istanbul ignore next */ []));
|
|
1106
|
+
readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1107
|
+
blockChange = output();
|
|
1108
|
+
embedUrl = computed(() => this.block().data['embedUrl'] ?? '', /* @ts-ignore */
|
|
1109
|
+
...(ngDevMode ? [{ debugName: "embedUrl" }] : /* istanbul ignore next */ []));
|
|
1110
|
+
url = computed(() => this.block().data['url'] ?? '', /* @ts-ignore */
|
|
1111
|
+
...(ngDevMode ? [{ debugName: "url" }] : /* istanbul ignore next */ []));
|
|
1112
|
+
aspectRatio = computed(() => Number(this.block().data['aspectRatio']) || 16 / 9, /* @ts-ignore */
|
|
1113
|
+
...(ngDevMode ? [{ debugName: "aspectRatio" }] : /* istanbul ignore next */ []));
|
|
1114
|
+
providerName = computed(() => this.block().data['provider'] ?? '', /* @ts-ignore */
|
|
1115
|
+
...(ngDevMode ? [{ debugName: "providerName" }] : /* istanbul ignore next */ []));
|
|
1116
|
+
paddingTop = computed(() => `${100 / this.aspectRatio()}%`, /* @ts-ignore */
|
|
1117
|
+
...(ngDevMode ? [{ debugName: "paddingTop" }] : /* istanbul ignore next */ []));
|
|
1118
|
+
/** Only ever wraps a provider-transformed URL — see the class doc. */
|
|
1119
|
+
safeSrc = computed(() => {
|
|
1120
|
+
const src = this.embedUrl();
|
|
1121
|
+
if (!src || !mkIsSafeUrl(src) || !/^https:\/\//i.test(src))
|
|
1122
|
+
return null;
|
|
1123
|
+
return this.sanitizer.bypassSecurityTrustResourceUrl(src);
|
|
1124
|
+
}, /* @ts-ignore */
|
|
1125
|
+
...(ngDevMode ? [{ debugName: "safeSrc" }] : /* istanbul ignore next */ []));
|
|
1126
|
+
hasFallback = computed(() => !this.embedUrl() && !!this.url() && mkIsSafeUrl(this.url()), /* @ts-ignore */
|
|
1127
|
+
...(ngDevMode ? [{ debugName: "hasFallback" }] : /* istanbul ignore next */ []));
|
|
1128
|
+
onUrl(event) {
|
|
1129
|
+
const raw = event.target.value.trim();
|
|
1130
|
+
this.resolve(raw);
|
|
1131
|
+
}
|
|
1132
|
+
resolve(raw) {
|
|
1133
|
+
const b = this.block();
|
|
1134
|
+
if (!raw) {
|
|
1135
|
+
this.blockChange.emit({ ...b, data: { ...b.data, url: '', embedUrl: '', provider: '' } });
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
for (const provider of this.ctx.embedProviders()) {
|
|
1139
|
+
if (provider.test.test(raw)) {
|
|
1140
|
+
const embedUrl = provider.toEmbedUrl(raw);
|
|
1141
|
+
if (embedUrl) {
|
|
1142
|
+
this.blockChange.emit({
|
|
1143
|
+
...b,
|
|
1144
|
+
data: {
|
|
1145
|
+
...b.data,
|
|
1146
|
+
url: raw,
|
|
1147
|
+
embedUrl,
|
|
1148
|
+
provider: provider.name,
|
|
1149
|
+
aspectRatio: provider.aspectRatio ?? 16 / 9,
|
|
1150
|
+
},
|
|
1151
|
+
});
|
|
1152
|
+
this.ctx.announce(this.i18n.blockEditor.embedAdded(provider.name));
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
// No provider matched — keep the URL for the safe link-card fallback.
|
|
1158
|
+
this.blockChange.emit({ ...b, data: { ...b.data, url: raw, embedUrl: '', provider: '' } });
|
|
1159
|
+
}
|
|
1160
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkEmbedBlock, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1161
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkEmbedBlock, isStandalone: true, selector: "mk-embed-block", inputs: { block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blockChange: "blockChange" }, ngImport: i0, template: "@if (safeSrc()) {\n <div class=\"mk-embed-block__frame\" [style.padding-top]=\"paddingTop()\">\n <iframe\n class=\"mk-embed-block__iframe\"\n [src]=\"safeSrc()\"\n [title]=\"i18n.blockEditor.embedTitle(providerName())\"\n loading=\"lazy\"\n referrerpolicy=\"strict-origin-when-cross-origin\"\n sandbox=\"allow-scripts allow-same-origin allow-presentation allow-popups\"\n allowfullscreen\n ></iframe>\n </div>\n} @else if (hasFallback()) {\n <a class=\"mk-embed-block__card\" [href]=\"url()\" target=\"_blank\" rel=\"noopener noreferrer\">\n <span class=\"mk-embed-block__card-icon\" aria-hidden=\"true\">\uD83D\uDD17</span>\n <span class=\"mk-embed-block__card-body\">\n <span class=\"mk-embed-block__card-title\">{{ i18n.blockEditor.externalContent }}</span>\n <span class=\"mk-embed-block__card-url\">{{ url() }}</span>\n </span>\n </a>\n}\n\n@if (!readonly()) {\n <label class=\"mk-embed-block__input\">\n <span class=\"mk-visually-hidden\">{{ i18n.blockEditor.embedUrl }}</span>\n <input\n type=\"url\"\n data-mk-focus\n [value]=\"url()\"\n [placeholder]=\"i18n.blockEditor.pasteEmbedUrl\"\n (change)=\"onUrl($event)\"\n />\n </label>\n @if (url() && !safeSrc() && !hasFallback()) {\n <p class=\"mk-embed-block__note\">{{ i18n.blockEditor.embedFallbackNote }}</p>\n }\n}\n", styles: [":host{display:block}.mk-embed-block__frame{position:relative;width:100%;overflow:hidden;border-radius:var(--mk-radius-md);background:var(--mk-surface-3)}.mk-embed-block__iframe{position:absolute;inset:0;width:100%;height:100%;border:0}.mk-embed-block__card{display:flex;align-items:center;gap:var(--mk-space-3);padding:var(--mk-space-3) var(--mk-space-4);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface-2);color:var(--mk-text);text-decoration:none}.mk-embed-block__card:hover{border-color:var(--mk-border-strong);background:var(--mk-surface-3)}.mk-embed-block__card-icon{font-size:var(--mk-font-size-xl)}.mk-embed-block__card-body{display:flex;flex-direction:column;min-width:0}.mk-embed-block__card-title{font-weight:var(--mk-font-weight-medium)}.mk-embed-block__card-url{color:var(--mk-text-muted);font-size:var(--mk-font-size-sm);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mk-embed-block__input{display:block;margin-top:var(--mk-space-2)}.mk-embed-block__input input{width:100%;height:var(--mk-control-height-sm);padding:0 var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface);color:var(--mk-text);font:inherit}.mk-embed-block__input input:focus-visible,.mk-embed-block__card:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-embed-block__note{margin:var(--mk-space-1) 0 0;color:var(--mk-text-subtle);font-size:var(--mk-font-size-xs)}.mk-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1162
|
+
}
|
|
1163
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkEmbedBlock, decorators: [{
|
|
1164
|
+
type: Component,
|
|
1165
|
+
args: [{ selector: 'mk-embed-block', changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (safeSrc()) {\n <div class=\"mk-embed-block__frame\" [style.padding-top]=\"paddingTop()\">\n <iframe\n class=\"mk-embed-block__iframe\"\n [src]=\"safeSrc()\"\n [title]=\"i18n.blockEditor.embedTitle(providerName())\"\n loading=\"lazy\"\n referrerpolicy=\"strict-origin-when-cross-origin\"\n sandbox=\"allow-scripts allow-same-origin allow-presentation allow-popups\"\n allowfullscreen\n ></iframe>\n </div>\n} @else if (hasFallback()) {\n <a class=\"mk-embed-block__card\" [href]=\"url()\" target=\"_blank\" rel=\"noopener noreferrer\">\n <span class=\"mk-embed-block__card-icon\" aria-hidden=\"true\">\uD83D\uDD17</span>\n <span class=\"mk-embed-block__card-body\">\n <span class=\"mk-embed-block__card-title\">{{ i18n.blockEditor.externalContent }}</span>\n <span class=\"mk-embed-block__card-url\">{{ url() }}</span>\n </span>\n </a>\n}\n\n@if (!readonly()) {\n <label class=\"mk-embed-block__input\">\n <span class=\"mk-visually-hidden\">{{ i18n.blockEditor.embedUrl }}</span>\n <input\n type=\"url\"\n data-mk-focus\n [value]=\"url()\"\n [placeholder]=\"i18n.blockEditor.pasteEmbedUrl\"\n (change)=\"onUrl($event)\"\n />\n </label>\n @if (url() && !safeSrc() && !hasFallback()) {\n <p class=\"mk-embed-block__note\">{{ i18n.blockEditor.embedFallbackNote }}</p>\n }\n}\n", styles: [":host{display:block}.mk-embed-block__frame{position:relative;width:100%;overflow:hidden;border-radius:var(--mk-radius-md);background:var(--mk-surface-3)}.mk-embed-block__iframe{position:absolute;inset:0;width:100%;height:100%;border:0}.mk-embed-block__card{display:flex;align-items:center;gap:var(--mk-space-3);padding:var(--mk-space-3) var(--mk-space-4);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface-2);color:var(--mk-text);text-decoration:none}.mk-embed-block__card:hover{border-color:var(--mk-border-strong);background:var(--mk-surface-3)}.mk-embed-block__card-icon{font-size:var(--mk-font-size-xl)}.mk-embed-block__card-body{display:flex;flex-direction:column;min-width:0}.mk-embed-block__card-title{font-weight:var(--mk-font-weight-medium)}.mk-embed-block__card-url{color:var(--mk-text-muted);font-size:var(--mk-font-size-sm);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mk-embed-block__input{display:block;margin-top:var(--mk-space-2)}.mk-embed-block__input input{width:100%;height:var(--mk-control-height-sm);padding:0 var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface);color:var(--mk-text);font:inherit}.mk-embed-block__input input:focus-visible,.mk-embed-block__card:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-embed-block__note{margin:var(--mk-space-1) 0 0;color:var(--mk-text-subtle);font-size:var(--mk-font-size-xs)}.mk-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}\n"] }]
|
|
1166
|
+
}], propDecorators: { block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: true }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], blockChange: [{ type: i0.Output, args: ["blockChange"] }] } });
|
|
1167
|
+
|
|
1168
|
+
const TONES = ['primary', 'neutral', 'success', 'warning', 'danger', 'info'];
|
|
1169
|
+
/**
|
|
1170
|
+
* Button/CTA block — a themed call-to-action link with configurable label,
|
|
1171
|
+
* href, tone, variant and alignment. Styling mirrors the mk-kit button tokens.
|
|
1172
|
+
*/
|
|
1173
|
+
class MkButtonBlock {
|
|
1174
|
+
i18n = inject(MK_I18N);
|
|
1175
|
+
block = input.required(/* @ts-ignore */
|
|
1176
|
+
...(ngDevMode ? [{ debugName: "block" }] : /* istanbul ignore next */ []));
|
|
1177
|
+
readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1178
|
+
blockChange = output();
|
|
1179
|
+
tones = TONES;
|
|
1180
|
+
aligns = ['left', 'center', 'right'];
|
|
1181
|
+
variants = ['solid', 'soft', 'outline'];
|
|
1182
|
+
data(key) {
|
|
1183
|
+
return this.block().data[key];
|
|
1184
|
+
}
|
|
1185
|
+
/** Localised captions for the tone / variant / alignment option buttons. */
|
|
1186
|
+
toneLabel(tone) {
|
|
1187
|
+
const s = this.i18n.blockEditor;
|
|
1188
|
+
const map = {
|
|
1189
|
+
primary: s.tonePrimary,
|
|
1190
|
+
neutral: s.toneNeutral,
|
|
1191
|
+
success: s.toneSuccess,
|
|
1192
|
+
warning: s.toneWarning,
|
|
1193
|
+
danger: s.toneDanger,
|
|
1194
|
+
info: s.toneInfo,
|
|
1195
|
+
};
|
|
1196
|
+
return map[tone];
|
|
1197
|
+
}
|
|
1198
|
+
variantLabel(variant) {
|
|
1199
|
+
const s = this.i18n.blockEditor;
|
|
1200
|
+
const map = {
|
|
1201
|
+
solid: s.variantSolid,
|
|
1202
|
+
soft: s.variantSoft,
|
|
1203
|
+
outline: s.variantOutline,
|
|
1204
|
+
};
|
|
1205
|
+
return map[variant] ?? variant;
|
|
1206
|
+
}
|
|
1207
|
+
alignLabel(align) {
|
|
1208
|
+
const s = this.i18n.blockEditor;
|
|
1209
|
+
return { left: s.alignLeft, center: s.alignCenter, right: s.alignRight }[align];
|
|
1210
|
+
}
|
|
1211
|
+
onLabel(event) {
|
|
1212
|
+
this.patch({ label: event.target.value });
|
|
1213
|
+
}
|
|
1214
|
+
onHref(event) {
|
|
1215
|
+
this.patch({ href: event.target.value.trim() });
|
|
1216
|
+
}
|
|
1217
|
+
setTone(tone) {
|
|
1218
|
+
this.patch({ tone });
|
|
1219
|
+
}
|
|
1220
|
+
setVariant(variant) {
|
|
1221
|
+
this.patch({ variant });
|
|
1222
|
+
}
|
|
1223
|
+
setAlign(align) {
|
|
1224
|
+
this.patch({ align });
|
|
1225
|
+
}
|
|
1226
|
+
patch(data) {
|
|
1227
|
+
const b = this.block();
|
|
1228
|
+
this.blockChange.emit({ ...b, data: { ...b.data, ...data } });
|
|
1229
|
+
}
|
|
1230
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkButtonBlock, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1231
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkButtonBlock, isStandalone: true, selector: "mk-button-block", inputs: { block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blockChange: "blockChange" }, ngImport: i0, template: "<div\n class=\"mk-button-block__preview\"\n [class.mk-button-block__preview--center]=\"(data('align') ?? 'left') === 'center'\"\n [class.mk-button-block__preview--right]=\"(data('align') ?? 'left') === 'right'\"\n>\n <a\n class=\"mk-cta\"\n [class.mk-cta--soft]=\"data('variant') === 'soft'\"\n [class.mk-cta--outline]=\"data('variant') === 'outline'\"\n [attr.data-tone]=\"data('tone') ?? 'primary'\"\n [attr.href]=\"readonly() ? (data('href') || null) : null\"\n [attr.target]=\"readonly() ? '_blank' : null\"\n rel=\"noopener noreferrer\"\n data-mk-focus\n >\n {{ data('label') || i18n.blockEditor.blockButton }}\n </a>\n</div>\n\n@if (!readonly()) {\n <div class=\"mk-button-block__settings\">\n <label class=\"mk-button-block__field\">\n <span>{{ i18n.blockEditor.buttonLabel }}</span>\n <input type=\"text\" [value]=\"data('label') ?? ''\" (input)=\"onLabel($event)\" />\n </label>\n <label class=\"mk-button-block__field\">\n <span>{{ i18n.blockEditor.buttonLink }}</span>\n <input type=\"url\" [value]=\"data('href') ?? ''\" placeholder=\"https://\u2026\" (input)=\"onHref($event)\" />\n </label>\n\n <div class=\"mk-button-block__field\">\n <span id=\"mk-cta-tone\">{{ i18n.blockEditor.buttonTone }}</span>\n <div class=\"mk-button-block__row\" role=\"group\" aria-labelledby=\"mk-cta-tone\">\n @for (t of tones; track t) {\n <button\n type=\"button\"\n class=\"mk-button-block__swatch\"\n [attr.data-tone]=\"t\"\n [class.mk-button-block__swatch--active]=\"(data('tone') ?? 'primary') === t\"\n [attr.aria-pressed]=\"(data('tone') ?? 'primary') === t\"\n [attr.aria-label]=\"toneLabel(t)\"\n [title]=\"toneLabel(t)\"\n (click)=\"setTone(t)\"\n ></button>\n }\n </div>\n </div>\n\n <div class=\"mk-button-block__field\">\n <span id=\"mk-cta-variant\">{{ i18n.blockEditor.buttonVariant }}</span>\n <div class=\"mk-button-block__row\" role=\"group\" aria-labelledby=\"mk-cta-variant\">\n @for (v of variants; track v) {\n <button\n type=\"button\"\n class=\"mk-button-block__pill\"\n [class.mk-button-block__pill--active]=\"(data('variant') ?? 'solid') === v\"\n [attr.aria-pressed]=\"(data('variant') ?? 'solid') === v\"\n (click)=\"setVariant(v)\"\n >\n {{ variantLabel(v) }}\n </button>\n }\n </div>\n </div>\n\n <div class=\"mk-button-block__field\">\n <span id=\"mk-cta-align\">{{ i18n.blockEditor.alignment }}</span>\n <div class=\"mk-button-block__row\" role=\"group\" aria-labelledby=\"mk-cta-align\">\n @for (a of aligns; track a) {\n <button\n type=\"button\"\n class=\"mk-button-block__pill\"\n [class.mk-button-block__pill--active]=\"(data('align') ?? 'left') === a\"\n [attr.aria-pressed]=\"(data('align') ?? 'left') === a\"\n (click)=\"setAlign(a)\"\n >\n {{ alignLabel(a) }}\n </button>\n }\n </div>\n </div>\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block}.mk-button-block__preview{text-align:start}.mk-button-block__preview--center{text-align:center}.mk-button-block__preview--right{text-align:right}.mk-cta{--_main: var(--mk-primary);--_hover: var(--mk-primary-hover);--_contrast: var(--mk-primary-contrast);--_subtle: var(--mk-primary-subtle);--_subtle-text: var(--mk-primary-subtle-text);display:inline-flex;align-items:center;justify-content:center;height:var(--mk-control-height-md);padding:0 var(--mk-space-5);border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);background:var(--_main);color:var(--_contrast);font-weight:var(--mk-font-weight-medium);text-decoration:none;cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-cta:hover{background:var(--_hover)}.mk-cta[data-tone=success]{--_main: var(--mk-success);--_hover: var(--mk-success-hover);--_contrast: var(--mk-success-contrast);--_subtle: var(--mk-success-subtle);--_subtle-text: var(--mk-success-subtle-text)}.mk-cta[data-tone=warning]{--_main: var(--mk-warning);--_hover: var(--mk-warning-hover);--_contrast: var(--mk-warning-contrast);--_subtle: var(--mk-warning-subtle);--_subtle-text: var(--mk-warning-subtle-text)}.mk-cta[data-tone=danger]{--_main: var(--mk-danger);--_hover: var(--mk-danger-hover);--_contrast: var(--mk-danger-contrast);--_subtle: var(--mk-danger-subtle);--_subtle-text: var(--mk-danger-subtle-text)}.mk-cta[data-tone=info]{--_main: var(--mk-info);--_hover: var(--mk-info-hover);--_contrast: var(--mk-info-contrast);--_subtle: var(--mk-info-subtle);--_subtle-text: var(--mk-info-subtle-text)}.mk-cta[data-tone=neutral]{--_main: var(--mk-surface-inverse);--_hover: var(--mk-surface-inverse);--_contrast: var(--mk-text-inverse);--_subtle: var(--mk-neutral-subtle);--_subtle-text: var(--mk-neutral-subtle-text)}.mk-cta--soft{background:var(--_subtle);color:var(--_subtle-text)}.mk-cta--soft:hover{background-image:linear-gradient(var(--mk-hover-overlay),var(--mk-hover-overlay))}.mk-cta--outline{background:transparent;color:var(--_subtle-text);border-color:color-mix(in srgb,var(--_main) 42%,var(--mk-border-strong))}.mk-cta--outline:hover{background:var(--_subtle)}.mk-cta:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-button-block__settings{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--mk-space-3);margin-top:var(--mk-space-3);padding:var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border-subtle);border-radius:var(--mk-radius-md);background:var(--mk-surface-2)}.mk-button-block__field{display:flex;flex-direction:column;gap:var(--mk-space-1);color:var(--mk-text-muted);font-size:var(--mk-font-size-xs)}.mk-button-block__field input{height:var(--mk-control-height-sm);padding:0 var(--mk-space-2);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text);font:inherit}.mk-button-block__row{display:flex;flex-wrap:wrap;gap:var(--mk-space-1)}.mk-button-block__swatch{width:24px;height:24px;border:2px solid transparent;border-radius:var(--mk-radius-circle);background:var(--mk-primary);cursor:pointer}.mk-button-block__swatch[data-tone=success]{background:var(--mk-success)}.mk-button-block__swatch[data-tone=warning]{background:var(--mk-warning)}.mk-button-block__swatch[data-tone=danger]{background:var(--mk-danger)}.mk-button-block__swatch[data-tone=info]{background:var(--mk-info)}.mk-button-block__swatch[data-tone=neutral]{background:var(--mk-surface-inverse)}.mk-button-block__swatch--active{border-color:var(--mk-text)}.mk-button-block__pill{height:var(--mk-control-height-sm);padding:0 var(--mk-space-2);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-xs);text-transform:capitalize;cursor:pointer}.mk-button-block__pill--active{background:var(--mk-primary);border-color:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-button-block__swatch:focus-visible,.mk-button-block__pill:focus-visible,.mk-button-block__field input: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 });
|
|
1232
|
+
}
|
|
1233
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkButtonBlock, decorators: [{
|
|
1234
|
+
type: Component,
|
|
1235
|
+
args: [{ selector: 'mk-button-block', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"mk-button-block__preview\"\n [class.mk-button-block__preview--center]=\"(data('align') ?? 'left') === 'center'\"\n [class.mk-button-block__preview--right]=\"(data('align') ?? 'left') === 'right'\"\n>\n <a\n class=\"mk-cta\"\n [class.mk-cta--soft]=\"data('variant') === 'soft'\"\n [class.mk-cta--outline]=\"data('variant') === 'outline'\"\n [attr.data-tone]=\"data('tone') ?? 'primary'\"\n [attr.href]=\"readonly() ? (data('href') || null) : null\"\n [attr.target]=\"readonly() ? '_blank' : null\"\n rel=\"noopener noreferrer\"\n data-mk-focus\n >\n {{ data('label') || i18n.blockEditor.blockButton }}\n </a>\n</div>\n\n@if (!readonly()) {\n <div class=\"mk-button-block__settings\">\n <label class=\"mk-button-block__field\">\n <span>{{ i18n.blockEditor.buttonLabel }}</span>\n <input type=\"text\" [value]=\"data('label') ?? ''\" (input)=\"onLabel($event)\" />\n </label>\n <label class=\"mk-button-block__field\">\n <span>{{ i18n.blockEditor.buttonLink }}</span>\n <input type=\"url\" [value]=\"data('href') ?? ''\" placeholder=\"https://\u2026\" (input)=\"onHref($event)\" />\n </label>\n\n <div class=\"mk-button-block__field\">\n <span id=\"mk-cta-tone\">{{ i18n.blockEditor.buttonTone }}</span>\n <div class=\"mk-button-block__row\" role=\"group\" aria-labelledby=\"mk-cta-tone\">\n @for (t of tones; track t) {\n <button\n type=\"button\"\n class=\"mk-button-block__swatch\"\n [attr.data-tone]=\"t\"\n [class.mk-button-block__swatch--active]=\"(data('tone') ?? 'primary') === t\"\n [attr.aria-pressed]=\"(data('tone') ?? 'primary') === t\"\n [attr.aria-label]=\"toneLabel(t)\"\n [title]=\"toneLabel(t)\"\n (click)=\"setTone(t)\"\n ></button>\n }\n </div>\n </div>\n\n <div class=\"mk-button-block__field\">\n <span id=\"mk-cta-variant\">{{ i18n.blockEditor.buttonVariant }}</span>\n <div class=\"mk-button-block__row\" role=\"group\" aria-labelledby=\"mk-cta-variant\">\n @for (v of variants; track v) {\n <button\n type=\"button\"\n class=\"mk-button-block__pill\"\n [class.mk-button-block__pill--active]=\"(data('variant') ?? 'solid') === v\"\n [attr.aria-pressed]=\"(data('variant') ?? 'solid') === v\"\n (click)=\"setVariant(v)\"\n >\n {{ variantLabel(v) }}\n </button>\n }\n </div>\n </div>\n\n <div class=\"mk-button-block__field\">\n <span id=\"mk-cta-align\">{{ i18n.blockEditor.alignment }}</span>\n <div class=\"mk-button-block__row\" role=\"group\" aria-labelledby=\"mk-cta-align\">\n @for (a of aligns; track a) {\n <button\n type=\"button\"\n class=\"mk-button-block__pill\"\n [class.mk-button-block__pill--active]=\"(data('align') ?? 'left') === a\"\n [attr.aria-pressed]=\"(data('align') ?? 'left') === a\"\n (click)=\"setAlign(a)\"\n >\n {{ alignLabel(a) }}\n </button>\n }\n </div>\n </div>\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block}.mk-button-block__preview{text-align:start}.mk-button-block__preview--center{text-align:center}.mk-button-block__preview--right{text-align:right}.mk-cta{--_main: var(--mk-primary);--_hover: var(--mk-primary-hover);--_contrast: var(--mk-primary-contrast);--_subtle: var(--mk-primary-subtle);--_subtle-text: var(--mk-primary-subtle-text);display:inline-flex;align-items:center;justify-content:center;height:var(--mk-control-height-md);padding:0 var(--mk-space-5);border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);background:var(--_main);color:var(--_contrast);font-weight:var(--mk-font-weight-medium);text-decoration:none;cursor:pointer;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-cta:hover{background:var(--_hover)}.mk-cta[data-tone=success]{--_main: var(--mk-success);--_hover: var(--mk-success-hover);--_contrast: var(--mk-success-contrast);--_subtle: var(--mk-success-subtle);--_subtle-text: var(--mk-success-subtle-text)}.mk-cta[data-tone=warning]{--_main: var(--mk-warning);--_hover: var(--mk-warning-hover);--_contrast: var(--mk-warning-contrast);--_subtle: var(--mk-warning-subtle);--_subtle-text: var(--mk-warning-subtle-text)}.mk-cta[data-tone=danger]{--_main: var(--mk-danger);--_hover: var(--mk-danger-hover);--_contrast: var(--mk-danger-contrast);--_subtle: var(--mk-danger-subtle);--_subtle-text: var(--mk-danger-subtle-text)}.mk-cta[data-tone=info]{--_main: var(--mk-info);--_hover: var(--mk-info-hover);--_contrast: var(--mk-info-contrast);--_subtle: var(--mk-info-subtle);--_subtle-text: var(--mk-info-subtle-text)}.mk-cta[data-tone=neutral]{--_main: var(--mk-surface-inverse);--_hover: var(--mk-surface-inverse);--_contrast: var(--mk-text-inverse);--_subtle: var(--mk-neutral-subtle);--_subtle-text: var(--mk-neutral-subtle-text)}.mk-cta--soft{background:var(--_subtle);color:var(--_subtle-text)}.mk-cta--soft:hover{background-image:linear-gradient(var(--mk-hover-overlay),var(--mk-hover-overlay))}.mk-cta--outline{background:transparent;color:var(--_subtle-text);border-color:color-mix(in srgb,var(--_main) 42%,var(--mk-border-strong))}.mk-cta--outline:hover{background:var(--_subtle)}.mk-cta:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-button-block__settings{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:var(--mk-space-3);margin-top:var(--mk-space-3);padding:var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border-subtle);border-radius:var(--mk-radius-md);background:var(--mk-surface-2)}.mk-button-block__field{display:flex;flex-direction:column;gap:var(--mk-space-1);color:var(--mk-text-muted);font-size:var(--mk-font-size-xs)}.mk-button-block__field input{height:var(--mk-control-height-sm);padding:0 var(--mk-space-2);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text);font:inherit}.mk-button-block__row{display:flex;flex-wrap:wrap;gap:var(--mk-space-1)}.mk-button-block__swatch{width:24px;height:24px;border:2px solid transparent;border-radius:var(--mk-radius-circle);background:var(--mk-primary);cursor:pointer}.mk-button-block__swatch[data-tone=success]{background:var(--mk-success)}.mk-button-block__swatch[data-tone=warning]{background:var(--mk-warning)}.mk-button-block__swatch[data-tone=danger]{background:var(--mk-danger)}.mk-button-block__swatch[data-tone=info]{background:var(--mk-info)}.mk-button-block__swatch[data-tone=neutral]{background:var(--mk-surface-inverse)}.mk-button-block__swatch--active{border-color:var(--mk-text)}.mk-button-block__pill{height:var(--mk-control-height-sm);padding:0 var(--mk-space-2);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text-muted);font:inherit;font-size:var(--mk-font-size-xs);text-transform:capitalize;cursor:pointer}.mk-button-block__pill--active{background:var(--mk-primary);border-color:var(--mk-primary);color:var(--mk-primary-contrast)}.mk-button-block__swatch:focus-visible,.mk-button-block__pill:focus-visible,.mk-button-block__field input:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}\n"] }]
|
|
1236
|
+
}], propDecorators: { block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: true }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], blockChange: [{ type: i0.Output, args: ["blockChange"] }] } });
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Shares ONE capture-phase document `pointerdown` listener between every
|
|
1240
|
+
* mounted {@link MkBlockInserter} (a block list renders an inserter per block,
|
|
1241
|
+
* so per-instance listeners made document dispatch O(blocks)). The listener is
|
|
1242
|
+
* attached lazily with the first registration and detached when the last
|
|
1243
|
+
* callback unregisters. No-ops on the server (SSR).
|
|
1244
|
+
*/
|
|
1245
|
+
class MkDocumentPointerdownRegistry {
|
|
1246
|
+
document = inject(DOCUMENT);
|
|
1247
|
+
callbacks = new Set();
|
|
1248
|
+
listener = (event) => {
|
|
1249
|
+
// Copy so a callback closing (and unregistering) mid-dispatch is safe.
|
|
1250
|
+
for (const callback of [...this.callbacks])
|
|
1251
|
+
callback(event);
|
|
1252
|
+
};
|
|
1253
|
+
/** Registers a callback; returns an unregister function. */
|
|
1254
|
+
register(callback) {
|
|
1255
|
+
if (typeof window === 'undefined')
|
|
1256
|
+
return () => { };
|
|
1257
|
+
if (this.callbacks.size === 0) {
|
|
1258
|
+
this.document.addEventListener('pointerdown', this.listener, true);
|
|
1259
|
+
}
|
|
1260
|
+
this.callbacks.add(callback);
|
|
1261
|
+
return () => {
|
|
1262
|
+
if (!this.callbacks.delete(callback))
|
|
1263
|
+
return;
|
|
1264
|
+
if (this.callbacks.size === 0) {
|
|
1265
|
+
this.document.removeEventListener('pointerdown', this.listener, true);
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDocumentPointerdownRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1270
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDocumentPointerdownRegistry, providedIn: 'root' });
|
|
1271
|
+
}
|
|
1272
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkDocumentPointerdownRegistry, decorators: [{
|
|
1273
|
+
type: Injectable,
|
|
1274
|
+
args: [{ providedIn: 'root' }]
|
|
1275
|
+
}] });
|
|
1276
|
+
/**
|
|
1277
|
+
* Block inserter — a "+ Add block" control that opens a filterable, keyboard
|
|
1278
|
+
* accessible palette of the configured blocks, grouped by `group`. Renders
|
|
1279
|
+
* either as a full button or a slim between-blocks divider trigger.
|
|
1280
|
+
*
|
|
1281
|
+
* Keyboard: type to filter, ArrowUp/Down to move, Enter to insert, Esc to
|
|
1282
|
+
* close. Implements the combobox → listbox pattern.
|
|
1283
|
+
*/
|
|
1284
|
+
class MkBlockInserter {
|
|
1285
|
+
host = inject(ElementRef);
|
|
1286
|
+
searchRef = viewChild('search', /* @ts-ignore */
|
|
1287
|
+
...(ngDevMode ? [{ debugName: "searchRef" }] : /* istanbul ignore next */ []));
|
|
1288
|
+
i18n = inject(MK_I18N);
|
|
1289
|
+
focusTimer = null;
|
|
1290
|
+
constructor() {
|
|
1291
|
+
// One shared document listener for all inserters (instead of N host
|
|
1292
|
+
// `(document:pointerdown)` bindings — one per rendered inserter).
|
|
1293
|
+
const unregister = inject(MkDocumentPointerdownRegistry).register((event) => this.onDocumentPointerdown(event));
|
|
1294
|
+
inject(DestroyRef).onDestroy(unregister);
|
|
1295
|
+
}
|
|
1296
|
+
ngOnDestroy() {
|
|
1297
|
+
if (this.focusTimer !== null) {
|
|
1298
|
+
clearTimeout(this.focusTimer);
|
|
1299
|
+
this.focusTimer = null;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
/** The blocks to offer. */
|
|
1303
|
+
definitions = input([], /* @ts-ignore */
|
|
1304
|
+
...(ngDevMode ? [{ debugName: "definitions" }] : /* istanbul ignore next */ []));
|
|
1305
|
+
/** Trigger presentation: `button` (default) or `slim` between-blocks rule. */
|
|
1306
|
+
variant = input('button', /* @ts-ignore */
|
|
1307
|
+
...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
1308
|
+
/** Trigger label. */
|
|
1309
|
+
label = input(this.i18n.blockEditor.addBlock, /* @ts-ignore */
|
|
1310
|
+
...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
1311
|
+
/** Disable the trigger. */
|
|
1312
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1313
|
+
/** When true, only blocks allowed inside columns are shown. */
|
|
1314
|
+
columnsOnly = input(false, { ...(ngDevMode ? { debugName: "columnsOnly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1315
|
+
/** Emitted with the chosen definition. */
|
|
1316
|
+
pick = output();
|
|
1317
|
+
open = signal(false, /* @ts-ignore */
|
|
1318
|
+
...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
1319
|
+
query = signal('', /* @ts-ignore */
|
|
1320
|
+
...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
|
|
1321
|
+
activeIndex = signal(0, /* @ts-ignore */
|
|
1322
|
+
...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
|
|
1323
|
+
panelId = mkUniqueId('mk-inserter-panel');
|
|
1324
|
+
listId = mkUniqueId('mk-inserter-list');
|
|
1325
|
+
available = computed(() => this.definitions().filter((d) => (this.columnsOnly() ? d.allowedInColumns !== false : true)), /* @ts-ignore */
|
|
1326
|
+
...(ngDevMode ? [{ debugName: "available" }] : /* istanbul ignore next */ []));
|
|
1327
|
+
/** Flat, filtered list in display order (used for keyboard navigation). */
|
|
1328
|
+
filtered = computed(() => {
|
|
1329
|
+
const q = this.query().trim().toLowerCase();
|
|
1330
|
+
if (!q)
|
|
1331
|
+
return this.available();
|
|
1332
|
+
return this.available().filter((d) => {
|
|
1333
|
+
const hay = [d.label, d.type, d.group ?? '', ...(d.keywords ?? [])].join(' ').toLowerCase();
|
|
1334
|
+
return hay.includes(q);
|
|
1335
|
+
});
|
|
1336
|
+
}, /* @ts-ignore */
|
|
1337
|
+
...(ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []));
|
|
1338
|
+
/**
|
|
1339
|
+
* Filtered list grouped by `group` for rendering, with each option carrying
|
|
1340
|
+
* its flat index (single pass — the template previously resolved each
|
|
1341
|
+
* option's index with three `indexOf` scans, O(n²) per render).
|
|
1342
|
+
*/
|
|
1343
|
+
groups = computed(() => {
|
|
1344
|
+
const map = new Map();
|
|
1345
|
+
this.filtered().forEach((def, index) => {
|
|
1346
|
+
const key = def.group ?? this.i18n.blockEditor.blocks;
|
|
1347
|
+
(map.get(key) ?? map.set(key, []).get(key)).push({ def, index });
|
|
1348
|
+
});
|
|
1349
|
+
return [...map.entries()].map(([name, items]) => ({ name, items }));
|
|
1350
|
+
}, /* @ts-ignore */
|
|
1351
|
+
...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
|
|
1352
|
+
optionId(index) {
|
|
1353
|
+
return `${this.listId}-opt-${index}`;
|
|
1354
|
+
}
|
|
1355
|
+
toggle() {
|
|
1356
|
+
if (this.disabled())
|
|
1357
|
+
return;
|
|
1358
|
+
this.open() ? this.close() : this.openPanel();
|
|
1359
|
+
}
|
|
1360
|
+
openPanel() {
|
|
1361
|
+
this.open.set(true);
|
|
1362
|
+
this.query.set('');
|
|
1363
|
+
this.activeIndex.set(0);
|
|
1364
|
+
if (this.focusTimer !== null)
|
|
1365
|
+
clearTimeout(this.focusTimer);
|
|
1366
|
+
this.focusTimer = setTimeout(() => {
|
|
1367
|
+
this.focusTimer = null;
|
|
1368
|
+
this.searchRef()?.nativeElement.focus();
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
close() {
|
|
1372
|
+
if (!this.open())
|
|
1373
|
+
return;
|
|
1374
|
+
this.open.set(false);
|
|
1375
|
+
}
|
|
1376
|
+
onQuery(event) {
|
|
1377
|
+
this.query.set(event.target.value);
|
|
1378
|
+
this.activeIndex.set(0);
|
|
1379
|
+
}
|
|
1380
|
+
onKeydown(event) {
|
|
1381
|
+
const list = this.filtered();
|
|
1382
|
+
switch (event.key) {
|
|
1383
|
+
case 'ArrowDown':
|
|
1384
|
+
event.preventDefault();
|
|
1385
|
+
this.activeIndex.update((i) => (list.length ? (i + 1) % list.length : 0));
|
|
1386
|
+
break;
|
|
1387
|
+
case 'ArrowUp':
|
|
1388
|
+
event.preventDefault();
|
|
1389
|
+
this.activeIndex.update((i) => (list.length ? (i - 1 + list.length) % list.length : 0));
|
|
1390
|
+
break;
|
|
1391
|
+
case 'Enter':
|
|
1392
|
+
event.preventDefault();
|
|
1393
|
+
if (list[this.activeIndex()])
|
|
1394
|
+
this.choose(list[this.activeIndex()]);
|
|
1395
|
+
break;
|
|
1396
|
+
case 'Escape':
|
|
1397
|
+
event.preventDefault();
|
|
1398
|
+
this.close();
|
|
1399
|
+
break;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
choose(def) {
|
|
1403
|
+
this.pick.emit(def);
|
|
1404
|
+
this.close();
|
|
1405
|
+
}
|
|
1406
|
+
onDocumentPointerdown(event) {
|
|
1407
|
+
if (!this.open())
|
|
1408
|
+
return;
|
|
1409
|
+
if (!this.host.nativeElement.contains(event.target))
|
|
1410
|
+
this.close();
|
|
1411
|
+
}
|
|
1412
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockInserter, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1413
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkBlockInserter, isStandalone: true, selector: "mk-block-inserter", inputs: { definitions: { classPropertyName: "definitions", publicName: "definitions", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, columnsOnly: { classPropertyName: "columnsOnly", publicName: "columnsOnly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pick: "pick" }, host: { properties: { "class.mk-block-inserter--slim": "variant() === 'slim'" }, classAttribute: "mk-block-inserter" }, viewQueries: [{ propertyName: "searchRef", first: true, predicate: ["search"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (variant() === 'slim') {\n <button\n type=\"button\"\n class=\"mk-block-inserter__slim-trigger\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"i18n.blockEditor.insertBlockHere\"\n (click)=\"toggle()\"\n >\n <span class=\"mk-block-inserter__plus\" aria-hidden=\"true\">\uFF0B</span>\n </button>\n} @else {\n <button\n type=\"button\"\n class=\"mk-block-inserter__trigger\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [disabled]=\"disabled()\"\n (click)=\"toggle()\"\n >\n <span class=\"mk-block-inserter__plus\" aria-hidden=\"true\">\uFF0B</span>\n {{ label() }}\n </button>\n}\n\n@if (open()) {\n <div class=\"mk-block-inserter__panel\" [id]=\"panelId\" role=\"dialog\" [attr.aria-label]=\"i18n.blockEditor.blockInserter\">\n <input\n #search\n type=\"text\"\n class=\"mk-block-inserter__search\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"true\"\n [attr.aria-controls]=\"listId\"\n [attr.aria-activedescendant]=\"filtered().length ? optionId(activeIndex()) : null\"\n [placeholder]=\"i18n.blockEditor.searchBlocks\"\n [value]=\"query()\"\n (input)=\"onQuery($event)\"\n (keydown)=\"onKeydown($event)\"\n />\n\n <div class=\"mk-block-inserter__list\" [id]=\"listId\" role=\"listbox\" [attr.aria-label]=\"i18n.blockEditor.blocks\">\n @for (group of groups(); track group.name) {\n <div class=\"mk-block-inserter__group\" role=\"group\" [attr.aria-label]=\"group.name\">\n <div class=\"mk-block-inserter__group-label\">{{ group.name }}</div>\n @for (item of group.items; track item.def.type) {\n <button\n type=\"button\"\n class=\"mk-block-inserter__option\"\n [class.mk-block-inserter__option--active]=\"item.index === activeIndex()\"\n [id]=\"optionId(item.index)\"\n role=\"option\"\n [attr.aria-selected]=\"item.index === activeIndex()\"\n (mouseenter)=\"activeIndex.set(item.index)\"\n (click)=\"choose(item.def)\"\n >\n <span class=\"mk-block-inserter__icon\" aria-hidden=\"true\">{{ item.def.icon ?? '\u25A2' }}</span>\n <span class=\"mk-block-inserter__text\">\n <span class=\"mk-block-inserter__name\">{{ item.def.label }}</span>\n @if (item.def.description) {\n <span class=\"mk-block-inserter__desc\">{{ item.def.description }}</span>\n }\n </span>\n </button>\n }\n </div>\n }\n @if (!filtered().length) {\n <p class=\"mk-block-inserter__empty\">{{ i18n.blockEditor.noBlocksMatch(query()) }}</p>\n }\n </div>\n </div>\n}\n", styles: [":host{position:relative;display:block}.mk-block-inserter__trigger{display:inline-flex;align-items:center;gap:var(--mk-space-2);height:var(--mk-control-height-md);padding:0 var(--mk-space-4);border:var(--mk-border-width) dashed var(--mk-border-strong);border-radius:var(--mk-radius-md);background:var(--mk-surface);color:var(--mk-text-muted);font:inherit;font-weight:var(--mk-font-weight-medium);cursor:pointer;transition:color var(--mk-duration-fast) var(--mk-ease-standard),border-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block-inserter__trigger:hover{color:var(--mk-primary);border-color:var(--mk-primary)}.mk-block-inserter--slim{height:0;overflow:visible}.mk-block-inserter__slim-trigger{display:flex;align-items:center;justify-content:center;width:100%;height:var(--mk-space-4);margin:calc(var(--mk-space-1) * -1) 0;border:none;background:transparent;color:var(--mk-primary);cursor:pointer;opacity:0;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block-inserter__slim-trigger:before{content:\"\";position:absolute;left:0;right:0;height:var(--mk-border-width-strong);background:var(--mk-primary);border-radius:var(--mk-radius-pill)}.mk-block-inserter--slim:hover .mk-block-inserter__slim-trigger,.mk-block-inserter__slim-trigger:focus-visible{opacity:1}.mk-block-inserter__slim-trigger .mk-block-inserter__plus{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:var(--mk-radius-circle);background:var(--mk-primary);color:var(--mk-primary-contrast);font-size:var(--mk-font-size-xs)}.mk-block-inserter__trigger:focus-visible,.mk-block-inserter__slim-trigger:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-block-inserter__panel{position:absolute;z-index:var(--mk-z-menu);top:calc(100% + var(--mk-space-2));inset-inline-start:0;width:min(320px,90vw);max-height:380px;display:flex;flex-direction:column;border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);background:var(--mk-surface);box-shadow:var(--mk-shadow-lg);overflow:hidden}.mk-block-inserter__search{margin:var(--mk-space-2);height:var(--mk-control-height-sm);padding:0 var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface-2);color:var(--mk-text);font:inherit}.mk-block-inserter__search:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-block-inserter__list{overflow-y:auto;padding:0 var(--mk-space-1) var(--mk-space-2)}.mk-block-inserter__group-label{padding:var(--mk-space-2) var(--mk-space-2) var(--mk-space-1);color:var(--mk-text-subtle);font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-semibold);text-transform:uppercase;letter-spacing:.04em}.mk-block-inserter__option{display:flex;align-items:center;gap:var(--mk-space-3);width:100%;padding:var(--mk-space-2);border:none;border-radius:var(--mk-radius-md);background:transparent;color:var(--mk-text);font:inherit;text-align:start;cursor:pointer}.mk-block-inserter__option--active{background:var(--mk-selected-bg);color:var(--mk-selected-text)}.mk-block-inserter__icon{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;flex:none;border-radius:var(--mk-radius-sm);background:var(--mk-surface-3);font-size:var(--mk-font-size-md)}.mk-block-inserter__text{display:flex;flex-direction:column;min-width:0}.mk-block-inserter__name{font-weight:var(--mk-font-weight-medium)}.mk-block-inserter__desc{color:var(--mk-text-subtle);font-size:var(--mk-font-size-xs)}.mk-block-inserter__option--active .mk-block-inserter__desc{color:inherit}.mk-block-inserter__empty{padding:var(--mk-space-4);margin:0;color:var(--mk-text-muted);font-size:var(--mk-font-size-sm);text-align:center}@media(prefers-reduced-motion:reduce){.mk-block-inserter__slim-trigger{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1414
|
+
}
|
|
1415
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockInserter, decorators: [{
|
|
1416
|
+
type: Component,
|
|
1417
|
+
args: [{ selector: 'mk-block-inserter', changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
1418
|
+
class: 'mk-block-inserter',
|
|
1419
|
+
'[class.mk-block-inserter--slim]': "variant() === 'slim'",
|
|
1420
|
+
}, template: "@if (variant() === 'slim') {\n <button\n type=\"button\"\n class=\"mk-block-inserter__slim-trigger\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"i18n.blockEditor.insertBlockHere\"\n (click)=\"toggle()\"\n >\n <span class=\"mk-block-inserter__plus\" aria-hidden=\"true\">\uFF0B</span>\n </button>\n} @else {\n <button\n type=\"button\"\n class=\"mk-block-inserter__trigger\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-controls]=\"panelId\"\n [disabled]=\"disabled()\"\n (click)=\"toggle()\"\n >\n <span class=\"mk-block-inserter__plus\" aria-hidden=\"true\">\uFF0B</span>\n {{ label() }}\n </button>\n}\n\n@if (open()) {\n <div class=\"mk-block-inserter__panel\" [id]=\"panelId\" role=\"dialog\" [attr.aria-label]=\"i18n.blockEditor.blockInserter\">\n <input\n #search\n type=\"text\"\n class=\"mk-block-inserter__search\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"true\"\n [attr.aria-controls]=\"listId\"\n [attr.aria-activedescendant]=\"filtered().length ? optionId(activeIndex()) : null\"\n [placeholder]=\"i18n.blockEditor.searchBlocks\"\n [value]=\"query()\"\n (input)=\"onQuery($event)\"\n (keydown)=\"onKeydown($event)\"\n />\n\n <div class=\"mk-block-inserter__list\" [id]=\"listId\" role=\"listbox\" [attr.aria-label]=\"i18n.blockEditor.blocks\">\n @for (group of groups(); track group.name) {\n <div class=\"mk-block-inserter__group\" role=\"group\" [attr.aria-label]=\"group.name\">\n <div class=\"mk-block-inserter__group-label\">{{ group.name }}</div>\n @for (item of group.items; track item.def.type) {\n <button\n type=\"button\"\n class=\"mk-block-inserter__option\"\n [class.mk-block-inserter__option--active]=\"item.index === activeIndex()\"\n [id]=\"optionId(item.index)\"\n role=\"option\"\n [attr.aria-selected]=\"item.index === activeIndex()\"\n (mouseenter)=\"activeIndex.set(item.index)\"\n (click)=\"choose(item.def)\"\n >\n <span class=\"mk-block-inserter__icon\" aria-hidden=\"true\">{{ item.def.icon ?? '\u25A2' }}</span>\n <span class=\"mk-block-inserter__text\">\n <span class=\"mk-block-inserter__name\">{{ item.def.label }}</span>\n @if (item.def.description) {\n <span class=\"mk-block-inserter__desc\">{{ item.def.description }}</span>\n }\n </span>\n </button>\n }\n </div>\n }\n @if (!filtered().length) {\n <p class=\"mk-block-inserter__empty\">{{ i18n.blockEditor.noBlocksMatch(query()) }}</p>\n }\n </div>\n </div>\n}\n", styles: [":host{position:relative;display:block}.mk-block-inserter__trigger{display:inline-flex;align-items:center;gap:var(--mk-space-2);height:var(--mk-control-height-md);padding:0 var(--mk-space-4);border:var(--mk-border-width) dashed var(--mk-border-strong);border-radius:var(--mk-radius-md);background:var(--mk-surface);color:var(--mk-text-muted);font:inherit;font-weight:var(--mk-font-weight-medium);cursor:pointer;transition:color var(--mk-duration-fast) var(--mk-ease-standard),border-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block-inserter__trigger:hover{color:var(--mk-primary);border-color:var(--mk-primary)}.mk-block-inserter--slim{height:0;overflow:visible}.mk-block-inserter__slim-trigger{display:flex;align-items:center;justify-content:center;width:100%;height:var(--mk-space-4);margin:calc(var(--mk-space-1) * -1) 0;border:none;background:transparent;color:var(--mk-primary);cursor:pointer;opacity:0;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block-inserter__slim-trigger:before{content:\"\";position:absolute;left:0;right:0;height:var(--mk-border-width-strong);background:var(--mk-primary);border-radius:var(--mk-radius-pill)}.mk-block-inserter--slim:hover .mk-block-inserter__slim-trigger,.mk-block-inserter__slim-trigger:focus-visible{opacity:1}.mk-block-inserter__slim-trigger .mk-block-inserter__plus{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:var(--mk-radius-circle);background:var(--mk-primary);color:var(--mk-primary-contrast);font-size:var(--mk-font-size-xs)}.mk-block-inserter__trigger:focus-visible,.mk-block-inserter__slim-trigger:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-block-inserter__panel{position:absolute;z-index:var(--mk-z-menu);top:calc(100% + var(--mk-space-2));inset-inline-start:0;width:min(320px,90vw);max-height:380px;display:flex;flex-direction:column;border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);background:var(--mk-surface);box-shadow:var(--mk-shadow-lg);overflow:hidden}.mk-block-inserter__search{margin:var(--mk-space-2);height:var(--mk-control-height-sm);padding:0 var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface-2);color:var(--mk-text);font:inherit}.mk-block-inserter__search:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-block-inserter__list{overflow-y:auto;padding:0 var(--mk-space-1) var(--mk-space-2)}.mk-block-inserter__group-label{padding:var(--mk-space-2) var(--mk-space-2) var(--mk-space-1);color:var(--mk-text-subtle);font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-semibold);text-transform:uppercase;letter-spacing:.04em}.mk-block-inserter__option{display:flex;align-items:center;gap:var(--mk-space-3);width:100%;padding:var(--mk-space-2);border:none;border-radius:var(--mk-radius-md);background:transparent;color:var(--mk-text);font:inherit;text-align:start;cursor:pointer}.mk-block-inserter__option--active{background:var(--mk-selected-bg);color:var(--mk-selected-text)}.mk-block-inserter__icon{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;flex:none;border-radius:var(--mk-radius-sm);background:var(--mk-surface-3);font-size:var(--mk-font-size-md)}.mk-block-inserter__text{display:flex;flex-direction:column;min-width:0}.mk-block-inserter__name{font-weight:var(--mk-font-weight-medium)}.mk-block-inserter__desc{color:var(--mk-text-subtle);font-size:var(--mk-font-size-xs)}.mk-block-inserter__option--active .mk-block-inserter__desc{color:inherit}.mk-block-inserter__empty{padding:var(--mk-space-4);margin:0;color:var(--mk-text-muted);font-size:var(--mk-font-size-sm);text-align:center}@media(prefers-reduced-motion:reduce){.mk-block-inserter__slim-trigger{transition:none}}\n"] }]
|
|
1421
|
+
}], ctorParameters: () => [], propDecorators: { searchRef: [{ type: i0.ViewChild, args: ['search', { isSignal: true }] }], definitions: [{ type: i0.Input, args: [{ isSignal: true, alias: "definitions", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], columnsOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnsOnly", required: false }] }], pick: [{ type: i0.Output, args: ["pick"] }] } });
|
|
1422
|
+
|
|
1423
|
+
/**
|
|
1424
|
+
* Column ratio presets offered per column count. An empty label means "equal
|
|
1425
|
+
* widths" and is rendered with the localised `ratioEqual` caption.
|
|
1426
|
+
*/
|
|
1427
|
+
const RATIO_PRESETS = {
|
|
1428
|
+
2: [
|
|
1429
|
+
{ value: '50-50', label: '50 / 50' },
|
|
1430
|
+
{ value: '66-33', label: '66 / 33' },
|
|
1431
|
+
{ value: '33-66', label: '33 / 66' },
|
|
1432
|
+
{ value: '75-25', label: '75 / 25' },
|
|
1433
|
+
{ value: '25-75', label: '25 / 75' },
|
|
1434
|
+
],
|
|
1435
|
+
3: [
|
|
1436
|
+
{ value: '33-33-33', label: '' },
|
|
1437
|
+
{ value: '50-25-25', label: '50 / 25 / 25' },
|
|
1438
|
+
{ value: '25-50-25', label: '25 / 50 / 25' },
|
|
1439
|
+
],
|
|
1440
|
+
4: [{ value: '25-25-25-25', label: '' }],
|
|
1441
|
+
};
|
|
1442
|
+
/**
|
|
1443
|
+
* Recursive block list — renders an ordered array of blocks, each wrapped with
|
|
1444
|
+
* move/duplicate/delete/transform chrome and the type-specific editor, with
|
|
1445
|
+
* inserters between. Layout (columns) blocks recurse into nested
|
|
1446
|
+
* `mk-block-list`s. Owns all structural operations for its own array and emits
|
|
1447
|
+
* the new array via `blocksChange`; nested lists bubble their changes upward.
|
|
1448
|
+
*/
|
|
1449
|
+
class MkBlockList {
|
|
1450
|
+
ctx = inject(MkBlockEditorContext);
|
|
1451
|
+
i18n = inject(MK_I18N);
|
|
1452
|
+
/** The blocks to render. */
|
|
1453
|
+
blocks = input.required(/* @ts-ignore */
|
|
1454
|
+
...(ngDevMode ? [{ debugName: "blocks" }] : /* istanbul ignore next */ []));
|
|
1455
|
+
/** Nesting depth (0 = top level). Used to forbid columns-in-columns. */
|
|
1456
|
+
depth = input(0, /* @ts-ignore */
|
|
1457
|
+
...(ngDevMode ? [{ debugName: "depth" }] : /* istanbul ignore next */ []));
|
|
1458
|
+
/** Restrict the inserter to column-safe blocks. */
|
|
1459
|
+
inColumn = input(false, { ...(ngDevMode ? { debugName: "inColumn" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1460
|
+
/** Emitted with the full, updated array on any structural or content change. */
|
|
1461
|
+
blocksChange = output();
|
|
1462
|
+
/** Which block's toolbar menu is open (by index), or -1. */
|
|
1463
|
+
menuOpen = signal(-1, /* @ts-ignore */
|
|
1464
|
+
...(ngDevMode ? [{ debugName: "menuOpen" }] : /* istanbul ignore next */ []));
|
|
1465
|
+
ratioPresets = RATIO_PRESETS;
|
|
1466
|
+
definition(type) {
|
|
1467
|
+
return this.ctx.definitionFor(type);
|
|
1468
|
+
}
|
|
1469
|
+
// --- content updates ------------------------------------------------------
|
|
1470
|
+
update(index, block) {
|
|
1471
|
+
const next = [...this.blocks()];
|
|
1472
|
+
next[index] = block;
|
|
1473
|
+
this.blocksChange.emit(next);
|
|
1474
|
+
}
|
|
1475
|
+
onColumnChange(blockIndex, colIndex, children) {
|
|
1476
|
+
const block = this.blocks()[blockIndex];
|
|
1477
|
+
const columns = [...(block.children ?? [])];
|
|
1478
|
+
columns[colIndex] = { ...columns[colIndex], children };
|
|
1479
|
+
this.update(blockIndex, { ...block, children: columns });
|
|
1480
|
+
}
|
|
1481
|
+
// --- structural ops -------------------------------------------------------
|
|
1482
|
+
insertAt(index, def) {
|
|
1483
|
+
const block = def.create();
|
|
1484
|
+
const next = [...this.blocks()];
|
|
1485
|
+
next.splice(index, 0, block);
|
|
1486
|
+
this.blocksChange.emit(next);
|
|
1487
|
+
this.ctx.announce(this.i18n.blockEditor.blockAdded(def.label));
|
|
1488
|
+
this.ctx.focusBlock(block.id, 'start');
|
|
1489
|
+
}
|
|
1490
|
+
duplicate(index) {
|
|
1491
|
+
const clone = mkCloneBlock(this.blocks()[index]);
|
|
1492
|
+
const next = [...this.blocks()];
|
|
1493
|
+
next.splice(index + 1, 0, clone);
|
|
1494
|
+
this.blocksChange.emit(next);
|
|
1495
|
+
this.menuOpen.set(-1);
|
|
1496
|
+
this.ctx.announce(this.i18n.blockEditor.blockDuplicated);
|
|
1497
|
+
}
|
|
1498
|
+
remove(index) {
|
|
1499
|
+
const removed = this.blocks()[index];
|
|
1500
|
+
const next = this.blocks().filter((_, i) => i !== index);
|
|
1501
|
+
this.blocksChange.emit(next);
|
|
1502
|
+
this.menuOpen.set(-1);
|
|
1503
|
+
this.ctx.announce(this.i18n.blockEditor.blockDeleted(this.ctx.labelFor(removed.type)));
|
|
1504
|
+
const focusTarget = next[index - 1] ?? next[index];
|
|
1505
|
+
if (focusTarget)
|
|
1506
|
+
this.ctx.focusBlock(focusTarget.id, 'end');
|
|
1507
|
+
}
|
|
1508
|
+
move(index, delta) {
|
|
1509
|
+
const target = index + delta;
|
|
1510
|
+
if (target < 0 || target >= this.blocks().length)
|
|
1511
|
+
return;
|
|
1512
|
+
const next = [...this.blocks()];
|
|
1513
|
+
[next[index], next[target]] = [next[target], next[index]];
|
|
1514
|
+
this.blocksChange.emit(next);
|
|
1515
|
+
this.ctx.announce(delta < 0 ? this.i18n.blockEditor.blockMovedUp : this.i18n.blockEditor.blockMovedDown);
|
|
1516
|
+
}
|
|
1517
|
+
transform(index, type) {
|
|
1518
|
+
const def = this.definition(type);
|
|
1519
|
+
if (!def)
|
|
1520
|
+
return;
|
|
1521
|
+
const source = this.blocks()[index];
|
|
1522
|
+
const created = def.create();
|
|
1523
|
+
// Carry rich-text HTML across text-type transforms where it makes sense.
|
|
1524
|
+
const carriesHtml = 'html' in created.data && typeof source.data['html'] === 'string';
|
|
1525
|
+
const data = carriesHtml ? { ...created.data, html: source.data['html'] } : created.data;
|
|
1526
|
+
const next = [...this.blocks()];
|
|
1527
|
+
next[index] = { ...created, id: source.id, data };
|
|
1528
|
+
this.blocksChange.emit(next);
|
|
1529
|
+
this.menuOpen.set(-1);
|
|
1530
|
+
this.ctx.announce(this.i18n.blockEditor.turnedInto(def.label));
|
|
1531
|
+
}
|
|
1532
|
+
// --- text-block keyboard intents -----------------------------------------
|
|
1533
|
+
onSplit(index, split) {
|
|
1534
|
+
const current = this.blocks()[index];
|
|
1535
|
+
const paragraphDef = this.definition('paragraph');
|
|
1536
|
+
const created = paragraphDef ? paragraphDef.create() : this.fallbackParagraph();
|
|
1537
|
+
const newBlock = { ...created, data: { ...created.data, html: split.after } };
|
|
1538
|
+
const next = [...this.blocks()];
|
|
1539
|
+
next[index] = { ...current, data: { ...current.data, html: split.before } };
|
|
1540
|
+
next.splice(index + 1, 0, newBlock);
|
|
1541
|
+
this.blocksChange.emit(next);
|
|
1542
|
+
this.ctx.focusBlock(newBlock.id, 'start');
|
|
1543
|
+
}
|
|
1544
|
+
onMergeBack(index) {
|
|
1545
|
+
if (index <= 0)
|
|
1546
|
+
return;
|
|
1547
|
+
const prev = this.blocks()[index - 1];
|
|
1548
|
+
this.remove(index);
|
|
1549
|
+
this.ctx.focusBlock(prev.id, 'end');
|
|
1550
|
+
}
|
|
1551
|
+
onArrowOut(index, dir) {
|
|
1552
|
+
const target = dir === 'up' ? index - 1 : index + 1;
|
|
1553
|
+
const block = this.blocks()[target];
|
|
1554
|
+
if (block)
|
|
1555
|
+
this.ctx.focusBlock(block.id, dir === 'up' ? 'end' : 'start');
|
|
1556
|
+
}
|
|
1557
|
+
// --- columns helpers ------------------------------------------------------
|
|
1558
|
+
setColumnCount(index, count) {
|
|
1559
|
+
const block = this.blocks()[index];
|
|
1560
|
+
const current = [...(block.children ?? [])];
|
|
1561
|
+
while (current.length < count)
|
|
1562
|
+
current.push({ id: mkBlockId('column'), type: 'column', data: {}, children: [] });
|
|
1563
|
+
current.length = count;
|
|
1564
|
+
const presets = RATIO_PRESETS[count] ?? RATIO_PRESETS[2];
|
|
1565
|
+
this.update(index, {
|
|
1566
|
+
...block,
|
|
1567
|
+
data: { ...block.data, count, ratio: presets[0].value },
|
|
1568
|
+
children: current,
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
setColumnData(index, patch) {
|
|
1572
|
+
const block = this.blocks()[index];
|
|
1573
|
+
this.update(index, { ...block, data: { ...block.data, ...patch } });
|
|
1574
|
+
}
|
|
1575
|
+
onRatioChange(index, event) {
|
|
1576
|
+
this.setColumnData(index, { ratio: event.target.value });
|
|
1577
|
+
}
|
|
1578
|
+
onGapChange(index, event) {
|
|
1579
|
+
this.setColumnData(index, { gap: Number(event.target.value) });
|
|
1580
|
+
}
|
|
1581
|
+
onAlignChange(index, event) {
|
|
1582
|
+
this.setColumnData(index, { align: event.target.value });
|
|
1583
|
+
}
|
|
1584
|
+
onJustifyChange(index, event) {
|
|
1585
|
+
this.setColumnData(index, { justify: event.target.value });
|
|
1586
|
+
}
|
|
1587
|
+
gridColumns(block) {
|
|
1588
|
+
const ratio = String(block.data['ratio'] ?? '');
|
|
1589
|
+
const parts = ratio.split('-').map((n) => parseFloat(n)).filter((n) => n > 0);
|
|
1590
|
+
if (parts.length >= 2)
|
|
1591
|
+
return parts.map((n) => `${n}fr`).join(' ');
|
|
1592
|
+
return `repeat(${block.children?.length ?? 2}, 1fr)`;
|
|
1593
|
+
}
|
|
1594
|
+
gridGap(block) {
|
|
1595
|
+
const g = Math.max(0, Math.min(8, Number(block.data['gap']) || 4));
|
|
1596
|
+
return `var(--mk-space-${g})`;
|
|
1597
|
+
}
|
|
1598
|
+
justifyValue(block) {
|
|
1599
|
+
const map = {
|
|
1600
|
+
start: 'flex-start', center: 'center', end: 'flex-end', between: 'space-between',
|
|
1601
|
+
};
|
|
1602
|
+
return map[String(block.data['justify'] ?? 'start')] ?? 'flex-start';
|
|
1603
|
+
}
|
|
1604
|
+
// --- toolbar menu ---------------------------------------------------------
|
|
1605
|
+
toggleMenu(index) {
|
|
1606
|
+
this.menuOpen.update((i) => (i === index ? -1 : index));
|
|
1607
|
+
}
|
|
1608
|
+
/** Escape inside the options popup closes it and restores the toggle's focus. */
|
|
1609
|
+
closeMenu(trigger, event) {
|
|
1610
|
+
event.preventDefault();
|
|
1611
|
+
event.stopPropagation();
|
|
1612
|
+
this.menuOpen.set(-1);
|
|
1613
|
+
trigger.focus();
|
|
1614
|
+
}
|
|
1615
|
+
transformsFor(type) {
|
|
1616
|
+
return this.definition(type)?.transforms ?? [];
|
|
1617
|
+
}
|
|
1618
|
+
fallbackParagraph() {
|
|
1619
|
+
return { id: mkBlockId('paragraph'), type: 'paragraph', data: { html: '' } };
|
|
1620
|
+
}
|
|
1621
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockList, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1622
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkBlockList, isStandalone: true, selector: "mk-block-list", inputs: { blocks: { classPropertyName: "blocks", publicName: "blocks", isSignal: true, isRequired: true, transformFunction: null }, depth: { classPropertyName: "depth", publicName: "depth", isSignal: true, isRequired: false, transformFunction: null }, inColumn: { classPropertyName: "inColumn", publicName: "inColumn", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { blocksChange: "blocksChange" }, host: { classAttribute: "mk-block-list" }, ngImport: i0, template: "@let readonly = ctx.readonly() || ctx.disabled();\n\n@for (block of blocks(); track block.id; let i = $index; let last = $last) {\n @if (!readonly) {\n <mk-block-inserter\n variant=\"slim\"\n [definitions]=\"ctx.definitions()\"\n [columnsOnly]=\"inColumn()\"\n (pick)=\"insertAt(i, $event)\"\n />\n }\n\n <div class=\"mk-block\" [attr.data-block-id]=\"block.id\" [class.mk-block--readonly]=\"readonly\">\n @if (!readonly) {\n <div class=\"mk-block__gutter\" aria-hidden=\"false\">\n <button\n type=\"button\"\n class=\"mk-block__move\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"i18n.blockEditor.moveBlockUp\"\n (click)=\"move(i, -1)\"\n >\n \u25B2\n </button>\n <span class=\"mk-block__handle\" [title]=\"i18n.blockEditor.dragHandle\" aria-hidden=\"true\">\u283F</span>\n <button\n type=\"button\"\n class=\"mk-block__move\"\n [disabled]=\"last\"\n [attr.aria-label]=\"i18n.blockEditor.moveBlockDown\"\n (click)=\"move(i, 1)\"\n >\n \u25BC\n </button>\n </div>\n\n <div class=\"mk-block__bar\">\n <span class=\"mk-block__type\">\n <span class=\"mk-block__type-icon\" aria-hidden=\"true\">{{ definition(block.type)?.icon }}</span>\n {{ definition(block.type)?.label ?? block.type }}\n </span>\n <div class=\"mk-block__actions\">\n <button\n #menuTrigger\n type=\"button\"\n class=\"mk-block__action\"\n [attr.aria-expanded]=\"menuOpen() === i\"\n [attr.aria-controls]=\"'mk-block-menu-' + block.id\"\n [attr.aria-label]=\"i18n.blockEditor.blockOptions\"\n (click)=\"toggleMenu(i)\"\n >\n \u22EF\n </button>\n @if (menuOpen() === i) {\n <div\n class=\"mk-block__menu\"\n [id]=\"'mk-block-menu-' + block.id\"\n (keydown.escape)=\"closeMenu(menuTrigger, $event)\"\n >\n <button type=\"button\" (click)=\"duplicate(i)\">{{ i18n.blockEditor.duplicate }}</button>\n @for (t of transformsFor(block.type); track t) {\n <button type=\"button\" (click)=\"transform(i, t)\">\n {{ i18n.blockEditor.turnInto(definition(t)?.label ?? t) }}\n </button>\n }\n <button type=\"button\" class=\"mk-block__menu-danger\" (click)=\"remove(i)\">\n {{ i18n.blockEditor.remove }}\n </button>\n </div>\n }\n </div>\n </div>\n }\n\n <div class=\"mk-block__body\">\n @switch (block.type) {\n @case ('paragraph') {\n <mk-paragraph-block\n [block]=\"block\"\n [readonly]=\"readonly\"\n [placeholder]=\"ctx.placeholder()\"\n (blockChange)=\"update(i, $event)\"\n (splitAt)=\"onSplit(i, $event)\"\n (removeEmpty)=\"onMergeBack(i)\"\n (arrowOut)=\"onArrowOut(i, $event)\"\n />\n }\n @case ('heading') {\n <mk-heading-block\n [block]=\"block\"\n [readonly]=\"readonly\"\n (blockChange)=\"update(i, $event)\"\n (splitAt)=\"onSplit(i, $event)\"\n (removeEmpty)=\"onMergeBack(i)\"\n (arrowOut)=\"onArrowOut(i, $event)\"\n />\n }\n @case ('quote') {\n <mk-quote-block\n [block]=\"block\"\n [readonly]=\"readonly\"\n (blockChange)=\"update(i, $event)\"\n (splitAt)=\"onSplit(i, $event)\"\n (removeEmpty)=\"onMergeBack(i)\"\n (arrowOut)=\"onArrowOut(i, $event)\"\n />\n }\n @case ('list') {\n <mk-list-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('code') {\n <mk-code-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('image') {\n <mk-image-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('embed') {\n <mk-embed-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('button') {\n <mk-button-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('divider') {\n <hr class=\"mk-block__divider\" data-mk-focus tabindex=\"-1\" />\n }\n @case ('columns') {\n @if (!readonly) {\n <div class=\"mk-columns-settings\" role=\"group\" [attr.aria-label]=\"i18n.blockEditor.columnSettings\">\n <label>\n <span>{{ i18n.blockEditor.columns }}</span>\n <span class=\"mk-columns-settings__counts\">\n @for (n of [2, 3, 4]; track n) {\n <button\n type=\"button\"\n [class.mk-columns-settings__count--active]=\"(block.data['count'] || 2) === n\"\n [attr.aria-pressed]=\"(block.data['count'] || 2) === n\"\n (click)=\"setColumnCount(i, n)\"\n >\n {{ n }}\n </button>\n }\n </span>\n </label>\n <label>\n <span>{{ i18n.blockEditor.ratio }}</span>\n <select [value]=\"block.data['ratio']\" (change)=\"onRatioChange(i, $event)\">\n @for (preset of ratioPresets[block.data['count'] || 2]; track preset.value) {\n <option [value]=\"preset.value\">{{ preset.label || i18n.blockEditor.ratioEqual }}</option>\n }\n </select>\n </label>\n <label>\n <span>{{ i18n.blockEditor.gap }}</span>\n <input\n type=\"range\"\n min=\"0\"\n max=\"8\"\n [value]=\"block.data['gap'] ?? 4\"\n (input)=\"onGapChange(i, $event)\"\n />\n </label>\n <label>\n <span>{{ i18n.blockEditor.align }}</span>\n <select [value]=\"block.data['align'] ?? 'stretch'\" (change)=\"onAlignChange(i, $event)\">\n <option value=\"stretch\">{{ i18n.blockEditor.alignStretch }}</option>\n <option value=\"start\">{{ i18n.blockEditor.alignTop }}</option>\n <option value=\"center\">{{ i18n.blockEditor.alignMiddle }}</option>\n <option value=\"end\">{{ i18n.blockEditor.alignBottom }}</option>\n </select>\n </label>\n <label>\n <span>{{ i18n.blockEditor.justify }}</span>\n <select [value]=\"block.data['justify'] ?? 'start'\" (change)=\"onJustifyChange(i, $event)\">\n <option value=\"start\">{{ i18n.blockEditor.justifyStart }}</option>\n <option value=\"center\">{{ i18n.blockEditor.justifyCenter }}</option>\n <option value=\"end\">{{ i18n.blockEditor.justifyEnd }}</option>\n <option value=\"between\">{{ i18n.blockEditor.justifyBetween }}</option>\n </select>\n </label>\n </div>\n }\n <div\n class=\"mk-columns\"\n [style.grid-template-columns]=\"gridColumns(block)\"\n [style.gap]=\"gridGap(block)\"\n [style.align-items]=\"block.data['align'] ?? 'stretch'\"\n [style.justify-content]=\"justifyValue(block)\"\n >\n @for (col of block.children ?? []; track col.id; let c = $index) {\n <div class=\"mk-column\">\n <mk-block-list\n [blocks]=\"col.children ?? []\"\n [depth]=\"depth() + 1\"\n [inColumn]=\"true\"\n (blocksChange)=\"onColumnChange(i, c, $event)\"\n />\n </div>\n }\n </div>\n }\n @default {\n <p class=\"mk-block__unknown\">{{ i18n.blockEditor.unknownBlock(block.type) }}</p>\n }\n }\n </div>\n </div>\n}\n\n@if (!readonly) {\n <mk-block-inserter\n class=\"mk-block-list__add\"\n [definitions]=\"ctx.definitions()\"\n [columnsOnly]=\"inColumn()\"\n [label]=\"blocks().length ? i18n.blockEditor.addBlock : i18n.blockEditor.addFirstBlock\"\n (pick)=\"insertAt(blocks().length, $event)\"\n />\n}\n", styles: [":host{display:block}.mk-block{position:relative;padding:var(--mk-space-2);padding-inline-start:var(--mk-space-8);border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block--readonly{padding-inline-start:var(--mk-space-2)}.mk-block:hover,.mk-block:focus-within{border-color:var(--mk-border-subtle);background:var(--mk-surface-2)}.mk-block__gutter{position:absolute;top:var(--mk-space-2);inset-inline-start:0;display:flex;flex-direction:column;align-items:center;gap:2px;opacity:0;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block:hover .mk-block__gutter,.mk-block:focus-within .mk-block__gutter{opacity:1}.mk-block__move{width:22px;height:18px;border:none;border-radius:var(--mk-radius-xs);background:transparent;color:var(--mk-text-muted);font-size:var(--mk-font-size-xs);line-height:1;cursor:pointer}.mk-block__move:hover:not(:disabled){background:var(--mk-hover-overlay);color:var(--mk-text)}.mk-block__move:disabled{opacity:.3;cursor:not-allowed}.mk-block__handle{color:var(--mk-text-subtle);font-size:var(--mk-font-size-sm);cursor:grab}.mk-block__bar{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--mk-space-1);opacity:0;height:20px;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block:hover .mk-block__bar,.mk-block:focus-within .mk-block__bar{opacity:1}.mk-block__type{display:inline-flex;align-items:center;gap:var(--mk-space-1);color:var(--mk-text-subtle);font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-medium)}.mk-block__type-icon{font-size:var(--mk-font-size-sm)}.mk-block__actions{position:relative}.mk-block__action{width:24px;height:20px;border:none;border-radius:var(--mk-radius-xs);background:transparent;color:var(--mk-text-muted);cursor:pointer}.mk-block__action:hover{background:var(--mk-hover-overlay);color:var(--mk-text)}.mk-block__menu{position:absolute;z-index:var(--mk-z-menu);top:calc(100% + var(--mk-space-1));inset-inline-end:0;display:flex;flex-direction:column;min-width:180px;padding:var(--mk-space-1);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface);box-shadow:var(--mk-shadow-lg)}.mk-block__menu button{padding:var(--mk-space-2) var(--mk-space-3);border:none;border-radius:var(--mk-radius-sm);background:transparent;color:var(--mk-text);font:inherit;font-size:var(--mk-font-size-sm);text-align:start;cursor:pointer}.mk-block__menu button:hover{background:var(--mk-hover-overlay)}.mk-block__menu-danger{color:var(--mk-danger-text)}.mk-block__divider{height:0;margin:var(--mk-space-3) 0;border:none;border-top:var(--mk-border-width-strong) solid var(--mk-border-strong)}.mk-block__unknown{padding:var(--mk-space-3);border:var(--mk-border-width) dashed var(--mk-border);border-radius:var(--mk-radius-md);color:var(--mk-text-muted);font-size:var(--mk-font-size-sm)}.mk-columns{display:grid;margin:var(--mk-space-2) 0}.mk-column{min-width:0;padding:var(--mk-space-1);border:var(--mk-border-width) dashed var(--mk-border-subtle);border-radius:var(--mk-radius-sm)}.mk-columns-settings{display:flex;flex-wrap:wrap;gap:var(--mk-space-3);margin-bottom:var(--mk-space-2);padding:var(--mk-space-2) var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border-subtle);border-radius:var(--mk-radius-md);background:var(--mk-surface-2)}.mk-columns-settings label{display:flex;flex-direction:column;gap:var(--mk-space-1);color:var(--mk-text-muted);font-size:var(--mk-font-size-xs)}.mk-columns-settings select,.mk-columns-settings input[type=range]{height:var(--mk-control-height-sm);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text);font:inherit;font-size:var(--mk-font-size-xs)}.mk-columns-settings__counts{display:inline-flex;gap:2px}.mk-columns-settings__counts button{width:28px;height:var(--mk-control-height-sm);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text-muted);font:inherit;cursor:pointer}.mk-columns-settings__count--active{background:var(--mk-primary);border-color:var(--mk-primary);color:var(--mk-primary-contrast)}@media(max-width:640px){.mk-columns{grid-template-columns:1fr!important}}button:focus-visible,select:focus-visible,input:focus-visible,[tabindex]:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}@media(prefers-reduced-motion:reduce){.mk-block,.mk-block__gutter,.mk-block__bar{transition:none}}\n"], dependencies: [{ kind: "component", type: MkBlockList, selector: "mk-block-list", inputs: ["blocks", "depth", "inColumn"], outputs: ["blocksChange"] }, { kind: "component", type: MkParagraphBlock, selector: "mk-paragraph-block", inputs: ["block", "readonly", "placeholder"], outputs: ["blockChange", "splitAt", "removeEmpty", "arrowOut"] }, { kind: "component", type: MkHeadingBlock, selector: "mk-heading-block", inputs: ["block", "readonly"], outputs: ["blockChange", "splitAt", "removeEmpty", "arrowOut"] }, { kind: "component", type: MkQuoteBlock, selector: "mk-quote-block", inputs: ["block", "readonly"], outputs: ["blockChange", "splitAt", "removeEmpty", "arrowOut"] }, { kind: "component", type: MkListBlock, selector: "mk-list-block", inputs: ["block", "readonly"], outputs: ["blockChange"] }, { kind: "component", type: MkCodeBlock, selector: "mk-code-block", inputs: ["block", "readonly"], outputs: ["blockChange"] }, { kind: "component", type: MkImageBlock, selector: "mk-image-block", inputs: ["block", "readonly"], outputs: ["blockChange"] }, { kind: "component", type: MkEmbedBlock, selector: "mk-embed-block", inputs: ["block", "readonly"], outputs: ["blockChange"] }, { kind: "component", type: MkButtonBlock, selector: "mk-button-block", inputs: ["block", "readonly"], outputs: ["blockChange"] }, { kind: "component", type: MkBlockInserter, selector: "mk-block-inserter", inputs: ["definitions", "variant", "label", "disabled", "columnsOnly"], outputs: ["pick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1623
|
+
}
|
|
1624
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockList, decorators: [{
|
|
1625
|
+
type: Component,
|
|
1626
|
+
args: [{ selector: 'mk-block-list', imports: [
|
|
1627
|
+
MkParagraphBlock,
|
|
1628
|
+
MkHeadingBlock,
|
|
1629
|
+
MkQuoteBlock,
|
|
1630
|
+
MkListBlock,
|
|
1631
|
+
MkCodeBlock,
|
|
1632
|
+
MkImageBlock,
|
|
1633
|
+
MkEmbedBlock,
|
|
1634
|
+
MkButtonBlock,
|
|
1635
|
+
MkBlockInserter,
|
|
1636
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'mk-block-list' }, template: "@let readonly = ctx.readonly() || ctx.disabled();\n\n@for (block of blocks(); track block.id; let i = $index; let last = $last) {\n @if (!readonly) {\n <mk-block-inserter\n variant=\"slim\"\n [definitions]=\"ctx.definitions()\"\n [columnsOnly]=\"inColumn()\"\n (pick)=\"insertAt(i, $event)\"\n />\n }\n\n <div class=\"mk-block\" [attr.data-block-id]=\"block.id\" [class.mk-block--readonly]=\"readonly\">\n @if (!readonly) {\n <div class=\"mk-block__gutter\" aria-hidden=\"false\">\n <button\n type=\"button\"\n class=\"mk-block__move\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"i18n.blockEditor.moveBlockUp\"\n (click)=\"move(i, -1)\"\n >\n \u25B2\n </button>\n <span class=\"mk-block__handle\" [title]=\"i18n.blockEditor.dragHandle\" aria-hidden=\"true\">\u283F</span>\n <button\n type=\"button\"\n class=\"mk-block__move\"\n [disabled]=\"last\"\n [attr.aria-label]=\"i18n.blockEditor.moveBlockDown\"\n (click)=\"move(i, 1)\"\n >\n \u25BC\n </button>\n </div>\n\n <div class=\"mk-block__bar\">\n <span class=\"mk-block__type\">\n <span class=\"mk-block__type-icon\" aria-hidden=\"true\">{{ definition(block.type)?.icon }}</span>\n {{ definition(block.type)?.label ?? block.type }}\n </span>\n <div class=\"mk-block__actions\">\n <button\n #menuTrigger\n type=\"button\"\n class=\"mk-block__action\"\n [attr.aria-expanded]=\"menuOpen() === i\"\n [attr.aria-controls]=\"'mk-block-menu-' + block.id\"\n [attr.aria-label]=\"i18n.blockEditor.blockOptions\"\n (click)=\"toggleMenu(i)\"\n >\n \u22EF\n </button>\n @if (menuOpen() === i) {\n <div\n class=\"mk-block__menu\"\n [id]=\"'mk-block-menu-' + block.id\"\n (keydown.escape)=\"closeMenu(menuTrigger, $event)\"\n >\n <button type=\"button\" (click)=\"duplicate(i)\">{{ i18n.blockEditor.duplicate }}</button>\n @for (t of transformsFor(block.type); track t) {\n <button type=\"button\" (click)=\"transform(i, t)\">\n {{ i18n.blockEditor.turnInto(definition(t)?.label ?? t) }}\n </button>\n }\n <button type=\"button\" class=\"mk-block__menu-danger\" (click)=\"remove(i)\">\n {{ i18n.blockEditor.remove }}\n </button>\n </div>\n }\n </div>\n </div>\n }\n\n <div class=\"mk-block__body\">\n @switch (block.type) {\n @case ('paragraph') {\n <mk-paragraph-block\n [block]=\"block\"\n [readonly]=\"readonly\"\n [placeholder]=\"ctx.placeholder()\"\n (blockChange)=\"update(i, $event)\"\n (splitAt)=\"onSplit(i, $event)\"\n (removeEmpty)=\"onMergeBack(i)\"\n (arrowOut)=\"onArrowOut(i, $event)\"\n />\n }\n @case ('heading') {\n <mk-heading-block\n [block]=\"block\"\n [readonly]=\"readonly\"\n (blockChange)=\"update(i, $event)\"\n (splitAt)=\"onSplit(i, $event)\"\n (removeEmpty)=\"onMergeBack(i)\"\n (arrowOut)=\"onArrowOut(i, $event)\"\n />\n }\n @case ('quote') {\n <mk-quote-block\n [block]=\"block\"\n [readonly]=\"readonly\"\n (blockChange)=\"update(i, $event)\"\n (splitAt)=\"onSplit(i, $event)\"\n (removeEmpty)=\"onMergeBack(i)\"\n (arrowOut)=\"onArrowOut(i, $event)\"\n />\n }\n @case ('list') {\n <mk-list-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('code') {\n <mk-code-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('image') {\n <mk-image-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('embed') {\n <mk-embed-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('button') {\n <mk-button-block [block]=\"block\" [readonly]=\"readonly\" (blockChange)=\"update(i, $event)\" />\n }\n @case ('divider') {\n <hr class=\"mk-block__divider\" data-mk-focus tabindex=\"-1\" />\n }\n @case ('columns') {\n @if (!readonly) {\n <div class=\"mk-columns-settings\" role=\"group\" [attr.aria-label]=\"i18n.blockEditor.columnSettings\">\n <label>\n <span>{{ i18n.blockEditor.columns }}</span>\n <span class=\"mk-columns-settings__counts\">\n @for (n of [2, 3, 4]; track n) {\n <button\n type=\"button\"\n [class.mk-columns-settings__count--active]=\"(block.data['count'] || 2) === n\"\n [attr.aria-pressed]=\"(block.data['count'] || 2) === n\"\n (click)=\"setColumnCount(i, n)\"\n >\n {{ n }}\n </button>\n }\n </span>\n </label>\n <label>\n <span>{{ i18n.blockEditor.ratio }}</span>\n <select [value]=\"block.data['ratio']\" (change)=\"onRatioChange(i, $event)\">\n @for (preset of ratioPresets[block.data['count'] || 2]; track preset.value) {\n <option [value]=\"preset.value\">{{ preset.label || i18n.blockEditor.ratioEqual }}</option>\n }\n </select>\n </label>\n <label>\n <span>{{ i18n.blockEditor.gap }}</span>\n <input\n type=\"range\"\n min=\"0\"\n max=\"8\"\n [value]=\"block.data['gap'] ?? 4\"\n (input)=\"onGapChange(i, $event)\"\n />\n </label>\n <label>\n <span>{{ i18n.blockEditor.align }}</span>\n <select [value]=\"block.data['align'] ?? 'stretch'\" (change)=\"onAlignChange(i, $event)\">\n <option value=\"stretch\">{{ i18n.blockEditor.alignStretch }}</option>\n <option value=\"start\">{{ i18n.blockEditor.alignTop }}</option>\n <option value=\"center\">{{ i18n.blockEditor.alignMiddle }}</option>\n <option value=\"end\">{{ i18n.blockEditor.alignBottom }}</option>\n </select>\n </label>\n <label>\n <span>{{ i18n.blockEditor.justify }}</span>\n <select [value]=\"block.data['justify'] ?? 'start'\" (change)=\"onJustifyChange(i, $event)\">\n <option value=\"start\">{{ i18n.blockEditor.justifyStart }}</option>\n <option value=\"center\">{{ i18n.blockEditor.justifyCenter }}</option>\n <option value=\"end\">{{ i18n.blockEditor.justifyEnd }}</option>\n <option value=\"between\">{{ i18n.blockEditor.justifyBetween }}</option>\n </select>\n </label>\n </div>\n }\n <div\n class=\"mk-columns\"\n [style.grid-template-columns]=\"gridColumns(block)\"\n [style.gap]=\"gridGap(block)\"\n [style.align-items]=\"block.data['align'] ?? 'stretch'\"\n [style.justify-content]=\"justifyValue(block)\"\n >\n @for (col of block.children ?? []; track col.id; let c = $index) {\n <div class=\"mk-column\">\n <mk-block-list\n [blocks]=\"col.children ?? []\"\n [depth]=\"depth() + 1\"\n [inColumn]=\"true\"\n (blocksChange)=\"onColumnChange(i, c, $event)\"\n />\n </div>\n }\n </div>\n }\n @default {\n <p class=\"mk-block__unknown\">{{ i18n.blockEditor.unknownBlock(block.type) }}</p>\n }\n }\n </div>\n </div>\n}\n\n@if (!readonly) {\n <mk-block-inserter\n class=\"mk-block-list__add\"\n [definitions]=\"ctx.definitions()\"\n [columnsOnly]=\"inColumn()\"\n [label]=\"blocks().length ? i18n.blockEditor.addBlock : i18n.blockEditor.addFirstBlock\"\n (pick)=\"insertAt(blocks().length, $event)\"\n />\n}\n", styles: [":host{display:block}.mk-block{position:relative;padding:var(--mk-space-2);padding-inline-start:var(--mk-space-8);border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);transition:border-color var(--mk-duration-fast) var(--mk-ease-standard),background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block--readonly{padding-inline-start:var(--mk-space-2)}.mk-block:hover,.mk-block:focus-within{border-color:var(--mk-border-subtle);background:var(--mk-surface-2)}.mk-block__gutter{position:absolute;top:var(--mk-space-2);inset-inline-start:0;display:flex;flex-direction:column;align-items:center;gap:2px;opacity:0;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block:hover .mk-block__gutter,.mk-block:focus-within .mk-block__gutter{opacity:1}.mk-block__move{width:22px;height:18px;border:none;border-radius:var(--mk-radius-xs);background:transparent;color:var(--mk-text-muted);font-size:var(--mk-font-size-xs);line-height:1;cursor:pointer}.mk-block__move:hover:not(:disabled){background:var(--mk-hover-overlay);color:var(--mk-text)}.mk-block__move:disabled{opacity:.3;cursor:not-allowed}.mk-block__handle{color:var(--mk-text-subtle);font-size:var(--mk-font-size-sm);cursor:grab}.mk-block__bar{display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--mk-space-1);opacity:0;height:20px;transition:opacity var(--mk-duration-fast) var(--mk-ease-standard)}.mk-block:hover .mk-block__bar,.mk-block:focus-within .mk-block__bar{opacity:1}.mk-block__type{display:inline-flex;align-items:center;gap:var(--mk-space-1);color:var(--mk-text-subtle);font-size:var(--mk-font-size-xs);font-weight:var(--mk-font-weight-medium)}.mk-block__type-icon{font-size:var(--mk-font-size-sm)}.mk-block__actions{position:relative}.mk-block__action{width:24px;height:20px;border:none;border-radius:var(--mk-radius-xs);background:transparent;color:var(--mk-text-muted);cursor:pointer}.mk-block__action:hover{background:var(--mk-hover-overlay);color:var(--mk-text)}.mk-block__menu{position:absolute;z-index:var(--mk-z-menu);top:calc(100% + var(--mk-space-1));inset-inline-end:0;display:flex;flex-direction:column;min-width:180px;padding:var(--mk-space-1);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-md);background:var(--mk-surface);box-shadow:var(--mk-shadow-lg)}.mk-block__menu button{padding:var(--mk-space-2) var(--mk-space-3);border:none;border-radius:var(--mk-radius-sm);background:transparent;color:var(--mk-text);font:inherit;font-size:var(--mk-font-size-sm);text-align:start;cursor:pointer}.mk-block__menu button:hover{background:var(--mk-hover-overlay)}.mk-block__menu-danger{color:var(--mk-danger-text)}.mk-block__divider{height:0;margin:var(--mk-space-3) 0;border:none;border-top:var(--mk-border-width-strong) solid var(--mk-border-strong)}.mk-block__unknown{padding:var(--mk-space-3);border:var(--mk-border-width) dashed var(--mk-border);border-radius:var(--mk-radius-md);color:var(--mk-text-muted);font-size:var(--mk-font-size-sm)}.mk-columns{display:grid;margin:var(--mk-space-2) 0}.mk-column{min-width:0;padding:var(--mk-space-1);border:var(--mk-border-width) dashed var(--mk-border-subtle);border-radius:var(--mk-radius-sm)}.mk-columns-settings{display:flex;flex-wrap:wrap;gap:var(--mk-space-3);margin-bottom:var(--mk-space-2);padding:var(--mk-space-2) var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border-subtle);border-radius:var(--mk-radius-md);background:var(--mk-surface-2)}.mk-columns-settings label{display:flex;flex-direction:column;gap:var(--mk-space-1);color:var(--mk-text-muted);font-size:var(--mk-font-size-xs)}.mk-columns-settings select,.mk-columns-settings input[type=range]{height:var(--mk-control-height-sm);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text);font:inherit;font-size:var(--mk-font-size-xs)}.mk-columns-settings__counts{display:inline-flex;gap:2px}.mk-columns-settings__counts button{width:28px;height:var(--mk-control-height-sm);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-sm);background:var(--mk-surface);color:var(--mk-text-muted);font:inherit;cursor:pointer}.mk-columns-settings__count--active{background:var(--mk-primary);border-color:var(--mk-primary);color:var(--mk-primary-contrast)}@media(max-width:640px){.mk-columns{grid-template-columns:1fr!important}}button:focus-visible,select:focus-visible,input:focus-visible,[tabindex]:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}@media(prefers-reduced-motion:reduce){.mk-block,.mk-block__gutter,.mk-block__bar{transition:none}}\n"] }]
|
|
1637
|
+
}], propDecorators: { blocks: [{ type: i0.Input, args: [{ isSignal: true, alias: "blocks", required: true }] }], depth: [{ type: i0.Input, args: [{ isSignal: true, alias: "depth", required: false }] }], inColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "inColumn", required: false }] }], blocksChange: [{ type: i0.Output, args: ["blocksChange"] }] } });
|
|
1638
|
+
|
|
1639
|
+
/**
|
|
1640
|
+
* HTML → {@link MkBlockDocument} parser — the inverse of {@link mkBlocksToHtml}.
|
|
1641
|
+
*
|
|
1642
|
+
* Lets the block editor back a **string-typed richtext field** (e.g. a CMS
|
|
1643
|
+
* `richtext` column that stores an HTML string): seed the editor from stored
|
|
1644
|
+
* HTML, and serialise back to HTML on change. Best-effort and forgiving — any
|
|
1645
|
+
* HTML parses, with unrecognised block-level wrappers recursed into and loose
|
|
1646
|
+
* inline runs grouped into paragraphs. Round-trips the blocks `mkBlocksToHtml`
|
|
1647
|
+
* emits (paragraph, heading, list, quote, code, image, divider, button, embed,
|
|
1648
|
+
* columns).
|
|
1649
|
+
*
|
|
1650
|
+
* Uses the DOM (`DOMParser`) — available in browsers and jsdom. In a pure Node
|
|
1651
|
+
* context without a DOM it falls back to a single sanitised paragraph.
|
|
1652
|
+
*/
|
|
1653
|
+
const HEADING_LEVEL = {
|
|
1654
|
+
h1: 1, h2: 2, h3: 3, h4: 4, h5: 4, h6: 4,
|
|
1655
|
+
};
|
|
1656
|
+
/** Block-level tags handled as their own block (others recurse or group). */
|
|
1657
|
+
const BLOCK_TAGS = new Set([
|
|
1658
|
+
'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'blockquote', 'pre',
|
|
1659
|
+
'figure', 'img', 'hr', 'div', 'section', 'article', 'table',
|
|
1660
|
+
]);
|
|
1661
|
+
function block(type, data, children) {
|
|
1662
|
+
const b = { id: mkBlockId(type), type, data };
|
|
1663
|
+
if (children)
|
|
1664
|
+
b.children = children;
|
|
1665
|
+
return b;
|
|
1666
|
+
}
|
|
1667
|
+
function parseRoot(html) {
|
|
1668
|
+
if (typeof DOMParser !== 'undefined') {
|
|
1669
|
+
return new DOMParser().parseFromString(html, 'text/html').body;
|
|
1670
|
+
}
|
|
1671
|
+
if (typeof document !== 'undefined') {
|
|
1672
|
+
const tpl = document.createElement('template');
|
|
1673
|
+
tpl.innerHTML = html;
|
|
1674
|
+
return tpl.content;
|
|
1675
|
+
}
|
|
1676
|
+
return null;
|
|
1677
|
+
}
|
|
1678
|
+
const TEXT_NODE = 3;
|
|
1679
|
+
const ELEMENT_NODE = 1;
|
|
1680
|
+
function isBlockElement(node) {
|
|
1681
|
+
return (node.nodeType === ELEMENT_NODE &&
|
|
1682
|
+
BLOCK_TAGS.has(node.tagName.toLowerCase()));
|
|
1683
|
+
}
|
|
1684
|
+
/** HTML for one node in an inline run (encoded text, element markup). */
|
|
1685
|
+
function nodeHtml(node) {
|
|
1686
|
+
if (node.nodeType === TEXT_NODE)
|
|
1687
|
+
return mkEscapeHtml(node.textContent ?? '');
|
|
1688
|
+
if (node.nodeType === ELEMENT_NODE)
|
|
1689
|
+
return node.outerHTML;
|
|
1690
|
+
return '';
|
|
1691
|
+
}
|
|
1692
|
+
function style(el, prop) {
|
|
1693
|
+
return el.style?.getPropertyValue(prop)?.trim() ?? '';
|
|
1694
|
+
}
|
|
1695
|
+
/** Walk a parent's children into blocks, grouping loose inline runs. */
|
|
1696
|
+
function collectBlocks(root, out) {
|
|
1697
|
+
let inline = [];
|
|
1698
|
+
const flush = () => {
|
|
1699
|
+
if (!inline.length)
|
|
1700
|
+
return;
|
|
1701
|
+
const html = inline.map(nodeHtml).join('').trim();
|
|
1702
|
+
if (html)
|
|
1703
|
+
out.push(block('paragraph', { html: sanitizeInlineHtml(html) }));
|
|
1704
|
+
inline = [];
|
|
1705
|
+
};
|
|
1706
|
+
for (const node of Array.from(root.childNodes)) {
|
|
1707
|
+
if (isBlockElement(node)) {
|
|
1708
|
+
flush();
|
|
1709
|
+
appendElement(node, out);
|
|
1710
|
+
}
|
|
1711
|
+
else if (node.nodeType === TEXT_NODE && !node.textContent?.trim()) {
|
|
1712
|
+
// ignore whitespace between blocks
|
|
1713
|
+
}
|
|
1714
|
+
else {
|
|
1715
|
+
inline.push(node);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
flush();
|
|
1719
|
+
}
|
|
1720
|
+
/** Map a block-level element to one or more blocks (appended to `out`). */
|
|
1721
|
+
function appendElement(el, out) {
|
|
1722
|
+
const tag = el.tagName.toLowerCase();
|
|
1723
|
+
if (tag === 'p') {
|
|
1724
|
+
out.push(block('paragraph', { html: sanitizeInlineHtml(el.innerHTML) }));
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
if (tag in HEADING_LEVEL) {
|
|
1728
|
+
out.push(block('heading', {
|
|
1729
|
+
level: HEADING_LEVEL[tag],
|
|
1730
|
+
html: sanitizeInlineHtml(el.innerHTML),
|
|
1731
|
+
}));
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
if (tag === 'ul' || tag === 'ol') {
|
|
1735
|
+
const items = Array.from(el.querySelectorAll(':scope > li')).map((li) => sanitizeInlineHtml(li.innerHTML));
|
|
1736
|
+
out.push(block('list', { ordered: tag === 'ol', items: items.length ? items : [''] }));
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
if (tag === 'blockquote') {
|
|
1740
|
+
const cite = el.querySelector('cite');
|
|
1741
|
+
const p = el.querySelector('p');
|
|
1742
|
+
const body = p ? p.innerHTML : el.innerHTML;
|
|
1743
|
+
out.push(block('quote', {
|
|
1744
|
+
html: sanitizeInlineHtml(body),
|
|
1745
|
+
citation: cite?.textContent?.trim() ?? '',
|
|
1746
|
+
}));
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
if (tag === 'pre') {
|
|
1750
|
+
const codeEl = el.querySelector('code') ?? el;
|
|
1751
|
+
const lang = (codeEl.className.match(/language-([\w-]+)/) ?? [])[1] ?? '';
|
|
1752
|
+
out.push(block('code', { code: codeEl.textContent ?? '', language: lang }));
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
if (tag === 'figure' || tag === 'img') {
|
|
1756
|
+
const img = tag === 'img' ? el : el.querySelector('img');
|
|
1757
|
+
if (img) {
|
|
1758
|
+
const width = parseInt(style(img, 'max-width'), 10);
|
|
1759
|
+
const align = style(el, 'text-align') === 'left'
|
|
1760
|
+
? 'left'
|
|
1761
|
+
: style(el, 'text-align') === 'right'
|
|
1762
|
+
? 'right'
|
|
1763
|
+
: 'center';
|
|
1764
|
+
out.push(block('image', {
|
|
1765
|
+
src: img.getAttribute('src') ?? '',
|
|
1766
|
+
alt: img.getAttribute('alt') ?? '',
|
|
1767
|
+
caption: el.querySelector('figcaption')?.textContent?.trim() ?? '',
|
|
1768
|
+
align,
|
|
1769
|
+
width: Number.isFinite(width) && width > 0 ? width : 100,
|
|
1770
|
+
}));
|
|
1771
|
+
}
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
if (tag === 'hr') {
|
|
1775
|
+
out.push(block('divider', {}));
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
// <div> / generic containers: recognise the wrappers mkBlocksToHtml emits,
|
|
1779
|
+
// otherwise recurse into the children.
|
|
1780
|
+
if (el.classList.contains('mk-columns')) {
|
|
1781
|
+
const columns = Array.from(el.querySelectorAll(':scope > .mk-column')).map((colEl) => {
|
|
1782
|
+
const children = [];
|
|
1783
|
+
collectBlocks(colEl, children);
|
|
1784
|
+
return block('column', {}, children);
|
|
1785
|
+
});
|
|
1786
|
+
out.push(block('columns', { count: columns.length || 2 }, columns));
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
const cta = el.querySelector(':scope > a.mk-cta, a.mk-cta');
|
|
1790
|
+
if (cta) {
|
|
1791
|
+
const tone = (cta.className.match(/mk-cta--([\w-]+)/) ?? [])[1] ?? 'primary';
|
|
1792
|
+
out.push(block('button', {
|
|
1793
|
+
label: cta.textContent?.trim() ?? 'Button',
|
|
1794
|
+
href: cta.getAttribute('href') ?? '',
|
|
1795
|
+
align: style(el, 'text-align') || 'left',
|
|
1796
|
+
tone,
|
|
1797
|
+
}));
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
if (el.classList.contains('mk-embed')) {
|
|
1801
|
+
const iframe = el.querySelector('iframe');
|
|
1802
|
+
out.push(block('embed', {
|
|
1803
|
+
url: '',
|
|
1804
|
+
embedUrl: iframe?.getAttribute('src') ?? '',
|
|
1805
|
+
aspectRatio: 16 / 9,
|
|
1806
|
+
}));
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
// Unknown container — recurse into its children.
|
|
1810
|
+
collectBlocks(el, out);
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* Parse an HTML string into a {@link MkBlockDocument}. Empty / whitespace input
|
|
1814
|
+
* yields an empty document.
|
|
1815
|
+
*/
|
|
1816
|
+
function mkHtmlToBlocks(html) {
|
|
1817
|
+
const source = String(html ?? '');
|
|
1818
|
+
if (!source.trim())
|
|
1819
|
+
return mkEmptyDocument();
|
|
1820
|
+
const root = parseRoot(source);
|
|
1821
|
+
if (!root) {
|
|
1822
|
+
// No DOM (server) — degrade to a single sanitised paragraph.
|
|
1823
|
+
return {
|
|
1824
|
+
version: MK_BLOCK_DOCUMENT_VERSION,
|
|
1825
|
+
blocks: [block('paragraph', { html: sanitizeInlineHtml(source) })],
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
const blocks = [];
|
|
1829
|
+
collectBlocks(root, blocks);
|
|
1830
|
+
return { version: MK_BLOCK_DOCUMENT_VERSION, blocks };
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
/**
|
|
1834
|
+
* Block editor — a configurable, Gutenberg-style block content editor. Renders
|
|
1835
|
+
* an ordered document of blocks (paragraph, heading, image, columns, embed,
|
|
1836
|
+
* list, quote, button, divider, code) with a filterable inserter, per-block
|
|
1837
|
+
* chrome (move/duplicate/transform/delete) and inline rich-text formatting.
|
|
1838
|
+
*
|
|
1839
|
+
* The palette is fully customisable: pass definitions via the `blocks` input or
|
|
1840
|
+
* the {@link MK_BLOCK_DEFINITIONS} token (mirrors `registerBlockType`). Both are
|
|
1841
|
+
* merged over {@link MK_DEFAULT_BLOCKS}.
|
|
1842
|
+
*
|
|
1843
|
+
* Implements `ControlValueAccessor` AND a two-way `value` model, so it works
|
|
1844
|
+
* with `[(ngModel)]`, reactive forms and `[(value)]`.
|
|
1845
|
+
*
|
|
1846
|
+
* ```html
|
|
1847
|
+
* <mk-block-editor [(value)]="doc" [uploadHandler]="upload" />
|
|
1848
|
+
* ```
|
|
1849
|
+
*/
|
|
1850
|
+
class MkBlockEditor {
|
|
1851
|
+
i18n = inject(MK_I18N);
|
|
1852
|
+
ctx = inject(MkBlockEditorContext);
|
|
1853
|
+
hostRef = inject(ElementRef);
|
|
1854
|
+
tokenDefinitions = inject(MK_BLOCK_DEFINITIONS, { optional: true });
|
|
1855
|
+
tokenUpload = inject(MK_BLOCK_UPLOAD_HANDLER, { optional: true });
|
|
1856
|
+
tokenEmbeds = inject(MK_BLOCK_EMBED_PROVIDERS, { optional: true });
|
|
1857
|
+
/** Two-way document value (always an `MkBlockDocument`, regardless of format). */
|
|
1858
|
+
value = model(mkEmptyDocument(), /* @ts-ignore */
|
|
1859
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
1860
|
+
/**
|
|
1861
|
+
* Form/CVA exchange format. `document` (default) reads/writes an
|
|
1862
|
+
* `MkBlockDocument`; `html` reads/writes an **HTML string** — so the editor
|
|
1863
|
+
* can back a string-typed richtext field (`[(ngModel)]="htmlString"`). The
|
|
1864
|
+
* two-way `value` model stays an `MkBlockDocument` either way.
|
|
1865
|
+
*/
|
|
1866
|
+
valueFormat = input('document', /* @ts-ignore */
|
|
1867
|
+
...(ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []));
|
|
1868
|
+
/** Custom / extended block definitions (merged over the defaults + token). */
|
|
1869
|
+
blocks = input(null, /* @ts-ignore */
|
|
1870
|
+
...(ngDevMode ? [{ debugName: "blocks" }] : /* istanbul ignore next */ []));
|
|
1871
|
+
/** Placeholder for empty text blocks. */
|
|
1872
|
+
placeholder = input(this.i18n.blockEditor.emptyBlockPlaceholder, /* @ts-ignore */
|
|
1873
|
+
...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
1874
|
+
/** Accessible label for the editor region. */
|
|
1875
|
+
ariaLabel = input(this.i18n.blockEditor.editorLabel, /* @ts-ignore */
|
|
1876
|
+
...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
1877
|
+
/** Read-only: renders content without editing chrome. */
|
|
1878
|
+
readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1879
|
+
/** Disabled (form-level). */
|
|
1880
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1881
|
+
/** Force the invalid visual + `aria-invalid` when used standalone. */
|
|
1882
|
+
invalid = input(false, { ...(ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
1883
|
+
/** Async upload handler (overrides the {@link MK_BLOCK_UPLOAD_HANDLER} token). */
|
|
1884
|
+
uploadHandler = input(null, /* @ts-ignore */
|
|
1885
|
+
...(ngDevMode ? [{ debugName: "uploadHandler" }] : /* istanbul ignore next */ []));
|
|
1886
|
+
/** Extra embed providers (merged with defaults + token). */
|
|
1887
|
+
embedProviders = input(null, /* @ts-ignore */
|
|
1888
|
+
...(ngDevMode ? [{ debugName: "embedProviders" }] : /* istanbul ignore next */ []));
|
|
1889
|
+
/** Fires on any document change (in addition to the `value` model). */
|
|
1890
|
+
change = output();
|
|
1891
|
+
/** Fires the serialised HTML on any change — convenient in `html` mode. */
|
|
1892
|
+
htmlChange = output();
|
|
1893
|
+
cvaDisabled = signal(false, /* @ts-ignore */
|
|
1894
|
+
...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
|
|
1895
|
+
/** Invalid visual, forced by the `invalid` input. */
|
|
1896
|
+
isInvalid = computed(() => this.invalid(), /* @ts-ignore */
|
|
1897
|
+
...(ngDevMode ? [{ debugName: "isInvalid" }] : /* istanbul ignore next */ []));
|
|
1898
|
+
onChange = () => { };
|
|
1899
|
+
onTouched = () => { };
|
|
1900
|
+
/** Effective, merged block definitions (defaults localised via MK_I18N). */
|
|
1901
|
+
definitions = computed(() => mkMergeBlockDefinitions(localizeDefaultBlocks(this.i18n.blockEditor), ...(this.tokenDefinitions ?? []), this.blocks()), /* @ts-ignore */
|
|
1902
|
+
...(ngDevMode ? [{ debugName: "definitions" }] : /* istanbul ignore next */ []));
|
|
1903
|
+
topBlocks = computed(() => this.value()?.blocks ?? [], /* @ts-ignore */
|
|
1904
|
+
...(ngDevMode ? [{ debugName: "topBlocks" }] : /* istanbul ignore next */ []));
|
|
1905
|
+
constructor() {
|
|
1906
|
+
this.ctx.hostRef = this.hostRef;
|
|
1907
|
+
// Connect the shared context straight to the input-derived signals (pull
|
|
1908
|
+
// based; no per-input mirror effects). Must happen before any child
|
|
1909
|
+
// renders, which the constructor guarantees: the context is provided by
|
|
1910
|
+
// this component, so children inject it only after we are constructed.
|
|
1911
|
+
this.ctx.definitions = this.definitions;
|
|
1912
|
+
this.ctx.readonly = this.readonly;
|
|
1913
|
+
this.ctx.disabled = computed(() => this.disabled() || this.cvaDisabled(), /* @ts-ignore */
|
|
1914
|
+
...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
1915
|
+
this.ctx.placeholder = this.placeholder;
|
|
1916
|
+
this.ctx.uploadHandler = computed(() => this.uploadHandler() ?? this.tokenUpload ?? null, /* @ts-ignore */
|
|
1917
|
+
...(ngDevMode ? [{ debugName: "uploadHandler" }] : /* istanbul ignore next */ []));
|
|
1918
|
+
this.ctx.embedProviders = computed(() => [
|
|
1919
|
+
...MK_DEFAULT_EMBED_PROVIDERS,
|
|
1920
|
+
...(this.tokenEmbeds?.flat() ?? []),
|
|
1921
|
+
...(this.embedProviders() ?? []),
|
|
1922
|
+
], /* @ts-ignore */
|
|
1923
|
+
...(ngDevMode ? [{ debugName: "embedProviders" }] : /* istanbul ignore next */ []));
|
|
1924
|
+
}
|
|
1925
|
+
onBlocksChange(blocks) {
|
|
1926
|
+
const next = {
|
|
1927
|
+
version: this.value()?.version ?? MK_BLOCK_DOCUMENT_VERSION,
|
|
1928
|
+
blocks,
|
|
1929
|
+
};
|
|
1930
|
+
this.value.set(next);
|
|
1931
|
+
// Serialising the whole document on every keystroke is expensive — do it
|
|
1932
|
+
// only when something actually consumes the HTML: `html` value format, or
|
|
1933
|
+
// a subscriber on `htmlChange`.
|
|
1934
|
+
const wantsHtml = this.valueFormat() === 'html' || hasObservers(this.htmlChange);
|
|
1935
|
+
const html = wantsHtml ? mkBlocksToHtml(next) : null;
|
|
1936
|
+
this.onChange(this.valueFormat() === 'html' ? html : next);
|
|
1937
|
+
this.onTouched();
|
|
1938
|
+
this.change.emit(next);
|
|
1939
|
+
if (html !== null)
|
|
1940
|
+
this.htmlChange.emit(html);
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Marks the control touched once focus leaves it entirely, so a form that
|
|
1944
|
+
* gates its errors on `touched` behaves the same here as on a native input.
|
|
1945
|
+
*/
|
|
1946
|
+
onFocusOut(event) {
|
|
1947
|
+
const next = event.relatedTarget;
|
|
1948
|
+
if (next && this.hostRef.nativeElement.contains(next))
|
|
1949
|
+
return;
|
|
1950
|
+
this.onTouched();
|
|
1951
|
+
}
|
|
1952
|
+
// --- ControlValueAccessor -------------------------------------------------
|
|
1953
|
+
writeValue(value) {
|
|
1954
|
+
if (this.valueFormat() === 'html') {
|
|
1955
|
+
this.value.set(typeof value === 'string'
|
|
1956
|
+
? mkHtmlToBlocks(value)
|
|
1957
|
+
: (value ?? mkEmptyDocument()));
|
|
1958
|
+
}
|
|
1959
|
+
else {
|
|
1960
|
+
this.value.set(value ?? mkEmptyDocument());
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
registerOnChange(fn) {
|
|
1964
|
+
this.onChange = fn;
|
|
1965
|
+
}
|
|
1966
|
+
registerOnTouched(fn) {
|
|
1967
|
+
this.onTouched = fn;
|
|
1968
|
+
}
|
|
1969
|
+
setDisabledState(isDisabled) {
|
|
1970
|
+
this.cvaDisabled.set(isDisabled);
|
|
1971
|
+
}
|
|
1972
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1973
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.7", type: MkBlockEditor, isStandalone: true, selector: "mk-block-editor", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, blocks: { classPropertyName: "blocks", publicName: "blocks", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", 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 }, uploadHandler: { classPropertyName: "uploadHandler", publicName: "uploadHandler", isSignal: true, isRequired: false, transformFunction: null }, embedProviders: { classPropertyName: "embedProviders", publicName: "embedProviders", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", change: "change", htmlChange: "htmlChange" }, host: { attributes: { "role": "group" }, listeners: { "focusout": "onFocusOut($event)" }, properties: { "attr.aria-label": "ariaLabel()", "class.mk-block-editor--readonly": "readonly()", "class.mk-block-editor--invalid": "isInvalid()", "attr.aria-invalid": "isInvalid() || null", "class.mk-block-editor--disabled": "disabled() || cvaDisabled()" }, classAttribute: "mk-block-editor" }, providers: [
|
|
1974
|
+
MkBlockEditorContext,
|
|
1975
|
+
{
|
|
1976
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1977
|
+
useExisting: forwardRef(() => MkBlockEditor),
|
|
1978
|
+
multi: true,
|
|
1979
|
+
},
|
|
1980
|
+
], ngImport: i0, template: "<mk-block-list [blocks]=\"topBlocks()\" [depth]=\"0\" (blocksChange)=\"onBlocksChange($event)\" />\n", styles: [":host{display:block;padding:var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);background:var(--mk-surface);color:var(--mk-text);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-relaxed)}:host(.mk-block-editor--disabled){opacity:.6;pointer-events:none}:host(.mk-block-editor--readonly){border-style:dashed}:host(.mk-block-editor--invalid){border-color:var(--mk-danger)}\n"], dependencies: [{ kind: "component", type: MkBlockList, selector: "mk-block-list", inputs: ["blocks", "depth", "inColumn"], outputs: ["blocksChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1981
|
+
}
|
|
1982
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockEditor, decorators: [{
|
|
1983
|
+
type: Component,
|
|
1984
|
+
args: [{ selector: 'mk-block-editor', imports: [MkBlockList], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
1985
|
+
class: 'mk-block-editor',
|
|
1986
|
+
role: 'group',
|
|
1987
|
+
'[attr.aria-label]': 'ariaLabel()',
|
|
1988
|
+
'[class.mk-block-editor--readonly]': 'readonly()',
|
|
1989
|
+
'[class.mk-block-editor--invalid]': 'isInvalid()',
|
|
1990
|
+
'[attr.aria-invalid]': 'isInvalid() || null',
|
|
1991
|
+
'[class.mk-block-editor--disabled]': 'disabled() || cvaDisabled()',
|
|
1992
|
+
'(focusout)': 'onFocusOut($event)',
|
|
1993
|
+
}, providers: [
|
|
1994
|
+
MkBlockEditorContext,
|
|
1995
|
+
{
|
|
1996
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1997
|
+
useExisting: forwardRef(() => MkBlockEditor),
|
|
1998
|
+
multi: true,
|
|
1999
|
+
},
|
|
2000
|
+
], template: "<mk-block-list [blocks]=\"topBlocks()\" [depth]=\"0\" (blocksChange)=\"onBlocksChange($event)\" />\n", styles: [":host{display:block;padding:var(--mk-space-3);border:var(--mk-border-width) solid var(--mk-border);border-radius:var(--mk-radius-lg);background:var(--mk-surface);color:var(--mk-text);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-relaxed)}:host(.mk-block-editor--disabled){opacity:.6;pointer-events:none}:host(.mk-block-editor--readonly){border-style:dashed}:host(.mk-block-editor--invalid){border-color:var(--mk-danger)}\n"] }]
|
|
2001
|
+
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], blocks: [{ type: i0.Input, args: [{ isSignal: true, alias: "blocks", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], uploadHandler: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadHandler", required: false }] }], embedProviders: [{ type: i0.Input, args: [{ isSignal: true, alias: "embedProviders", required: false }] }], change: [{ type: i0.Output, args: ["change"] }], htmlChange: [{ type: i0.Output, args: ["htmlChange"] }] } });
|
|
2002
|
+
/**
|
|
2003
|
+
* Whether an `output()` emitter currently has any subscribers (template
|
|
2004
|
+
* `(htmlChange)` bindings and programmatic `.subscribe()` both register one).
|
|
2005
|
+
* Reads `OutputEmitterRef`'s internal `listeners` array — not public API, so
|
|
2006
|
+
* if the field ever disappears in a future Angular this reports `true` and we
|
|
2007
|
+
* fall back to always serialising (the documented pre-optimisation behaviour).
|
|
2008
|
+
*/
|
|
2009
|
+
function hasObservers(ref) {
|
|
2010
|
+
const listeners = ref.listeners;
|
|
2011
|
+
if (listeners === undefined)
|
|
2012
|
+
return true;
|
|
2013
|
+
return (listeners?.length ?? 0) > 0;
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* Returns {@link MK_DEFAULT_BLOCKS} with the built-in labels, descriptions and
|
|
2017
|
+
* group headings resolved from the active i18n strings (custom definitions
|
|
2018
|
+
* merged on top keep their own text). The Button block's `create()` is wrapped
|
|
2019
|
+
* so a fresh block starts with the localised default label.
|
|
2020
|
+
*/
|
|
2021
|
+
function localizeDefaultBlocks(s) {
|
|
2022
|
+
const text = {
|
|
2023
|
+
paragraph: { label: s.blockParagraph, description: s.blockParagraphDesc },
|
|
2024
|
+
heading: { label: s.blockHeading, description: s.blockHeadingDesc },
|
|
2025
|
+
list: { label: s.blockList, description: s.blockListDesc },
|
|
2026
|
+
quote: { label: s.blockQuote, description: s.blockQuoteDesc },
|
|
2027
|
+
code: { label: s.blockCode, description: s.blockCodeDesc },
|
|
2028
|
+
image: { label: s.blockImage, description: s.blockImageDesc },
|
|
2029
|
+
embed: { label: s.blockEmbed, description: s.blockEmbedDesc },
|
|
2030
|
+
button: { label: s.blockButton, description: s.blockButtonDesc },
|
|
2031
|
+
divider: { label: s.blockDivider, description: s.blockDividerDesc },
|
|
2032
|
+
columns: { label: s.blockColumns, description: s.blockColumnsDesc },
|
|
2033
|
+
};
|
|
2034
|
+
const groups = {
|
|
2035
|
+
Text: s.groupText,
|
|
2036
|
+
Media: s.groupMedia,
|
|
2037
|
+
Layout: s.groupLayout,
|
|
2038
|
+
};
|
|
2039
|
+
return MK_DEFAULT_BLOCKS.map((def) => {
|
|
2040
|
+
const t = text[def.type];
|
|
2041
|
+
const localized = {
|
|
2042
|
+
...def,
|
|
2043
|
+
label: t?.label ?? def.label,
|
|
2044
|
+
description: t?.description ?? def.description,
|
|
2045
|
+
group: groups[def.group ?? ''] ?? def.group,
|
|
2046
|
+
};
|
|
2047
|
+
if (def.type === 'button') {
|
|
2048
|
+
localized.create = () => {
|
|
2049
|
+
const b = def.create();
|
|
2050
|
+
return { ...b, data: { ...b.data, label: s.buttonDefaultLabel } };
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
return localized;
|
|
2054
|
+
});
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
/**
|
|
2058
|
+
* Block renderer — read-only, themed display of a {@link MkBlockDocument} for
|
|
2059
|
+
* published content pages and blog posts. Reuses the same layout CSS as the
|
|
2060
|
+
* editor so authored columns look identical when published.
|
|
2061
|
+
*
|
|
2062
|
+
* SECURITY MODEL: rich-text HTML is passed through {@link sanitizeInlineHtml}
|
|
2063
|
+
* and then bound with `[innerHTML]`, so Angular's `DomSanitizer` sanitises it
|
|
2064
|
+
* again at render time (never bypassed for author HTML). The ONLY bypass is for
|
|
2065
|
+
* a stored embed's `embedUrl`, which came from an allow-listed provider.
|
|
2066
|
+
*/
|
|
2067
|
+
class MkBlockRenderer {
|
|
2068
|
+
i18n = inject(MK_I18N);
|
|
2069
|
+
sanitizer = inject(DomSanitizer);
|
|
2070
|
+
safeSrcCache = new Map();
|
|
2071
|
+
/** The document to render. */
|
|
2072
|
+
value = input(null, /* @ts-ignore */
|
|
2073
|
+
...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
2074
|
+
/** Internal: a block subtree (used when recursing into columns). */
|
|
2075
|
+
blocks = input(null, /* @ts-ignore */
|
|
2076
|
+
...(ngDevMode ? [{ debugName: "blocks" }] : /* istanbul ignore next */ []));
|
|
2077
|
+
items = computed(() => this.blocks() ?? this.value()?.blocks ?? [], /* @ts-ignore */
|
|
2078
|
+
...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
2079
|
+
/** Sanitised rich-text HTML for `[innerHTML]` binding. */
|
|
2080
|
+
safeHtml(html) {
|
|
2081
|
+
return sanitizeInlineHtml(String(html ?? ''));
|
|
2082
|
+
}
|
|
2083
|
+
listItems(block) {
|
|
2084
|
+
const items = block.data['items'];
|
|
2085
|
+
return Array.isArray(items) ? items : [];
|
|
2086
|
+
}
|
|
2087
|
+
headingLevel(block) {
|
|
2088
|
+
const l = block.data['level'];
|
|
2089
|
+
return [1, 2, 3, 4].includes(l) ? l : 2;
|
|
2090
|
+
}
|
|
2091
|
+
isImageSafe(block) {
|
|
2092
|
+
const src = String(block.data['src'] ?? '');
|
|
2093
|
+
return !!src && mkIsSafeUrl(src);
|
|
2094
|
+
}
|
|
2095
|
+
buttonHref(block) {
|
|
2096
|
+
const href = String(block.data['href'] ?? '');
|
|
2097
|
+
return mkIsSafeUrl(href) && href ? href : '#';
|
|
2098
|
+
}
|
|
2099
|
+
paddingTop(block) {
|
|
2100
|
+
const ratio = Number(block.data['aspectRatio']) || 16 / 9;
|
|
2101
|
+
return `${100 / ratio}%`;
|
|
2102
|
+
}
|
|
2103
|
+
/** Trusts only a provider-transformed https embed URL — see class doc. */
|
|
2104
|
+
embedSrc(block) {
|
|
2105
|
+
const src = String(block.data['embedUrl'] ?? '');
|
|
2106
|
+
if (!src || !/^https:\/\//i.test(src) || !mkIsSafeUrl(src))
|
|
2107
|
+
return null;
|
|
2108
|
+
let trusted = this.safeSrcCache.get(src);
|
|
2109
|
+
if (!trusted) {
|
|
2110
|
+
trusted = this.sanitizer.bypassSecurityTrustResourceUrl(src);
|
|
2111
|
+
this.safeSrcCache.set(src, trusted);
|
|
2112
|
+
}
|
|
2113
|
+
return trusted;
|
|
2114
|
+
}
|
|
2115
|
+
fallbackUrl(block) {
|
|
2116
|
+
const url = String(block.data['url'] ?? '');
|
|
2117
|
+
return mkIsSafeUrl(url) && url ? url : null;
|
|
2118
|
+
}
|
|
2119
|
+
gridColumns(block) {
|
|
2120
|
+
const ratio = String(block.data['ratio'] ?? '');
|
|
2121
|
+
const parts = ratio.split('-').map((n) => parseFloat(n)).filter((n) => n > 0);
|
|
2122
|
+
if (parts.length >= 2)
|
|
2123
|
+
return parts.map((n) => `${n}fr`).join(' ');
|
|
2124
|
+
return `repeat(${block.children?.length ?? 2}, 1fr)`;
|
|
2125
|
+
}
|
|
2126
|
+
gridGap(block) {
|
|
2127
|
+
const g = Math.max(0, Math.min(8, Number(block.data['gap']) || 4));
|
|
2128
|
+
return `var(--mk-space-${g})`;
|
|
2129
|
+
}
|
|
2130
|
+
justifyValue(block) {
|
|
2131
|
+
const map = {
|
|
2132
|
+
start: 'flex-start', center: 'center', end: 'flex-end', between: 'space-between',
|
|
2133
|
+
};
|
|
2134
|
+
return map[String(block.data['justify'] ?? 'start')] ?? 'flex-start';
|
|
2135
|
+
}
|
|
2136
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockRenderer, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2137
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: MkBlockRenderer, isStandalone: true, selector: "mk-block-renderer", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, blocks: { classPropertyName: "blocks", publicName: "blocks", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "mk-block-renderer" }, ngImport: i0, template: "@for (block of items(); track block.id) {\n @switch (block.type) {\n @case ('paragraph') {\n <p class=\"mk-r-paragraph\" [innerHTML]=\"safeHtml(block.data['html'])\"></p>\n }\n @case ('heading') {\n @switch (headingLevel(block)) {\n @case (1) { <h1 class=\"mk-r-heading\" [innerHTML]=\"safeHtml(block.data['html'])\"></h1> }\n @case (2) { <h2 class=\"mk-r-heading\" [innerHTML]=\"safeHtml(block.data['html'])\"></h2> }\n @case (3) { <h3 class=\"mk-r-heading\" [innerHTML]=\"safeHtml(block.data['html'])\"></h3> }\n @case (4) { <h4 class=\"mk-r-heading\" [innerHTML]=\"safeHtml(block.data['html'])\"></h4> }\n }\n }\n @case ('list') {\n @if (block.data['ordered']) {\n <ol class=\"mk-r-list\">\n @for (item of listItems(block); track $index) {\n <li [innerHTML]=\"safeHtml(item)\"></li>\n }\n </ol>\n } @else {\n <ul class=\"mk-r-list\">\n @for (item of listItems(block); track $index) {\n <li [innerHTML]=\"safeHtml(item)\"></li>\n }\n </ul>\n }\n }\n @case ('quote') {\n <blockquote class=\"mk-r-quote\">\n <p [innerHTML]=\"safeHtml(block.data['html'])\"></p>\n @if (block.data['citation']) {\n <cite class=\"mk-r-cite\">{{ block.data['citation'] }}</cite>\n }\n </blockquote>\n }\n @case ('code') {\n <pre class=\"mk-r-code\"><code>{{ block.data['code'] }}</code></pre>\n }\n @case ('image') {\n @if (isImageSafe(block)) {\n <figure\n class=\"mk-r-figure\"\n [class.mk-r-figure--left]=\"block.data['align'] === 'left'\"\n [class.mk-r-figure--right]=\"block.data['align'] === 'right'\"\n >\n <img\n [src]=\"block.data['src']\"\n [alt]=\"block.data['alt'] ?? ''\"\n [style.width.%]=\"block.data['width'] || 100\"\n loading=\"lazy\"\n />\n @if (block.data['caption']) {\n <figcaption>{{ block.data['caption'] }}</figcaption>\n }\n </figure>\n }\n }\n @case ('button') {\n <div\n class=\"mk-r-cta-wrap\"\n [class.mk-r-cta-wrap--center]=\"block.data['align'] === 'center'\"\n [class.mk-r-cta-wrap--right]=\"block.data['align'] === 'right'\"\n >\n <a\n class=\"mk-cta\"\n [class.mk-cta--soft]=\"block.data['variant'] === 'soft'\"\n [class.mk-cta--outline]=\"block.data['variant'] === 'outline'\"\n [attr.data-tone]=\"block.data['tone'] ?? 'primary'\"\n [href]=\"buttonHref(block)\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {{ block.data['label'] || i18n.blockEditor.blockButton }}\n </a>\n </div>\n }\n @case ('divider') {\n <hr class=\"mk-r-divider\" />\n }\n @case ('embed') {\n @if (embedSrc(block); as src) {\n <div class=\"mk-r-embed\" [style.padding-top]=\"paddingTop(block)\">\n <iframe\n [src]=\"src\"\n [title]=\"i18n.blockEditor.embeddedContent\"\n loading=\"lazy\"\n referrerpolicy=\"strict-origin-when-cross-origin\"\n sandbox=\"allow-scripts allow-same-origin allow-presentation allow-popups\"\n allowfullscreen\n ></iframe>\n </div>\n } @else if (fallbackUrl(block); as url) {\n <p class=\"mk-r-embed-fallback\">\n <a [href]=\"url\" target=\"_blank\" rel=\"noopener noreferrer\">{{ url }}</a>\n </p>\n }\n }\n @case ('columns') {\n <div\n class=\"mk-columns\"\n [style.grid-template-columns]=\"gridColumns(block)\"\n [style.gap]=\"gridGap(block)\"\n [style.align-items]=\"block.data['align'] ?? 'stretch'\"\n [style.justify-content]=\"justifyValue(block)\"\n >\n @for (col of block.children ?? []; track col.id) {\n <div class=\"mk-column\">\n <mk-block-renderer [blocks]=\"col.children ?? []\" />\n </div>\n }\n </div>\n }\n }\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;color:var(--mk-text);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-relaxed)}.mk-r-paragraph{margin:0 0 var(--mk-space-4)}.mk-r-heading{margin:var(--mk-space-6) 0 var(--mk-space-3);line-height:var(--mk-line-height-snug);color:var(--mk-text)}h1.mk-r-heading{font-size:var(--mk-font-size-3xl);font-weight:var(--mk-font-weight-bold)}h2.mk-r-heading{font-size:var(--mk-font-size-2xl);font-weight:var(--mk-font-weight-bold)}h3.mk-r-heading{font-size:var(--mk-font-size-xl);font-weight:var(--mk-font-weight-semibold)}h4.mk-r-heading{font-size:var(--mk-font-size-lg);font-weight:var(--mk-font-weight-semibold)}.mk-r-paragraph a,.mk-r-list a,.mk-r-quote a{color:var(--mk-primary);text-decoration:underline}.mk-r-paragraph code,.mk-r-list 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-r-list{margin:0 0 var(--mk-space-4);padding-inline-start:var(--mk-space-6)}.mk-r-list li{margin:var(--mk-space-1) 0}.mk-r-quote{margin:0 0 var(--mk-space-4);padding-inline-start:var(--mk-space-4);border-inline-start:4px solid var(--mk-primary);font-size:var(--mk-font-size-lg);font-style:italic}.mk-r-quote p{margin:0}.mk-r-cite{display:block;margin-top:var(--mk-space-2);color:var(--mk-text-muted);font-size:var(--mk-font-size-sm);font-style:normal}.mk-r-code{margin:0 0 var(--mk-space-4);padding:var(--mk-space-3);border-radius:var(--mk-radius-md);background:var(--mk-code-bg);color:var(--mk-text);font-family:var(--mk-font-mono);font-size:var(--mk-font-size-sm);overflow-x:auto}.mk-r-figure{margin:0 0 var(--mk-space-4);text-align:center}.mk-r-figure--left{text-align:left}.mk-r-figure--right{text-align:right}.mk-r-figure img{max-width:100%;height:auto;border-radius:var(--mk-radius-md)}.mk-r-figure figcaption{margin-top:var(--mk-space-2);color:var(--mk-text-muted);font-size:var(--mk-font-size-sm)}.mk-r-divider{height:0;margin:var(--mk-space-6) 0;border:none;border-top:var(--mk-border-width-strong) solid var(--mk-border-strong)}.mk-r-cta-wrap{margin:0 0 var(--mk-space-4);text-align:start}.mk-r-cta-wrap--center{text-align:center}.mk-r-cta-wrap--right{text-align:right}.mk-cta{--_main: var(--mk-primary);--_hover: var(--mk-primary-hover);--_contrast: var(--mk-primary-contrast);--_subtle: var(--mk-primary-subtle);--_subtle-text: var(--mk-primary-subtle-text);display:inline-flex;align-items:center;justify-content:center;height:var(--mk-control-height-md);padding:0 var(--mk-space-5);border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);background:var(--_main);color:var(--_contrast);font-weight:var(--mk-font-weight-medium);text-decoration:none;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-cta:hover{background:var(--_hover)}.mk-cta[data-tone=success]{--_main: var(--mk-success);--_hover: var(--mk-success-hover);--_contrast: var(--mk-success-contrast);--_subtle: var(--mk-success-subtle);--_subtle-text: var(--mk-success-subtle-text)}.mk-cta[data-tone=warning]{--_main: var(--mk-warning);--_hover: var(--mk-warning-hover);--_contrast: var(--mk-warning-contrast);--_subtle: var(--mk-warning-subtle);--_subtle-text: var(--mk-warning-subtle-text)}.mk-cta[data-tone=danger]{--_main: var(--mk-danger);--_hover: var(--mk-danger-hover);--_contrast: var(--mk-danger-contrast);--_subtle: var(--mk-danger-subtle);--_subtle-text: var(--mk-danger-subtle-text)}.mk-cta[data-tone=info]{--_main: var(--mk-info);--_hover: var(--mk-info-hover);--_contrast: var(--mk-info-contrast);--_subtle: var(--mk-info-subtle);--_subtle-text: var(--mk-info-subtle-text)}.mk-cta[data-tone=neutral]{--_main: var(--mk-surface-inverse);--_hover: var(--mk-surface-inverse);--_contrast: var(--mk-text-inverse);--_subtle: var(--mk-neutral-subtle);--_subtle-text: var(--mk-neutral-subtle-text)}.mk-cta--soft{background:var(--_subtle);color:var(--_subtle-text)}.mk-cta--outline{background:transparent;color:var(--_subtle-text);border-color:color-mix(in srgb,var(--_main) 42%,var(--mk-border-strong))}.mk-cta:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-r-embed{position:relative;width:100%;margin:0 0 var(--mk-space-4);overflow:hidden;border-radius:var(--mk-radius-md);background:var(--mk-surface-3)}.mk-r-embed iframe{position:absolute;inset:0;width:100%;height:100%;border:0}.mk-r-embed-fallback a{color:var(--mk-primary)}.mk-columns{display:grid;margin:0 0 var(--mk-space-4)}.mk-column{min-width:0}@media(max-width:640px){.mk-columns{grid-template-columns:1fr!important}}\n"], dependencies: [{ kind: "component", type: MkBlockRenderer, selector: "mk-block-renderer", inputs: ["value", "blocks"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2138
|
+
}
|
|
2139
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: MkBlockRenderer, decorators: [{
|
|
2140
|
+
type: Component,
|
|
2141
|
+
args: [{ selector: 'mk-block-renderer', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'mk-block-renderer' }, template: "@for (block of items(); track block.id) {\n @switch (block.type) {\n @case ('paragraph') {\n <p class=\"mk-r-paragraph\" [innerHTML]=\"safeHtml(block.data['html'])\"></p>\n }\n @case ('heading') {\n @switch (headingLevel(block)) {\n @case (1) { <h1 class=\"mk-r-heading\" [innerHTML]=\"safeHtml(block.data['html'])\"></h1> }\n @case (2) { <h2 class=\"mk-r-heading\" [innerHTML]=\"safeHtml(block.data['html'])\"></h2> }\n @case (3) { <h3 class=\"mk-r-heading\" [innerHTML]=\"safeHtml(block.data['html'])\"></h3> }\n @case (4) { <h4 class=\"mk-r-heading\" [innerHTML]=\"safeHtml(block.data['html'])\"></h4> }\n }\n }\n @case ('list') {\n @if (block.data['ordered']) {\n <ol class=\"mk-r-list\">\n @for (item of listItems(block); track $index) {\n <li [innerHTML]=\"safeHtml(item)\"></li>\n }\n </ol>\n } @else {\n <ul class=\"mk-r-list\">\n @for (item of listItems(block); track $index) {\n <li [innerHTML]=\"safeHtml(item)\"></li>\n }\n </ul>\n }\n }\n @case ('quote') {\n <blockquote class=\"mk-r-quote\">\n <p [innerHTML]=\"safeHtml(block.data['html'])\"></p>\n @if (block.data['citation']) {\n <cite class=\"mk-r-cite\">{{ block.data['citation'] }}</cite>\n }\n </blockquote>\n }\n @case ('code') {\n <pre class=\"mk-r-code\"><code>{{ block.data['code'] }}</code></pre>\n }\n @case ('image') {\n @if (isImageSafe(block)) {\n <figure\n class=\"mk-r-figure\"\n [class.mk-r-figure--left]=\"block.data['align'] === 'left'\"\n [class.mk-r-figure--right]=\"block.data['align'] === 'right'\"\n >\n <img\n [src]=\"block.data['src']\"\n [alt]=\"block.data['alt'] ?? ''\"\n [style.width.%]=\"block.data['width'] || 100\"\n loading=\"lazy\"\n />\n @if (block.data['caption']) {\n <figcaption>{{ block.data['caption'] }}</figcaption>\n }\n </figure>\n }\n }\n @case ('button') {\n <div\n class=\"mk-r-cta-wrap\"\n [class.mk-r-cta-wrap--center]=\"block.data['align'] === 'center'\"\n [class.mk-r-cta-wrap--right]=\"block.data['align'] === 'right'\"\n >\n <a\n class=\"mk-cta\"\n [class.mk-cta--soft]=\"block.data['variant'] === 'soft'\"\n [class.mk-cta--outline]=\"block.data['variant'] === 'outline'\"\n [attr.data-tone]=\"block.data['tone'] ?? 'primary'\"\n [href]=\"buttonHref(block)\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n {{ block.data['label'] || i18n.blockEditor.blockButton }}\n </a>\n </div>\n }\n @case ('divider') {\n <hr class=\"mk-r-divider\" />\n }\n @case ('embed') {\n @if (embedSrc(block); as src) {\n <div class=\"mk-r-embed\" [style.padding-top]=\"paddingTop(block)\">\n <iframe\n [src]=\"src\"\n [title]=\"i18n.blockEditor.embeddedContent\"\n loading=\"lazy\"\n referrerpolicy=\"strict-origin-when-cross-origin\"\n sandbox=\"allow-scripts allow-same-origin allow-presentation allow-popups\"\n allowfullscreen\n ></iframe>\n </div>\n } @else if (fallbackUrl(block); as url) {\n <p class=\"mk-r-embed-fallback\">\n <a [href]=\"url\" target=\"_blank\" rel=\"noopener noreferrer\">{{ url }}</a>\n </p>\n }\n }\n @case ('columns') {\n <div\n class=\"mk-columns\"\n [style.grid-template-columns]=\"gridColumns(block)\"\n [style.gap]=\"gridGap(block)\"\n [style.align-items]=\"block.data['align'] ?? 'stretch'\"\n [style.justify-content]=\"justifyValue(block)\"\n >\n @for (col of block.children ?? []; track col.id) {\n <div class=\"mk-column\">\n <mk-block-renderer [blocks]=\"col.children ?? []\" />\n </div>\n }\n </div>\n }\n }\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;color:var(--mk-text);font-family:var(--mk-font-sans);font-size:var(--mk-font-size-md);line-height:var(--mk-line-height-relaxed)}.mk-r-paragraph{margin:0 0 var(--mk-space-4)}.mk-r-heading{margin:var(--mk-space-6) 0 var(--mk-space-3);line-height:var(--mk-line-height-snug);color:var(--mk-text)}h1.mk-r-heading{font-size:var(--mk-font-size-3xl);font-weight:var(--mk-font-weight-bold)}h2.mk-r-heading{font-size:var(--mk-font-size-2xl);font-weight:var(--mk-font-weight-bold)}h3.mk-r-heading{font-size:var(--mk-font-size-xl);font-weight:var(--mk-font-weight-semibold)}h4.mk-r-heading{font-size:var(--mk-font-size-lg);font-weight:var(--mk-font-weight-semibold)}.mk-r-paragraph a,.mk-r-list a,.mk-r-quote a{color:var(--mk-primary);text-decoration:underline}.mk-r-paragraph code,.mk-r-list 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-r-list{margin:0 0 var(--mk-space-4);padding-inline-start:var(--mk-space-6)}.mk-r-list li{margin:var(--mk-space-1) 0}.mk-r-quote{margin:0 0 var(--mk-space-4);padding-inline-start:var(--mk-space-4);border-inline-start:4px solid var(--mk-primary);font-size:var(--mk-font-size-lg);font-style:italic}.mk-r-quote p{margin:0}.mk-r-cite{display:block;margin-top:var(--mk-space-2);color:var(--mk-text-muted);font-size:var(--mk-font-size-sm);font-style:normal}.mk-r-code{margin:0 0 var(--mk-space-4);padding:var(--mk-space-3);border-radius:var(--mk-radius-md);background:var(--mk-code-bg);color:var(--mk-text);font-family:var(--mk-font-mono);font-size:var(--mk-font-size-sm);overflow-x:auto}.mk-r-figure{margin:0 0 var(--mk-space-4);text-align:center}.mk-r-figure--left{text-align:left}.mk-r-figure--right{text-align:right}.mk-r-figure img{max-width:100%;height:auto;border-radius:var(--mk-radius-md)}.mk-r-figure figcaption{margin-top:var(--mk-space-2);color:var(--mk-text-muted);font-size:var(--mk-font-size-sm)}.mk-r-divider{height:0;margin:var(--mk-space-6) 0;border:none;border-top:var(--mk-border-width-strong) solid var(--mk-border-strong)}.mk-r-cta-wrap{margin:0 0 var(--mk-space-4);text-align:start}.mk-r-cta-wrap--center{text-align:center}.mk-r-cta-wrap--right{text-align:right}.mk-cta{--_main: var(--mk-primary);--_hover: var(--mk-primary-hover);--_contrast: var(--mk-primary-contrast);--_subtle: var(--mk-primary-subtle);--_subtle-text: var(--mk-primary-subtle-text);display:inline-flex;align-items:center;justify-content:center;height:var(--mk-control-height-md);padding:0 var(--mk-space-5);border:var(--mk-border-width) solid transparent;border-radius:var(--mk-radius-md);background:var(--_main);color:var(--_contrast);font-weight:var(--mk-font-weight-medium);text-decoration:none;transition:background-color var(--mk-duration-fast) var(--mk-ease-standard)}.mk-cta:hover{background:var(--_hover)}.mk-cta[data-tone=success]{--_main: var(--mk-success);--_hover: var(--mk-success-hover);--_contrast: var(--mk-success-contrast);--_subtle: var(--mk-success-subtle);--_subtle-text: var(--mk-success-subtle-text)}.mk-cta[data-tone=warning]{--_main: var(--mk-warning);--_hover: var(--mk-warning-hover);--_contrast: var(--mk-warning-contrast);--_subtle: var(--mk-warning-subtle);--_subtle-text: var(--mk-warning-subtle-text)}.mk-cta[data-tone=danger]{--_main: var(--mk-danger);--_hover: var(--mk-danger-hover);--_contrast: var(--mk-danger-contrast);--_subtle: var(--mk-danger-subtle);--_subtle-text: var(--mk-danger-subtle-text)}.mk-cta[data-tone=info]{--_main: var(--mk-info);--_hover: var(--mk-info-hover);--_contrast: var(--mk-info-contrast);--_subtle: var(--mk-info-subtle);--_subtle-text: var(--mk-info-subtle-text)}.mk-cta[data-tone=neutral]{--_main: var(--mk-surface-inverse);--_hover: var(--mk-surface-inverse);--_contrast: var(--mk-text-inverse);--_subtle: var(--mk-neutral-subtle);--_subtle-text: var(--mk-neutral-subtle-text)}.mk-cta--soft{background:var(--_subtle);color:var(--_subtle-text)}.mk-cta--outline{background:transparent;color:var(--_subtle-text);border-color:color-mix(in srgb,var(--_main) 42%,var(--mk-border-strong))}.mk-cta:focus-visible{outline:var(--mk-focus-ring-width) solid var(--mk-focus-ring);outline-offset:var(--mk-focus-ring-offset)}.mk-r-embed{position:relative;width:100%;margin:0 0 var(--mk-space-4);overflow:hidden;border-radius:var(--mk-radius-md);background:var(--mk-surface-3)}.mk-r-embed iframe{position:absolute;inset:0;width:100%;height:100%;border:0}.mk-r-embed-fallback a{color:var(--mk-primary)}.mk-columns{display:grid;margin:0 0 var(--mk-space-4)}.mk-column{min-width:0}@media(max-width:640px){.mk-columns{grid-template-columns:1fr!important}}\n"] }]
|
|
2142
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], blocks: [{ type: i0.Input, args: [{ isSignal: true, alias: "blocks", required: false }] }] } });
|
|
2143
|
+
|
|
2144
|
+
/**
|
|
2145
|
+
* BLOCK EDITOR group barrel for @mk-kit/ui.
|
|
2146
|
+
*
|
|
2147
|
+
* Public surface for the Gutenberg-style block content editor and its
|
|
2148
|
+
* read-only renderer. Built-in block edit components stay internal — consumers
|
|
2149
|
+
* interact through the editor, the registry and the serializers.
|
|
2150
|
+
*/
|
|
2151
|
+
// Editor + renderer components
|
|
2152
|
+
|
|
2153
|
+
/**
|
|
2154
|
+
* Generated bundle index. Do not edit.
|
|
2155
|
+
*/
|
|
2156
|
+
|
|
2157
|
+
export { MK_BLOCK_DEFINITIONS, MK_BLOCK_DOCUMENT_VERSION, MK_BLOCK_EMBED_PROVIDERS, MK_BLOCK_UPLOAD_HANDLER, MK_DEFAULT_BLOCKS, MK_DEFAULT_EMBED_PROVIDERS, MkBlockEditor, MkBlockRenderer, mkBlockId, mkBlocksToHtml, mkBlocksToText, mkCloneBlock, mkEmptyDocument, mkEscapeAttr, mkEscapeHtml, mkHtmlToBlocks, mkIsSafeUrl, mkMergeBlockDefinitions };
|
|
2158
|
+
//# sourceMappingURL=mk-kit-ui-block-editor.mjs.map
|