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/LICENSE +21 -0
- package/README.md +263 -0
- package/dist/chunk-ZDGUOGND.mjs +441 -0
- package/dist/chunk-ZDGUOGND.mjs.map +1 -0
- package/dist/index.d.mts +73 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +1896 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1439 -0
- package/dist/index.mjs.map +1 -0
- package/dist/state-CZIMHTJ3.mjs +25 -0
- package/dist/state-CZIMHTJ3.mjs.map +1 -0
- package/package.json +62 -0
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
|