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