rich-html-editor 0.2.1

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.js ADDED
@@ -0,0 +1,1896 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // src/core/events.ts
34
+ function getEditorEventEmitter() {
35
+ return editorEventEmitter;
36
+ }
37
+ var EditorEventEmitter, editorEventEmitter;
38
+ var init_events = __esm({
39
+ "src/core/events.ts"() {
40
+ "use strict";
41
+ EditorEventEmitter = class {
42
+ constructor() {
43
+ this.listeners = /* @__PURE__ */ new Map();
44
+ }
45
+ on(type, handler) {
46
+ if (!this.listeners.has(type)) {
47
+ this.listeners.set(type, /* @__PURE__ */ new Set());
48
+ }
49
+ this.listeners.get(type).add(handler);
50
+ return () => this.off(type, handler);
51
+ }
52
+ once(type, handler) {
53
+ const unsubscribe = this.on(type, (event) => {
54
+ handler(event);
55
+ unsubscribe();
56
+ });
57
+ }
58
+ off(type, handler) {
59
+ const handlers = this.listeners.get(type);
60
+ if (handlers) {
61
+ handlers.delete(handler);
62
+ if (handlers.size === 0) this.listeners.delete(type);
63
+ }
64
+ }
65
+ emit(event) {
66
+ const handlers = this.listeners.get(event.type);
67
+ if (handlers) {
68
+ handlers.forEach((handler) => {
69
+ try {
70
+ handler(event);
71
+ } catch (error) {
72
+ console.error(
73
+ `[rich-html-editor] Error in event handler for ${event.type}:`,
74
+ error
75
+ );
76
+ }
77
+ });
78
+ }
79
+ }
80
+ removeAllListeners(type) {
81
+ if (type) this.listeners.delete(type);
82
+ else this.listeners.clear();
83
+ }
84
+ listenerCount(type) {
85
+ var _a, _b;
86
+ return (_b = (_a = this.listeners.get(type)) == null ? void 0 : _a.size) != null ? _b : 0;
87
+ }
88
+ };
89
+ editorEventEmitter = new EditorEventEmitter();
90
+ }
91
+ });
92
+
93
+ // src/core/constants.ts
94
+ var TOOLBAR_ID, STYLE_ID, CLASS_EDITABLE, CLASS_ACTIVE, DEFAULT_MAX_STACK, TOOLBAR_BG, TOOLBAR_BORDER, BUTTON_BORDER, BUTTON_ACTIVE_BG, BUTTON_BG, BUTTON_COLOR, INFO_COLOR, HOVER_OUTLINE, ACTIVE_OUTLINE, LABEL_BOLD, LABEL_ITALIC, LABEL_UNDERLINE, LABEL_STRIKETHROUGH, LABEL_UNDO, LABEL_REDO, LABEL_LINK, LABEL_ALIGN_LEFT, LABEL_ALIGN_CENTER, LABEL_ALIGN_RIGHT, FONT_OPTIONS, SIZE_OPTIONS, FORMAT_OPTIONS;
95
+ var init_constants = __esm({
96
+ "src/core/constants.ts"() {
97
+ "use strict";
98
+ TOOLBAR_ID = "editor-toolbar";
99
+ STYLE_ID = "editor-styles";
100
+ CLASS_EDITABLE = "editor-editable-element";
101
+ CLASS_ACTIVE = "editor-active-element";
102
+ DEFAULT_MAX_STACK = 60;
103
+ TOOLBAR_BG = "#f8fafc";
104
+ TOOLBAR_BORDER = "#e5e7eb";
105
+ BUTTON_BORDER = "#d1d5db";
106
+ BUTTON_ACTIVE_BG = "#e0e7ff";
107
+ BUTTON_BG = "#fff";
108
+ BUTTON_COLOR = "#222";
109
+ INFO_COLOR = "#888";
110
+ HOVER_OUTLINE = "#2563eb";
111
+ ACTIVE_OUTLINE = "#16a34a";
112
+ LABEL_BOLD = "<b>B</b>";
113
+ LABEL_ITALIC = "<i>I</i>";
114
+ LABEL_UNDERLINE = "<u>U</u>";
115
+ LABEL_STRIKETHROUGH = "<s>S</s>";
116
+ LABEL_UNDO = "\u21BA";
117
+ LABEL_REDO = "\u21BB";
118
+ LABEL_LINK = "\u{1F517}";
119
+ LABEL_ALIGN_LEFT = `
120
+ <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
121
+ <rect x="1" y="2" width="10" height="2" rx="0.5" fill="currentColor" />
122
+ <rect x="1" y="6" width="14" height="2" rx="0.5" fill="currentColor" />
123
+ <rect x="1" y="10" width="10" height="2" rx="0.5" fill="currentColor" />
124
+ <rect x="1" y="14" width="14" height="2" rx="0.5" fill="currentColor" />
125
+ </svg>
126
+ `;
127
+ LABEL_ALIGN_CENTER = `
128
+ <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
129
+ <rect x="3" y="2" width="10" height="2" rx="0.5" fill="currentColor" />
130
+ <rect x="1" y="6" width="14" height="2" rx="0.5" fill="currentColor" />
131
+ <rect x="3" y="10" width="10" height="2" rx="0.5" fill="currentColor" />
132
+ <rect x="1" y="14" width="14" height="2" rx="0.5" fill="currentColor" />
133
+ </svg>
134
+ `;
135
+ LABEL_ALIGN_RIGHT = `
136
+ <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
137
+ <rect x="5" y="2" width="10" height="2" rx="0.5" fill="currentColor" />
138
+ <rect x="1" y="6" width="14" height="2" rx="0.5" fill="currentColor" />
139
+ <rect x="5" y="10" width="10" height="2" rx="0.5" fill="currentColor" />
140
+ <rect x="1" y="14" width="14" height="2" rx="0.5" fill="currentColor" />
141
+ </svg>
142
+ `;
143
+ FONT_OPTIONS = [
144
+ { label: "Arial", value: "Arial" },
145
+ { label: "Helvetica", value: "Helvetica, Arial, sans-serif" },
146
+ { label: "Verdana", value: "Verdana, Geneva, sans-serif" },
147
+ { label: "Tahoma", value: "Tahoma, Geneva, sans-serif" },
148
+ { label: "Trebuchet MS", value: "Trebuchet MS, Helvetica, sans-serif" },
149
+ { label: "Georgia", value: "Georgia, serif" },
150
+ { label: "Times New Roman", value: "Times New Roman, Times, serif" },
151
+ { label: "Palatino", value: "Palatino, 'Palatino Linotype', serif" },
152
+ { label: "Garamond", value: "Garamond, serif" },
153
+ { label: "Book Antiqua", value: "'Book Antiqua', Palatino, serif" },
154
+ { label: "Courier New", value: "'Courier New', Courier, monospace" },
155
+ { label: "Lucida Console", value: "'Lucida Console', Monaco, monospace" },
156
+ { label: "Impact", value: "Impact, Charcoal, sans-serif" },
157
+ { label: "Comic Sans MS", value: "'Comic Sans MS', 'Comic Sans', cursive" },
158
+ { label: "Segoe UI", value: "'Segoe UI', Tahoma, Geneva, sans-serif" },
159
+ {
160
+ label: "Roboto",
161
+ value: "Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif"
162
+ },
163
+ { label: "Open Sans", value: "'Open Sans', Arial, sans-serif" },
164
+ { label: "Lato", value: "Lato, 'Helvetica Neue', Arial, sans-serif" },
165
+ { label: "Montserrat", value: "Montserrat, Arial, sans-serif" },
166
+ { label: "Source Sans Pro", value: "'Source Sans Pro', Arial, sans-serif" },
167
+ { label: "Fira Sans", value: "'Fira Sans', Arial, sans-serif" },
168
+ { label: "Ubuntu", value: "Ubuntu, Arial, sans-serif" },
169
+ { label: "Noto Sans", value: "'Noto Sans', Arial, sans-serif" },
170
+ { label: "Droid Sans", value: "'Droid Sans', Arial, sans-serif" },
171
+ {
172
+ label: "Helvetica Neue",
173
+ value: "'Helvetica Neue', Helvetica, Arial, sans-serif"
174
+ },
175
+ {
176
+ label: "System UI",
177
+ value: "system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
178
+ }
179
+ ];
180
+ SIZE_OPTIONS = [
181
+ { label: "8", value: "8" },
182
+ { label: "9", value: "9" },
183
+ { label: "10", value: "10" },
184
+ { label: "11", value: "11" },
185
+ { label: "12", value: "12" },
186
+ { label: "14", value: "14" },
187
+ { label: "16", value: "16" },
188
+ { label: "18", value: "18" },
189
+ { label: "20", value: "20" },
190
+ { label: "22", value: "22" },
191
+ { label: "24", value: "24" },
192
+ { label: "26", value: "26" },
193
+ { label: "28", value: "28" },
194
+ { label: "36", value: "36" },
195
+ { label: "48", value: "48" },
196
+ { label: "72", value: "72" }
197
+ ];
198
+ FORMAT_OPTIONS = [
199
+ { label: "Heading 1", value: "h1" },
200
+ { label: "Heading 2", value: "h2" },
201
+ { label: "Heading 3", value: "h3" },
202
+ { label: "Heading 4", value: "h4" },
203
+ { label: "Heading 5", value: "h5" },
204
+ { label: "Heading 6", value: "h6" }
205
+ ];
206
+ }
207
+ });
208
+
209
+ // src/utils/sanitize.ts
210
+ function sanitizeHtml(html, ctx) {
211
+ if (!html) return "";
212
+ let win = null;
213
+ if (ctx && ctx.defaultView) {
214
+ win = ctx.defaultView;
215
+ } else if (ctx && ctx.document) {
216
+ win = ctx;
217
+ } else if (typeof window !== "undefined") {
218
+ win = window;
219
+ }
220
+ if (win) {
221
+ try {
222
+ const DOMPurify = (0, import_dompurify.default)(win);
223
+ try {
224
+ DOMPurify.addHook("uponSanitizeAttribute", (node, data) => {
225
+ try {
226
+ if (data && data.attrName && data.attrName.startsWith("data-")) {
227
+ data.keepAttr = true;
228
+ }
229
+ } catch (e) {
230
+ }
231
+ });
232
+ } catch (e) {
233
+ }
234
+ return DOMPurify.sanitize(html, {
235
+ // Use sensible defaults: allow common formatting tags but strip scripts
236
+ ALLOWED_TAGS: [
237
+ "a",
238
+ "b",
239
+ "i",
240
+ "em",
241
+ "strong",
242
+ "u",
243
+ "p",
244
+ "div",
245
+ "span",
246
+ // Common semantic elements: preserve document structure so undo/redo
247
+ // does not flatten header/section/nav into plain content.
248
+ "header",
249
+ "nav",
250
+ "section",
251
+ "main",
252
+ "footer",
253
+ "article",
254
+ "aside",
255
+ "figure",
256
+ "figcaption",
257
+ "time",
258
+ // Interactive / form elements that may appear in content
259
+ "button",
260
+ "input",
261
+ "label",
262
+ "select",
263
+ "option",
264
+ "textarea",
265
+ "details",
266
+ "summary",
267
+ // Allow <style> tags so user/content-provided CSS is preserved
268
+ // when taking snapshots and during undo/redo operations.
269
+ // DOMPurify will still sanitize the contents of style blocks.
270
+ "style",
271
+ // Preserve linked stylesheets so page/editor styling isn't lost
272
+ "link",
273
+ "ul",
274
+ "ol",
275
+ "li",
276
+ "br",
277
+ "hr",
278
+ "blockquote",
279
+ "pre",
280
+ "code",
281
+ "h1",
282
+ "h2",
283
+ "h3",
284
+ "h4",
285
+ "h5",
286
+ "h6",
287
+ "img"
288
+ ],
289
+ ALLOWED_ATTR: [
290
+ "href",
291
+ "title",
292
+ "alt",
293
+ "src",
294
+ "class",
295
+ "style",
296
+ // Attributes used by <link> tags
297
+ "rel",
298
+ "type",
299
+ "media"
300
+ ],
301
+ // Also allow `id` attributes so element ids survive sanitization.
302
+ ADD_ATTR: ["id"]
303
+ });
304
+ } catch (e) {
305
+ }
306
+ }
307
+ return html.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "").replace(/on[a-z]+=\"[^"]*\"/gi, "");
308
+ }
309
+ var import_dompurify;
310
+ var init_sanitize = __esm({
311
+ "src/utils/sanitize.ts"() {
312
+ "use strict";
313
+ import_dompurify = __toESM(require("dompurify"));
314
+ }
315
+ });
316
+
317
+ // src/core/state.ts
318
+ var state_exports = {};
319
+ __export(state_exports, {
320
+ _getCurrentEditable: () => _getCurrentEditable,
321
+ _getDoc: () => _getDoc,
322
+ _getRedoStack: () => _getRedoStack,
323
+ _getUndoStack: () => _getUndoStack,
324
+ _setCurrentEditable: () => _setCurrentEditable,
325
+ _setDoc: () => _setDoc,
326
+ _setRedoStack: () => _setRedoStack,
327
+ _setUndoStack: () => _setUndoStack,
328
+ pushStandaloneSnapshot: () => pushStandaloneSnapshot,
329
+ setMaxStackSize: () => setMaxStackSize
330
+ });
331
+ function _setDoc(doc) {
332
+ _doc = doc;
333
+ }
334
+ function _getDoc() {
335
+ return _doc;
336
+ }
337
+ function _setUndoStack(stack) {
338
+ _undoStack = stack;
339
+ editorEventEmitter.emit({
340
+ type: "undoStateChanged",
341
+ timestamp: Date.now(),
342
+ data: { canUndo: _undoStack.length > 1 }
343
+ });
344
+ }
345
+ function _getUndoStack() {
346
+ return _undoStack;
347
+ }
348
+ function _setRedoStack(stack) {
349
+ _redoStack = stack;
350
+ editorEventEmitter.emit({
351
+ type: "redoStateChanged",
352
+ timestamp: Date.now(),
353
+ data: { canRedo: _redoStack.length > 0 }
354
+ });
355
+ }
356
+ function _getRedoStack() {
357
+ return _redoStack;
358
+ }
359
+ function _setCurrentEditable(el) {
360
+ _currentEditable = el;
361
+ editorEventEmitter.emit({
362
+ type: "selectionChanged",
363
+ timestamp: Date.now(),
364
+ data: { element: el == null ? void 0 : el.tagName }
365
+ });
366
+ }
367
+ function _getCurrentEditable() {
368
+ return _currentEditable;
369
+ }
370
+ function pushStandaloneSnapshot(clearRedo = true) {
371
+ if (!_doc) return;
372
+ const clone = _doc.documentElement.cloneNode(true);
373
+ const toolbarNode = clone.querySelector(`#${TOOLBAR_ID}`);
374
+ if (toolbarNode && toolbarNode.parentNode)
375
+ toolbarNode.parentNode.removeChild(toolbarNode);
376
+ const styleNode = clone.querySelector(`#${STYLE_ID}`);
377
+ if (styleNode && styleNode.parentNode)
378
+ styleNode.parentNode.removeChild(styleNode);
379
+ try {
380
+ const editableNodes = clone.querySelectorAll(
381
+ "[contenteditable], ." + CLASS_EDITABLE + ", ." + CLASS_ACTIVE
382
+ );
383
+ editableNodes.forEach((el) => {
384
+ try {
385
+ if (el instanceof Element) {
386
+ if (el.hasAttribute("contenteditable"))
387
+ el.removeAttribute("contenteditable");
388
+ if (el.hasAttribute("tabindex")) el.removeAttribute("tabindex");
389
+ el.classList.remove(CLASS_EDITABLE, CLASS_ACTIVE);
390
+ }
391
+ } catch (e) {
392
+ }
393
+ });
394
+ } catch (e) {
395
+ }
396
+ try {
397
+ const scripts = Array.from(
398
+ clone.querySelectorAll("script")
399
+ );
400
+ scripts.forEach((s) => {
401
+ var _a;
402
+ try {
403
+ const code = s.textContent || "";
404
+ const attrs = {};
405
+ Array.from(s.attributes).forEach((a) => attrs[a.name] = a.value);
406
+ const placeholder = clone.ownerDocument.createElement("span");
407
+ try {
408
+ const safe = typeof btoa !== "undefined" ? btoa(unescape(encodeURIComponent(code))) : encodeURIComponent(code);
409
+ placeholder.setAttribute("data-rhe-script", safe);
410
+ } catch (e) {
411
+ placeholder.setAttribute("data-rhe-script", encodeURIComponent(code));
412
+ }
413
+ if (Object.keys(attrs).length) {
414
+ placeholder.setAttribute(
415
+ "data-rhe-script-attrs",
416
+ encodeURIComponent(JSON.stringify(attrs))
417
+ );
418
+ }
419
+ const parentMarker = s.closest("[data-rhe-id]");
420
+ if (parentMarker && parentMarker.getAttribute("data-rhe-id")) {
421
+ placeholder.setAttribute(
422
+ "data-rhe-script-parent",
423
+ parentMarker.getAttribute("data-rhe-id")
424
+ );
425
+ } else {
426
+ placeholder.setAttribute("data-rhe-script-parent", "head");
427
+ }
428
+ (_a = s.parentNode) == null ? void 0 : _a.replaceChild(placeholder, s);
429
+ } catch (e) {
430
+ }
431
+ });
432
+ } catch (e) {
433
+ }
434
+ const snapRaw = clone.outerHTML;
435
+ const snap = sanitizeHtml(snapRaw, _doc);
436
+ if (!_undoStack.length || _undoStack[_undoStack.length - 1] !== snap) {
437
+ _undoStack.push(snap);
438
+ if (_undoStack.length > _maxStackSize) _undoStack.shift();
439
+ editorEventEmitter.emit({
440
+ type: "contentChanged",
441
+ timestamp: Date.now(),
442
+ data: { htmlLength: snap.length }
443
+ });
444
+ }
445
+ if (clearRedo) {
446
+ _redoStack = [];
447
+ editorEventEmitter.emit({
448
+ type: "redoStateChanged",
449
+ timestamp: Date.now(),
450
+ data: { canRedo: false }
451
+ });
452
+ }
453
+ }
454
+ function setMaxStackSize(size) {
455
+ _maxStackSize = Math.max(1, size);
456
+ }
457
+ var _doc, _undoStack, _redoStack, _currentEditable, _maxStackSize;
458
+ var init_state = __esm({
459
+ "src/core/state.ts"() {
460
+ "use strict";
461
+ init_events();
462
+ init_constants();
463
+ init_sanitize();
464
+ _doc = null;
465
+ _undoStack = [];
466
+ _redoStack = [];
467
+ _currentEditable = null;
468
+ _maxStackSize = DEFAULT_MAX_STACK;
469
+ }
470
+ });
471
+
472
+ // src/index.ts
473
+ var index_exports = {};
474
+ __export(index_exports, {
475
+ editorEventEmitter: () => editorEventEmitter,
476
+ getCleanHTML: () => getCleanHTML,
477
+ getEditorEventEmitter: () => getEditorEventEmitter,
478
+ initRichEditor: () => initRichEditor
479
+ });
480
+ module.exports = __toCommonJS(index_exports);
481
+
482
+ // src/core/editor.ts
483
+ init_state();
484
+
485
+ // src/dom/styles.ts
486
+ init_constants();
487
+ function injectStyles(doc) {
488
+ const styleId = STYLE_ID;
489
+ let styleEl = doc.getElementById(styleId);
490
+ const css = `
491
+ .${CLASS_EDITABLE}{outline:2px dashed ${HOVER_OUTLINE};cursor:text}
492
+ .${CLASS_ACTIVE}{outline:2px solid ${ACTIVE_OUTLINE};cursor:text}
493
+ #${TOOLBAR_ID}{
494
+ position: sticky;
495
+ top: 0;
496
+ left: 0;
497
+ right: 0;
498
+ z-index: 9999;
499
+ display: flex;
500
+ align-items: center;
501
+ gap: 8px;
502
+ padding: 8px 12px;
503
+ background: ${TOOLBAR_BG};
504
+ border-bottom: 1px solid ${TOOLBAR_BORDER};
505
+ font-family: inherit;
506
+ box-shadow: 0 6px 18px rgba(2,6,23,0.08);
507
+ backdrop-filter: blur(6px);
508
+ /* Allow toolbar items to wrap onto multiple lines on narrow screens */
509
+ flex-wrap: wrap;
510
+ justify-content: flex-start;
511
+ }
512
+ #${TOOLBAR_ID} button{
513
+ padding: 6px 8px;
514
+ border: 1px solid ${BUTTON_BORDER};
515
+ background: ${BUTTON_BG};
516
+ color: ${BUTTON_COLOR};
517
+ border-radius: 8px;
518
+ font-weight: 500;
519
+ cursor: pointer;
520
+ transition: transform .12s ease, box-shadow .12s ease, background .12s ease;
521
+ outline: none;
522
+ margin-right: 2px;
523
+ }
524
+ #${TOOLBAR_ID} button[aria-pressed="true"]{
525
+ background: ${BUTTON_ACTIVE_BG};
526
+ font-weight: 600;
527
+ box-shadow: 0 6px 12px rgba(99,102,241,0.12);
528
+ }
529
+ #${TOOLBAR_ID} button:hover:not(:disabled){
530
+ transform: translateY(-2px);
531
+ box-shadow: 0 8px 20px rgba(2,6,23,0.08);
532
+ }
533
+ #${TOOLBAR_ID} select{
534
+ padding: 6px 8px;
535
+ border-radius: 8px;
536
+ border: 1px solid ${BUTTON_BORDER};
537
+ background: #fff;
538
+ font-family: inherit;
539
+ }
540
+ #${TOOLBAR_ID} input[type="color"]{
541
+ border-radius: 8px;
542
+ border: 1px solid ${BUTTON_BORDER};
543
+ background: #fff;
544
+ font-family: inherit;
545
+ }
546
+ #${TOOLBAR_ID} .color-input-label{
547
+ display: inline-flex;
548
+ align-items: center;
549
+ gap: 8px;
550
+ padding: 0;
551
+ background: transparent;
552
+ border: none;
553
+ }
554
+
555
+ /* Labeled color inputs (e.g. "Text Color <input type=color>") */
556
+ #${TOOLBAR_ID} .color-label{
557
+ display: inline-flex;
558
+ align-items: center;
559
+ gap: 8px;
560
+ padding: 0;
561
+ background: transparent;
562
+ border: none;
563
+ }
564
+ #${TOOLBAR_ID} .color-input-label .color-icon{
565
+ display: inline-flex;
566
+ align-items: center;
567
+ justify-content: center;
568
+ }
569
+ #${TOOLBAR_ID} .text-color-icon{
570
+ font-weight: 700;
571
+ font-size: 14px;
572
+ line-height: 1;
573
+ display: inline-block;
574
+ padding-bottom: 2px;
575
+ border-bottom: 3px solid currentColor;
576
+ transform-origin: center;
577
+ }
578
+ #${TOOLBAR_ID} .highlight-icon{
579
+ width: 16px;
580
+ height: 16px;
581
+ display: inline-block;
582
+ }
583
+ #${TOOLBAR_ID} .color-icon svg{
584
+ width: 16px;
585
+ height: 16px;
586
+ display: block;
587
+ }
588
+ /* Text color wrapper: A with a small swatch on the right */
589
+ #${TOOLBAR_ID} .text-color-wrapper{
590
+ display: inline-flex;
591
+ align-items: center;
592
+ gap: 6px;
593
+ }
594
+ #${TOOLBAR_ID} .text-color-wrapper .text-A{
595
+ font-weight: 700;
596
+ font-size: 14px;
597
+ line-height: 1;
598
+ }
599
+ #${TOOLBAR_ID} .text-color-wrapper .color-swatch{
600
+ width: 12px;
601
+ height: 12px;
602
+ border-radius: 3px;
603
+ border: 1px solid rgba(0,0,0,0.12);
604
+ box-shadow: 0 1px 0 rgba(255,255,255,0.5) inset;
605
+ background: currentColor;
606
+ }
607
+
608
+ /* Highlight wrapper: small colored bar under/behind the A to mimic highlighter */
609
+ #${TOOLBAR_ID} .highlight-wrapper{
610
+ display: inline-flex;
611
+ align-items: center;
612
+ gap: 6px;
613
+ position: relative;
614
+ }
615
+ #${TOOLBAR_ID} .highlight-wrapper .highlight-bar{
616
+ position: absolute;
617
+ left: 0;
618
+ right: 0;
619
+ bottom: 2px;
620
+ height: 8px;
621
+ border-radius: 3px;
622
+ background: #ffeb3b; /* default yellow */
623
+ z-index: 0;
624
+ }
625
+ #${TOOLBAR_ID} .highlight-wrapper .text-A{
626
+ position: relative;
627
+ z-index: 1;
628
+ font-weight: 700;
629
+ font-size: 14px;
630
+ line-height: 1;
631
+ padding: 0 4px;
632
+ }
633
+ #${TOOLBAR_ID} span{
634
+ color: ${INFO_COLOR};
635
+ font-size: 90%;
636
+ }
637
+
638
+ /* Grouping and separators */
639
+ #${TOOLBAR_ID} .toolbar-group{
640
+ display: flex;
641
+ align-items: center;
642
+ gap: 6px;
643
+ }
644
+ #${TOOLBAR_ID} .toolbar-group{
645
+ /* groups may wrap internally to avoid overflow on narrow screens */
646
+ flex-wrap: wrap;
647
+ }
648
+ /* Overflow button + menu styling */
649
+ #${TOOLBAR_ID} .toolbar-overflow-btn{
650
+ display: none;
651
+ align-items: center;
652
+ justify-content: center;
653
+ padding: 6px 8px;
654
+ border-radius: 8px;
655
+ border: 1px solid ${BUTTON_BORDER};
656
+ background: ${BUTTON_BG};
657
+ color: ${BUTTON_COLOR};
658
+ font-weight: 600;
659
+ }
660
+ #${TOOLBAR_ID} .toolbar-overflow-menu{
661
+ position: absolute;
662
+ top: calc(100% + 6px);
663
+ right: 12px;
664
+ min-width: 160px;
665
+ background: #fff;
666
+ border: 1px solid rgba(15,23,42,0.06);
667
+ border-radius: 8px;
668
+ padding: 8px;
669
+ box-shadow: 0 12px 40px rgba(2,6,23,0.12);
670
+ display: flex;
671
+ flex-direction: column;
672
+ gap: 6px;
673
+ z-index: 10000;
674
+ }
675
+ #${TOOLBAR_ID} .toolbar-overflow-menu[hidden]{
676
+ display: none;
677
+ }
678
+ #${TOOLBAR_ID} .toolbar-sep{
679
+ width: 1px;
680
+ height: 28px;
681
+ background: rgba(15,23,42,0.06);
682
+ margin: 0 8px;
683
+ border-radius: 1px;
684
+ }
685
+ #${TOOLBAR_ID} .toolbar-spacer{
686
+ flex: 1 1 auto;
687
+ }
688
+ #${TOOLBAR_ID} button svg{
689
+ width: 16px;
690
+ height: 16px;
691
+ display: block;
692
+ /* Default icon appearance */
693
+ fill: none;
694
+ }
695
+
696
+ /* Active/pressed: switch to filled appearance */
697
+ #${TOOLBAR_ID} button[aria-pressed="true"] svg{
698
+ fill: currentColor;
699
+ }
700
+
701
+ /* Focus and accessibility */
702
+ #${TOOLBAR_ID} button:focus{
703
+ outline: none;
704
+ box-shadow: 0 0 0 4px rgba(99,102,241,0.12);
705
+ }
706
+
707
+ /* Disabled state */
708
+ #${TOOLBAR_ID} button:disabled{
709
+ opacity: 0.48;
710
+ cursor: not-allowed;
711
+ }
712
+ /* Responsive tweaks: reduce spacing and allow horizontal scroll on very small screens */
713
+ @media (max-width: 720px){
714
+ #${TOOLBAR_ID}{
715
+ padding: 6px 8px;
716
+ gap: 6px;
717
+ }
718
+ #${TOOLBAR_ID} button,
719
+ #${TOOLBAR_ID} select,
720
+ #${TOOLBAR_ID} input[type="color"]{
721
+ padding: 4px 6px;
722
+ border-radius: 6px;
723
+ }
724
+ /* Hide visual separators to save horizontal space */
725
+ #${TOOLBAR_ID} .toolbar-sep{
726
+ display: none;
727
+ }
728
+ /* Collapse labeled color text visually but preserve accessibility on the input */
729
+ #${TOOLBAR_ID} .color-label{
730
+ font-size: 0;
731
+ }
732
+ #${TOOLBAR_ID} .color-label input{
733
+ font-size: initial;
734
+ }
735
+ }
736
+ @media (max-width: 420px){
737
+ /* On very small screens prefer a single-line scrollable toolbar */
738
+ #${TOOLBAR_ID}{
739
+ flex-wrap: nowrap;
740
+ overflow-x: auto;
741
+ -webkit-overflow-scrolling: touch;
742
+ }
743
+ #${TOOLBAR_ID} .toolbar-group{
744
+ flex: 0 0 auto;
745
+ }
746
+ #${TOOLBAR_ID} button{ margin-right: 6px; }
747
+ /* Show overflow button and hide the groups marked for collapse */
748
+ #${TOOLBAR_ID} .toolbar-overflow-btn{
749
+ display: inline-flex;
750
+ }
751
+ #${TOOLBAR_ID} .collapse-on-small{
752
+ display: none;
753
+ }
754
+ }
755
+ `;
756
+ if (!styleEl) {
757
+ styleEl = doc.createElement("style");
758
+ styleEl.id = styleId;
759
+ doc.head.appendChild(styleEl);
760
+ }
761
+ styleEl.textContent = css;
762
+ }
763
+
764
+ // src/toolbar/toolbar.ts
765
+ init_constants();
766
+ function injectToolbar(doc, options) {
767
+ const existing = doc.getElementById(TOOLBAR_ID);
768
+ if (existing) existing.remove();
769
+ const toolbar = doc.createElement("div");
770
+ toolbar.id = TOOLBAR_ID;
771
+ toolbar.setAttribute("role", "toolbar");
772
+ toolbar.setAttribute("aria-label", "Rich text editor toolbar");
773
+ function makeButton(label, title, command, value, isActive, disabled) {
774
+ const btn = doc.createElement("button");
775
+ btn.type = "button";
776
+ if (label && label.trim().startsWith("<")) {
777
+ btn.innerHTML = label;
778
+ } else {
779
+ btn.textContent = label;
780
+ }
781
+ btn.title = title;
782
+ btn.setAttribute("aria-label", title);
783
+ if (typeof isActive !== "undefined")
784
+ btn.setAttribute("aria-pressed", String(!!isActive));
785
+ btn.tabIndex = 0;
786
+ if (disabled) btn.disabled = true;
787
+ btn.onclick = () => options.onCommand(command, value);
788
+ btn.addEventListener("keydown", (ev) => {
789
+ if (ev.key === "Enter" || ev.key === " ") {
790
+ ev.preventDefault();
791
+ btn.click();
792
+ }
793
+ });
794
+ return btn;
795
+ }
796
+ function makeSelect(title, command, optionsList, initialValue) {
797
+ const select = doc.createElement("select");
798
+ select.title = title;
799
+ select.setAttribute("aria-label", title);
800
+ select.appendChild(new Option(title, "", true, true));
801
+ for (const opt of optionsList) {
802
+ select.appendChild(new Option(opt.label, opt.value));
803
+ }
804
+ try {
805
+ if (initialValue) select.value = initialValue;
806
+ } catch (e) {
807
+ }
808
+ select.onchange = (e) => {
809
+ const val = e.target.value;
810
+ options.onCommand(command, val);
811
+ select.selectedIndex = 0;
812
+ };
813
+ return select;
814
+ }
815
+ function makeColorInput(title, command, initialColor) {
816
+ const input = doc.createElement("input");
817
+ input.type = "color";
818
+ input.className = "toolbar-color-input";
819
+ const wrapper = doc.createElement("label");
820
+ wrapper.className = "color-label";
821
+ wrapper.appendChild(doc.createTextNode(title + " "));
822
+ wrapper.appendChild(input);
823
+ let savedRange = null;
824
+ input.addEventListener("pointerdown", () => {
825
+ const s = doc.getSelection();
826
+ if (s && s.rangeCount) savedRange = s.getRangeAt(0).cloneRange();
827
+ });
828
+ input.onchange = (e) => {
829
+ try {
830
+ const s = doc.getSelection();
831
+ if (savedRange && s) {
832
+ s.removeAllRanges();
833
+ s.addRange(savedRange);
834
+ }
835
+ } catch (err) {
836
+ }
837
+ options.onCommand(command, e.target.value);
838
+ savedRange = null;
839
+ };
840
+ function rgbToHex(input2) {
841
+ if (!input2) return null;
842
+ const v = input2.trim();
843
+ if (v.startsWith("#")) {
844
+ if (v.length === 4) {
845
+ return ("#" + v[1] + v[1] + v[2] + v[2] + v[3] + v[3]).toLowerCase();
846
+ }
847
+ return v.toLowerCase();
848
+ }
849
+ const rgbMatch = v.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
850
+ if (rgbMatch) {
851
+ const r = Number(rgbMatch[1]);
852
+ const g = Number(rgbMatch[2]);
853
+ const b = Number(rgbMatch[3]);
854
+ const hex = "#" + [r, g, b].map((n) => n.toString(16).padStart(2, "0")).join("").toLowerCase();
855
+ return hex;
856
+ }
857
+ return null;
858
+ }
859
+ const setColor = (val) => {
860
+ if (!val) return;
861
+ const hex = rgbToHex(val) || val;
862
+ try {
863
+ if (hex && hex.startsWith("#") && input.value !== hex) {
864
+ input.value = hex;
865
+ }
866
+ } catch (e) {
867
+ }
868
+ };
869
+ if (initialColor) setColor(initialColor);
870
+ input.addEventListener("input", (e) => {
871
+ const val = e.target.value;
872
+ setColor(val);
873
+ });
874
+ input.title = title;
875
+ input.setAttribute("aria-label", title);
876
+ return wrapper;
877
+ }
878
+ const format = options.getFormatState();
879
+ function makeGroup() {
880
+ const g = doc.createElement("div");
881
+ g.className = "toolbar-group";
882
+ return g;
883
+ }
884
+ function makeSep() {
885
+ const s = doc.createElement("div");
886
+ s.className = "toolbar-sep";
887
+ return s;
888
+ }
889
+ const undoBtn = makeButton(
890
+ LABEL_UNDO,
891
+ "Undo",
892
+ "undo",
893
+ void 0,
894
+ false,
895
+ !options.canUndo()
896
+ );
897
+ undoBtn.onclick = () => options.onUndo();
898
+ const redoBtn = makeButton(
899
+ LABEL_REDO,
900
+ "Redo",
901
+ "redo",
902
+ void 0,
903
+ false,
904
+ !options.canRedo()
905
+ );
906
+ redoBtn.onclick = () => options.onRedo();
907
+ const grp1 = makeGroup();
908
+ grp1.appendChild(undoBtn);
909
+ grp1.appendChild(redoBtn);
910
+ toolbar.appendChild(grp1);
911
+ toolbar.appendChild(makeSep());
912
+ const grp2 = makeGroup();
913
+ grp2.className = "toolbar-group collapse-on-small";
914
+ grp2.appendChild(
915
+ makeSelect(
916
+ "Format",
917
+ "formatBlock",
918
+ FORMAT_OPTIONS,
919
+ format.formatBlock
920
+ )
921
+ );
922
+ grp2.appendChild(
923
+ makeSelect("Font", "fontName", FONT_OPTIONS, format.fontName)
924
+ );
925
+ grp2.appendChild(
926
+ makeSelect("Size", "fontSize", SIZE_OPTIONS, format.fontSize)
927
+ );
928
+ toolbar.appendChild(grp2);
929
+ toolbar.appendChild(makeSep());
930
+ const grp3 = makeGroup();
931
+ grp3.appendChild(
932
+ makeButton(LABEL_BOLD, "Bold", "bold", void 0, format.bold)
933
+ );
934
+ grp3.appendChild(
935
+ makeButton(LABEL_ITALIC, "Italic", "italic", void 0, format.italic)
936
+ );
937
+ grp3.appendChild(
938
+ makeButton(
939
+ LABEL_UNDERLINE,
940
+ "Underline",
941
+ "underline",
942
+ void 0,
943
+ format.underline
944
+ )
945
+ );
946
+ grp3.appendChild(makeButton(LABEL_STRIKETHROUGH, "Strikethrough", "strike"));
947
+ toolbar.appendChild(grp3);
948
+ toolbar.appendChild(makeSep());
949
+ const grp4 = makeGroup();
950
+ grp4.appendChild(makeButton(LABEL_ALIGN_LEFT, "Align left", "align", "left"));
951
+ grp4.appendChild(
952
+ makeButton(LABEL_ALIGN_CENTER, "Align center", "align", "center")
953
+ );
954
+ grp4.appendChild(
955
+ makeButton(LABEL_ALIGN_RIGHT, "Align right", "align", "right")
956
+ );
957
+ toolbar.appendChild(grp4);
958
+ toolbar.appendChild(makeSep());
959
+ const grp5 = makeGroup();
960
+ grp5.className = "toolbar-group collapse-on-small";
961
+ grp5.appendChild(
962
+ makeColorInput("Text color", "foreColor", format.foreColor)
963
+ );
964
+ grp5.appendChild(
965
+ makeColorInput(
966
+ "Highlight color",
967
+ "hiliteColor",
968
+ format.hiliteColor
969
+ )
970
+ );
971
+ toolbar.appendChild(grp5);
972
+ toolbar.appendChild(makeSep());
973
+ const grp6 = makeGroup();
974
+ grp6.className = "toolbar-group collapse-on-small";
975
+ grp6.appendChild(makeButton(LABEL_LINK, "Insert link", "link"));
976
+ toolbar.appendChild(grp6);
977
+ const overflowBtn = doc.createElement("button");
978
+ overflowBtn.type = "button";
979
+ overflowBtn.className = "toolbar-overflow-btn";
980
+ overflowBtn.title = "More";
981
+ overflowBtn.setAttribute("aria-label", "More toolbar actions");
982
+ overflowBtn.setAttribute("aria-haspopup", "true");
983
+ overflowBtn.setAttribute("aria-expanded", "false");
984
+ overflowBtn.tabIndex = 0;
985
+ overflowBtn.innerHTML = "\u22EF";
986
+ const overflowMenu = doc.createElement("div");
987
+ overflowMenu.className = "toolbar-overflow-menu";
988
+ overflowMenu.setAttribute("role", "menu");
989
+ overflowMenu.hidden = true;
990
+ function openOverflow() {
991
+ overflowMenu.hidden = false;
992
+ overflowBtn.setAttribute("aria-expanded", "true");
993
+ const first = overflowMenu.querySelector(
994
+ "button, select, input"
995
+ );
996
+ first == null ? void 0 : first.focus();
997
+ }
998
+ function closeOverflow() {
999
+ overflowMenu.hidden = true;
1000
+ overflowBtn.setAttribute("aria-expanded", "false");
1001
+ overflowBtn.focus();
1002
+ }
1003
+ overflowBtn.addEventListener("click", (e) => {
1004
+ if (overflowMenu.hidden) openOverflow();
1005
+ else closeOverflow();
1006
+ });
1007
+ overflowBtn.addEventListener("keydown", (e) => {
1008
+ if (e.key === "Enter" || e.key === " ") {
1009
+ e.preventDefault();
1010
+ if (overflowMenu.hidden) openOverflow();
1011
+ else closeOverflow();
1012
+ }
1013
+ if (e.key === "ArrowDown") {
1014
+ e.preventDefault();
1015
+ if (overflowMenu.hidden) openOverflow();
1016
+ }
1017
+ });
1018
+ overflowMenu.addEventListener("keydown", (e) => {
1019
+ if (e.key === "Escape") {
1020
+ e.preventDefault();
1021
+ closeOverflow();
1022
+ }
1023
+ });
1024
+ doc.addEventListener("pointerdown", (ev) => {
1025
+ if (!overflowMenu.hidden && !overflowMenu.contains(ev.target) && ev.target !== overflowBtn) {
1026
+ closeOverflow();
1027
+ }
1028
+ });
1029
+ overflowMenu.appendChild(
1030
+ makeSelect(
1031
+ "Format",
1032
+ "formatBlock",
1033
+ FORMAT_OPTIONS,
1034
+ format.formatBlock
1035
+ )
1036
+ );
1037
+ overflowMenu.appendChild(
1038
+ makeSelect("Font", "fontName", FONT_OPTIONS, format.fontName)
1039
+ );
1040
+ overflowMenu.appendChild(
1041
+ makeSelect("Size", "fontSize", SIZE_OPTIONS, format.fontSize)
1042
+ );
1043
+ overflowMenu.appendChild(
1044
+ makeColorInput("Text color", "foreColor", format.foreColor)
1045
+ );
1046
+ overflowMenu.appendChild(
1047
+ makeColorInput(
1048
+ "Highlight color",
1049
+ "hiliteColor",
1050
+ format.hiliteColor
1051
+ )
1052
+ );
1053
+ overflowMenu.appendChild(makeButton(LABEL_LINK, "Insert link", "link"));
1054
+ const overflowWrap = makeGroup();
1055
+ overflowWrap.className = "toolbar-group toolbar-overflow-wrap";
1056
+ overflowWrap.appendChild(overflowBtn);
1057
+ overflowWrap.appendChild(overflowMenu);
1058
+ toolbar.appendChild(overflowWrap);
1059
+ toolbar.addEventListener("keydown", (e) => {
1060
+ const focusable = Array.from(
1061
+ toolbar.querySelectorAll("button, select, input, [tabindex]")
1062
+ ).filter((el) => !el.hasAttribute("disabled"));
1063
+ if (!focusable.length) return;
1064
+ const idx = focusable.indexOf(document.activeElement);
1065
+ if (e.key === "ArrowRight") {
1066
+ e.preventDefault();
1067
+ const next = focusable[Math.min(focusable.length - 1, Math.max(0, idx + 1))];
1068
+ next == null ? void 0 : next.focus();
1069
+ } else if (e.key === "ArrowLeft") {
1070
+ e.preventDefault();
1071
+ const prev = focusable[Math.max(0, idx - 1)] || focusable[0];
1072
+ prev == null ? void 0 : prev.focus();
1073
+ } else if (e.key === "Home") {
1074
+ e.preventDefault();
1075
+ focusable[0].focus();
1076
+ } else if (e.key === "End") {
1077
+ e.preventDefault();
1078
+ focusable[focusable.length - 1].focus();
1079
+ }
1080
+ });
1081
+ doc.body.insertBefore(toolbar, doc.body.firstChild);
1082
+ }
1083
+
1084
+ // src/dom/handlers.ts
1085
+ init_state();
1086
+
1087
+ // src/dom/candidates.ts
1088
+ function isEditableCandidate(el) {
1089
+ if (!el) return false;
1090
+ const tag = el.tagName;
1091
+ const DISALLOWED = [
1092
+ "HTML",
1093
+ "HEAD",
1094
+ "BODY",
1095
+ "SCRIPT",
1096
+ "STYLE",
1097
+ "LINK",
1098
+ "META",
1099
+ "NOSCRIPT"
1100
+ ];
1101
+ if (DISALLOWED.includes(tag)) return false;
1102
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return false;
1103
+ return true;
1104
+ }
1105
+
1106
+ // src/dom/format.ts
1107
+ function computeFormatState(doc) {
1108
+ var _a, _b;
1109
+ try {
1110
+ const s = doc.getSelection();
1111
+ let el = null;
1112
+ if (s && s.anchorNode)
1113
+ el = s.anchorNode.nodeType === Node.ELEMENT_NODE ? s.anchorNode : s.anchorNode.parentElement;
1114
+ if (!el)
1115
+ return {
1116
+ bold: false,
1117
+ italic: false,
1118
+ underline: false,
1119
+ foreColor: null,
1120
+ hiliteColor: null,
1121
+ fontName: null,
1122
+ fontSize: null,
1123
+ formatBlock: null
1124
+ };
1125
+ const computed = (_a = doc.defaultView) == null ? void 0 : _a.getComputedStyle(el);
1126
+ const bold = !!(el.closest("strong, b") || computed && (computed.fontWeight === "700" || Number(computed.fontWeight) >= 700));
1127
+ const italic = !!(el.closest("em, i") || computed && computed.fontStyle === "italic");
1128
+ const underline = !!(el.closest("u") || computed && (computed.textDecorationLine || "").includes("underline"));
1129
+ const foreColor = ((_b = el.closest("font[color]")) == null ? void 0 : _b.getAttribute(
1130
+ "color"
1131
+ )) || computed && computed.color || null;
1132
+ const mark = el.closest("mark");
1133
+ const hiliteColor = mark && (mark.getAttribute("style") || "") || (computed && computed.backgroundColor && computed.backgroundColor !== "rgba(0, 0, 0, 0)" ? computed.backgroundColor : null);
1134
+ const fontName = computed && computed.fontFamily || null;
1135
+ const fontSize = computed && computed.fontSize || null;
1136
+ let blockEl = el;
1137
+ while (blockEl && blockEl.parentElement) {
1138
+ const tag = blockEl.tagName;
1139
+ if ([
1140
+ "P",
1141
+ "DIV",
1142
+ "SECTION",
1143
+ "ARTICLE",
1144
+ "LI",
1145
+ "TD",
1146
+ "BLOCKQUOTE",
1147
+ "H1",
1148
+ "H2",
1149
+ "H3",
1150
+ "H4",
1151
+ "H5",
1152
+ "H6"
1153
+ ].includes(tag)) {
1154
+ break;
1155
+ }
1156
+ blockEl = blockEl.parentElement;
1157
+ }
1158
+ const formatBlock = blockEl ? blockEl.tagName.toLowerCase() : null;
1159
+ return {
1160
+ bold,
1161
+ italic,
1162
+ underline,
1163
+ foreColor,
1164
+ hiliteColor,
1165
+ fontName,
1166
+ fontSize,
1167
+ formatBlock
1168
+ };
1169
+ } catch (err) {
1170
+ return {
1171
+ bold: false,
1172
+ italic: false,
1173
+ underline: false,
1174
+ foreColor: null,
1175
+ hiliteColor: null,
1176
+ fontName: null,
1177
+ fontSize: null,
1178
+ formatBlock: null
1179
+ };
1180
+ }
1181
+ }
1182
+ function getElementLabel(el) {
1183
+ if (!el) return null;
1184
+ const id = el.id ? `#${el.id}` : "";
1185
+ const cls = el.className ? `.${String(el.className).split(" ")[0]}` : "";
1186
+ const tag = el.tagName.toLowerCase();
1187
+ return `${tag}${id}${cls}`;
1188
+ }
1189
+
1190
+ // src/core/sanitizeURL.ts
1191
+ function sanitizeURL(url) {
1192
+ if (!url) return "";
1193
+ const trimmed = url.trim();
1194
+ if (!trimmed) return "";
1195
+ if (trimmed.toLowerCase().startsWith("javascript:") || trimmed.toLowerCase().startsWith("data:")) {
1196
+ console.warn("Blocked potentially dangerous URL protocol");
1197
+ return "";
1198
+ }
1199
+ if (!trimmed.startsWith("http") && !trimmed.startsWith("#")) {
1200
+ return "https://" + trimmed;
1201
+ }
1202
+ return trimmed;
1203
+ }
1204
+
1205
+ // src/core/history.ts
1206
+ init_state();
1207
+ init_sanitize();
1208
+ function handleUndo() {
1209
+ try {
1210
+ const doc = _getDoc();
1211
+ if (!doc) {
1212
+ console.warn(
1213
+ "[rich-html-editor] handleUndo called before initialization"
1214
+ );
1215
+ return;
1216
+ }
1217
+ if (_getUndoStack().length < 2) return;
1218
+ const undoStack = _getUndoStack();
1219
+ const redoStack = _getRedoStack();
1220
+ const current = undoStack.pop();
1221
+ redoStack.push(current);
1222
+ const prev = undoStack[undoStack.length - 1];
1223
+ if (!doc.documentElement) {
1224
+ throw new Error("Document is missing documentElement");
1225
+ }
1226
+ const safe = sanitizeHtml(prev.replace(/^<!doctype html>\n?/i, ""), doc);
1227
+ try {
1228
+ const parser = new DOMParser();
1229
+ const parsed = parser.parseFromString(safe, "text/html");
1230
+ if (parsed && parsed.body && doc.body) {
1231
+ const parsedEls = parsed.body.querySelectorAll("[data-rhe-id]");
1232
+ if (parsedEls && parsedEls.length) {
1233
+ const loadPromises = [];
1234
+ parsedEls.forEach((pe) => {
1235
+ const id = pe.getAttribute("data-rhe-id");
1236
+ if (!id) return;
1237
+ const local = doc.body.querySelector(`[data-rhe-id="${id}"]`);
1238
+ if (!local) return;
1239
+ try {
1240
+ Array.from(local.attributes).forEach((a) => {
1241
+ if (a.name !== "data-rhe-id") local.removeAttribute(a.name);
1242
+ });
1243
+ Array.from(pe.attributes).forEach((a) => {
1244
+ if (a.name !== "data-rhe-id")
1245
+ local.setAttribute(a.name, a.value);
1246
+ });
1247
+ } catch (err) {
1248
+ }
1249
+ try {
1250
+ local.innerHTML = pe.innerHTML;
1251
+ } catch (err) {
1252
+ }
1253
+ try {
1254
+ const placeholders = pe.querySelectorAll("[data-rhe-script]");
1255
+ placeholders.forEach((ph) => {
1256
+ const encoded = ph.getAttribute("data-rhe-script") || "";
1257
+ let code = "";
1258
+ try {
1259
+ code = typeof atob !== "undefined" ? decodeURIComponent(escape(atob(encoded))) : decodeURIComponent(encoded);
1260
+ } catch (e) {
1261
+ try {
1262
+ code = decodeURIComponent(encoded);
1263
+ } catch (er) {
1264
+ code = "";
1265
+ }
1266
+ }
1267
+ const attrsRaw = ph.getAttribute("data-rhe-script-attrs");
1268
+ let attrs = {};
1269
+ if (attrsRaw) {
1270
+ try {
1271
+ attrs = JSON.parse(decodeURIComponent(attrsRaw));
1272
+ } catch (e) {
1273
+ attrs = {};
1274
+ }
1275
+ }
1276
+ const parentId = ph.getAttribute("data-rhe-script-parent");
1277
+ try {
1278
+ const s = doc.createElement("script");
1279
+ try {
1280
+ s.type = "text/javascript";
1281
+ s.async = false;
1282
+ } catch (err) {
1283
+ }
1284
+ Object.keys(attrs).forEach(
1285
+ (k) => s.setAttribute(k, attrs[k])
1286
+ );
1287
+ if (attrs.src) {
1288
+ const p = new Promise((resolve) => {
1289
+ s.addEventListener("load", () => resolve());
1290
+ s.addEventListener("error", () => resolve());
1291
+ });
1292
+ loadPromises.push(p);
1293
+ s.src = attrs.src;
1294
+ } else {
1295
+ s.textContent = code;
1296
+ }
1297
+ if (parentId === "head") {
1298
+ doc.head.appendChild(s);
1299
+ } else {
1300
+ const target = doc.body.querySelector(
1301
+ `[data-rhe-id="${parentId}"]`
1302
+ );
1303
+ if (target) target.appendChild(s);
1304
+ else doc.body.appendChild(s);
1305
+ }
1306
+ } catch (e) {
1307
+ }
1308
+ });
1309
+ } catch (e) {
1310
+ }
1311
+ });
1312
+ try {
1313
+ if (loadPromises.length) {
1314
+ const waiter = Promise.allSettled ? Promise.allSettled(loadPromises) : Promise.all(
1315
+ loadPromises.map((p) => p.catch(() => void 0))
1316
+ );
1317
+ waiter.then(() => {
1318
+ try {
1319
+ doc.dispatchEvent(new Event("rhe:scripts-restored"));
1320
+ } catch (e) {
1321
+ }
1322
+ });
1323
+ } else {
1324
+ try {
1325
+ doc.dispatchEvent(new Event("rhe:scripts-restored"));
1326
+ } catch (e) {
1327
+ }
1328
+ }
1329
+ } catch (e) {
1330
+ }
1331
+ } else {
1332
+ doc.body.innerHTML = parsed.body.innerHTML;
1333
+ }
1334
+ } else {
1335
+ doc.documentElement.innerHTML = safe;
1336
+ }
1337
+ } catch (err) {
1338
+ doc.documentElement.innerHTML = safe;
1339
+ }
1340
+ injectStyles(doc);
1341
+ try {
1342
+ doc.dispatchEvent(new Event("selectionchange"));
1343
+ } catch (err) {
1344
+ }
1345
+ } catch (error) {
1346
+ const message = error instanceof Error ? error.message : String(error);
1347
+ console.error("[rich-html-editor] Undo failed:", message);
1348
+ }
1349
+ }
1350
+ function handleRedo() {
1351
+ try {
1352
+ const doc = _getDoc();
1353
+ if (!doc) {
1354
+ console.warn(
1355
+ "[rich-html-editor] handleRedo called before initialization"
1356
+ );
1357
+ return;
1358
+ }
1359
+ if (!_getRedoStack().length) return;
1360
+ const undoStack = _getUndoStack();
1361
+ const redoStack = _getRedoStack();
1362
+ const next = redoStack.pop();
1363
+ undoStack.push(next);
1364
+ if (!doc.documentElement) {
1365
+ throw new Error("Document is missing documentElement");
1366
+ }
1367
+ const safeNext = sanitizeHtml(
1368
+ next.replace(/^<!doctype html>\n?/i, ""),
1369
+ doc
1370
+ );
1371
+ try {
1372
+ const parser = new DOMParser();
1373
+ const parsed = parser.parseFromString(safeNext, "text/html");
1374
+ if (parsed && parsed.body && doc.body) {
1375
+ const parsedEls = parsed.body.querySelectorAll("[data-rhe-id]");
1376
+ if (parsedEls && parsedEls.length) {
1377
+ const loadPromises = [];
1378
+ parsedEls.forEach((pe) => {
1379
+ const id = pe.getAttribute("data-rhe-id");
1380
+ if (!id) return;
1381
+ const local = doc.body.querySelector(`[data-rhe-id="${id}"]`);
1382
+ if (!local) return;
1383
+ try {
1384
+ Array.from(local.attributes).forEach((a) => {
1385
+ if (a.name !== "data-rhe-id") local.removeAttribute(a.name);
1386
+ });
1387
+ Array.from(pe.attributes).forEach((a) => {
1388
+ if (a.name !== "data-rhe-id")
1389
+ local.setAttribute(a.name, a.value);
1390
+ });
1391
+ } catch (err) {
1392
+ }
1393
+ try {
1394
+ local.innerHTML = pe.innerHTML;
1395
+ } catch (err) {
1396
+ }
1397
+ try {
1398
+ const placeholders = pe.querySelectorAll("[data-rhe-script]");
1399
+ placeholders.forEach((ph) => {
1400
+ const encoded = ph.getAttribute("data-rhe-script") || "";
1401
+ let code = "";
1402
+ try {
1403
+ code = typeof atob !== "undefined" ? decodeURIComponent(escape(atob(encoded))) : decodeURIComponent(encoded);
1404
+ } catch (e) {
1405
+ try {
1406
+ code = decodeURIComponent(encoded);
1407
+ } catch (er) {
1408
+ code = "";
1409
+ }
1410
+ }
1411
+ const attrsRaw = ph.getAttribute("data-rhe-script-attrs");
1412
+ let attrs = {};
1413
+ if (attrsRaw) {
1414
+ try {
1415
+ attrs = JSON.parse(decodeURIComponent(attrsRaw));
1416
+ } catch (e) {
1417
+ attrs = {};
1418
+ }
1419
+ }
1420
+ const parentId = ph.getAttribute("data-rhe-script-parent");
1421
+ try {
1422
+ const s = doc.createElement("script");
1423
+ try {
1424
+ s.type = "text/javascript";
1425
+ s.async = false;
1426
+ } catch (err) {
1427
+ }
1428
+ Object.keys(attrs).forEach(
1429
+ (k) => s.setAttribute(k, attrs[k])
1430
+ );
1431
+ if (attrs.src) {
1432
+ const p = new Promise((resolve) => {
1433
+ s.addEventListener("load", () => resolve());
1434
+ s.addEventListener("error", () => resolve());
1435
+ });
1436
+ loadPromises.push(p);
1437
+ s.src = attrs.src;
1438
+ } else {
1439
+ s.textContent = code;
1440
+ }
1441
+ if (parentId === "head") {
1442
+ doc.head.appendChild(s);
1443
+ } else {
1444
+ const target = doc.body.querySelector(
1445
+ `[data-rhe-id="${parentId}"]`
1446
+ );
1447
+ if (target) target.appendChild(s);
1448
+ else doc.body.appendChild(s);
1449
+ }
1450
+ } catch (e) {
1451
+ }
1452
+ });
1453
+ } catch (e) {
1454
+ }
1455
+ });
1456
+ try {
1457
+ if (loadPromises.length) {
1458
+ const waiter = Promise.allSettled ? Promise.allSettled(loadPromises) : Promise.all(
1459
+ loadPromises.map((p) => p.catch(() => void 0))
1460
+ );
1461
+ waiter.then(() => {
1462
+ try {
1463
+ doc.dispatchEvent(new Event("rhe:scripts-restored"));
1464
+ } catch (e) {
1465
+ }
1466
+ });
1467
+ } else {
1468
+ try {
1469
+ doc.dispatchEvent(new Event("rhe:scripts-restored"));
1470
+ } catch (e) {
1471
+ }
1472
+ }
1473
+ } catch (e) {
1474
+ }
1475
+ } else {
1476
+ doc.body.innerHTML = parsed.body.innerHTML;
1477
+ }
1478
+ } else {
1479
+ doc.documentElement.innerHTML = safeNext;
1480
+ }
1481
+ } catch (err) {
1482
+ doc.documentElement.innerHTML = safeNext;
1483
+ }
1484
+ injectStyles(doc);
1485
+ try {
1486
+ doc.dispatchEvent(new Event("selectionchange"));
1487
+ } catch (err) {
1488
+ }
1489
+ } catch (error) {
1490
+ const message = error instanceof Error ? error.message : String(error);
1491
+ console.error("[rich-html-editor] Redo failed:", message);
1492
+ }
1493
+ }
1494
+
1495
+ // src/core/formatActions.ts
1496
+ init_state();
1497
+ function handleToolbarCommand(command, value) {
1498
+ try {
1499
+ const doc = _getDoc();
1500
+ if (!doc) {
1501
+ console.warn(
1502
+ "[rich-html-editor] handleToolbarCommand called before initialization"
1503
+ );
1504
+ return;
1505
+ }
1506
+ if (command === "undo") return;
1507
+ if (command === "redo") return;
1508
+ if (command === "link") {
1509
+ const url = window.prompt("Enter URL (https://...):", "https://");
1510
+ if (url) {
1511
+ const sanitized = sanitizeURL(url);
1512
+ if (sanitized) applyStandaloneCommand("link", sanitized);
1513
+ }
1514
+ return;
1515
+ }
1516
+ applyStandaloneCommand(command, value);
1517
+ } catch (error) {
1518
+ const message = error instanceof Error ? error.message : String(error);
1519
+ console.error("[rich-html-editor] Command handler failed:", message);
1520
+ }
1521
+ }
1522
+ function applyStandaloneCommand(command, value) {
1523
+ try {
1524
+ const doc = _getDoc();
1525
+ if (!doc) {
1526
+ console.warn(
1527
+ "[rich-html-editor] applyStandaloneCommand called before initialization"
1528
+ );
1529
+ return;
1530
+ }
1531
+ if (command === "bold") wrapSelectionWithElement(doc, "strong");
1532
+ else if (command === "italic") wrapSelectionWithElement(doc, "em");
1533
+ else if (command === "underline") wrapSelectionWithElement(doc, "u");
1534
+ else if (command === "strike") wrapSelectionWithElement(doc, "s");
1535
+ else if (command === "fontName")
1536
+ wrapSelectionWithElement(doc, "span", { fontFamily: value });
1537
+ else if (command === "fontSize") {
1538
+ const raw = value || "14";
1539
+ const n = parseInt(raw, 10);
1540
+ const sz = Number.isFinite(n) ? `${n}px` : raw;
1541
+ wrapSelectionWithElement(doc, "span", { fontSize: sz });
1542
+ } else if (command === "link") {
1543
+ const sel = doc.getSelection();
1544
+ if (!sel || !sel.rangeCount) return;
1545
+ const range = sel.getRangeAt(0);
1546
+ const content = range.extractContents();
1547
+ const a = doc.createElement("a");
1548
+ a.href = sanitizeURL(value || "#");
1549
+ a.appendChild(content);
1550
+ range.insertNode(a);
1551
+ } else if (command === "foreColor")
1552
+ wrapSelectionWithElement(doc, "span", { color: value });
1553
+ else if (command === "hiliteColor")
1554
+ wrapSelectionWithElement(doc, "span", { backgroundColor: value });
1555
+ else if (command === "align") {
1556
+ const sel = doc.getSelection();
1557
+ const node = (sel == null ? void 0 : sel.anchorNode) || null;
1558
+ const block = findBlockAncestor(node);
1559
+ if (block) block.style.textAlign = value || "left";
1560
+ else wrapSelectionWithElement(doc, "div", { textAlign: value });
1561
+ } else if (command === "formatBlock") {
1562
+ const sel = doc.getSelection();
1563
+ const node = (sel == null ? void 0 : sel.anchorNode) || null;
1564
+ const block = findBlockAncestor(node);
1565
+ const tag = (value || "p").toLowerCase();
1566
+ if (block && block.parentElement) {
1567
+ const newEl = doc.createElement(tag);
1568
+ newEl.className = block.className || "";
1569
+ while (block.firstChild) newEl.appendChild(block.firstChild);
1570
+ block.parentElement.replaceChild(newEl, block);
1571
+ } else {
1572
+ wrapSelectionWithElement(doc, tag);
1573
+ }
1574
+ }
1575
+ pushStandaloneSnapshot();
1576
+ } catch (error) {
1577
+ const message = error instanceof Error ? error.message : String(error);
1578
+ console.error("[rich-html-editor] Apply command failed:", message);
1579
+ }
1580
+ }
1581
+ function wrapSelectionWithElement(doc, tagName, style) {
1582
+ const sel = doc.getSelection();
1583
+ if (!sel) return;
1584
+ if (!sel.rangeCount) return;
1585
+ const range = sel.getRangeAt(0);
1586
+ if (range.collapsed) {
1587
+ const el = doc.createElement(tagName);
1588
+ if (style) Object.assign(el.style, style);
1589
+ const zw = doc.createTextNode("\u200B");
1590
+ el.appendChild(zw);
1591
+ range.insertNode(el);
1592
+ const newRange2 = doc.createRange();
1593
+ newRange2.setStart(zw, 1);
1594
+ newRange2.collapse(true);
1595
+ sel.removeAllRanges();
1596
+ sel.addRange(newRange2);
1597
+ return;
1598
+ }
1599
+ const content = range.extractContents();
1600
+ const wrapper = doc.createElement(tagName);
1601
+ if (style) Object.assign(wrapper.style, style);
1602
+ wrapper.appendChild(content);
1603
+ range.insertNode(wrapper);
1604
+ sel.removeAllRanges();
1605
+ const newRange = doc.createRange();
1606
+ newRange.selectNodeContents(wrapper);
1607
+ sel.addRange(newRange);
1608
+ }
1609
+ function findBlockAncestor(node) {
1610
+ let n = node;
1611
+ const BLOCKS = [
1612
+ "P",
1613
+ "DIV",
1614
+ "SECTION",
1615
+ "ARTICLE",
1616
+ "LI",
1617
+ "TD",
1618
+ "BLOCKQUOTE",
1619
+ "H1",
1620
+ "H2",
1621
+ "H3",
1622
+ "H4",
1623
+ "H5",
1624
+ "H6"
1625
+ ];
1626
+ while (n) {
1627
+ if (n.nodeType === Node.ELEMENT_NODE) {
1628
+ const el = n;
1629
+ if (BLOCKS.includes(el.tagName)) return el;
1630
+ }
1631
+ n = n.parentNode;
1632
+ }
1633
+ return null;
1634
+ }
1635
+
1636
+ // src/dom/handlers.ts
1637
+ init_constants();
1638
+ function attachStandaloneHandlers(doc) {
1639
+ try {
1640
+ const selector = [
1641
+ "p",
1642
+ "div",
1643
+ "section",
1644
+ "article",
1645
+ "header",
1646
+ "footer",
1647
+ "aside",
1648
+ "nav",
1649
+ "span",
1650
+ "h1",
1651
+ "h2",
1652
+ "h3",
1653
+ "h4",
1654
+ "h5",
1655
+ "h6",
1656
+ "li",
1657
+ "figure",
1658
+ "figcaption",
1659
+ "blockquote",
1660
+ "pre",
1661
+ "code"
1662
+ ].join(",");
1663
+ const candidates = Array.from(
1664
+ doc.querySelectorAll(selector)
1665
+ ).filter((el) => isEditableCandidate(el));
1666
+ candidates.forEach((target) => {
1667
+ if (!target.hasAttribute("data-rhe-id")) {
1668
+ const uid = `rhe-init-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
1669
+ try {
1670
+ target.setAttribute("data-rhe-id", uid);
1671
+ } catch (err) {
1672
+ }
1673
+ }
1674
+ });
1675
+ } catch (err) {
1676
+ }
1677
+ doc.addEventListener(
1678
+ "click",
1679
+ (e) => {
1680
+ var _a, _b;
1681
+ const target = e.target;
1682
+ if (!isEditableCandidate(target)) return;
1683
+ if (_getCurrentEditable() && _getCurrentEditable() !== target) {
1684
+ (_a = _getCurrentEditable()) == null ? void 0 : _a.removeAttribute("contenteditable");
1685
+ (_b = _getCurrentEditable()) == null ? void 0 : _b.classList.remove(CLASS_ACTIVE);
1686
+ }
1687
+ if (!target.hasAttribute("data-rhe-id")) {
1688
+ const uid = `rhe-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
1689
+ try {
1690
+ target.setAttribute("data-rhe-id", uid);
1691
+ } catch (err) {
1692
+ }
1693
+ }
1694
+ _setCurrentEditable(target);
1695
+ target.classList.add(CLASS_ACTIVE);
1696
+ target.setAttribute("contenteditable", "true");
1697
+ target.focus();
1698
+ },
1699
+ true
1700
+ );
1701
+ doc.addEventListener("selectionchange", () => {
1702
+ injectToolbar(doc, {
1703
+ onCommand: handleToolbarCommand,
1704
+ canUndo: () => _getUndoStack().length > 1,
1705
+ canRedo: () => _getRedoStack().length > 0,
1706
+ onUndo: handleUndo,
1707
+ onRedo: handleRedo,
1708
+ getFormatState: () => computeFormatState(doc),
1709
+ getSelectedElementInfo: () => getElementLabel(_getCurrentEditable())
1710
+ });
1711
+ });
1712
+ doc.addEventListener("input", () => pushStandaloneSnapshot(), true);
1713
+ doc.addEventListener(
1714
+ "keydown",
1715
+ (e) => {
1716
+ const meta = e.ctrlKey || e.metaKey;
1717
+ if (!meta) return;
1718
+ const key = e.key.toLowerCase();
1719
+ if (key === "b") {
1720
+ e.preventDefault();
1721
+ handleToolbarCommand("bold");
1722
+ return;
1723
+ }
1724
+ if (key === "i") {
1725
+ e.preventDefault();
1726
+ handleToolbarCommand("italic");
1727
+ return;
1728
+ }
1729
+ if (key === "u") {
1730
+ e.preventDefault();
1731
+ handleToolbarCommand("underline");
1732
+ return;
1733
+ }
1734
+ if (key === "z") {
1735
+ e.preventDefault();
1736
+ if (e.shiftKey) {
1737
+ handleRedo();
1738
+ } else {
1739
+ handleUndo();
1740
+ }
1741
+ return;
1742
+ }
1743
+ if (key === "y") {
1744
+ e.preventDefault();
1745
+ handleRedo();
1746
+ return;
1747
+ }
1748
+ },
1749
+ true
1750
+ );
1751
+ }
1752
+
1753
+ // src/core/editor.ts
1754
+ init_constants();
1755
+ function initRichEditor(iframe, config) {
1756
+ try {
1757
+ if (!iframe || !(iframe instanceof HTMLIFrameElement)) {
1758
+ throw new Error("Invalid iframe element provided to initRichEditor");
1759
+ }
1760
+ const doc = iframe.contentDocument;
1761
+ if (!doc) {
1762
+ throw new Error(
1763
+ "Unable to access iframe contentDocument. Ensure iframe src is same-origin."
1764
+ );
1765
+ }
1766
+ _setDoc(doc);
1767
+ injectStyles(doc);
1768
+ _setUndoStack([]);
1769
+ _setRedoStack([]);
1770
+ _setCurrentEditable(null);
1771
+ if (config == null ? void 0 : config.maxStackSize) {
1772
+ Promise.resolve().then(() => (init_state(), state_exports)).then((m) => m.setMaxStackSize(config.maxStackSize)).catch(() => {
1773
+ });
1774
+ }
1775
+ attachStandaloneHandlers(doc);
1776
+ pushStandaloneSnapshot();
1777
+ injectToolbar(doc, {
1778
+ onCommand: handleToolbarCommand,
1779
+ canUndo: () => _getUndoStack().length > 1,
1780
+ canRedo: () => _getRedoStack().length > 0,
1781
+ onUndo: handleUndo,
1782
+ onRedo: handleRedo,
1783
+ getFormatState: () => computeFormatState(doc),
1784
+ getSelectedElementInfo: () => getElementLabel(_getCurrentEditable())
1785
+ });
1786
+ } catch (error) {
1787
+ const message = error instanceof Error ? error.message : String(error);
1788
+ console.error("[rich-html-editor] Failed to initialize editor:", message);
1789
+ throw error;
1790
+ }
1791
+ }
1792
+ function getCleanHTML() {
1793
+ try {
1794
+ const doc = _getDoc();
1795
+ if (!doc) {
1796
+ console.warn(
1797
+ "[rich-html-editor] getCleanHTML called before editor initialization"
1798
+ );
1799
+ return "";
1800
+ }
1801
+ if (!doc.documentElement) {
1802
+ throw new Error("Document is missing documentElement");
1803
+ }
1804
+ const clone = doc.documentElement.cloneNode(true);
1805
+ const toolbarNode = clone.querySelector(`#${TOOLBAR_ID}`);
1806
+ if (toolbarNode && toolbarNode.parentNode)
1807
+ toolbarNode.parentNode.removeChild(toolbarNode);
1808
+ const styleNode = clone.querySelector(`#${STYLE_ID}`);
1809
+ if (styleNode && styleNode.parentNode)
1810
+ styleNode.parentNode.removeChild(styleNode);
1811
+ try {
1812
+ const cleanElement = (el) => {
1813
+ try {
1814
+ if (el.hasAttribute("contenteditable"))
1815
+ el.removeAttribute("contenteditable");
1816
+ if (el.hasAttribute("tabindex")) el.removeAttribute("tabindex");
1817
+ } catch (e) {
1818
+ }
1819
+ try {
1820
+ const attrs = Array.from(el.attributes || []);
1821
+ attrs.forEach((a) => {
1822
+ const rawName = a.name;
1823
+ const name = rawName.toLowerCase();
1824
+ if (name.startsWith("on")) {
1825
+ try {
1826
+ el.removeAttribute(rawName);
1827
+ } catch (e) {
1828
+ }
1829
+ return;
1830
+ }
1831
+ if (name === "data-rhe-id" || name.startsWith("data-rhe-") || name === "data-rhe") {
1832
+ try {
1833
+ el.removeAttribute(rawName);
1834
+ } catch (e) {
1835
+ }
1836
+ return;
1837
+ }
1838
+ });
1839
+ } catch (e) {
1840
+ }
1841
+ try {
1842
+ if (el.id) {
1843
+ const id = el.id;
1844
+ if (id === TOOLBAR_ID || id === STYLE_ID || id.startsWith("editor-") || id.startsWith("rhe-")) {
1845
+ el.removeAttribute("id");
1846
+ }
1847
+ }
1848
+ } catch (e) {
1849
+ }
1850
+ try {
1851
+ const cls = Array.from(el.classList || []);
1852
+ cls.forEach((c) => {
1853
+ if (c === CLASS_EDITABLE || c === CLASS_ACTIVE || c.startsWith("editor-") || c.startsWith("rhe-")) {
1854
+ try {
1855
+ el.classList.remove(c);
1856
+ } catch (e) {
1857
+ }
1858
+ }
1859
+ });
1860
+ if (el.hasAttribute("class") && (el.getAttribute("class") || "").trim() === "") {
1861
+ try {
1862
+ el.removeAttribute("class");
1863
+ } catch (e) {
1864
+ }
1865
+ }
1866
+ } catch (e) {
1867
+ }
1868
+ try {
1869
+ const children = Array.from(el.children || []);
1870
+ children.forEach((child) => cleanElement(child));
1871
+ } catch (e) {
1872
+ }
1873
+ };
1874
+ if (clone.nodeType === Node.ELEMENT_NODE) {
1875
+ cleanElement(clone);
1876
+ }
1877
+ } catch (e) {
1878
+ }
1879
+ return "<!doctype html>\n" + clone.outerHTML;
1880
+ } catch (error) {
1881
+ const message = error instanceof Error ? error.message : String(error);
1882
+ console.error("[rich-html-editor] Failed to get clean HTML:", message);
1883
+ throw error;
1884
+ }
1885
+ }
1886
+
1887
+ // src/index.ts
1888
+ init_events();
1889
+ // Annotate the CommonJS export names for ESM import in node:
1890
+ 0 && (module.exports = {
1891
+ editorEventEmitter,
1892
+ getCleanHTML,
1893
+ getEditorEventEmitter,
1894
+ initRichEditor
1895
+ });
1896
+ //# sourceMappingURL=index.js.map