@oix1987/yjd 1.0.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +223 -142
- package/core.js +82 -0
- package/dist/core.esm.js +2 -0
- package/dist/core.esm.js.map +1 -0
- package/dist/rich-editor.esm.js +1 -1
- package/dist/rich-editor.esm.js.map +1 -1
- package/dist/rich-editor.min.js +1 -1
- package/dist/rich-editor.min.js.map +1 -1
- package/index.d.ts +230 -103
- package/index.js +297 -0
- package/lib/core/editor.js +1885 -0
- package/lib/core/format.js +540 -0
- package/lib/core/module.js +81 -0
- package/lib/core/registry.js +158 -0
- package/lib/formats/background.js +213 -0
- package/lib/formats/bold.js +49 -0
- package/lib/formats/capitalization.js +579 -0
- package/lib/formats/color.js +183 -0
- package/lib/formats/emoji.js +282 -0
- package/lib/formats/font-family.js +548 -0
- package/lib/formats/heading.js +502 -0
- package/lib/formats/image.js +341 -0
- package/lib/formats/import.js +385 -0
- package/lib/formats/indent.js +297 -0
- package/lib/formats/italic.js +27 -0
- package/lib/formats/line-height.js +562 -0
- package/lib/formats/link.js +251 -0
- package/lib/formats/list.js +635 -0
- package/lib/formats/strike.js +31 -0
- package/lib/formats/subscript.js +40 -0
- package/lib/formats/superscript.js +39 -0
- package/lib/formats/table.js +293 -0
- package/lib/formats/tag.js +304 -0
- package/lib/formats/text-align.js +422 -0
- package/lib/formats/text-size.js +498 -0
- package/lib/formats/underline.js +30 -0
- package/lib/formats/video.js +381 -0
- package/lib/modules/block-toolbar.js +639 -0
- package/lib/modules/code-view.js +447 -0
- package/lib/modules/find-replace.js +273 -0
- package/lib/modules/history.js +425 -0
- package/lib/modules/mention.js +200 -0
- package/lib/modules/resize-handles.js +701 -0
- package/lib/modules/slash-menu.js +183 -0
- package/lib/modules/table-toolbar.js +635 -0
- package/lib/modules/toolbar.js +607 -0
- package/lib/serialize.js +241 -0
- package/lib/static.js +28 -0
- package/lib/styles-loader.js +142 -0
- package/{dist → lib}/styles.css +1392 -35
- package/lib/styles.css.js +2 -0
- package/lib/styles.min.css +1 -0
- package/lib/ui/color-picker.js +296 -0
- package/lib/ui/customselect.js +351 -0
- package/lib/ui/emoji-picker.js +196 -0
- package/lib/ui/icons.js +145 -0
- package/lib/ui/image-popup.js +435 -0
- package/lib/ui/import-popup.js +288 -0
- package/lib/ui/link-popup.js +139 -0
- package/lib/ui/list-picker.js +307 -0
- package/lib/ui/select-button.js +68 -0
- package/lib/ui/table-popup.js +171 -0
- package/lib/ui/tag-popup.js +249 -0
- package/lib/ui/text-align-picker.js +278 -0
- package/lib/ui/video-popup.js +413 -0
- package/lib/utils/exec-command.js +72 -0
- package/lib/utils/history-helper.js +50 -0
- package/lib/utils/popup-helper.js +219 -0
- package/lib/utils/popup-positioning.js +234 -0
- package/lib/utils/sanitize.js +164 -0
- package/package.json +51 -32
- package/umd-entry.js +19 -0
package/lib/serialize.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialization for yjd content — HTML <-> Markdown and HTML <-> JSON.
|
|
3
|
+
*
|
|
4
|
+
* Targeted at the HTML yjd emits (headings, inline marks, lists, links,
|
|
5
|
+
* images, blockquote, code, tables, hr, and mention tokens). Browser-only
|
|
6
|
+
* (uses the DOM). Zero dependencies.
|
|
7
|
+
*
|
|
8
|
+
* import { htmlToMarkdown, markdownToHtml, domToJson, jsonToHtml } from '.../serialize.js'
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/* ============================ HTML -> Markdown ============================ */
|
|
12
|
+
|
|
13
|
+
export function htmlToMarkdown(html) {
|
|
14
|
+
const root = document.createElement('div');
|
|
15
|
+
root.innerHTML = html || '';
|
|
16
|
+
return blocksToMd(root, 0).replace(/\n{3,}/g, '\n\n').trim() + '\n';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function blocksToMd(parent, depth) {
|
|
20
|
+
let out = '';
|
|
21
|
+
parent.childNodes.forEach((node) => { out += nodeBlock(node, depth); });
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function nodeBlock(node, depth) {
|
|
26
|
+
if (node.nodeType === 3) {
|
|
27
|
+
const t = node.textContent.replace(/\s+/g, ' ');
|
|
28
|
+
return t.trim() ? t + '\n\n' : '';
|
|
29
|
+
}
|
|
30
|
+
if (node.nodeType !== 1) return '';
|
|
31
|
+
const tag = node.tagName;
|
|
32
|
+
switch (tag) {
|
|
33
|
+
case 'H1': case 'H2': case 'H3': case 'H4': case 'H5': case 'H6':
|
|
34
|
+
return '#'.repeat(+tag[1]) + ' ' + inline(node) + '\n\n';
|
|
35
|
+
case 'P': case 'DIV': {
|
|
36
|
+
const c = inline(node);
|
|
37
|
+
return c.trim() ? c + '\n\n' : '';
|
|
38
|
+
}
|
|
39
|
+
case 'BLOCKQUOTE':
|
|
40
|
+
return inline(node).split('\n').map((l) => '> ' + l).join('\n') + '\n\n';
|
|
41
|
+
case 'PRE':
|
|
42
|
+
return '```\n' + node.textContent.replace(/\n$/, '') + '\n```\n\n';
|
|
43
|
+
case 'UL': return listToMd(node, depth, false) + '\n';
|
|
44
|
+
case 'OL': return listToMd(node, depth, true) + '\n';
|
|
45
|
+
case 'HR': return '---\n\n';
|
|
46
|
+
case 'TABLE': return tableToMd(node) + '\n';
|
|
47
|
+
case 'FIGURE': return blocksToMd(node, depth);
|
|
48
|
+
case 'IMG': return imgToMd(node) + '\n\n';
|
|
49
|
+
case 'BR': return '\n';
|
|
50
|
+
default:
|
|
51
|
+
return inline(node) + '\n\n';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function listToMd(node, depth, ordered) {
|
|
56
|
+
let out = '', i = 1;
|
|
57
|
+
node.childNodes.forEach((li) => {
|
|
58
|
+
if (li.nodeType !== 1 || li.tagName !== 'LI') return;
|
|
59
|
+
const marker = ordered ? (i++) + '. ' : '- ';
|
|
60
|
+
const pad = ' '.repeat(depth);
|
|
61
|
+
let text = '', nested = '';
|
|
62
|
+
li.childNodes.forEach((ch) => {
|
|
63
|
+
if (ch.nodeType === 1 && (ch.tagName === 'UL' || ch.tagName === 'OL')) {
|
|
64
|
+
nested += listToMd(ch, depth + 1, ch.tagName === 'OL');
|
|
65
|
+
} else {
|
|
66
|
+
text += inlineNode(ch);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
out += pad + marker + text.trim() + '\n' + nested;
|
|
70
|
+
});
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function tableToMd(node) {
|
|
75
|
+
const rows = [...node.querySelectorAll('tr')];
|
|
76
|
+
if (!rows.length) return '';
|
|
77
|
+
const cells = (r) => [...r.children].map((c) => inline(c).replace(/\|/g, '\\|').trim());
|
|
78
|
+
const head = cells(rows[0]);
|
|
79
|
+
let out = '| ' + head.join(' | ') + ' |\n| ' + head.map(() => '---').join(' | ') + ' |\n';
|
|
80
|
+
rows.slice(1).forEach((r) => { out += '| ' + cells(r).join(' | ') + ' |\n'; });
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function imgToMd(node) {
|
|
85
|
+
return ` || ''})`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function inline(node) {
|
|
89
|
+
let out = '';
|
|
90
|
+
node.childNodes.forEach((ch) => { out += inlineNode(ch); });
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function inlineNode(node) {
|
|
95
|
+
if (node.nodeType === 3) return node.textContent;
|
|
96
|
+
if (node.nodeType !== 1) return '';
|
|
97
|
+
const tag = node.tagName;
|
|
98
|
+
if (node.classList && node.classList.contains('mention')) {
|
|
99
|
+
const id = node.getAttribute('data-id') || '';
|
|
100
|
+
const name = (node.textContent || '').replace(/^[@#]/, '');
|
|
101
|
+
const trig = (node.textContent || '@')[0];
|
|
102
|
+
return `${trig}[${name}](${id})`;
|
|
103
|
+
}
|
|
104
|
+
switch (tag) {
|
|
105
|
+
case 'B': case 'STRONG': return '**' + inline(node) + '**';
|
|
106
|
+
case 'I': case 'EM': return '*' + inline(node) + '*';
|
|
107
|
+
case 'S': case 'STRIKE': case 'DEL': return '~~' + inline(node) + '~~';
|
|
108
|
+
case 'U': return '<u>' + inline(node) + '</u>';
|
|
109
|
+
case 'CODE': return '`' + node.textContent + '`';
|
|
110
|
+
case 'A': return '[' + inline(node) + '](' + (node.getAttribute('href') || '') + ')';
|
|
111
|
+
case 'IMG': return imgToMd(node);
|
|
112
|
+
case 'BR': return ' \n';
|
|
113
|
+
default: return inline(node); // spans (colour/font) → keep text only
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* ============================ Markdown -> HTML ============================ */
|
|
118
|
+
|
|
119
|
+
export function markdownToHtml(md) {
|
|
120
|
+
const lines = (md || '').replace(/\r\n/g, '\n').split('\n');
|
|
121
|
+
let html = '', i = 0;
|
|
122
|
+
const isList = (l) => /^\s*([-*+]|\d+\.)\s+/.test(l);
|
|
123
|
+
while (i < lines.length) {
|
|
124
|
+
const line = lines[i];
|
|
125
|
+
if (/^\s*$/.test(line)) { i++; continue; }
|
|
126
|
+
if (/^---+$/.test(line.trim())) { html += '<hr>'; i++; continue; }
|
|
127
|
+
const h = line.match(/^(#{1,6})\s+(.*)$/);
|
|
128
|
+
if (h) { html += `<h${h[1].length}>${inlineMd(h[2])}</h${h[1].length}>`; i++; continue; }
|
|
129
|
+
if (/^```/.test(line)) {
|
|
130
|
+
i++; let code = '';
|
|
131
|
+
while (i < lines.length && !/^```/.test(lines[i])) { code += lines[i] + '\n'; i++; }
|
|
132
|
+
i++; html += '<pre>' + escapeHtml(code.replace(/\n$/, '')) + '</pre>'; continue;
|
|
133
|
+
}
|
|
134
|
+
if (/^>\s?/.test(line)) {
|
|
135
|
+
const q = [];
|
|
136
|
+
while (i < lines.length && /^>\s?/.test(lines[i])) { q.push(lines[i].replace(/^>\s?/, '')); i++; }
|
|
137
|
+
html += '<blockquote>' + inlineMd(q.join(' ')) + '</blockquote>'; continue;
|
|
138
|
+
}
|
|
139
|
+
if (/^\|.*\|\s*$/.test(line) && i + 1 < lines.length && /^\|[\s:|-]+\|\s*$/.test(lines[i + 1])) {
|
|
140
|
+
const r = parseTable(lines, i); html += r.html; i = r.next; continue;
|
|
141
|
+
}
|
|
142
|
+
if (isList(line)) { const r = parseList(lines, i, 0); html += r.html; i = r.next; continue; }
|
|
143
|
+
const para = [line]; i++;
|
|
144
|
+
while (i < lines.length && !/^\s*$/.test(lines[i]) && !/^(#{1,6}\s|>|```)/.test(lines[i]) &&
|
|
145
|
+
!/^---+$/.test(lines[i].trim()) && !isList(lines[i])) { para.push(lines[i]); i++; }
|
|
146
|
+
html += '<p>' + inlineMd(para.join('\n').trim()) + '</p>';
|
|
147
|
+
}
|
|
148
|
+
return html;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function indentOf(l) { return (l.match(/^(\s*)/)[1] || '').length; }
|
|
152
|
+
|
|
153
|
+
function parseList(lines, start, baseIndent) {
|
|
154
|
+
const ordered = /^\s*\d+\./.test(lines[start]);
|
|
155
|
+
let i = start, html = '<' + (ordered ? 'ol' : 'ul') + '>';
|
|
156
|
+
while (i < lines.length) {
|
|
157
|
+
const l = lines[i];
|
|
158
|
+
if (/^\s*$/.test(l)) { i++; continue; }
|
|
159
|
+
const ind = indentOf(l);
|
|
160
|
+
const m = l.match(/^\s*([-*+]|\d+\.)\s+(.*)$/);
|
|
161
|
+
if (!m || ind < baseIndent) break;
|
|
162
|
+
if (ind > baseIndent) { // nested list belongs to previous <li>
|
|
163
|
+
const r = parseList(lines, i, ind);
|
|
164
|
+
html = html.replace(/<\/li>$/, r.html + '</li>');
|
|
165
|
+
i = r.next; continue;
|
|
166
|
+
}
|
|
167
|
+
html += '<li>' + inlineMd(m[2]) + '</li>';
|
|
168
|
+
i++;
|
|
169
|
+
}
|
|
170
|
+
return { html: html + '</' + (ordered ? 'ol' : 'ul') + '>', next: i };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function parseTable(lines, start) {
|
|
174
|
+
const row = (l) => l.trim().replace(/^\||\|$/g, '').split('|').map((c) => c.trim());
|
|
175
|
+
const head = row(lines[start]);
|
|
176
|
+
let i = start + 2, body = '';
|
|
177
|
+
while (i < lines.length && /^\|.*\|\s*$/.test(lines[i])) {
|
|
178
|
+
body += '<tr>' + row(lines[i]).map((c) => `<td>${inlineMd(c)}</td>`).join('') + '</tr>';
|
|
179
|
+
i++;
|
|
180
|
+
}
|
|
181
|
+
const thead = '<tr>' + head.map((c) => `<td><b>${inlineMd(c)}</b></td>`).join('') + '</tr>';
|
|
182
|
+
return { html: `<table class="rich-editor-table"><tbody>${thead}${body}</tbody></table>`, next: i };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function inlineMd(s) {
|
|
186
|
+
// images, mentions, links, then marks. Order matters.
|
|
187
|
+
return s
|
|
188
|
+
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, a, src) => `<img class="inserted-image" src="${attr(src)}" alt="${attr(a)}" style="max-width:100%;height:auto">`)
|
|
189
|
+
.replace(/([@#])\[([^\]]+)\]\(([^)]+)\)/g, (_, t, name, id) => `<span class="mention" data-id="${attr(id)}">${t}${escapeHtml(name)}</span>`)
|
|
190
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, t, href) => `<a href="${attr(href)}" target="_blank" rel="noopener noreferrer">${escapeHtml(t)}</a>`)
|
|
191
|
+
.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>')
|
|
192
|
+
.replace(/(^|[^*])\*([^*]+)\*/g, '$1<i>$2</i>')
|
|
193
|
+
.replace(/~~([^~]+)~~/g, '<s>$1</s>')
|
|
194
|
+
.replace(/`([^`]+)`/g, (_, c) => '<code>' + escapeHtml(c) + '</code>')
|
|
195
|
+
.replace(/\n/g, '<br>');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function escapeHtml(s) { return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
199
|
+
function attr(s) { return String(s).replace(/"/g, '"').replace(/</g, '<'); }
|
|
200
|
+
|
|
201
|
+
/* ============================== HTML <-> JSON ============================= */
|
|
202
|
+
|
|
203
|
+
export function domToJson(html) {
|
|
204
|
+
const root = document.createElement('div');
|
|
205
|
+
root.innerHTML = html || '';
|
|
206
|
+
return { type: 'doc', content: [...root.childNodes].map(nodeToJson).filter(Boolean) };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function nodeToJson(node) {
|
|
210
|
+
if (node.nodeType === 3) {
|
|
211
|
+
const text = node.textContent;
|
|
212
|
+
return text ? { text } : null;
|
|
213
|
+
}
|
|
214
|
+
if (node.nodeType !== 1) return null;
|
|
215
|
+
const obj = { tag: node.tagName.toLowerCase() };
|
|
216
|
+
if (node.attributes.length) {
|
|
217
|
+
obj.attrs = {};
|
|
218
|
+
for (const a of node.attributes) obj.attrs[a.name] = a.value;
|
|
219
|
+
}
|
|
220
|
+
const kids = [...node.childNodes].map(nodeToJson).filter(Boolean);
|
|
221
|
+
if (kids.length) obj.content = kids;
|
|
222
|
+
return obj;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function jsonToHtml(json) {
|
|
226
|
+
const nodes = json && json.content ? json.content : (Array.isArray(json) ? json : []);
|
|
227
|
+
return nodes.map(jsonNodeToHtml).join('');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function jsonNodeToHtml(n) {
|
|
231
|
+
if (n == null) return '';
|
|
232
|
+
if (n.text != null) return escapeHtml(n.text);
|
|
233
|
+
if (!n.tag) return '';
|
|
234
|
+
const attrs = n.attrs
|
|
235
|
+
? Object.entries(n.attrs).map(([k, v]) => ` ${k}="${attr(v)}"`).join('')
|
|
236
|
+
: '';
|
|
237
|
+
const inner = (n.content || []).map(jsonNodeToHtml).join('');
|
|
238
|
+
const VOID = new Set(['img', 'hr', 'br', 'input']);
|
|
239
|
+
if (VOID.has(n.tag)) return `<${n.tag}${attrs}>`;
|
|
240
|
+
return `<${n.tag}${attrs}>${inner}</${n.tag}>`;
|
|
241
|
+
}
|
package/lib/static.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* renderStatic — paint stored HTML into a read-only view that looks exactly
|
|
3
|
+
* like the editor's content area, without booting an editor.
|
|
4
|
+
*
|
|
5
|
+
* It sanitizes the HTML (same allowlist the editor uses on paste/setContent)
|
|
6
|
+
* and tags the host element with `.yjd-content`, the class the stylesheet uses
|
|
7
|
+
* to style typography, lists, tables, images, and mention tokens. Load the
|
|
8
|
+
* editor stylesheet (or StylesLoader.loadStyles()) on the page for it to match.
|
|
9
|
+
*
|
|
10
|
+
* import { renderStatic } from '@oix1987/yjd/core';
|
|
11
|
+
* renderStatic(post.body_html, document.querySelector('#post'));
|
|
12
|
+
*
|
|
13
|
+
* @param {string} html Stored HTML (untrusted — it is sanitized).
|
|
14
|
+
* @param {Element} [target] Element to render into. If omitted, a fresh
|
|
15
|
+
* <div.yjd-content> is created and returned.
|
|
16
|
+
* @returns {Element} the element the content was rendered into.
|
|
17
|
+
*/
|
|
18
|
+
import { sanitizeHtml } from './utils/sanitize.js';
|
|
19
|
+
|
|
20
|
+
export function renderStatic(html, target) {
|
|
21
|
+
const safe = sanitizeHtml(html || '');
|
|
22
|
+
const el = target || document.createElement('div');
|
|
23
|
+
el.classList.add('yjd-content');
|
|
24
|
+
el.innerHTML = safe;
|
|
25
|
+
return el;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default renderStatic;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import cssText from './styles.css.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CSS Loader - Load và inject CSS styles vào DOM
|
|
5
|
+
* Thay thế cho việc sử dụng inline styles
|
|
6
|
+
*
|
|
7
|
+
* CSS được import trực tiếp dưới dạng chuỗi (sinh từ styles.css), nên hoạt động
|
|
8
|
+
* cả với native ESM trong trình duyệt lẫn khi đóng gói bằng Rollup/CDN — không
|
|
9
|
+
* còn phụ thuộc vào fetch runtime (vốn làm hỏng việc dùng qua npm/CDN).
|
|
10
|
+
*/
|
|
11
|
+
class StylesLoader {
|
|
12
|
+
static loaded = false;
|
|
13
|
+
static styleElement = null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Load CSS (inject inlined stylesheet vào <head>)
|
|
17
|
+
* Trả về Promise để giữ tương thích với code gọi cũ (.catch/.then).
|
|
18
|
+
*/
|
|
19
|
+
static loadStyles() {
|
|
20
|
+
if (this.loaded) return Promise.resolve();
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
this.styleElement = document.createElement('style');
|
|
24
|
+
this.styleElement.id = 'rich-editor-styles';
|
|
25
|
+
this.styleElement.textContent = cssText;
|
|
26
|
+
document.head.appendChild(this.styleElement);
|
|
27
|
+
this.loaded = true;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
// Fallback: load minimal styles
|
|
30
|
+
this.loadFallbackStyles();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Load minimal fallback styles nếu không thể load từ file
|
|
38
|
+
*/
|
|
39
|
+
static loadFallbackStyles() {
|
|
40
|
+
const fallbackCSS = `
|
|
41
|
+
.yjd-rich-editor {
|
|
42
|
+
position: relative;
|
|
43
|
+
background: #fff;
|
|
44
|
+
border: 1px solid #ddd;
|
|
45
|
+
border-radius: 4px;
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-direction: column;
|
|
48
|
+
font-family: system-ui, sans-serif;
|
|
49
|
+
}
|
|
50
|
+
.yjd-rich-editor .rich-editor-area {
|
|
51
|
+
flex: 1;
|
|
52
|
+
padding: 20px;
|
|
53
|
+
outline: none;
|
|
54
|
+
min-height: 100px;
|
|
55
|
+
}
|
|
56
|
+
.yjd-rich-editor .rich-editor-toolbar {
|
|
57
|
+
display: flex;
|
|
58
|
+
gap: 4px;
|
|
59
|
+
padding: 8px;
|
|
60
|
+
border-bottom: 1px solid #ddd;
|
|
61
|
+
background: #f9f9f9;
|
|
62
|
+
}
|
|
63
|
+
.yjd-rich-editor .rich-editor-toolbar-btn {
|
|
64
|
+
padding: 4px 8px;
|
|
65
|
+
border: 1px solid #ccc;
|
|
66
|
+
border-radius: 3px;
|
|
67
|
+
background: #fff;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
}
|
|
70
|
+
.yjd-rich-editor .table-grid-selector {
|
|
71
|
+
position: absolute;
|
|
72
|
+
background: white;
|
|
73
|
+
border: 1px solid #ccc;
|
|
74
|
+
border-radius: 4px;
|
|
75
|
+
padding: 10px;
|
|
76
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
77
|
+
z-index: 1000;
|
|
78
|
+
display: none;
|
|
79
|
+
}
|
|
80
|
+
.yjd-rich-editor .table-grid-cell {
|
|
81
|
+
width: 20px;
|
|
82
|
+
height: 20px;
|
|
83
|
+
border: 1px solid #ddd;
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
background: white;
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
this.styleElement = document.createElement('style');
|
|
90
|
+
this.styleElement.id = 'rich-editor-styles-fallback';
|
|
91
|
+
this.styleElement.textContent = fallbackCSS;
|
|
92
|
+
document.head.appendChild(this.styleElement);
|
|
93
|
+
|
|
94
|
+
this.loaded = true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Unload styles
|
|
99
|
+
*/
|
|
100
|
+
static unloadStyles() {
|
|
101
|
+
if (this.styleElement && this.styleElement.parentNode) {
|
|
102
|
+
this.styleElement.parentNode.removeChild(this.styleElement);
|
|
103
|
+
this.styleElement = null;
|
|
104
|
+
this.loaded = false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if styles are loaded
|
|
110
|
+
*/
|
|
111
|
+
static isLoaded() {
|
|
112
|
+
return this.loaded;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Reload styles
|
|
117
|
+
*/
|
|
118
|
+
static async reloadStyles() {
|
|
119
|
+
this.unloadStyles();
|
|
120
|
+
await this.loadStyles();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Add custom CSS
|
|
125
|
+
*/
|
|
126
|
+
static addCustomCSS(css, id = 'rich-editor-custom') {
|
|
127
|
+
// Remove existing custom styles
|
|
128
|
+
const existing = document.getElementById(id);
|
|
129
|
+
if (existing) {
|
|
130
|
+
existing.remove();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Add new custom styles
|
|
134
|
+
const style = document.createElement('style');
|
|
135
|
+
style.id = id;
|
|
136
|
+
style.textContent = css;
|
|
137
|
+
document.head.appendChild(style);
|
|
138
|
+
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default StylesLoader;
|