@azlib/editor 0.2.0 → 0.3.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/dist/index.cjs +861 -226
- package/dist/index.d.cts +43 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +43 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +860 -229
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import DOMPurify from "isomorphic-dompurify";
|
|
2
|
-
import { Schema } from "prosemirror-model";
|
|
3
|
-
import { useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { DOMParser, DOMSerializer, Schema } from "prosemirror-model";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
5
|
//#region src/core/errors.ts
|
|
6
6
|
const createDiagnostic = (code, severity, message, location) => ({
|
|
@@ -108,8 +108,7 @@ var EditorHistory = class {
|
|
|
108
108
|
}
|
|
109
109
|
};
|
|
110
110
|
//#endregion
|
|
111
|
-
//#region src/
|
|
112
|
-
const stripHtml = (value) => value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
111
|
+
//#region src/core/schema.ts
|
|
113
112
|
const allowedStyleProperties = new Set([
|
|
114
113
|
"font-family",
|
|
115
114
|
"font-size",
|
|
@@ -140,6 +139,7 @@ const normalizeStyleValue = (property, value) => {
|
|
|
140
139
|
return null;
|
|
141
140
|
};
|
|
142
141
|
const normalizeStyleAttribute = (styleValue) => {
|
|
142
|
+
if (!styleValue) return null;
|
|
143
143
|
const entries = styleValue.split(";").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => {
|
|
144
144
|
const separator = entry.indexOf(":");
|
|
145
145
|
if (separator < 0) return null;
|
|
@@ -153,60 +153,350 @@ const normalizeStyleAttribute = (styleValue) => {
|
|
|
153
153
|
if (entries.length === 0) return null;
|
|
154
154
|
return entries.join(";");
|
|
155
155
|
};
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
156
|
+
const editorNodeNames = {
|
|
157
|
+
doc: "doc",
|
|
158
|
+
paragraph: "paragraph",
|
|
159
|
+
heading: "heading",
|
|
160
|
+
text: "text",
|
|
161
|
+
bulletList: "bullet_list",
|
|
162
|
+
orderedList: "ordered_list",
|
|
163
|
+
listItem: "list_item",
|
|
164
|
+
hardBreak: "hard_break",
|
|
165
|
+
blockquote: "blockquote",
|
|
166
|
+
codeBlock: "code_block",
|
|
167
|
+
image: "image",
|
|
168
|
+
video: "video"
|
|
169
|
+
};
|
|
170
|
+
const editorMarkNames = {
|
|
171
|
+
strong: "strong",
|
|
172
|
+
em: "em",
|
|
173
|
+
underline: "underline",
|
|
174
|
+
link: "link",
|
|
175
|
+
strike: "strike",
|
|
176
|
+
code: "code",
|
|
177
|
+
style: "style",
|
|
178
|
+
formula: "formula"
|
|
179
|
+
};
|
|
180
|
+
const createEditorSchema = () => new Schema({
|
|
181
|
+
nodes: {
|
|
182
|
+
doc: { content: "block+" },
|
|
183
|
+
paragraph: {
|
|
184
|
+
content: "inline*",
|
|
185
|
+
group: "block",
|
|
186
|
+
attrs: {
|
|
187
|
+
style: { default: null },
|
|
188
|
+
dir: { default: null }
|
|
189
|
+
},
|
|
190
|
+
parseDOM: [{
|
|
191
|
+
tag: "p",
|
|
192
|
+
getAttrs: (dom) => {
|
|
193
|
+
if (!(dom instanceof HTMLElement)) return null;
|
|
194
|
+
return {
|
|
195
|
+
style: normalizeStyleAttribute(dom.getAttribute("style") || ""),
|
|
196
|
+
dir: dom.getAttribute("dir") || null
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}],
|
|
200
|
+
toDOM: (node) => {
|
|
201
|
+
const attrs = {};
|
|
202
|
+
if (node.attrs.style) attrs.style = node.attrs.style;
|
|
203
|
+
if (node.attrs.dir) attrs.dir = node.attrs.dir;
|
|
204
|
+
return [
|
|
205
|
+
"p",
|
|
206
|
+
attrs,
|
|
207
|
+
0
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
heading: {
|
|
212
|
+
attrs: {
|
|
213
|
+
level: { default: 1 },
|
|
214
|
+
style: { default: null },
|
|
215
|
+
dir: { default: null }
|
|
216
|
+
},
|
|
217
|
+
content: "inline*",
|
|
218
|
+
group: "block",
|
|
219
|
+
defining: true,
|
|
220
|
+
parseDOM: [
|
|
221
|
+
{
|
|
222
|
+
tag: "h1",
|
|
223
|
+
getAttrs: (dom) => {
|
|
224
|
+
if (!(dom instanceof HTMLElement)) return { level: 1 };
|
|
225
|
+
return {
|
|
226
|
+
level: 1,
|
|
227
|
+
style: normalizeStyleAttribute(dom.getAttribute("style") || ""),
|
|
228
|
+
dir: dom.getAttribute("dir") || null
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
tag: "h2",
|
|
234
|
+
getAttrs: (dom) => {
|
|
235
|
+
if (!(dom instanceof HTMLElement)) return { level: 2 };
|
|
236
|
+
return {
|
|
237
|
+
level: 2,
|
|
238
|
+
style: normalizeStyleAttribute(dom.getAttribute("style") || ""),
|
|
239
|
+
dir: dom.getAttribute("dir") || null
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
tag: "h3",
|
|
245
|
+
getAttrs: (dom) => {
|
|
246
|
+
if (!(dom instanceof HTMLElement)) return { level: 3 };
|
|
247
|
+
return {
|
|
248
|
+
level: 3,
|
|
249
|
+
style: normalizeStyleAttribute(dom.getAttribute("style") || ""),
|
|
250
|
+
dir: dom.getAttribute("dir") || null
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
],
|
|
255
|
+
toDOM: (node) => {
|
|
256
|
+
const attrs = {};
|
|
257
|
+
if (node.attrs.style) attrs.style = node.attrs.style;
|
|
258
|
+
if (node.attrs.dir) attrs.dir = node.attrs.dir;
|
|
259
|
+
return [
|
|
260
|
+
`h${Math.max(1, Math.min(3, Number(node.attrs.level) || 1))}`,
|
|
261
|
+
attrs,
|
|
262
|
+
0
|
|
263
|
+
];
|
|
186
264
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
265
|
+
},
|
|
266
|
+
blockquote: {
|
|
267
|
+
content: "block+",
|
|
268
|
+
group: "block",
|
|
269
|
+
attrs: {
|
|
270
|
+
style: { default: null },
|
|
271
|
+
dir: { default: null }
|
|
272
|
+
},
|
|
273
|
+
parseDOM: [{
|
|
274
|
+
tag: "blockquote",
|
|
275
|
+
getAttrs: (dom) => {
|
|
276
|
+
if (!(dom instanceof HTMLElement)) return null;
|
|
277
|
+
return {
|
|
278
|
+
style: normalizeStyleAttribute(dom.getAttribute("style") || ""),
|
|
279
|
+
dir: dom.getAttribute("dir") || null
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}],
|
|
283
|
+
toDOM: (node) => {
|
|
284
|
+
const attrs = {};
|
|
285
|
+
if (node.attrs.style) attrs.style = node.attrs.style;
|
|
286
|
+
if (node.attrs.dir) attrs.dir = node.attrs.dir;
|
|
287
|
+
return [
|
|
288
|
+
"blockquote",
|
|
289
|
+
attrs,
|
|
290
|
+
0
|
|
291
|
+
];
|
|
190
292
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
293
|
+
},
|
|
294
|
+
code_block: {
|
|
295
|
+
content: "inline*",
|
|
296
|
+
group: "block",
|
|
297
|
+
code: true,
|
|
298
|
+
defining: true,
|
|
299
|
+
attrs: {
|
|
300
|
+
style: { default: null },
|
|
301
|
+
dir: { default: null }
|
|
302
|
+
},
|
|
303
|
+
parseDOM: [{
|
|
304
|
+
tag: "pre",
|
|
305
|
+
preserveWhitespace: "full",
|
|
306
|
+
getAttrs: (dom) => {
|
|
307
|
+
if (!(dom instanceof HTMLElement)) return null;
|
|
308
|
+
return {
|
|
309
|
+
style: normalizeStyleAttribute(dom.getAttribute("style") || ""),
|
|
310
|
+
dir: dom.getAttribute("dir") || null
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}],
|
|
314
|
+
toDOM: (node) => {
|
|
315
|
+
const attrs = {};
|
|
316
|
+
if (node.attrs.style) attrs.style = node.attrs.style;
|
|
317
|
+
if (node.attrs.dir) attrs.dir = node.attrs.dir;
|
|
318
|
+
return [
|
|
319
|
+
"pre",
|
|
320
|
+
attrs,
|
|
321
|
+
["code", 0]
|
|
322
|
+
];
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
bullet_list: {
|
|
326
|
+
content: "list_item+",
|
|
327
|
+
group: "block",
|
|
328
|
+
parseDOM: [{ tag: "ul" }],
|
|
329
|
+
toDOM: () => ["ul", 0]
|
|
330
|
+
},
|
|
331
|
+
ordered_list: {
|
|
332
|
+
attrs: { order: { default: 1 } },
|
|
333
|
+
content: "list_item+",
|
|
334
|
+
group: "block",
|
|
335
|
+
parseDOM: [{
|
|
336
|
+
tag: "ol",
|
|
337
|
+
getAttrs: (dom) => {
|
|
338
|
+
if (!(dom instanceof HTMLOListElement)) return { order: 1 };
|
|
339
|
+
return { order: dom.start || 1 };
|
|
340
|
+
}
|
|
341
|
+
}],
|
|
342
|
+
toDOM: (node) => [
|
|
343
|
+
"ol",
|
|
344
|
+
{ start: node.attrs.order || 1 },
|
|
345
|
+
0
|
|
346
|
+
]
|
|
347
|
+
},
|
|
348
|
+
list_item: {
|
|
349
|
+
content: "paragraph block*",
|
|
350
|
+
parseDOM: [{ tag: "li" }],
|
|
351
|
+
toDOM: () => ["li", 0]
|
|
352
|
+
},
|
|
353
|
+
image: {
|
|
354
|
+
inline: true,
|
|
355
|
+
attrs: {
|
|
356
|
+
src: {},
|
|
357
|
+
alt: { default: null }
|
|
358
|
+
},
|
|
359
|
+
group: "inline",
|
|
360
|
+
draggable: true,
|
|
361
|
+
parseDOM: [{
|
|
362
|
+
tag: "img[src]",
|
|
363
|
+
getAttrs: (dom) => {
|
|
364
|
+
if (!(dom instanceof HTMLElement)) return null;
|
|
365
|
+
return {
|
|
366
|
+
src: dom.getAttribute("src"),
|
|
367
|
+
alt: dom.getAttribute("alt")
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
}],
|
|
371
|
+
toDOM: (node) => ["img", {
|
|
372
|
+
src: node.attrs.src,
|
|
373
|
+
alt: node.attrs.alt
|
|
374
|
+
}]
|
|
375
|
+
},
|
|
376
|
+
video: {
|
|
377
|
+
inline: true,
|
|
378
|
+
attrs: {
|
|
379
|
+
src: {},
|
|
380
|
+
controls: { default: "true" }
|
|
381
|
+
},
|
|
382
|
+
group: "inline",
|
|
383
|
+
parseDOM: [{
|
|
384
|
+
tag: "video[src]",
|
|
385
|
+
getAttrs: (dom) => {
|
|
386
|
+
if (!(dom instanceof HTMLElement)) return null;
|
|
387
|
+
return {
|
|
388
|
+
src: dom.getAttribute("src"),
|
|
389
|
+
controls: dom.getAttribute("controls") || "true"
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}],
|
|
393
|
+
toDOM: (node) => ["video", {
|
|
394
|
+
src: node.attrs.src,
|
|
395
|
+
controls: node.attrs.controls
|
|
396
|
+
}]
|
|
397
|
+
},
|
|
398
|
+
text: { group: "inline" },
|
|
399
|
+
hard_break: {
|
|
400
|
+
inline: true,
|
|
401
|
+
group: "inline",
|
|
402
|
+
selectable: false,
|
|
403
|
+
parseDOM: [{ tag: "br" }],
|
|
404
|
+
toDOM: () => ["br"]
|
|
197
405
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
406
|
+
},
|
|
407
|
+
marks: {
|
|
408
|
+
strong: {
|
|
409
|
+
parseDOM: [{ tag: "strong" }, {
|
|
410
|
+
tag: "b",
|
|
411
|
+
getAttrs: () => null
|
|
412
|
+
}],
|
|
413
|
+
toDOM: () => ["strong", 0]
|
|
414
|
+
},
|
|
415
|
+
em: {
|
|
416
|
+
parseDOM: [{ tag: "em" }, {
|
|
417
|
+
tag: "i",
|
|
418
|
+
getAttrs: () => null
|
|
419
|
+
}],
|
|
420
|
+
toDOM: () => ["em", 0]
|
|
421
|
+
},
|
|
422
|
+
underline: {
|
|
423
|
+
parseDOM: [{ tag: "u" }],
|
|
424
|
+
toDOM: () => ["u", 0]
|
|
425
|
+
},
|
|
426
|
+
strike: {
|
|
427
|
+
parseDOM: [
|
|
428
|
+
{ tag: "s" },
|
|
429
|
+
{ tag: "del" },
|
|
430
|
+
{ tag: "strike" }
|
|
431
|
+
],
|
|
432
|
+
toDOM: () => ["s", 0]
|
|
433
|
+
},
|
|
434
|
+
code: {
|
|
435
|
+
parseDOM: [{ tag: "code" }],
|
|
436
|
+
toDOM: () => ["code", 0]
|
|
437
|
+
},
|
|
438
|
+
style: {
|
|
439
|
+
attrs: { style: { default: null } },
|
|
440
|
+
parseDOM: [{
|
|
441
|
+
tag: "span[style]",
|
|
442
|
+
getAttrs: (dom) => {
|
|
443
|
+
if (!(dom instanceof HTMLElement)) return null;
|
|
444
|
+
const normalized = normalizeStyleAttribute(dom.getAttribute("style") || "");
|
|
445
|
+
return normalized ? { style: normalized } : false;
|
|
446
|
+
}
|
|
447
|
+
}],
|
|
448
|
+
toDOM: (node) => [
|
|
449
|
+
"span",
|
|
450
|
+
{ style: node.attrs.style },
|
|
451
|
+
0
|
|
452
|
+
]
|
|
453
|
+
},
|
|
454
|
+
link: {
|
|
455
|
+
attrs: {
|
|
456
|
+
href: { default: null },
|
|
457
|
+
title: { default: null },
|
|
458
|
+
style: { default: null }
|
|
459
|
+
},
|
|
460
|
+
inclusive: false,
|
|
461
|
+
parseDOM: [{
|
|
462
|
+
tag: "a",
|
|
463
|
+
getAttrs: (dom) => {
|
|
464
|
+
if (!(dom instanceof HTMLElement)) return false;
|
|
465
|
+
const href = dom.getAttribute("href");
|
|
466
|
+
const style = dom.getAttribute("style");
|
|
467
|
+
if (!href && !style) return false;
|
|
468
|
+
return {
|
|
469
|
+
href: href || null,
|
|
470
|
+
title: dom.getAttribute("title") || null,
|
|
471
|
+
style: normalizeStyleAttribute(style || "")
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
}],
|
|
475
|
+
toDOM: (node) => {
|
|
476
|
+
const attrs = {};
|
|
477
|
+
if (node.attrs.href) attrs.href = node.attrs.href;
|
|
478
|
+
if (node.attrs.title) attrs.title = node.attrs.title;
|
|
479
|
+
if (node.attrs.style) attrs.style = node.attrs.style;
|
|
480
|
+
return [
|
|
481
|
+
"a",
|
|
482
|
+
attrs,
|
|
483
|
+
0
|
|
484
|
+
];
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
formula: {
|
|
488
|
+
parseDOM: [{ tag: "span[data-formula]" }],
|
|
489
|
+
toDOM: () => [
|
|
490
|
+
"span",
|
|
491
|
+
{ "data-formula": "true" },
|
|
492
|
+
0
|
|
493
|
+
]
|
|
205
494
|
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
const editorSchema = createEditorSchema();
|
|
498
|
+
//#endregion
|
|
499
|
+
//#region src/transforms/html.ts
|
|
210
500
|
const sanitizeHtml = (payload) => DOMPurify.sanitize(payload, {
|
|
211
501
|
ALLOWED_TAGS: [
|
|
212
502
|
"p",
|
|
@@ -223,6 +513,7 @@ const sanitizeHtml = (payload) => DOMPurify.sanitize(payload, {
|
|
|
223
513
|
"blockquote",
|
|
224
514
|
"h1",
|
|
225
515
|
"h2",
|
|
516
|
+
"h3",
|
|
226
517
|
"ul",
|
|
227
518
|
"ol",
|
|
228
519
|
"li",
|
|
@@ -258,19 +549,29 @@ const sanitizeHtml = (payload) => DOMPurify.sanitize(payload, {
|
|
|
258
549
|
"select"
|
|
259
550
|
]
|
|
260
551
|
});
|
|
552
|
+
const normalizeHtml = (value) => {
|
|
553
|
+
if (typeof globalThis.DOMParser === "undefined") return value.trim().length > 0 ? value : "<p></p>";
|
|
554
|
+
const domDoc = new globalThis.DOMParser().parseFromString(`<body>${value}</body>`, "text/html");
|
|
555
|
+
const pmNode = DOMParser.fromSchema(editorSchema).parse(domDoc.body);
|
|
556
|
+
const fragment = DOMSerializer.fromSchema(editorSchema).serializeFragment(pmNode.content);
|
|
557
|
+
const container = document.createElement("div");
|
|
558
|
+
container.appendChild(fragment);
|
|
559
|
+
const normalized = container.innerHTML.trim();
|
|
560
|
+
return normalized.length > 0 ? normalized : "<p></p>";
|
|
561
|
+
};
|
|
261
562
|
const importHtml = (payload) => {
|
|
262
563
|
const diagnostics = [];
|
|
263
564
|
const normalized = normalizeHtml(sanitizeHtml(payload));
|
|
264
565
|
if (normalized !== payload) diagnostics.push(createDiagnostic("SANITIZED_CONTENT", "warning", "Input HTML was sanitized to remove unsupported or unsafe markup."));
|
|
265
566
|
if (normalized.trim().length === 0) diagnostics.push(createDiagnostic("EMPTY_HTML", "info", "The provided HTML content is empty after normalization."));
|
|
266
567
|
return {
|
|
267
|
-
richText:
|
|
568
|
+
richText: normalized,
|
|
268
569
|
sanitizedHtml: normalized,
|
|
269
570
|
diagnostics
|
|
270
571
|
};
|
|
271
572
|
};
|
|
272
573
|
const exportHtml = (richText) => {
|
|
273
|
-
return normalizeHtml(
|
|
574
|
+
return normalizeHtml(richText);
|
|
274
575
|
};
|
|
275
576
|
//#endregion
|
|
276
577
|
//#region src/transforms/diagnostics.ts
|
|
@@ -301,26 +602,224 @@ const applyImportFallback = (input, base) => {
|
|
|
301
602
|
//#endregion
|
|
302
603
|
//#region src/transforms/markdown.ts
|
|
303
604
|
const normalizeMarkdown = (value) => value.replace(/\r\n?/g, "\n");
|
|
605
|
+
const escapeHtml = (text) => {
|
|
606
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
607
|
+
};
|
|
608
|
+
const parseInline = (text) => {
|
|
609
|
+
let html = escapeHtml(text);
|
|
610
|
+
html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
|
611
|
+
html = html.replace(/__(.*?)__/g, "<strong>$1</strong>");
|
|
612
|
+
html = html.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
|
613
|
+
html = html.replace(/_(.*?)_/g, "<em>$1</em>");
|
|
614
|
+
html = html.replace(/~~(.*?)~~/g, "<s>$1</s>");
|
|
615
|
+
html = html.replace(/`(.*?)`/g, "<code>$1</code>");
|
|
616
|
+
html = html.replace(/\[(.*?)\]\((.*?)\)/g, "<a href=\"$2\">$1</a>");
|
|
617
|
+
return html;
|
|
618
|
+
};
|
|
619
|
+
const markdownToHtml = (markdown) => {
|
|
620
|
+
const lines = normalizeMarkdown(markdown).split("\n");
|
|
621
|
+
const blocks = [];
|
|
622
|
+
let currentBlockType = null;
|
|
623
|
+
let blockLines = [];
|
|
624
|
+
const closeCurrentBlock = () => {
|
|
625
|
+
if (!currentBlockType) return;
|
|
626
|
+
if (currentBlockType === "paragraph") {
|
|
627
|
+
const content = parseInline(blockLines.join("\n"));
|
|
628
|
+
blocks.push(`<p>${content}</p>`);
|
|
629
|
+
} else if (currentBlockType === "blockquote") {
|
|
630
|
+
const content = markdownToHtml(blockLines.join("\n"));
|
|
631
|
+
blocks.push(`<blockquote>${content}</blockquote>`);
|
|
632
|
+
} else if (currentBlockType === "code_block") {
|
|
633
|
+
const content = escapeHtml(blockLines.join("\n"));
|
|
634
|
+
blocks.push(`<pre><code>${content}</code></pre>`);
|
|
635
|
+
} else if (currentBlockType === "ul" || currentBlockType === "ol") {
|
|
636
|
+
const tag = currentBlockType;
|
|
637
|
+
const itemBlocks = [];
|
|
638
|
+
let currentItemLines = [];
|
|
639
|
+
const closeItem = () => {
|
|
640
|
+
if (currentItemLines.length > 0) {
|
|
641
|
+
const innerHtml = markdownToHtml(currentItemLines.join("\n"));
|
|
642
|
+
itemBlocks.push(`<li>${innerHtml}</li>`);
|
|
643
|
+
currentItemLines = [];
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
for (const line of blockLines) {
|
|
647
|
+
const ulMatch = /^[*-+]\s+(.*)$/.exec(line.trim());
|
|
648
|
+
const olMatch = /^\d+\.\s+(.*)$/.exec(line.trim());
|
|
649
|
+
if (ulMatch) {
|
|
650
|
+
closeItem();
|
|
651
|
+
currentItemLines.push(ulMatch[1]);
|
|
652
|
+
} else if (olMatch) {
|
|
653
|
+
closeItem();
|
|
654
|
+
currentItemLines.push(olMatch[1]);
|
|
655
|
+
} else currentItemLines.push(line.trimStart());
|
|
656
|
+
}
|
|
657
|
+
closeItem();
|
|
658
|
+
blocks.push(`<${tag}>${itemBlocks.join("")}</${tag}>`);
|
|
659
|
+
}
|
|
660
|
+
blockLines = [];
|
|
661
|
+
currentBlockType = null;
|
|
662
|
+
};
|
|
663
|
+
let i = 0;
|
|
664
|
+
while (i < lines.length) {
|
|
665
|
+
const line = lines[i];
|
|
666
|
+
if (line.startsWith("```")) {
|
|
667
|
+
if (currentBlockType === "code_block") closeCurrentBlock();
|
|
668
|
+
else {
|
|
669
|
+
closeCurrentBlock();
|
|
670
|
+
currentBlockType = "code_block";
|
|
671
|
+
}
|
|
672
|
+
i++;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
if (currentBlockType === "code_block") {
|
|
676
|
+
blockLines.push(line);
|
|
677
|
+
i++;
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
const trimmed = line.trim();
|
|
681
|
+
if (trimmed === "") {
|
|
682
|
+
closeCurrentBlock();
|
|
683
|
+
i++;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
const headerMatch = /^(#{1,6})\s+(.*)$/.exec(trimmed);
|
|
687
|
+
if (headerMatch) {
|
|
688
|
+
closeCurrentBlock();
|
|
689
|
+
const level = headerMatch[1].length;
|
|
690
|
+
const content = parseInline(headerMatch[2]);
|
|
691
|
+
blocks.push(`<h${level}>${content}</h${level}>`);
|
|
692
|
+
i++;
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
if (trimmed.startsWith(">")) {
|
|
696
|
+
if (currentBlockType !== "blockquote") {
|
|
697
|
+
closeCurrentBlock();
|
|
698
|
+
currentBlockType = "blockquote";
|
|
699
|
+
}
|
|
700
|
+
const rest = line.startsWith("> ") ? line.slice(2) : line.slice(1);
|
|
701
|
+
blockLines.push(rest);
|
|
702
|
+
i++;
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (/^[*-+]\s+(.*)$/.exec(trimmed)) {
|
|
706
|
+
if (currentBlockType !== "ul") {
|
|
707
|
+
closeCurrentBlock();
|
|
708
|
+
currentBlockType = "ul";
|
|
709
|
+
}
|
|
710
|
+
blockLines.push(line);
|
|
711
|
+
i++;
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
if (/^\d+\.\s+(.*)$/.exec(trimmed)) {
|
|
715
|
+
if (currentBlockType !== "ol") {
|
|
716
|
+
closeCurrentBlock();
|
|
717
|
+
currentBlockType = "ol";
|
|
718
|
+
}
|
|
719
|
+
blockLines.push(line);
|
|
720
|
+
i++;
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
if (!currentBlockType) currentBlockType = "paragraph";
|
|
724
|
+
blockLines.push(line);
|
|
725
|
+
i++;
|
|
726
|
+
}
|
|
727
|
+
closeCurrentBlock();
|
|
728
|
+
return blocks.join("");
|
|
729
|
+
};
|
|
730
|
+
const serializeChild = (child, index, parent) => {
|
|
731
|
+
if (child.isText) {
|
|
732
|
+
let text = child.text || "";
|
|
733
|
+
child.marks.forEach((mark) => {
|
|
734
|
+
if (mark.type.name === "strong") text = `**${text}**`;
|
|
735
|
+
else if (mark.type.name === "em") text = `*${text}*`;
|
|
736
|
+
else if (mark.type.name === "strike") text = `~~${text}~~`;
|
|
737
|
+
else if (mark.type.name === "code") text = `\`${text}\``;
|
|
738
|
+
else if (mark.type.name === "link") {
|
|
739
|
+
const href = mark.attrs.href || "";
|
|
740
|
+
text = `[${text}](${href})`;
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
return text;
|
|
744
|
+
}
|
|
745
|
+
if (child.type.name === "hard_break") return "\n";
|
|
746
|
+
if (child.type.name === "image") {
|
|
747
|
+
const src = child.attrs.src || "";
|
|
748
|
+
return ``;
|
|
749
|
+
}
|
|
750
|
+
if (child.type.name === "video") return `[Video: ${child.attrs.src || ""}]`;
|
|
751
|
+
let inner = "";
|
|
752
|
+
child.forEach((c, idx) => {
|
|
753
|
+
inner += serializeChild(c, idx, child);
|
|
754
|
+
});
|
|
755
|
+
if (child.type.name === "paragraph") return `${inner}\n\n`;
|
|
756
|
+
if (child.type.name === "heading") return `${"#".repeat(child.attrs.level || 1)} ${inner}\n\n`;
|
|
757
|
+
if (child.type.name === "blockquote") return `${inner.trim().split("\n").map((line) => `> ${line}`).join("\n")}\n\n`;
|
|
758
|
+
if (child.type.name === "code_block") return `\`\`\`\n${inner.trim()}\n\`\`\`\n\n`;
|
|
759
|
+
if (child.type.name === "bullet_list") {
|
|
760
|
+
let listResult = "";
|
|
761
|
+
child.forEach((item) => {
|
|
762
|
+
const indented = serializeNode(item).trim().split("\n").map((line, idx) => {
|
|
763
|
+
if (idx === 0) return `* ${line}`;
|
|
764
|
+
return ` ${line}`;
|
|
765
|
+
}).join("\n");
|
|
766
|
+
listResult += `${indented}\n`;
|
|
767
|
+
});
|
|
768
|
+
return `${listResult}\n`;
|
|
769
|
+
}
|
|
770
|
+
if (child.type.name === "ordered_list") {
|
|
771
|
+
let listResult = "";
|
|
772
|
+
let count = child.attrs.order || 1;
|
|
773
|
+
child.forEach((item) => {
|
|
774
|
+
const indented = serializeNode(item).trim().split("\n").map((line, idx) => {
|
|
775
|
+
if (idx === 0) return `${count}. ${line}`;
|
|
776
|
+
return ` ${line}`;
|
|
777
|
+
}).join("\n");
|
|
778
|
+
listResult += `${indented}\n`;
|
|
779
|
+
count++;
|
|
780
|
+
});
|
|
781
|
+
return `${listResult}\n`;
|
|
782
|
+
}
|
|
783
|
+
if (child.type.name === "list_item") {
|
|
784
|
+
let itemInner = "";
|
|
785
|
+
child.forEach((c, idx) => {
|
|
786
|
+
itemInner += serializeChild(c, idx, child);
|
|
787
|
+
});
|
|
788
|
+
return itemInner;
|
|
789
|
+
}
|
|
790
|
+
return inner;
|
|
791
|
+
};
|
|
792
|
+
const serializeNode = (node) => {
|
|
793
|
+
let result = "";
|
|
794
|
+
node.forEach((child, index) => {
|
|
795
|
+
result += serializeChild(child, index, node);
|
|
796
|
+
});
|
|
797
|
+
return result.trim();
|
|
798
|
+
};
|
|
304
799
|
const importMarkdown = (payload) => {
|
|
305
800
|
const diagnostics = [];
|
|
306
801
|
const normalized = normalizeMarkdown(payload);
|
|
307
802
|
if (normalized.trim().length === 0) diagnostics.push(createDiagnostic("EMPTY_MARKDOWN", "info", "The provided markdown content is empty."));
|
|
308
803
|
return {
|
|
309
|
-
richText: normalized,
|
|
804
|
+
richText: markdownToHtml(normalized),
|
|
310
805
|
diagnostics
|
|
311
806
|
};
|
|
312
807
|
};
|
|
313
|
-
const exportMarkdown = (richText) =>
|
|
808
|
+
const exportMarkdown = (richText) => {
|
|
809
|
+
if (typeof globalThis.DOMParser === "undefined") return richText;
|
|
810
|
+
const domDoc = new globalThis.DOMParser().parseFromString(`<body>${richText}</body>`, "text/html");
|
|
811
|
+
return serializeNode(DOMParser.fromSchema(editorSchema).parse(domDoc.body));
|
|
812
|
+
};
|
|
314
813
|
//#endregion
|
|
315
814
|
//#region src/transforms/representationSwitch.ts
|
|
316
815
|
const importRepresentation = (input, base) => {
|
|
317
816
|
if (input.format === "rich") {
|
|
318
|
-
const normalizedHtml = normalizeHtml(
|
|
817
|
+
const normalizedHtml = normalizeHtml(input.payload);
|
|
319
818
|
return {
|
|
320
819
|
document: {
|
|
321
820
|
...base,
|
|
322
821
|
content: {
|
|
323
|
-
richText:
|
|
822
|
+
richText: normalizedHtml,
|
|
324
823
|
html: normalizedHtml
|
|
325
824
|
}
|
|
326
825
|
},
|
|
@@ -332,7 +831,10 @@ const importRepresentation = (input, base) => {
|
|
|
332
831
|
return {
|
|
333
832
|
document: {
|
|
334
833
|
...base,
|
|
335
|
-
content: {
|
|
834
|
+
content: {
|
|
835
|
+
richText: result.richText,
|
|
836
|
+
html: result.richText
|
|
837
|
+
},
|
|
336
838
|
metadata: {
|
|
337
839
|
...base.metadata,
|
|
338
840
|
importedFrom: "markdown"
|
|
@@ -395,6 +897,11 @@ const createEditor = (config = {}) => {
|
|
|
395
897
|
const history = new EditorHistory();
|
|
396
898
|
let isDestroyed = false;
|
|
397
899
|
let currentDocument = createDefaultDocument();
|
|
900
|
+
const listeners = {
|
|
901
|
+
change: /* @__PURE__ */ new Set(),
|
|
902
|
+
selectionchange: /* @__PURE__ */ new Set()
|
|
903
|
+
};
|
|
904
|
+
let activeHandler = null;
|
|
398
905
|
if (config.initialContent) {
|
|
399
906
|
const initialized = fromInput(config.initialContent, currentDocument);
|
|
400
907
|
currentDocument = initialized.document;
|
|
@@ -407,6 +914,7 @@ const createEditor = (config = {}) => {
|
|
|
407
914
|
};
|
|
408
915
|
const emitChange = () => {
|
|
409
916
|
config.onChange?.(currentDocument);
|
|
917
|
+
listeners.change.forEach((cb) => cb());
|
|
410
918
|
};
|
|
411
919
|
return {
|
|
412
920
|
getDocument: () => currentDocument,
|
|
@@ -416,6 +924,21 @@ const createEditor = (config = {}) => {
|
|
|
416
924
|
document: currentDocument,
|
|
417
925
|
diagnostics: []
|
|
418
926
|
};
|
|
927
|
+
if (!(config.commandCapabilities ? new Set(config.commandCapabilities) : new Set([
|
|
928
|
+
"bold",
|
|
929
|
+
"italic",
|
|
930
|
+
"underline",
|
|
931
|
+
"heading",
|
|
932
|
+
"list",
|
|
933
|
+
"align",
|
|
934
|
+
"link",
|
|
935
|
+
"undo",
|
|
936
|
+
"redo"
|
|
937
|
+
])).has(commandName)) return {
|
|
938
|
+
ok: false,
|
|
939
|
+
document: currentDocument,
|
|
940
|
+
diagnostics: [createUnknownCommandDiagnostic(commandName)]
|
|
941
|
+
};
|
|
419
942
|
const result = executeCommand(registry, commandName, { document: currentDocument }, params);
|
|
420
943
|
if (commandName === "undo") {
|
|
421
944
|
const next = history.undo(currentDocument);
|
|
@@ -481,141 +1004,27 @@ const createEditor = (config = {}) => {
|
|
|
481
1004
|
unmount: () => {},
|
|
482
1005
|
destroy: () => {
|
|
483
1006
|
isDestroyed = true;
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
};
|
|
487
|
-
//#endregion
|
|
488
|
-
//#region src/core/schema.ts
|
|
489
|
-
const editorNodeNames = {
|
|
490
|
-
doc: "doc",
|
|
491
|
-
paragraph: "paragraph",
|
|
492
|
-
heading: "heading",
|
|
493
|
-
text: "text",
|
|
494
|
-
bulletList: "bullet_list",
|
|
495
|
-
orderedList: "ordered_list",
|
|
496
|
-
listItem: "list_item",
|
|
497
|
-
hardBreak: "hard_break"
|
|
498
|
-
};
|
|
499
|
-
const editorMarkNames = {
|
|
500
|
-
strong: "strong",
|
|
501
|
-
em: "em",
|
|
502
|
-
underline: "underline",
|
|
503
|
-
link: "link"
|
|
504
|
-
};
|
|
505
|
-
const createEditorSchema = () => new Schema({
|
|
506
|
-
nodes: {
|
|
507
|
-
doc: { content: "block+" },
|
|
508
|
-
paragraph: {
|
|
509
|
-
content: "inline*",
|
|
510
|
-
group: "block",
|
|
511
|
-
parseDOM: [{ tag: "p" }],
|
|
512
|
-
toDOM: () => ["p", 0]
|
|
513
|
-
},
|
|
514
|
-
heading: {
|
|
515
|
-
attrs: { level: { default: 1 } },
|
|
516
|
-
content: "inline*",
|
|
517
|
-
group: "block",
|
|
518
|
-
defining: true,
|
|
519
|
-
parseDOM: [
|
|
520
|
-
{
|
|
521
|
-
tag: "h1",
|
|
522
|
-
attrs: { level: 1 }
|
|
523
|
-
},
|
|
524
|
-
{
|
|
525
|
-
tag: "h2",
|
|
526
|
-
attrs: { level: 2 }
|
|
527
|
-
},
|
|
528
|
-
{
|
|
529
|
-
tag: "h3",
|
|
530
|
-
attrs: { level: 3 }
|
|
531
|
-
}
|
|
532
|
-
],
|
|
533
|
-
toDOM: (node) => [`h${Math.max(1, Math.min(3, Number(node.attrs.level) || 1))}`, 0]
|
|
534
|
-
},
|
|
535
|
-
bullet_list: {
|
|
536
|
-
content: "list_item+",
|
|
537
|
-
group: "block",
|
|
538
|
-
parseDOM: [{ tag: "ul" }],
|
|
539
|
-
toDOM: () => ["ul", 0]
|
|
540
|
-
},
|
|
541
|
-
ordered_list: {
|
|
542
|
-
attrs: { order: { default: 1 } },
|
|
543
|
-
content: "list_item+",
|
|
544
|
-
group: "block",
|
|
545
|
-
parseDOM: [{
|
|
546
|
-
tag: "ol",
|
|
547
|
-
getAttrs: (dom) => {
|
|
548
|
-
if (!(dom instanceof HTMLOListElement)) return { order: 1 };
|
|
549
|
-
return { order: dom.start || 1 };
|
|
550
|
-
}
|
|
551
|
-
}],
|
|
552
|
-
toDOM: (node) => [
|
|
553
|
-
"ol",
|
|
554
|
-
{ start: node.attrs.order || 1 },
|
|
555
|
-
0
|
|
556
|
-
]
|
|
557
|
-
},
|
|
558
|
-
list_item: {
|
|
559
|
-
content: "paragraph block*",
|
|
560
|
-
parseDOM: [{ tag: "li" }],
|
|
561
|
-
toDOM: () => ["li", 0]
|
|
1007
|
+
listeners.change.clear();
|
|
1008
|
+
listeners.selectionchange.clear();
|
|
562
1009
|
},
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
571
|
-
},
|
|
572
|
-
marks: {
|
|
573
|
-
strong: {
|
|
574
|
-
parseDOM: [{ tag: "strong" }, {
|
|
575
|
-
tag: "b",
|
|
576
|
-
getAttrs: () => null
|
|
577
|
-
}],
|
|
578
|
-
toDOM: () => ["strong", 0]
|
|
1010
|
+
isMarkActive: (name) => activeHandler?.isMarkActive(name) ?? false,
|
|
1011
|
+
getActiveBlockType: () => activeHandler?.getActiveBlockType() ?? "paragraph",
|
|
1012
|
+
on: (event, callback) => {
|
|
1013
|
+
listeners[event].add(callback);
|
|
1014
|
+
return () => {
|
|
1015
|
+
listeners[event].delete(callback);
|
|
1016
|
+
};
|
|
579
1017
|
},
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
}],
|
|
585
|
-
toDOM: () => ["em", 0]
|
|
1018
|
+
toolbar: config.toolbar,
|
|
1019
|
+
placeholder: config.placeholder,
|
|
1020
|
+
_registerActiveHandler: (handler) => {
|
|
1021
|
+
activeHandler = handler;
|
|
586
1022
|
},
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
toDOM: () => ["u", 0]
|
|
590
|
-
},
|
|
591
|
-
link: {
|
|
592
|
-
attrs: {
|
|
593
|
-
href: {},
|
|
594
|
-
title: { default: null }
|
|
595
|
-
},
|
|
596
|
-
inclusive: false,
|
|
597
|
-
parseDOM: [{
|
|
598
|
-
tag: "a[href]",
|
|
599
|
-
getAttrs: (dom) => {
|
|
600
|
-
if (!(dom instanceof HTMLAnchorElement)) return false;
|
|
601
|
-
return {
|
|
602
|
-
href: dom.getAttribute("href"),
|
|
603
|
-
title: dom.getAttribute("title")
|
|
604
|
-
};
|
|
605
|
-
}
|
|
606
|
-
}],
|
|
607
|
-
toDOM: (node) => [
|
|
608
|
-
"a",
|
|
609
|
-
{
|
|
610
|
-
href: node.attrs.href,
|
|
611
|
-
title: node.attrs.title
|
|
612
|
-
},
|
|
613
|
-
0
|
|
614
|
-
]
|
|
1023
|
+
_triggerSelectionChange: () => {
|
|
1024
|
+
listeners.selectionchange.forEach((cb) => cb());
|
|
615
1025
|
}
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
const editorSchema = createEditorSchema();
|
|
1026
|
+
};
|
|
1027
|
+
};
|
|
619
1028
|
//#endregion
|
|
620
1029
|
//#region src/core/toolbarModel.ts
|
|
621
1030
|
const defaultToolbarActions = [
|
|
@@ -709,40 +1118,40 @@ const createToolbarIcon = (command, fallback) => {
|
|
|
709
1118
|
};
|
|
710
1119
|
const selectOptionLabels = {
|
|
711
1120
|
font: {
|
|
712
|
-
default: "
|
|
713
|
-
serif: "
|
|
714
|
-
monospace: "
|
|
1121
|
+
default: "Font",
|
|
1122
|
+
serif: "Serif",
|
|
1123
|
+
monospace: "Monospace"
|
|
715
1124
|
},
|
|
716
1125
|
size: {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
large: "
|
|
720
|
-
huge: "
|
|
1126
|
+
normal: "Normal (16px)",
|
|
1127
|
+
small: "Small (12px)",
|
|
1128
|
+
large: "Large (20px)",
|
|
1129
|
+
huge: "Huge (28px)"
|
|
721
1130
|
},
|
|
722
1131
|
header: {
|
|
723
|
-
normal: "
|
|
724
|
-
h1: "
|
|
725
|
-
h2: "
|
|
1132
|
+
normal: "Normal Text",
|
|
1133
|
+
h1: "Heading 1",
|
|
1134
|
+
h2: "Heading 2"
|
|
726
1135
|
},
|
|
727
1136
|
script: {
|
|
728
|
-
normal: "
|
|
729
|
-
sub: "
|
|
730
|
-
super: "
|
|
1137
|
+
normal: "Script",
|
|
1138
|
+
sub: "Subscript",
|
|
1139
|
+
super: "Superscript"
|
|
731
1140
|
},
|
|
732
1141
|
align: {
|
|
733
|
-
left: "
|
|
734
|
-
center: "
|
|
735
|
-
right: "
|
|
736
|
-
justify: "
|
|
1142
|
+
left: "Align Left",
|
|
1143
|
+
center: "Align Center",
|
|
1144
|
+
right: "Align Right",
|
|
1145
|
+
justify: "Justify"
|
|
737
1146
|
},
|
|
738
1147
|
list: {
|
|
739
|
-
none: "
|
|
740
|
-
ordered: "
|
|
741
|
-
bullet: "
|
|
1148
|
+
none: "No List",
|
|
1149
|
+
ordered: "Numbered List",
|
|
1150
|
+
bullet: "Bulleted List"
|
|
742
1151
|
},
|
|
743
1152
|
direction: {
|
|
744
|
-
ltr: "
|
|
745
|
-
rtl: "
|
|
1153
|
+
ltr: "Left-to-Right",
|
|
1154
|
+
rtl: "Right-to-Left"
|
|
746
1155
|
}
|
|
747
1156
|
};
|
|
748
1157
|
const safeUrl = (value) => {
|
|
@@ -1082,11 +1491,13 @@ const createSelect = (format, options, editable, onMutate) => {
|
|
|
1082
1491
|
});
|
|
1083
1492
|
return select;
|
|
1084
1493
|
};
|
|
1085
|
-
const buildToolbar = (editable, requestEmbedValue, onMutate) => {
|
|
1494
|
+
const buildToolbar = (editable, requestEmbedValue, onMutate, toolbarOption) => {
|
|
1495
|
+
if (toolbarOption === false) return null;
|
|
1086
1496
|
const toolbar = document.createElement("div");
|
|
1087
1497
|
toolbar.className = "az-rich-editor-toolbar";
|
|
1088
1498
|
toolbar.setAttribute("role", "toolbar");
|
|
1089
1499
|
toolbar.setAttribute("aria-label", "Rich editor toolbar");
|
|
1500
|
+
const allowed = Array.isArray(toolbarOption) ? new Set(toolbarOption) : null;
|
|
1090
1501
|
[
|
|
1091
1502
|
{
|
|
1092
1503
|
format: "font",
|
|
@@ -1099,8 +1510,8 @@ const buildToolbar = (editable, requestEmbedValue, onMutate) => {
|
|
|
1099
1510
|
{
|
|
1100
1511
|
format: "size",
|
|
1101
1512
|
options: [
|
|
1102
|
-
"small",
|
|
1103
1513
|
"normal",
|
|
1514
|
+
"small",
|
|
1104
1515
|
"large",
|
|
1105
1516
|
"huge"
|
|
1106
1517
|
]
|
|
@@ -1142,7 +1553,9 @@ const buildToolbar = (editable, requestEmbedValue, onMutate) => {
|
|
|
1142
1553
|
format: "direction",
|
|
1143
1554
|
options: ["ltr", "rtl"]
|
|
1144
1555
|
}
|
|
1145
|
-
].forEach((config) =>
|
|
1556
|
+
].forEach((config) => {
|
|
1557
|
+
if (!allowed || allowed.has(config.format)) toolbar.append(createSelect(config.format, config.options, editable, onMutate));
|
|
1558
|
+
});
|
|
1146
1559
|
[
|
|
1147
1560
|
"bold",
|
|
1148
1561
|
"italic",
|
|
@@ -1158,10 +1571,12 @@ const buildToolbar = (editable, requestEmbedValue, onMutate) => {
|
|
|
1158
1571
|
"video",
|
|
1159
1572
|
"formula",
|
|
1160
1573
|
"clean"
|
|
1161
|
-
].forEach((command) =>
|
|
1162
|
-
|
|
1574
|
+
].forEach((command) => {
|
|
1575
|
+
if (!allowed || allowed.has(command)) toolbar.append(renderButton(command, editable, requestEmbedValue, onMutate));
|
|
1576
|
+
});
|
|
1577
|
+
return toolbar.childNodes.length > 0 ? toolbar : null;
|
|
1163
1578
|
};
|
|
1164
|
-
const mountRichTextEditor = ({ host, initialHtml, disabled = false, onChange, requestEmbedValue }) => {
|
|
1579
|
+
const mountRichTextEditor = ({ host, initialHtml, disabled = false, onChange, requestEmbedValue, toolbar: toolbarOption, placeholder, onSelectionChange }) => {
|
|
1165
1580
|
host.innerHTML = "";
|
|
1166
1581
|
const wrapper = document.createElement("div");
|
|
1167
1582
|
wrapper.className = "az-rich-editor";
|
|
@@ -1171,23 +1586,161 @@ const mountRichTextEditor = ({ host, initialHtml, disabled = false, onChange, re
|
|
|
1171
1586
|
editable.setAttribute("role", "textbox");
|
|
1172
1587
|
editable.setAttribute("aria-multiline", "true");
|
|
1173
1588
|
editable.setAttribute("contenteditable", disabled ? "false" : "true");
|
|
1174
|
-
editable.dataset.placeholder = "Compose an epic...";
|
|
1589
|
+
editable.dataset.placeholder = placeholder ?? "Compose an epic...";
|
|
1175
1590
|
editable.innerHTML = initialHtml;
|
|
1176
1591
|
const emitChange = () => onChange(editable.innerHTML);
|
|
1177
|
-
const toolbar = buildToolbar(editable, requestEmbedValue, emitChange);
|
|
1592
|
+
const toolbar = buildToolbar(editable, requestEmbedValue, emitChange, toolbarOption);
|
|
1178
1593
|
const inputHandler = () => emitChange();
|
|
1179
1594
|
editable.addEventListener("input", inputHandler);
|
|
1180
|
-
|
|
1595
|
+
const keydownHandler = (event) => {
|
|
1596
|
+
const range = getSelectionRange(editable);
|
|
1597
|
+
if (!range) return;
|
|
1598
|
+
if (event.metaKey || event.ctrlKey) {
|
|
1599
|
+
const key = event.key.toLowerCase();
|
|
1600
|
+
if (key === "b") {
|
|
1601
|
+
event.preventDefault();
|
|
1602
|
+
applyButtonCommand("bold", editable, requestEmbedValue);
|
|
1603
|
+
emitChange();
|
|
1604
|
+
} else if (key === "i") {
|
|
1605
|
+
event.preventDefault();
|
|
1606
|
+
applyButtonCommand("italic", editable, requestEmbedValue);
|
|
1607
|
+
emitChange();
|
|
1608
|
+
} else if (key === "u") {
|
|
1609
|
+
event.preventDefault();
|
|
1610
|
+
applyButtonCommand("underline", editable, requestEmbedValue);
|
|
1611
|
+
emitChange();
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (event.key === "Tab") {
|
|
1615
|
+
if (closestByTag(getBlockElement(range, editable), "li", editable)) {
|
|
1616
|
+
event.preventDefault();
|
|
1617
|
+
applyIndent(editable, range, event.shiftKey ? "-" : "+");
|
|
1618
|
+
emitChange();
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
};
|
|
1622
|
+
editable.addEventListener("keydown", keydownHandler);
|
|
1623
|
+
const checkMarkActive = (markName) => {
|
|
1624
|
+
const selection = window.getSelection();
|
|
1625
|
+
if (!selection || selection.rangeCount === 0) return false;
|
|
1626
|
+
const range = selection.getRangeAt(0);
|
|
1627
|
+
if (!editable.contains(range.commonAncestorContainer)) return false;
|
|
1628
|
+
const tags = {
|
|
1629
|
+
strong: ["strong", "b"],
|
|
1630
|
+
bold: ["strong", "b"],
|
|
1631
|
+
em: ["em", "i"],
|
|
1632
|
+
italic: ["em", "i"],
|
|
1633
|
+
underline: ["u"],
|
|
1634
|
+
strike: [
|
|
1635
|
+
"s",
|
|
1636
|
+
"del",
|
|
1637
|
+
"strike"
|
|
1638
|
+
],
|
|
1639
|
+
code: ["code"],
|
|
1640
|
+
link: ["a"]
|
|
1641
|
+
}[markName];
|
|
1642
|
+
if (!tags) return false;
|
|
1643
|
+
let node = range.startContainer;
|
|
1644
|
+
while (node && node !== editable) {
|
|
1645
|
+
if (node instanceof HTMLElement) {
|
|
1646
|
+
const tagName = node.tagName.toLowerCase();
|
|
1647
|
+
if (tags.includes(tagName)) return true;
|
|
1648
|
+
if (markName === "code" && node.style.fontFamily === "monospace") return true;
|
|
1649
|
+
}
|
|
1650
|
+
node = node.parentNode;
|
|
1651
|
+
}
|
|
1652
|
+
return false;
|
|
1653
|
+
};
|
|
1654
|
+
const checkActiveBlockType = () => {
|
|
1655
|
+
const selection = window.getSelection();
|
|
1656
|
+
if (!selection || selection.rangeCount === 0) return "paragraph";
|
|
1657
|
+
const range = selection.getRangeAt(0);
|
|
1658
|
+
if (!editable.contains(range.commonAncestorContainer)) return "paragraph";
|
|
1659
|
+
let node = range.startContainer;
|
|
1660
|
+
while (node && node !== editable) {
|
|
1661
|
+
if (node instanceof HTMLElement) {
|
|
1662
|
+
const tagName = node.tagName.toLowerCase();
|
|
1663
|
+
if (tagName === "h1" || tagName === "h2" || tagName === "h3") return "heading";
|
|
1664
|
+
if (tagName === "blockquote") return "blockquote";
|
|
1665
|
+
if (tagName === "pre") return "code_block";
|
|
1666
|
+
if (tagName === "ol") return "ordered_list";
|
|
1667
|
+
if (tagName === "ul") return "bullet_list";
|
|
1668
|
+
}
|
|
1669
|
+
node = node.parentNode;
|
|
1670
|
+
}
|
|
1671
|
+
return "paragraph";
|
|
1672
|
+
};
|
|
1673
|
+
const updateToolbarStates = () => {
|
|
1674
|
+
if (!toolbar) return;
|
|
1675
|
+
toolbar.querySelectorAll(".az-rich-editor-toolbar-button").forEach((btn) => {
|
|
1676
|
+
const command = btn.getAttribute("aria-label");
|
|
1677
|
+
if (!command) return;
|
|
1678
|
+
let isActive = false;
|
|
1679
|
+
if (command === "Bold") isActive = checkMarkActive("strong");
|
|
1680
|
+
else if (command === "Italic") isActive = checkMarkActive("em");
|
|
1681
|
+
else if (command === "Underline") isActive = checkMarkActive("underline");
|
|
1682
|
+
else if (command === "Strike") isActive = checkMarkActive("strike");
|
|
1683
|
+
else if (command === "Inline code") isActive = checkMarkActive("code");
|
|
1684
|
+
else if (command === "Blockquote") isActive = checkActiveBlockType() === "blockquote";
|
|
1685
|
+
else if (command === "Code block") isActive = checkActiveBlockType() === "code_block";
|
|
1686
|
+
if (isActive) {
|
|
1687
|
+
btn.setAttribute("data-active", "true");
|
|
1688
|
+
btn.classList.add("active");
|
|
1689
|
+
} else {
|
|
1690
|
+
btn.removeAttribute("data-active");
|
|
1691
|
+
btn.classList.remove("active");
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
toolbar.querySelectorAll(".az-rich-editor-toolbar-select").forEach((select) => {
|
|
1695
|
+
const label = select.getAttribute("aria-label");
|
|
1696
|
+
if (label === "header") if (checkActiveBlockType() === "heading") {
|
|
1697
|
+
const selection = window.getSelection();
|
|
1698
|
+
if (selection && selection.rangeCount > 0) {
|
|
1699
|
+
const level = getBlockElement(selection.getRangeAt(0), editable).tagName.toLowerCase();
|
|
1700
|
+
if (level === "h1" || level === "h2" || level === "h3") select.value = level;
|
|
1701
|
+
else select.value = "normal";
|
|
1702
|
+
}
|
|
1703
|
+
} else select.value = "normal";
|
|
1704
|
+
else if (label === "align") {
|
|
1705
|
+
const selection = window.getSelection();
|
|
1706
|
+
if (selection && selection.rangeCount > 0) select.value = getBlockElement(selection.getRangeAt(0), editable).style.textAlign || "left";
|
|
1707
|
+
} else if (label === "direction") {
|
|
1708
|
+
const selection = window.getSelection();
|
|
1709
|
+
if (selection && selection.rangeCount > 0) select.value = getBlockElement(selection.getRangeAt(0), editable).getAttribute("dir") || "ltr";
|
|
1710
|
+
}
|
|
1711
|
+
});
|
|
1712
|
+
};
|
|
1713
|
+
const selectionHandler = () => {
|
|
1714
|
+
const selection = window.getSelection();
|
|
1715
|
+
if (selection && selection.rangeCount > 0) {
|
|
1716
|
+
const range = selection.getRangeAt(0);
|
|
1717
|
+
if (editable.contains(range.commonAncestorContainer)) {
|
|
1718
|
+
onSelectionChange?.();
|
|
1719
|
+
updateToolbarStates();
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
document.addEventListener("selectionchange", selectionHandler);
|
|
1724
|
+
if (toolbar) {
|
|
1725
|
+
wrapper.append(toolbar);
|
|
1726
|
+
updateToolbarStates();
|
|
1727
|
+
}
|
|
1728
|
+
wrapper.append(editable);
|
|
1181
1729
|
host.append(wrapper);
|
|
1182
1730
|
return {
|
|
1183
1731
|
destroy: () => {
|
|
1184
1732
|
editable.removeEventListener("input", inputHandler);
|
|
1733
|
+
editable.removeEventListener("keydown", keydownHandler);
|
|
1734
|
+
document.removeEventListener("selectionchange", selectionHandler);
|
|
1185
1735
|
host.innerHTML = "";
|
|
1186
1736
|
},
|
|
1187
1737
|
getHtml: () => editable.innerHTML,
|
|
1188
1738
|
setHtml: (html) => {
|
|
1189
1739
|
editable.innerHTML = html;
|
|
1190
|
-
|
|
1740
|
+
updateToolbarStates();
|
|
1741
|
+
},
|
|
1742
|
+
isMarkActive: (markName) => checkMarkActive(markName),
|
|
1743
|
+
getActiveBlockType: () => checkActiveBlockType()
|
|
1191
1744
|
};
|
|
1192
1745
|
};
|
|
1193
1746
|
//#endregion
|
|
@@ -1195,6 +1748,7 @@ const mountRichTextEditor = ({ host, initialHtml, disabled = false, onChange, re
|
|
|
1195
1748
|
const createDomAdapter = (editor) => {
|
|
1196
1749
|
let target = null;
|
|
1197
1750
|
let mountedEditor = null;
|
|
1751
|
+
let unsubscribeChange = null;
|
|
1198
1752
|
return {
|
|
1199
1753
|
mount: (nextTarget) => {
|
|
1200
1754
|
target = nextTarget;
|
|
@@ -1204,6 +1758,11 @@ const createDomAdapter = (editor) => {
|
|
|
1204
1758
|
host: nextTarget,
|
|
1205
1759
|
initialHtml: editor.export("html").payload,
|
|
1206
1760
|
requestEmbedValue: () => null,
|
|
1761
|
+
toolbar: editor.toolbar,
|
|
1762
|
+
placeholder: editor.placeholder,
|
|
1763
|
+
onSelectionChange: () => {
|
|
1764
|
+
editor._triggerSelectionChange?.();
|
|
1765
|
+
},
|
|
1207
1766
|
onChange: (html) => {
|
|
1208
1767
|
editor.import({
|
|
1209
1768
|
format: "html",
|
|
@@ -1211,15 +1770,30 @@ const createDomAdapter = (editor) => {
|
|
|
1211
1770
|
});
|
|
1212
1771
|
}
|
|
1213
1772
|
});
|
|
1773
|
+
editor._registerActiveHandler?.({
|
|
1774
|
+
isMarkActive: (name) => mountedEditor?.isMarkActive(name) ?? false,
|
|
1775
|
+
getActiveBlockType: () => mountedEditor?.getActiveBlockType() ?? "paragraph"
|
|
1776
|
+
});
|
|
1777
|
+
unsubscribeChange = editor.on("change", () => {
|
|
1778
|
+
if (!mountedEditor) return;
|
|
1779
|
+
const nextHtml = editor.export("html").payload;
|
|
1780
|
+
if (mountedEditor.getHtml() !== nextHtml) mountedEditor.setHtml(nextHtml);
|
|
1781
|
+
});
|
|
1214
1782
|
},
|
|
1215
1783
|
unmount: () => {
|
|
1216
1784
|
if (!target) return;
|
|
1785
|
+
unsubscribeChange?.();
|
|
1786
|
+
unsubscribeChange = null;
|
|
1787
|
+
editor._registerActiveHandler?.(null);
|
|
1217
1788
|
mountedEditor?.destroy();
|
|
1218
1789
|
mountedEditor = null;
|
|
1219
1790
|
editor.unmount();
|
|
1220
1791
|
target = null;
|
|
1221
1792
|
},
|
|
1222
1793
|
destroy: () => {
|
|
1794
|
+
unsubscribeChange?.();
|
|
1795
|
+
unsubscribeChange = null;
|
|
1796
|
+
editor._registerActiveHandler?.(null);
|
|
1223
1797
|
mountedEditor?.destroy();
|
|
1224
1798
|
mountedEditor = null;
|
|
1225
1799
|
editor.destroy();
|
|
@@ -1229,6 +1803,40 @@ const createDomAdapter = (editor) => {
|
|
|
1229
1803
|
};
|
|
1230
1804
|
//#endregion
|
|
1231
1805
|
//#region src/adapters/react/useEditorAdapter.tsx
|
|
1806
|
+
const EditorContext = createContext(null);
|
|
1807
|
+
const useEditor = () => {
|
|
1808
|
+
return useContext(EditorContext);
|
|
1809
|
+
};
|
|
1810
|
+
const useEditorState = () => {
|
|
1811
|
+
const editor = useContext(EditorContext);
|
|
1812
|
+
if (!editor) throw new Error("useEditorState must be used inside an EditorProvider");
|
|
1813
|
+
const [document, setDocument] = useState(() => editor.getDocument());
|
|
1814
|
+
const [selectionKey, setSelectionKey] = useState(0);
|
|
1815
|
+
useEffect(() => {
|
|
1816
|
+
const unsubChange = editor.on("change", () => {
|
|
1817
|
+
setDocument(editor.getDocument());
|
|
1818
|
+
});
|
|
1819
|
+
const unsubSelection = editor.on("selectionchange", () => {
|
|
1820
|
+
setSelectionKey((k) => k + 1);
|
|
1821
|
+
});
|
|
1822
|
+
return () => {
|
|
1823
|
+
unsubChange();
|
|
1824
|
+
unsubSelection();
|
|
1825
|
+
};
|
|
1826
|
+
}, [editor]);
|
|
1827
|
+
return {
|
|
1828
|
+
editor,
|
|
1829
|
+
document,
|
|
1830
|
+
isMarkActive: useCallback((name) => editor.isMarkActive(name), [editor, selectionKey]),
|
|
1831
|
+
activeBlockType: useCallback(() => editor.getActiveBlockType(), [editor, selectionKey])()
|
|
1832
|
+
};
|
|
1833
|
+
};
|
|
1834
|
+
function EditorProvider({ editor, children }) {
|
|
1835
|
+
return /* @__PURE__ */ jsx(EditorContext.Provider, {
|
|
1836
|
+
value: editor,
|
|
1837
|
+
children
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1232
1840
|
const useEditorAdapter = (config) => {
|
|
1233
1841
|
const editor = useMemo(() => createEditor(config), [config]);
|
|
1234
1842
|
useEffect(() => () => {
|
|
@@ -1236,13 +1844,27 @@ const useEditorAdapter = (config) => {
|
|
|
1236
1844
|
}, [editor]);
|
|
1237
1845
|
return editor;
|
|
1238
1846
|
};
|
|
1239
|
-
function RichEditorAdapter({ className, disabled = false, initialContent, onChange, onError, onRequestEmbedValue }) {
|
|
1240
|
-
const
|
|
1847
|
+
function RichEditorAdapter({ className, disabled = false, initialContent, onChange, onError, onRequestEmbedValue, toolbar, placeholder, editor: propEditor }) {
|
|
1848
|
+
const contextEditor = useEditor();
|
|
1849
|
+
const fallbackEditor = useEditorAdapter({
|
|
1241
1850
|
initialContent,
|
|
1242
1851
|
onChange,
|
|
1243
|
-
onError
|
|
1852
|
+
onError,
|
|
1853
|
+
toolbar,
|
|
1854
|
+
placeholder
|
|
1244
1855
|
});
|
|
1856
|
+
const editor = propEditor ?? contextEditor ?? fallbackEditor;
|
|
1245
1857
|
const hostRef = useRef(null);
|
|
1858
|
+
const mountedRef = useRef(null);
|
|
1859
|
+
useEffect(() => {
|
|
1860
|
+
if (editor._registerActiveHandler) editor._registerActiveHandler({
|
|
1861
|
+
isMarkActive: (name) => mountedRef.current?.isMarkActive(name) ?? false,
|
|
1862
|
+
getActiveBlockType: () => mountedRef.current?.getActiveBlockType() ?? "paragraph"
|
|
1863
|
+
});
|
|
1864
|
+
return () => {
|
|
1865
|
+
editor._registerActiveHandler?.(null);
|
|
1866
|
+
};
|
|
1867
|
+
}, [editor]);
|
|
1246
1868
|
useEffect(() => {
|
|
1247
1869
|
const host = hostRef.current;
|
|
1248
1870
|
if (!host) return;
|
|
@@ -1251,6 +1873,11 @@ function RichEditorAdapter({ className, disabled = false, initialContent, onChan
|
|
|
1251
1873
|
initialHtml: editor.export("html").payload,
|
|
1252
1874
|
disabled,
|
|
1253
1875
|
requestEmbedValue: onRequestEmbedValue,
|
|
1876
|
+
toolbar: toolbar ?? editor.toolbar,
|
|
1877
|
+
placeholder: placeholder ?? editor.placeholder,
|
|
1878
|
+
onSelectionChange: () => {
|
|
1879
|
+
editor._triggerSelectionChange?.();
|
|
1880
|
+
},
|
|
1254
1881
|
onChange: (html) => {
|
|
1255
1882
|
editor.import({
|
|
1256
1883
|
format: "html",
|
|
@@ -1258,13 +1885,17 @@ function RichEditorAdapter({ className, disabled = false, initialContent, onChan
|
|
|
1258
1885
|
});
|
|
1259
1886
|
}
|
|
1260
1887
|
});
|
|
1888
|
+
mountedRef.current = mounted;
|
|
1261
1889
|
return () => {
|
|
1262
1890
|
mounted.destroy();
|
|
1891
|
+
mountedRef.current = null;
|
|
1263
1892
|
};
|
|
1264
1893
|
}, [
|
|
1265
1894
|
disabled,
|
|
1266
1895
|
editor,
|
|
1267
|
-
onRequestEmbedValue
|
|
1896
|
+
onRequestEmbedValue,
|
|
1897
|
+
toolbar,
|
|
1898
|
+
placeholder
|
|
1268
1899
|
]);
|
|
1269
1900
|
return /* @__PURE__ */ jsx("div", {
|
|
1270
1901
|
"aria-label": "Rich editor",
|
|
@@ -1273,6 +1904,6 @@ function RichEditorAdapter({ className, disabled = false, initialContent, onChan
|
|
|
1273
1904
|
});
|
|
1274
1905
|
}
|
|
1275
1906
|
//#endregion
|
|
1276
|
-
export { RichEditorAdapter, createDomAdapter, createEditor, createEditorSchema, defaultToolbarActions, editorMarkNames, editorNodeNames, editorSchema, exportRepresentation, importRepresentation, useEditorAdapter };
|
|
1907
|
+
export { EditorContext, EditorProvider, RichEditorAdapter, createDomAdapter, createEditor, createEditorSchema, defaultToolbarActions, editorMarkNames, editorNodeNames, editorSchema, exportRepresentation, importRepresentation, useEditor, useEditorAdapter, useEditorState };
|
|
1277
1908
|
|
|
1278
1909
|
//# sourceMappingURL=index.mjs.map
|