@overlap/rte 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Editor.d.ts.map +1 -1
- package/dist/components/Icons.d.ts +9 -0
- package/dist/components/Icons.d.ts.map +1 -1
- package/dist/hooks/useEditorEvents.d.ts.map +1 -1
- package/dist/index.d.ts +33 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +1007 -8
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1016 -7
- package/dist/index.js.map +1 -1
- package/dist/plugins/alignment.d.ts +9 -0
- package/dist/plugins/alignment.d.ts.map +1 -0
- package/dist/plugins/index.d.ts +4 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/linkDialog.d.ts +17 -0
- package/dist/plugins/linkDialog.d.ts.map +1 -0
- package/dist/plugins/table.d.ts +7 -0
- package/dist/plugins/table.d.ts.map +1 -0
- package/dist/styles.css +291 -0
- package/dist/utils/content.d.ts.map +1 -1
- package/dist/utils/table.d.ts +30 -0
- package/dist/utils/table.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -23,6 +23,15 @@ const UploadIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { wi
|
|
|
23
23
|
const IndentIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M3 21h18v-2H3v2zM3 8l4 4-4 4V8zm8 9h10v-2H11v2zM3 3v2h18V3H3zm8 6h10V7H11v2zm0 4h10v-2H11v2z" }) }));
|
|
24
24
|
const OutdentIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M3 21h18v-2H3v2zM11 8l4 4-4 4V8zM3 3v2h18V3H3zm0 4h10v2H3V7zm0 4h10v2H3v-2zm0 4h18v2H3v-2z" }) }));
|
|
25
25
|
const CheckboxIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M19 3H5c-1.11 0-2 .89-2 2v14c0 1.11.89 2 2 2h14c1.11 0 2-.89 2-2V5c0-1.11-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" }) }));
|
|
26
|
+
const StrikethroughIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M10 19h4v-3h-4v3zM5 4v3h5v3h4V7h5V4H5zM3 14h18v-2H3v2z" }) }));
|
|
27
|
+
const SubscriptIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M22 18h-2v1h3v1h-4v-2.5c0-.83.67-1.5 1.5-1.5h1.5v-1h-3v-1h2.5c.83 0 1.5.67 1.5 1.5v1c0 .83-.67 1.5-1.5 1.5zM5.88 18h2.66l3.4-5.42h.12l3.4 5.42h2.66l-4.65-7.27L17.81 4h-2.68l-3.07 4.99h-.12L8.87 4H6.19l4.32 6.73L5.88 18z" }) }));
|
|
28
|
+
const SuperscriptIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M22 7h-2v1h3v1h-4V6.5c0-.83.67-1.5 1.5-1.5h1.5V4h-3V3h2.5c.83 0 1.5.67 1.5 1.5v1c0 .83-.67 1.5-1.5 1.5zM5.88 20h2.66l3.4-5.42h.12l3.4 5.42h2.66l-4.65-7.27L17.81 6h-2.68l-3.07 4.99h-.12L8.87 6H6.19l4.32 6.73L5.88 20z" }) }));
|
|
29
|
+
const CodeIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z" }) }));
|
|
30
|
+
const AlignLeftIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M15 15H3v2h12v-2zm0-8H3v2h12V7zM3 13h18v-2H3v2zm0 8h18v-2H3v2zM3 3v2h18V3H3z" }) }));
|
|
31
|
+
const AlignCenterIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M7 15v2h10v-2H7zm-4 6h18v-2H3v2zm0-8h18v-2H3v2zm4-6v2h10V7H7zM3 3v2h18V3H3z" }) }));
|
|
32
|
+
const AlignRightIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M3 21h18v-2H3v2zm6-4h12v-2H9v2zm-6-4h18v-2H3v2zm6-4h12V7H9v2zM3 3v2h18V3H3z" }) }));
|
|
33
|
+
const AlignJustifyIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M3 21h18v-2H3v2zm0-4h18v-2H3v2zm0-4h18v-2H3v2zm0-4h18V7H3v2zm0-6v2h18V3H3z" }) }));
|
|
34
|
+
const TableIcon = ({ width = 18, height = 18, className, }) => (jsx("svg", { width: width, height: height, viewBox: "0 0 24 24", fill: "currentColor", className: className, children: jsx("path", { d: "M3 3v18h18V3H3zm8 16H5v-6h6v6zm0-8H5V5h6v6zm8 8h-6v-6h6v6zm0-8h-6V5h6v6z" }) }));
|
|
26
35
|
const iconMap = {
|
|
27
36
|
"mdi:format-bold": BoldIcon,
|
|
28
37
|
"mdi:format-italic": ItalicIcon,
|
|
@@ -46,6 +55,15 @@ const iconMap = {
|
|
|
46
55
|
"mdi:format-indent-increase": IndentIcon,
|
|
47
56
|
"mdi:format-indent-decrease": OutdentIcon,
|
|
48
57
|
"mdi:checkbox-marked-outline": CheckboxIcon,
|
|
58
|
+
"mdi:format-strikethrough": StrikethroughIcon,
|
|
59
|
+
"mdi:format-subscript": SubscriptIcon,
|
|
60
|
+
"mdi:format-superscript": SuperscriptIcon,
|
|
61
|
+
"mdi:code-tags": CodeIcon,
|
|
62
|
+
"mdi:format-align-left": AlignLeftIcon,
|
|
63
|
+
"mdi:format-align-center": AlignCenterIcon,
|
|
64
|
+
"mdi:format-align-right": AlignRightIcon,
|
|
65
|
+
"mdi:format-align-justify": AlignJustifyIcon,
|
|
66
|
+
"mdi:table": TableIcon,
|
|
49
67
|
};
|
|
50
68
|
const Icon = ({ icon, width = 18, height = 18, className }) => {
|
|
51
69
|
const IconComponent = iconMap[icon];
|
|
@@ -409,6 +427,62 @@ const defaultHeadings$1 = ["h1", "h2", "h3", "h4", "h5", "h6"];
|
|
|
409
427
|
const boldPlugin = createInlinePlugin("bold", "bold", "mdi:format-bold", "Fett");
|
|
410
428
|
const italicPlugin = createInlinePlugin("italic", "italic", "mdi:format-italic", "Kursiv");
|
|
411
429
|
const underlinePlugin = createInlinePlugin("underline", "underline", "mdi:format-underline", "Unterstrichen");
|
|
430
|
+
const strikethroughPlugin = createInlinePlugin("strikethrough", "strikeThrough", "mdi:format-strikethrough", "Durchgestrichen");
|
|
431
|
+
const subscriptPlugin = createInlinePlugin("subscript", "subscript", "mdi:format-subscript", "Tiefgestellt");
|
|
432
|
+
const superscriptPlugin = createInlinePlugin("superscript", "superscript", "mdi:format-superscript", "Hochgestellt");
|
|
433
|
+
const codeInlinePlugin = {
|
|
434
|
+
name: "codeInline",
|
|
435
|
+
type: "inline",
|
|
436
|
+
renderButton: (props) => (jsx("button", { type: "button", onClick: props.onClick, disabled: props.disabled, className: `rte-toolbar-button ${props.isActive ? "rte-toolbar-button-active" : ""}`, title: "Code", "aria-label": "Code", children: jsx(IconWrapper, { icon: "mdi:code-tags", width: 18, height: 18 }) })),
|
|
437
|
+
execute: (editor) => {
|
|
438
|
+
const selection = editor.getSelection();
|
|
439
|
+
if (!selection || selection.rangeCount === 0)
|
|
440
|
+
return;
|
|
441
|
+
const range = selection.getRangeAt(0);
|
|
442
|
+
const container = range.commonAncestorContainer;
|
|
443
|
+
const element = container.nodeType === Node.TEXT_NODE
|
|
444
|
+
? container.parentElement
|
|
445
|
+
: container;
|
|
446
|
+
const existingCode = element?.closest("code");
|
|
447
|
+
if (existingCode) {
|
|
448
|
+
// Unwrap code
|
|
449
|
+
const parent = existingCode.parentNode;
|
|
450
|
+
if (parent) {
|
|
451
|
+
while (existingCode.firstChild) {
|
|
452
|
+
parent.insertBefore(existingCode.firstChild, existingCode);
|
|
453
|
+
}
|
|
454
|
+
parent.removeChild(existingCode);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else if (!range.collapsed) {
|
|
458
|
+
// Wrap in code
|
|
459
|
+
const code = document.createElement("code");
|
|
460
|
+
try {
|
|
461
|
+
range.surroundContents(code);
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
// If surroundContents fails (partial selection), use extractContents
|
|
465
|
+
const fragment = range.extractContents();
|
|
466
|
+
code.appendChild(fragment);
|
|
467
|
+
range.insertNode(code);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
isActive: () => {
|
|
472
|
+
if (typeof document === "undefined")
|
|
473
|
+
return false;
|
|
474
|
+
const selection = document.getSelection();
|
|
475
|
+
if (!selection || selection.rangeCount === 0)
|
|
476
|
+
return false;
|
|
477
|
+
const range = selection.getRangeAt(0);
|
|
478
|
+
const container = range.commonAncestorContainer;
|
|
479
|
+
const element = container.nodeType === Node.TEXT_NODE
|
|
480
|
+
? container.parentElement
|
|
481
|
+
: container;
|
|
482
|
+
return element?.closest("code") !== null;
|
|
483
|
+
},
|
|
484
|
+
canExecute: () => true,
|
|
485
|
+
};
|
|
412
486
|
const undoPlugin = createCommandPlugin("undo", "undo", "mdi:undo", "Rückgängig");
|
|
413
487
|
const redoPlugin = createCommandPlugin("redo", "redo", "mdi:redo", "Wiederholen");
|
|
414
488
|
/**
|
|
@@ -1239,10 +1313,13 @@ function domToContent(element) {
|
|
|
1239
1313
|
const attributes = {};
|
|
1240
1314
|
const src = el.getAttribute("src");
|
|
1241
1315
|
const alt = el.getAttribute("alt");
|
|
1316
|
+
const attachmentId = el.getAttribute("data-attachment-id");
|
|
1242
1317
|
if (src)
|
|
1243
1318
|
attributes.src = src;
|
|
1244
1319
|
if (alt)
|
|
1245
1320
|
attributes.alt = alt;
|
|
1321
|
+
if (attachmentId)
|
|
1322
|
+
attributes["data-attachment-id"] = attachmentId;
|
|
1246
1323
|
return {
|
|
1247
1324
|
type: "image",
|
|
1248
1325
|
attributes: Object.keys(attributes).length > 0
|
|
@@ -1250,6 +1327,45 @@ function domToContent(element) {
|
|
|
1250
1327
|
: undefined,
|
|
1251
1328
|
};
|
|
1252
1329
|
}
|
|
1330
|
+
// Table elements
|
|
1331
|
+
if (["table", "thead", "tbody", "tr", "td", "th"].includes(tagName)) {
|
|
1332
|
+
const children = [];
|
|
1333
|
+
const attributes = {};
|
|
1334
|
+
// Preserve class
|
|
1335
|
+
const cls = el.getAttribute("class");
|
|
1336
|
+
if (cls)
|
|
1337
|
+
attributes.class = cls;
|
|
1338
|
+
// Table cell attributes
|
|
1339
|
+
if (tagName === "td" || tagName === "th") {
|
|
1340
|
+
const colspan = el.getAttribute("colspan");
|
|
1341
|
+
const rowspan = el.getAttribute("rowspan");
|
|
1342
|
+
if (colspan && colspan !== "1")
|
|
1343
|
+
attributes.colspan = colspan;
|
|
1344
|
+
if (rowspan && rowspan !== "1")
|
|
1345
|
+
attributes.rowspan = rowspan;
|
|
1346
|
+
// Preserve text-align on cells
|
|
1347
|
+
const textAlign = el.style.textAlign;
|
|
1348
|
+
if (textAlign && textAlign !== "left" && textAlign !== "start") {
|
|
1349
|
+
attributes.textAlign = textAlign;
|
|
1350
|
+
}
|
|
1351
|
+
// Preserve background-color
|
|
1352
|
+
const bgColor = el.style.backgroundColor;
|
|
1353
|
+
if (bgColor)
|
|
1354
|
+
attributes.backgroundColor = bgColor;
|
|
1355
|
+
}
|
|
1356
|
+
Array.from(el.childNodes).forEach((child) => {
|
|
1357
|
+
const processed = processNode(child);
|
|
1358
|
+
if (processed)
|
|
1359
|
+
children.push(processed);
|
|
1360
|
+
});
|
|
1361
|
+
return {
|
|
1362
|
+
type: tagName,
|
|
1363
|
+
children: children.length > 0 ? children : [],
|
|
1364
|
+
attributes: Object.keys(attributes).length > 0
|
|
1365
|
+
? attributes
|
|
1366
|
+
: undefined,
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1253
1369
|
// Block elements
|
|
1254
1370
|
if ([
|
|
1255
1371
|
"p",
|
|
@@ -1267,6 +1383,11 @@ function domToContent(element) {
|
|
|
1267
1383
|
].includes(tagName)) {
|
|
1268
1384
|
const children = [];
|
|
1269
1385
|
const attributes = {};
|
|
1386
|
+
// Preserve text-align on block elements
|
|
1387
|
+
const textAlign = el.style.textAlign;
|
|
1388
|
+
if (textAlign && textAlign !== "left" && textAlign !== "start" && textAlign !== "") {
|
|
1389
|
+
attributes.textAlign = textAlign;
|
|
1390
|
+
}
|
|
1270
1391
|
// Detect checkbox lists (own + Lexical + GitHub formats)
|
|
1271
1392
|
if (tagName === "ul" && isCheckboxList(el)) {
|
|
1272
1393
|
attributes.class = "rte-checkbox-list";
|
|
@@ -1316,6 +1437,9 @@ function domToContent(element) {
|
|
|
1316
1437
|
"strike",
|
|
1317
1438
|
"a",
|
|
1318
1439
|
"span",
|
|
1440
|
+
"sub",
|
|
1441
|
+
"sup",
|
|
1442
|
+
"code",
|
|
1319
1443
|
].includes(tagName)) {
|
|
1320
1444
|
const children = [];
|
|
1321
1445
|
Array.from(el.childNodes).forEach((child) => {
|
|
@@ -1327,16 +1451,32 @@ function domToContent(element) {
|
|
|
1327
1451
|
Array.from(el.attributes).forEach((attr) => {
|
|
1328
1452
|
attributes[attr.name] = attr.value;
|
|
1329
1453
|
});
|
|
1330
|
-
// Links
|
|
1454
|
+
// Links — capture all relevant attributes
|
|
1331
1455
|
if (tagName === "a") {
|
|
1456
|
+
const linkAttrs = {};
|
|
1332
1457
|
const href = el.getAttribute("href");
|
|
1333
1458
|
if (href)
|
|
1334
|
-
|
|
1459
|
+
linkAttrs.href = href;
|
|
1460
|
+
const target = el.getAttribute("target");
|
|
1461
|
+
if (target)
|
|
1462
|
+
linkAttrs.target = target;
|
|
1463
|
+
const rel = el.getAttribute("rel");
|
|
1464
|
+
if (rel)
|
|
1465
|
+
linkAttrs.rel = rel;
|
|
1466
|
+
const title = el.getAttribute("title");
|
|
1467
|
+
if (title)
|
|
1468
|
+
linkAttrs.title = title;
|
|
1469
|
+
const pageRef = el.getAttribute("data-page-ref");
|
|
1470
|
+
if (pageRef)
|
|
1471
|
+
linkAttrs["data-page-ref"] = pageRef;
|
|
1472
|
+
const urlExtra = el.getAttribute("data-url-extra");
|
|
1473
|
+
if (urlExtra)
|
|
1474
|
+
linkAttrs["data-url-extra"] = urlExtra;
|
|
1335
1475
|
return {
|
|
1336
1476
|
type: "link",
|
|
1337
1477
|
children: children.length > 0 ? children : undefined,
|
|
1338
|
-
attributes: Object.keys(
|
|
1339
|
-
?
|
|
1478
|
+
attributes: Object.keys(linkAttrs).length > 0
|
|
1479
|
+
? linkAttrs
|
|
1340
1480
|
: undefined,
|
|
1341
1481
|
};
|
|
1342
1482
|
}
|
|
@@ -1400,7 +1540,13 @@ function domToContent(element) {
|
|
|
1400
1540
|
tagName === "del" ||
|
|
1401
1541
|
tagName === "strike"
|
|
1402
1542
|
? "strikethrough"
|
|
1403
|
-
: tagName
|
|
1543
|
+
: tagName === "sub"
|
|
1544
|
+
? "subscript"
|
|
1545
|
+
: tagName === "sup"
|
|
1546
|
+
? "superscript"
|
|
1547
|
+
: tagName === "code"
|
|
1548
|
+
? "code"
|
|
1549
|
+
: tagName;
|
|
1404
1550
|
return {
|
|
1405
1551
|
type,
|
|
1406
1552
|
children: children.length > 0 ? children : undefined,
|
|
@@ -1461,6 +1607,9 @@ function contentToDOM(content, container, customLinkComponent, customHeadingRend
|
|
|
1461
1607
|
underline: "u",
|
|
1462
1608
|
strikethrough: "s",
|
|
1463
1609
|
link: "a",
|
|
1610
|
+
subscript: "sub",
|
|
1611
|
+
superscript: "sup",
|
|
1612
|
+
code: "code",
|
|
1464
1613
|
};
|
|
1465
1614
|
let tagName = tagMap[node.type] || node.type;
|
|
1466
1615
|
if (node.type === "link" && customLinkComponent) {
|
|
@@ -1481,17 +1630,49 @@ function contentToDOM(content, container, customLinkComponent, customHeadingRend
|
|
|
1481
1630
|
else if (key === "backgroundColor") {
|
|
1482
1631
|
element.style.backgroundColor = value;
|
|
1483
1632
|
}
|
|
1633
|
+
else if (key === "textAlign") {
|
|
1634
|
+
element.style.textAlign = value;
|
|
1635
|
+
}
|
|
1484
1636
|
else if (key === "href" && tagName === "a") {
|
|
1485
1637
|
element.setAttribute("href", value);
|
|
1486
1638
|
}
|
|
1639
|
+
else if (key === "target" && tagName === "a") {
|
|
1640
|
+
element.setAttribute("target", value);
|
|
1641
|
+
}
|
|
1642
|
+
else if (key === "rel" && tagName === "a") {
|
|
1643
|
+
element.setAttribute("rel", value);
|
|
1644
|
+
}
|
|
1645
|
+
else if (key === "title" && tagName === "a") {
|
|
1646
|
+
element.setAttribute("title", value);
|
|
1647
|
+
}
|
|
1648
|
+
else if (key === "data-page-ref" && tagName === "a") {
|
|
1649
|
+
element.setAttribute("data-page-ref", value);
|
|
1650
|
+
}
|
|
1651
|
+
else if (key === "data-url-extra" && tagName === "a") {
|
|
1652
|
+
element.setAttribute("data-url-extra", value);
|
|
1653
|
+
}
|
|
1654
|
+
else if (key === "data-attachment-id") {
|
|
1655
|
+
element.setAttribute("data-attachment-id", value);
|
|
1656
|
+
}
|
|
1657
|
+
else if (key === "colspan") {
|
|
1658
|
+
element.setAttribute("colspan", value);
|
|
1659
|
+
}
|
|
1660
|
+
else if (key === "rowspan") {
|
|
1661
|
+
element.setAttribute("rowspan", value);
|
|
1662
|
+
}
|
|
1487
1663
|
else if (key === "class") {
|
|
1488
1664
|
element.className = value;
|
|
1489
1665
|
}
|
|
1666
|
+
else if (key === "checkboxChecked") ;
|
|
1490
1667
|
else {
|
|
1491
1668
|
element.setAttribute(key, value);
|
|
1492
1669
|
}
|
|
1493
1670
|
});
|
|
1494
1671
|
}
|
|
1672
|
+
// Ensure tables have the rte-table class
|
|
1673
|
+
if (node.type === "table" && !element.classList.contains("rte-table")) {
|
|
1674
|
+
element.classList.add("rte-table");
|
|
1675
|
+
}
|
|
1495
1676
|
if (node.children) {
|
|
1496
1677
|
node.children.forEach((child) => {
|
|
1497
1678
|
element.appendChild(createNode(child));
|
|
@@ -2031,6 +2212,255 @@ function useCheckbox({ editorRef, isUpdatingRef, pushToHistory, notifyChange, ge
|
|
|
2031
2212
|
};
|
|
2032
2213
|
}
|
|
2033
2214
|
|
|
2215
|
+
/**
|
|
2216
|
+
* Table DOM manipulation utilities for contentEditable.
|
|
2217
|
+
* Pure DOM functions — no React dependency.
|
|
2218
|
+
*/
|
|
2219
|
+
/* ── Helpers ──────────────────────────────────────────────────────────── */
|
|
2220
|
+
/** Get the <td>/<th> cell that contains the current selection. */
|
|
2221
|
+
function getActiveCell() {
|
|
2222
|
+
const sel = document.getSelection();
|
|
2223
|
+
if (!sel || sel.rangeCount === 0)
|
|
2224
|
+
return null;
|
|
2225
|
+
const range = sel.getRangeAt(0);
|
|
2226
|
+
const container = range.commonAncestorContainer;
|
|
2227
|
+
const el = container.nodeType === Node.TEXT_NODE
|
|
2228
|
+
? container.parentElement
|
|
2229
|
+
: container;
|
|
2230
|
+
return el?.closest("td, th");
|
|
2231
|
+
}
|
|
2232
|
+
/** Get the <table> that contains the current selection. */
|
|
2233
|
+
function getActiveTable() {
|
|
2234
|
+
const cell = getActiveCell();
|
|
2235
|
+
return cell?.closest("table") ?? null;
|
|
2236
|
+
}
|
|
2237
|
+
/** Get the <tr> that contains the current selection. */
|
|
2238
|
+
function getActiveRow() {
|
|
2239
|
+
const cell = getActiveCell();
|
|
2240
|
+
return cell?.closest("tr") ?? null;
|
|
2241
|
+
}
|
|
2242
|
+
/** Place the cursor inside a cell (at the start). */
|
|
2243
|
+
function focusCell(cell) {
|
|
2244
|
+
const range = document.createRange();
|
|
2245
|
+
const sel = document.getSelection();
|
|
2246
|
+
if (!sel)
|
|
2247
|
+
return;
|
|
2248
|
+
if (cell.firstChild) {
|
|
2249
|
+
range.setStart(cell.firstChild, 0);
|
|
2250
|
+
}
|
|
2251
|
+
else {
|
|
2252
|
+
// Empty cell — add a <br> so the cursor has something to land on
|
|
2253
|
+
const br = document.createElement("br");
|
|
2254
|
+
cell.appendChild(br);
|
|
2255
|
+
range.setStart(cell, 0);
|
|
2256
|
+
}
|
|
2257
|
+
range.collapse(true);
|
|
2258
|
+
sel.removeAllRanges();
|
|
2259
|
+
sel.addRange(range);
|
|
2260
|
+
}
|
|
2261
|
+
/** Count the number of columns in a table (from the first row). */
|
|
2262
|
+
function getColumnCount(table) {
|
|
2263
|
+
const firstRow = table.querySelector("tr");
|
|
2264
|
+
return firstRow ? firstRow.cells.length : 0;
|
|
2265
|
+
}
|
|
2266
|
+
/* ── Create ───────────────────────────────────────────────────────────── */
|
|
2267
|
+
/** Create a new table element with the given dimensions. */
|
|
2268
|
+
function createTable(rows, cols, withHeader = false) {
|
|
2269
|
+
const table = document.createElement("table");
|
|
2270
|
+
table.classList.add("rte-table");
|
|
2271
|
+
if (withHeader) {
|
|
2272
|
+
const thead = document.createElement("thead");
|
|
2273
|
+
const tr = document.createElement("tr");
|
|
2274
|
+
for (let c = 0; c < cols; c++) {
|
|
2275
|
+
const th = document.createElement("th");
|
|
2276
|
+
th.innerHTML = "<br>";
|
|
2277
|
+
tr.appendChild(th);
|
|
2278
|
+
}
|
|
2279
|
+
thead.appendChild(tr);
|
|
2280
|
+
table.appendChild(thead);
|
|
2281
|
+
}
|
|
2282
|
+
const tbody = document.createElement("tbody");
|
|
2283
|
+
const dataRows = withHeader ? rows - 1 : rows;
|
|
2284
|
+
for (let r = 0; r < dataRows; r++) {
|
|
2285
|
+
const tr = document.createElement("tr");
|
|
2286
|
+
for (let c = 0; c < cols; c++) {
|
|
2287
|
+
const td = document.createElement("td");
|
|
2288
|
+
td.innerHTML = "<br>";
|
|
2289
|
+
tr.appendChild(td);
|
|
2290
|
+
}
|
|
2291
|
+
tbody.appendChild(tr);
|
|
2292
|
+
}
|
|
2293
|
+
table.appendChild(tbody);
|
|
2294
|
+
return table;
|
|
2295
|
+
}
|
|
2296
|
+
/* ── Row operations ───────────────────────────────────────────────────── */
|
|
2297
|
+
/** Insert a row above or below the currently selected row. */
|
|
2298
|
+
function insertRow(position) {
|
|
2299
|
+
const row = getActiveRow();
|
|
2300
|
+
const table = getActiveTable();
|
|
2301
|
+
if (!row || !table)
|
|
2302
|
+
return;
|
|
2303
|
+
const cols = row.cells.length;
|
|
2304
|
+
const newRow = document.createElement("tr");
|
|
2305
|
+
for (let c = 0; c < cols; c++) {
|
|
2306
|
+
const td = document.createElement("td");
|
|
2307
|
+
td.innerHTML = "<br>";
|
|
2308
|
+
newRow.appendChild(td);
|
|
2309
|
+
}
|
|
2310
|
+
if (position === "above") {
|
|
2311
|
+
row.parentNode?.insertBefore(newRow, row);
|
|
2312
|
+
}
|
|
2313
|
+
else {
|
|
2314
|
+
row.parentNode?.insertBefore(newRow, row.nextSibling);
|
|
2315
|
+
}
|
|
2316
|
+
focusCell(newRow.cells[0]);
|
|
2317
|
+
}
|
|
2318
|
+
/** Delete the row that contains the current selection. */
|
|
2319
|
+
function deleteRow() {
|
|
2320
|
+
const row = getActiveRow();
|
|
2321
|
+
const table = getActiveTable();
|
|
2322
|
+
if (!row || !table)
|
|
2323
|
+
return;
|
|
2324
|
+
const allRows = table.querySelectorAll("tr");
|
|
2325
|
+
if (allRows.length <= 1) {
|
|
2326
|
+
// Last row — remove the whole table
|
|
2327
|
+
deleteTable();
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
// Focus adjacent row before deleting
|
|
2331
|
+
const nextRow = (row.nextElementSibling ||
|
|
2332
|
+
row.previousElementSibling);
|
|
2333
|
+
row.remove();
|
|
2334
|
+
if (nextRow?.cells[0]) {
|
|
2335
|
+
focusCell(nextRow.cells[0]);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
/* ── Column operations ────────────────────────────────────────────────── */
|
|
2339
|
+
/** Insert a column to the left or right of the currently selected cell. */
|
|
2340
|
+
function insertColumn(position) {
|
|
2341
|
+
const cell = getActiveCell();
|
|
2342
|
+
const table = getActiveTable();
|
|
2343
|
+
if (!cell || !table)
|
|
2344
|
+
return;
|
|
2345
|
+
const cellIndex = cell.cellIndex;
|
|
2346
|
+
const rows = table.querySelectorAll("tr");
|
|
2347
|
+
rows.forEach((row) => {
|
|
2348
|
+
const tag = row.parentElement?.tagName === "THEAD" ? "th" : "td";
|
|
2349
|
+
const newCell = document.createElement(tag);
|
|
2350
|
+
newCell.innerHTML = "<br>";
|
|
2351
|
+
const refCell = row.cells[cellIndex];
|
|
2352
|
+
if (!refCell) {
|
|
2353
|
+
row.appendChild(newCell);
|
|
2354
|
+
}
|
|
2355
|
+
else if (position === "left") {
|
|
2356
|
+
row.insertBefore(newCell, refCell);
|
|
2357
|
+
}
|
|
2358
|
+
else {
|
|
2359
|
+
row.insertBefore(newCell, refCell.nextSibling);
|
|
2360
|
+
}
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
/** Delete the column that contains the currently selected cell. */
|
|
2364
|
+
function deleteColumn() {
|
|
2365
|
+
const cell = getActiveCell();
|
|
2366
|
+
const table = getActiveTable();
|
|
2367
|
+
if (!cell || !table)
|
|
2368
|
+
return;
|
|
2369
|
+
const cellIndex = cell.cellIndex;
|
|
2370
|
+
const colCount = getColumnCount(table);
|
|
2371
|
+
if (colCount <= 1) {
|
|
2372
|
+
// Last column — remove the whole table
|
|
2373
|
+
deleteTable();
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
const rows = table.querySelectorAll("tr");
|
|
2377
|
+
rows.forEach((row) => {
|
|
2378
|
+
if (row.cells[cellIndex]) {
|
|
2379
|
+
row.cells[cellIndex].remove();
|
|
2380
|
+
}
|
|
2381
|
+
});
|
|
2382
|
+
// Focus an adjacent cell
|
|
2383
|
+
const activeRow = getActiveRow();
|
|
2384
|
+
if (activeRow) {
|
|
2385
|
+
const idx = Math.min(cellIndex, activeRow.cells.length - 1);
|
|
2386
|
+
if (activeRow.cells[idx])
|
|
2387
|
+
focusCell(activeRow.cells[idx]);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
/* ── Table delete ─────────────────────────────────────────────────────── */
|
|
2391
|
+
/** Remove the entire table and place cursor after it. */
|
|
2392
|
+
function deleteTable() {
|
|
2393
|
+
const table = getActiveTable();
|
|
2394
|
+
if (!table)
|
|
2395
|
+
return;
|
|
2396
|
+
const parent = table.parentNode;
|
|
2397
|
+
const nextSibling = table.nextSibling;
|
|
2398
|
+
// Create a paragraph to place the cursor
|
|
2399
|
+
const p = document.createElement("p");
|
|
2400
|
+
p.innerHTML = "<br>";
|
|
2401
|
+
if (nextSibling) {
|
|
2402
|
+
parent?.insertBefore(p, nextSibling);
|
|
2403
|
+
}
|
|
2404
|
+
else {
|
|
2405
|
+
parent?.appendChild(p);
|
|
2406
|
+
}
|
|
2407
|
+
table.remove();
|
|
2408
|
+
// Focus the new paragraph
|
|
2409
|
+
const range = document.createRange();
|
|
2410
|
+
const sel = document.getSelection();
|
|
2411
|
+
if (sel && p.firstChild) {
|
|
2412
|
+
range.setStart(p, 0);
|
|
2413
|
+
range.collapse(true);
|
|
2414
|
+
sel.removeAllRanges();
|
|
2415
|
+
sel.addRange(range);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
/* ── Tab navigation ───────────────────────────────────────────────────── */
|
|
2419
|
+
/**
|
|
2420
|
+
* Move focus to the next or previous table cell.
|
|
2421
|
+
* Returns true if navigation happened (caller should preventDefault).
|
|
2422
|
+
*/
|
|
2423
|
+
function navigateTableCell(direction) {
|
|
2424
|
+
const cell = getActiveCell();
|
|
2425
|
+
if (!cell)
|
|
2426
|
+
return false;
|
|
2427
|
+
const row = cell.closest("tr");
|
|
2428
|
+
const table = cell.closest("table");
|
|
2429
|
+
if (!row || !table)
|
|
2430
|
+
return false;
|
|
2431
|
+
const cellIndex = cell.cellIndex;
|
|
2432
|
+
const allRows = Array.from(table.querySelectorAll("tr"));
|
|
2433
|
+
const rowIndex = allRows.indexOf(row);
|
|
2434
|
+
let targetCell = null;
|
|
2435
|
+
if (direction === "next") {
|
|
2436
|
+
if (cellIndex < row.cells.length - 1) {
|
|
2437
|
+
targetCell = row.cells[cellIndex + 1];
|
|
2438
|
+
}
|
|
2439
|
+
else if (rowIndex < allRows.length - 1) {
|
|
2440
|
+
targetCell = allRows[rowIndex + 1].cells[0];
|
|
2441
|
+
}
|
|
2442
|
+
else {
|
|
2443
|
+
// Last cell of last row — add a new row
|
|
2444
|
+
insertRow("below");
|
|
2445
|
+
return true;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
else {
|
|
2449
|
+
if (cellIndex > 0) {
|
|
2450
|
+
targetCell = row.cells[cellIndex - 1];
|
|
2451
|
+
}
|
|
2452
|
+
else if (rowIndex > 0) {
|
|
2453
|
+
const prevRow = allRows[rowIndex - 1];
|
|
2454
|
+
targetCell = prevRow.cells[prevRow.cells.length - 1];
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
if (targetCell) {
|
|
2458
|
+
focusCell(targetCell);
|
|
2459
|
+
return true;
|
|
2460
|
+
}
|
|
2461
|
+
return false;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2034
2464
|
/**
|
|
2035
2465
|
* Hook that sets up input, keyup, and keydown event listeners on the editor.
|
|
2036
2466
|
*/
|
|
@@ -2060,8 +2490,22 @@ function useEditorEvents({ editorRef, historyRef, isUpdatingRef, notifyChange, h
|
|
|
2060
2490
|
// Checkbox keyboard navigation
|
|
2061
2491
|
if (handleCheckboxKeyDown(e))
|
|
2062
2492
|
return;
|
|
2063
|
-
// Tab: indent/outdent in lists
|
|
2493
|
+
// Tab: navigate table cells OR indent/outdent in lists
|
|
2064
2494
|
if (e.key === "Tab" && !isModifierPressed && !e.altKey) {
|
|
2495
|
+
// Table tab navigation takes priority
|
|
2496
|
+
if (getActiveCell()) {
|
|
2497
|
+
e.preventDefault();
|
|
2498
|
+
e.stopPropagation();
|
|
2499
|
+
e.stopImmediatePropagation();
|
|
2500
|
+
navigateTableCell(e.shiftKey ? "prev" : "next");
|
|
2501
|
+
setTimeout(() => {
|
|
2502
|
+
if (editor) {
|
|
2503
|
+
const content = domToContent(editor);
|
|
2504
|
+
notifyChange(content);
|
|
2505
|
+
}
|
|
2506
|
+
}, 0);
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2065
2509
|
e.preventDefault();
|
|
2066
2510
|
e.stopPropagation();
|
|
2067
2511
|
e.stopImmediatePropagation();
|
|
@@ -2093,6 +2537,19 @@ function useEditorEvents({ editorRef, historyRef, isUpdatingRef, notifyChange, h
|
|
|
2093
2537
|
return;
|
|
2094
2538
|
}
|
|
2095
2539
|
}
|
|
2540
|
+
// Cmd/Ctrl+K: trigger link button click (if present in toolbar)
|
|
2541
|
+
if (isModifierPressed && e.key === "k") {
|
|
2542
|
+
e.preventDefault();
|
|
2543
|
+
e.stopPropagation();
|
|
2544
|
+
// Find and click the link button in the toolbar
|
|
2545
|
+
const editorContainer = editor.closest(".rte-container");
|
|
2546
|
+
if (editorContainer) {
|
|
2547
|
+
const linkBtn = editorContainer.querySelector('button[aria-label="Link"], button[aria-label="Link einfügen"]');
|
|
2548
|
+
if (linkBtn)
|
|
2549
|
+
linkBtn.click();
|
|
2550
|
+
}
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2096
2553
|
// Undo/Redo shortcuts
|
|
2097
2554
|
if (isModifierPressed && e.key === "z" && !e.shiftKey) {
|
|
2098
2555
|
e.preventDefault();
|
|
@@ -2592,8 +3049,75 @@ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholde
|
|
|
2592
3049
|
if (onEditorAPIReady)
|
|
2593
3050
|
onEditorAPIReady(editorAPI);
|
|
2594
3051
|
}, [editorAPI, onEditorAPIReady]);
|
|
3052
|
+
// --- Helper: insert an image file via the onImageUpload callback ---
|
|
3053
|
+
const insertImageFile = useCallback(async (file) => {
|
|
3054
|
+
if (!onImageUpload || !file.type.startsWith("image/"))
|
|
3055
|
+
return;
|
|
3056
|
+
const editor = editorRef.current;
|
|
3057
|
+
if (!editor)
|
|
3058
|
+
return;
|
|
3059
|
+
try {
|
|
3060
|
+
// Show a placeholder while uploading
|
|
3061
|
+
const placeholder = document.createElement("img");
|
|
3062
|
+
placeholder.setAttribute("data-uploading", "true");
|
|
3063
|
+
placeholder.style.maxWidth = "100%";
|
|
3064
|
+
placeholder.style.height = "auto";
|
|
3065
|
+
placeholder.style.display = "block";
|
|
3066
|
+
placeholder.style.margin = "16px 0";
|
|
3067
|
+
placeholder.style.opacity = "0.5";
|
|
3068
|
+
// Use a tiny transparent gif as placeholder src
|
|
3069
|
+
placeholder.src =
|
|
3070
|
+
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
|
3071
|
+
placeholder.alt = file.name;
|
|
3072
|
+
const sel = window.getSelection();
|
|
3073
|
+
if (sel && sel.rangeCount > 0) {
|
|
3074
|
+
const range = sel.getRangeAt(0);
|
|
3075
|
+
range.deleteContents();
|
|
3076
|
+
range.insertNode(placeholder);
|
|
3077
|
+
range.setStartAfter(placeholder);
|
|
3078
|
+
range.collapse(true);
|
|
3079
|
+
sel.removeAllRanges();
|
|
3080
|
+
sel.addRange(range);
|
|
3081
|
+
}
|
|
3082
|
+
else {
|
|
3083
|
+
editor.appendChild(placeholder);
|
|
3084
|
+
}
|
|
3085
|
+
// Upload
|
|
3086
|
+
const url = await onImageUpload(file);
|
|
3087
|
+
// Replace placeholder with final image
|
|
3088
|
+
placeholder.src = url;
|
|
3089
|
+
placeholder.removeAttribute("data-uploading");
|
|
3090
|
+
placeholder.style.opacity = "1";
|
|
3091
|
+
// Preserve data-attachment-id if returned in a special format
|
|
3092
|
+
// The onImageUpload callback can return "url|attachmentId"
|
|
3093
|
+
if (url.includes("|__aid__:")) {
|
|
3094
|
+
const [realUrl, aid] = url.split("|__aid__:");
|
|
3095
|
+
placeholder.src = realUrl;
|
|
3096
|
+
placeholder.setAttribute("data-attachment-id", aid);
|
|
3097
|
+
}
|
|
3098
|
+
notifyChange(domToContent(editor));
|
|
3099
|
+
}
|
|
3100
|
+
catch (err) {
|
|
3101
|
+
console.error("Image upload failed:", err);
|
|
3102
|
+
// Remove failed placeholder
|
|
3103
|
+
const failedImg = editor.querySelector('img[data-uploading="true"]');
|
|
3104
|
+
failedImg?.remove();
|
|
3105
|
+
}
|
|
3106
|
+
}, [onImageUpload, notifyChange]);
|
|
2595
3107
|
// --- Paste handler ---
|
|
2596
3108
|
const handlePaste = (e) => {
|
|
3109
|
+
// Check for pasted image files first
|
|
3110
|
+
const items = e.clipboardData.items;
|
|
3111
|
+
for (let i = 0; i < items.length; i++) {
|
|
3112
|
+
const item = items[i];
|
|
3113
|
+
if (item.type.startsWith("image/")) {
|
|
3114
|
+
e.preventDefault();
|
|
3115
|
+
const file = item.getAsFile();
|
|
3116
|
+
if (file)
|
|
3117
|
+
insertImageFile(file);
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
2597
3121
|
e.preventDefault();
|
|
2598
3122
|
const html = e.clipboardData.getData("text/html");
|
|
2599
3123
|
const text = e.clipboardData.getData("text/plain");
|
|
@@ -2660,7 +3184,22 @@ const Editor = ({ initialContent, onChange, plugins: providedPlugins, placeholde
|
|
|
2660
3184
|
}),
|
|
2661
3185
|
}
|
|
2662
3186
|
: {};
|
|
2663
|
-
return (jsxs("div", { className: `rte-container ${className || ""}`, style: containerStyle, children: [jsx(Toolbar, { plugins: plugins, editorAPI: editorAPI, className: toolbarClassName }), jsx("div", { ref: editorRef, contentEditable: true, className: `rte-editor ${editorClassName || ""}`, "data-placeholder": placeholder, onPaste: handlePaste,
|
|
3187
|
+
return (jsxs("div", { className: `rte-container ${className || ""}`, style: containerStyle, children: [jsx(Toolbar, { plugins: plugins, editorAPI: editorAPI, className: toolbarClassName }), jsx("div", { ref: editorRef, contentEditable: true, className: `rte-editor ${editorClassName || ""}`, "data-placeholder": placeholder, onPaste: handlePaste, onDrop: (e) => {
|
|
3188
|
+
const files = e.dataTransfer.files;
|
|
3189
|
+
for (let i = 0; i < files.length; i++) {
|
|
3190
|
+
if (files[i].type.startsWith("image/")) {
|
|
3191
|
+
e.preventDefault();
|
|
3192
|
+
insertImageFile(files[i]);
|
|
3193
|
+
return;
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
}, onDragOver: (e) => {
|
|
3197
|
+
// Allow drop
|
|
3198
|
+
const types = e.dataTransfer.types;
|
|
3199
|
+
if (types && Array.from(types).includes("Files")) {
|
|
3200
|
+
e.preventDefault();
|
|
3201
|
+
}
|
|
3202
|
+
}, suppressContentEditableWarning: true })] }));
|
|
2664
3203
|
};
|
|
2665
3204
|
// --- Helper: Insert Image ---
|
|
2666
3205
|
function handleInsertImage(editor, value, isUpdatingRef, historyRef, notifyChange) {
|
|
@@ -3015,5 +3554,465 @@ const orderedListPlugin = {
|
|
|
3015
3554
|
},
|
|
3016
3555
|
};
|
|
3017
3556
|
|
|
3018
|
-
|
|
3557
|
+
const alignmentLabels = {
|
|
3558
|
+
left: "Links",
|
|
3559
|
+
center: "Zentriert",
|
|
3560
|
+
right: "Rechts",
|
|
3561
|
+
justify: "Blocksatz",
|
|
3562
|
+
};
|
|
3563
|
+
const alignmentIcons = {
|
|
3564
|
+
left: "mdi:format-align-left",
|
|
3565
|
+
center: "mdi:format-align-center",
|
|
3566
|
+
right: "mdi:format-align-right",
|
|
3567
|
+
justify: "mdi:format-align-justify",
|
|
3568
|
+
};
|
|
3569
|
+
const alignmentCommands = {
|
|
3570
|
+
left: "justifyLeft",
|
|
3571
|
+
center: "justifyCenter",
|
|
3572
|
+
right: "justifyRight",
|
|
3573
|
+
justify: "justifyFull",
|
|
3574
|
+
};
|
|
3575
|
+
/**
|
|
3576
|
+
* Detects current text alignment at cursor position.
|
|
3577
|
+
*/
|
|
3578
|
+
function detectCurrentAlignment() {
|
|
3579
|
+
if (typeof document === "undefined")
|
|
3580
|
+
return undefined;
|
|
3581
|
+
const selection = document.getSelection();
|
|
3582
|
+
if (!selection || selection.rangeCount === 0)
|
|
3583
|
+
return undefined;
|
|
3584
|
+
const range = selection.getRangeAt(0);
|
|
3585
|
+
const container = range.commonAncestorContainer;
|
|
3586
|
+
const element = container.nodeType === Node.TEXT_NODE
|
|
3587
|
+
? container.parentElement
|
|
3588
|
+
: container;
|
|
3589
|
+
if (!element)
|
|
3590
|
+
return undefined;
|
|
3591
|
+
// Walk up to find the block-level element
|
|
3592
|
+
let block = element;
|
|
3593
|
+
const blockTags = new Set([
|
|
3594
|
+
"P",
|
|
3595
|
+
"DIV",
|
|
3596
|
+
"H1",
|
|
3597
|
+
"H2",
|
|
3598
|
+
"H3",
|
|
3599
|
+
"H4",
|
|
3600
|
+
"H5",
|
|
3601
|
+
"H6",
|
|
3602
|
+
"LI",
|
|
3603
|
+
"BLOCKQUOTE",
|
|
3604
|
+
"TD",
|
|
3605
|
+
"TH",
|
|
3606
|
+
]);
|
|
3607
|
+
while (block && !blockTags.has(block.tagName)) {
|
|
3608
|
+
block = block.parentElement;
|
|
3609
|
+
}
|
|
3610
|
+
if (!block)
|
|
3611
|
+
return undefined;
|
|
3612
|
+
const align = block.style.textAlign || window.getComputedStyle(block).textAlign;
|
|
3613
|
+
if (align === "center")
|
|
3614
|
+
return "center";
|
|
3615
|
+
if (align === "right")
|
|
3616
|
+
return "right";
|
|
3617
|
+
if (align === "justify")
|
|
3618
|
+
return "justify";
|
|
3619
|
+
return "left";
|
|
3620
|
+
}
|
|
3621
|
+
/**
|
|
3622
|
+
* Creates an alignment plugin with a dropdown.
|
|
3623
|
+
* @param alignments - Which alignments to offer, defaults to all four.
|
|
3624
|
+
*/
|
|
3625
|
+
function createAlignmentPlugin(alignments = ["left", "center", "right", "justify"]) {
|
|
3626
|
+
const options = alignments.map((a) => ({
|
|
3627
|
+
value: a,
|
|
3628
|
+
label: alignmentLabels[a] || a,
|
|
3629
|
+
icon: alignmentIcons[a],
|
|
3630
|
+
}));
|
|
3631
|
+
return {
|
|
3632
|
+
name: "alignment",
|
|
3633
|
+
type: "block",
|
|
3634
|
+
renderButton: (props) => {
|
|
3635
|
+
const onSelect = props.onSelect;
|
|
3636
|
+
const currentValue = props.currentValue ||
|
|
3637
|
+
detectCurrentAlignment() ||
|
|
3638
|
+
"left";
|
|
3639
|
+
return (jsx(Dropdown, { icon: alignmentIcons[currentValue] || "mdi:format-align-left", label: "Ausrichtung", options: options, onSelect: (value) => {
|
|
3640
|
+
if (onSelect)
|
|
3641
|
+
onSelect(value);
|
|
3642
|
+
}, currentValue: currentValue, disabled: props.disabled }));
|
|
3643
|
+
},
|
|
3644
|
+
getCurrentValue: () => detectCurrentAlignment(),
|
|
3645
|
+
execute: (_editor, value) => {
|
|
3646
|
+
if (!value)
|
|
3647
|
+
return;
|
|
3648
|
+
const command = alignmentCommands[value];
|
|
3649
|
+
if (command) {
|
|
3650
|
+
document.execCommand(command, false);
|
|
3651
|
+
}
|
|
3652
|
+
},
|
|
3653
|
+
isActive: () => {
|
|
3654
|
+
const align = detectCurrentAlignment();
|
|
3655
|
+
return align !== undefined && align !== "left";
|
|
3656
|
+
},
|
|
3657
|
+
canExecute: () => true,
|
|
3658
|
+
};
|
|
3659
|
+
}
|
|
3660
|
+
/** Pre-built alignment plugin with all four options */
|
|
3661
|
+
const alignmentPlugin = createAlignmentPlugin();
|
|
3662
|
+
|
|
3663
|
+
const InsertTableDialog = ({ onInsert, onClose, }) => {
|
|
3664
|
+
const [rows, setRows] = useState(3);
|
|
3665
|
+
const [cols, setCols] = useState(3);
|
|
3666
|
+
const dialogRef = useRef(null);
|
|
3667
|
+
useEffect(() => {
|
|
3668
|
+
const handler = (e) => {
|
|
3669
|
+
if (dialogRef.current &&
|
|
3670
|
+
!dialogRef.current.contains(e.target)) {
|
|
3671
|
+
onClose();
|
|
3672
|
+
}
|
|
3673
|
+
};
|
|
3674
|
+
document.addEventListener("mousedown", handler);
|
|
3675
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
3676
|
+
}, [onClose]);
|
|
3677
|
+
return (jsxs("div", { className: "rte-table-insert-dialog", ref: dialogRef, children: [jsx("div", { className: "rte-table-insert-title", children: "Tabelle einf\u00FCgen" }), jsxs("div", { className: "rte-table-insert-fields", children: [jsxs("label", { className: "rte-table-insert-label", children: [jsx("span", { children: "Zeilen" }), jsx("input", { type: "number", min: 1, max: 20, value: rows, onChange: (e) => setRows(Math.max(1, Math.min(20, parseInt(e.target.value) || 1))), className: "rte-table-insert-input" })] }), jsxs("label", { className: "rte-table-insert-label", children: [jsx("span", { children: "Spalten" }), jsx("input", { type: "number", min: 1, max: 10, value: cols, onChange: (e) => setCols(Math.max(1, Math.min(10, parseInt(e.target.value) || 1))), className: "rte-table-insert-input" })] })] }), jsx("button", { type: "button", className: "rte-table-insert-btn", onClick: () => onInsert(rows, cols), children: "Einf\u00FCgen" })] }));
|
|
3678
|
+
};
|
|
3679
|
+
const TableContextMenu = ({ x, y, onClose, }) => {
|
|
3680
|
+
const ref = useRef(null);
|
|
3681
|
+
useEffect(() => {
|
|
3682
|
+
const handler = (e) => {
|
|
3683
|
+
if (ref.current && !ref.current.contains(e.target)) {
|
|
3684
|
+
onClose();
|
|
3685
|
+
}
|
|
3686
|
+
};
|
|
3687
|
+
document.addEventListener("mousedown", handler);
|
|
3688
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
3689
|
+
}, [onClose]);
|
|
3690
|
+
const action = (fn) => {
|
|
3691
|
+
fn();
|
|
3692
|
+
onClose();
|
|
3693
|
+
};
|
|
3694
|
+
return (jsxs("div", { ref: ref, className: "rte-table-context-menu", style: { position: "fixed", left: x, top: y }, children: [jsx("button", { type: "button", onClick: () => action(() => insertRow("above")), children: "Zeile oben einf\u00FCgen" }), jsx("button", { type: "button", onClick: () => action(() => insertRow("below")), children: "Zeile unten einf\u00FCgen" }), jsx("div", { className: "rte-table-context-divider" }), jsx("button", { type: "button", onClick: () => action(() => insertColumn("left")), children: "Spalte links einf\u00FCgen" }), jsx("button", { type: "button", onClick: () => action(() => insertColumn("right")), children: "Spalte rechts einf\u00FCgen" }), jsx("div", { className: "rte-table-context-divider" }), jsx("button", { type: "button", className: "rte-table-context-danger", onClick: () => action(deleteRow), children: "Zeile l\u00F6schen" }), jsx("button", { type: "button", className: "rte-table-context-danger", onClick: () => action(deleteColumn), children: "Spalte l\u00F6schen" }), jsx("button", { type: "button", className: "rte-table-context-danger", onClick: () => action(deleteTable), children: "Tabelle l\u00F6schen" })] }));
|
|
3695
|
+
};
|
|
3696
|
+
const TableToolbarButton = (props) => {
|
|
3697
|
+
const [showDialog, setShowDialog] = useState(false);
|
|
3698
|
+
const handleInsert = useCallback((rows, cols) => {
|
|
3699
|
+
setShowDialog(false);
|
|
3700
|
+
if (!props.editorAPI)
|
|
3701
|
+
return;
|
|
3702
|
+
const sel = document.getSelection();
|
|
3703
|
+
if (!sel || sel.rangeCount === 0)
|
|
3704
|
+
return;
|
|
3705
|
+
const range = sel.getRangeAt(0);
|
|
3706
|
+
// Find the editor's contentEditable root
|
|
3707
|
+
const container = range.commonAncestorContainer;
|
|
3708
|
+
const editorEl = container.nodeType === Node.TEXT_NODE
|
|
3709
|
+
? container.parentElement
|
|
3710
|
+
: container;
|
|
3711
|
+
const editorRoot = editorEl?.closest("[contenteditable]");
|
|
3712
|
+
if (!editorRoot)
|
|
3713
|
+
return;
|
|
3714
|
+
const table = createTable(rows, cols);
|
|
3715
|
+
// Insert after the current block element
|
|
3716
|
+
let block = editorEl;
|
|
3717
|
+
while (block &&
|
|
3718
|
+
block !== editorRoot &&
|
|
3719
|
+
block.parentElement !== editorRoot) {
|
|
3720
|
+
block = block.parentElement;
|
|
3721
|
+
}
|
|
3722
|
+
if (block && block !== editorRoot) {
|
|
3723
|
+
block.parentNode?.insertBefore(table, block.nextSibling);
|
|
3724
|
+
}
|
|
3725
|
+
else {
|
|
3726
|
+
editorRoot.appendChild(table);
|
|
3727
|
+
}
|
|
3728
|
+
// Add a paragraph after the table so the user can continue typing
|
|
3729
|
+
const p = document.createElement("p");
|
|
3730
|
+
p.innerHTML = "<br>";
|
|
3731
|
+
table.parentNode?.insertBefore(p, table.nextSibling);
|
|
3732
|
+
// Focus the first cell
|
|
3733
|
+
const firstCell = table.querySelector("td, th");
|
|
3734
|
+
if (firstCell) {
|
|
3735
|
+
const newRange = document.createRange();
|
|
3736
|
+
newRange.setStart(firstCell, 0);
|
|
3737
|
+
newRange.collapse(true);
|
|
3738
|
+
sel.removeAllRanges();
|
|
3739
|
+
sel.addRange(newRange);
|
|
3740
|
+
}
|
|
3741
|
+
}, [props.editorAPI]);
|
|
3742
|
+
return (jsxs("div", { style: { position: "relative" }, children: [jsx("button", { type: "button", onClick: () => setShowDialog(!showDialog), disabled: props.disabled, className: `rte-toolbar-button ${props.isActive ? "rte-toolbar-button-active" : ""}`, title: "Tabelle", "aria-label": "Tabelle", children: jsx(IconWrapper, { icon: "mdi:table", width: 18, height: 18 }) }), showDialog && (jsx(InsertTableDialog, { onInsert: handleInsert, onClose: () => setShowDialog(false) }))] }));
|
|
3743
|
+
};
|
|
3744
|
+
/* ══════════════════════════════════════════════════════════════════════════
|
|
3745
|
+
Table Plugin export
|
|
3746
|
+
══════════════════════════════════════════════════════════════════════ */
|
|
3747
|
+
const tablePlugin = {
|
|
3748
|
+
name: "table",
|
|
3749
|
+
type: "command",
|
|
3750
|
+
renderButton: (props) => (jsx(TableToolbarButton, { ...props, editorAPI: props.editorAPI })),
|
|
3751
|
+
execute: () => {
|
|
3752
|
+
// Insertion is handled by the dialog component
|
|
3753
|
+
},
|
|
3754
|
+
isActive: () => getActiveTable() !== null,
|
|
3755
|
+
canExecute: () => true,
|
|
3756
|
+
};
|
|
3757
|
+
/* ══════════════════════════════════════════════════════════════════════════
|
|
3758
|
+
TableContextMenuProvider — wrap the editor to enable right-click menu
|
|
3759
|
+
══════════════════════════════════════════════════════════════════════ */
|
|
3760
|
+
const TableContextMenuProvider = ({ children }) => {
|
|
3761
|
+
const [menu, setMenu] = useState(null);
|
|
3762
|
+
useEffect(() => {
|
|
3763
|
+
const handler = (e) => {
|
|
3764
|
+
const target = e.target;
|
|
3765
|
+
const cell = target.closest("td, th");
|
|
3766
|
+
const editorRoot = target.closest("[contenteditable]");
|
|
3767
|
+
if (cell && editorRoot) {
|
|
3768
|
+
e.preventDefault();
|
|
3769
|
+
setMenu({ x: e.clientX, y: e.clientY });
|
|
3770
|
+
}
|
|
3771
|
+
};
|
|
3772
|
+
document.addEventListener("contextmenu", handler);
|
|
3773
|
+
return () => document.removeEventListener("contextmenu", handler);
|
|
3774
|
+
}, []);
|
|
3775
|
+
return (jsxs(Fragment, { children: [children, menu && (jsx(TableContextMenu, { x: menu.x, y: menu.y, onClose: () => setMenu(null) }))] }));
|
|
3776
|
+
};
|
|
3777
|
+
|
|
3778
|
+
const EMPTY_LINK = {
|
|
3779
|
+
url: "",
|
|
3780
|
+
target: "_self",
|
|
3781
|
+
rel: "",
|
|
3782
|
+
title: "",
|
|
3783
|
+
pageRef: "",
|
|
3784
|
+
urlExtra: "",
|
|
3785
|
+
};
|
|
3786
|
+
const LinkDialog = ({ initialData, options, onSave, onRemove, onClose, isEditing, }) => {
|
|
3787
|
+
const [data, setData] = useState(initialData);
|
|
3788
|
+
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
3789
|
+
const dialogRef = useRef(null);
|
|
3790
|
+
const urlInputRef = useRef(null);
|
|
3791
|
+
useEffect(() => {
|
|
3792
|
+
urlInputRef.current?.focus();
|
|
3793
|
+
}, []);
|
|
3794
|
+
useEffect(() => {
|
|
3795
|
+
const handler = (e) => {
|
|
3796
|
+
if (dialogRef.current &&
|
|
3797
|
+
!dialogRef.current.contains(e.target)) {
|
|
3798
|
+
onClose();
|
|
3799
|
+
}
|
|
3800
|
+
};
|
|
3801
|
+
// Delay to avoid closing immediately on the same click
|
|
3802
|
+
const timer = setTimeout(() => {
|
|
3803
|
+
document.addEventListener("mousedown", handler);
|
|
3804
|
+
}, 50);
|
|
3805
|
+
return () => {
|
|
3806
|
+
clearTimeout(timer);
|
|
3807
|
+
document.removeEventListener("mousedown", handler);
|
|
3808
|
+
};
|
|
3809
|
+
}, [onClose]);
|
|
3810
|
+
const handleKeyDown = (e) => {
|
|
3811
|
+
if (e.key === "Enter") {
|
|
3812
|
+
e.preventDefault();
|
|
3813
|
+
onSave(data);
|
|
3814
|
+
}
|
|
3815
|
+
else if (e.key === "Escape") {
|
|
3816
|
+
e.preventDefault();
|
|
3817
|
+
onClose();
|
|
3818
|
+
}
|
|
3819
|
+
};
|
|
3820
|
+
const set = (key, value) => setData((prev) => ({ ...prev, [key]: value }));
|
|
3821
|
+
const hasAdvancedOptions = options.enableRel ||
|
|
3822
|
+
options.enableTitle ||
|
|
3823
|
+
options.enableUrlExtra ||
|
|
3824
|
+
options.enablePageRef;
|
|
3825
|
+
return (jsxs("div", { ref: dialogRef, className: "rte-link-dialog", onKeyDown: handleKeyDown, children: [jsx("div", { className: "rte-link-dialog-title", children: isEditing ? "Link bearbeiten" : "Link einfügen" }), jsxs("div", { className: "rte-link-dialog-field", children: [jsx("label", { className: "rte-link-dialog-label", children: "URL" }), jsx("input", { ref: urlInputRef, type: "url", className: "rte-link-dialog-input", value: data.url, onChange: (e) => set("url", e.target.value), placeholder: "https://..." })] }), options.enableTarget && (jsxs("div", { className: "rte-link-dialog-field", children: [jsx("label", { className: "rte-link-dialog-label", children: "Ziel" }), jsxs("select", { className: "rte-link-dialog-select", value: data.target, onChange: (e) => set("target", e.target.value), children: [jsx("option", { value: "_self", children: "Gleiches Fenster" }), jsx("option", { value: "_blank", children: "Neues Fenster" })] })] })), hasAdvancedOptions && (jsxs(Fragment, { children: [jsx("button", { type: "button", className: "rte-link-dialog-toggle", onClick: () => setShowAdvanced(!showAdvanced), children: showAdvanced ? "Erweitert ausblenden" : "Erweitert anzeigen" }), showAdvanced && (jsxs("div", { className: "rte-link-dialog-advanced", children: [options.enablePageRef && (jsxs("div", { className: "rte-link-dialog-field", children: [jsx("label", { className: "rte-link-dialog-label", children: "Seitenreferenz" }), jsx("input", { type: "text", className: "rte-link-dialog-input", value: data.pageRef, onChange: (e) => set("pageRef", e.target.value), placeholder: "page-id" })] })), options.enableUrlExtra && (jsxs("div", { className: "rte-link-dialog-field", children: [jsx("label", { className: "rte-link-dialog-label", children: "URL Extra" }), jsx("input", { type: "text", className: "rte-link-dialog-input", value: data.urlExtra, onChange: (e) => set("urlExtra", e.target.value), placeholder: "?param=value oder #anchor" })] })), options.enableRel && (jsxs("div", { className: "rte-link-dialog-field", children: [jsx("label", { className: "rte-link-dialog-label", children: "Rel" }), jsx("input", { type: "text", className: "rte-link-dialog-input", value: data.rel, onChange: (e) => set("rel", e.target.value), placeholder: "noopener noreferrer" })] })), options.enableTitle && (jsxs("div", { className: "rte-link-dialog-field", children: [jsx("label", { className: "rte-link-dialog-label", children: "Titel" }), jsx("input", { type: "text", className: "rte-link-dialog-input", value: data.title, onChange: (e) => set("title", e.target.value), placeholder: "Link-Titel" })] }))] }))] })), jsxs("div", { className: "rte-link-dialog-actions", children: [jsx("button", { type: "button", className: "rte-link-dialog-btn rte-link-dialog-btn-primary", onClick: () => onSave(data), disabled: !data.url.trim(), children: isEditing ? "Speichern" : "Einfügen" }), isEditing && (jsx("button", { type: "button", className: "rte-link-dialog-btn rte-link-dialog-btn-danger", onClick: onRemove, children: "Entfernen" })), jsx("button", { type: "button", className: "rte-link-dialog-btn", onClick: onClose, children: "Abbrechen" })] })] }));
|
|
3826
|
+
};
|
|
3827
|
+
const LinkToolbarButton = (props) => {
|
|
3828
|
+
const [showDialog, setShowDialog] = useState(false);
|
|
3829
|
+
const [linkData, setLinkData] = useState(EMPTY_LINK);
|
|
3830
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
3831
|
+
const savedRangeRef = useRef(null);
|
|
3832
|
+
const openDialog = useCallback(() => {
|
|
3833
|
+
const sel = document.getSelection();
|
|
3834
|
+
if (!sel || sel.rangeCount === 0)
|
|
3835
|
+
return;
|
|
3836
|
+
// Save the current selection
|
|
3837
|
+
savedRangeRef.current = sel.getRangeAt(0).cloneRange();
|
|
3838
|
+
const range = sel.getRangeAt(0);
|
|
3839
|
+
const container = range.commonAncestorContainer;
|
|
3840
|
+
const element = container.nodeType === Node.TEXT_NODE
|
|
3841
|
+
? container.parentElement
|
|
3842
|
+
: container;
|
|
3843
|
+
const existingLink = element?.closest("a");
|
|
3844
|
+
if (existingLink) {
|
|
3845
|
+
setIsEditing(true);
|
|
3846
|
+
setLinkData({
|
|
3847
|
+
url: existingLink.getAttribute("href") || "",
|
|
3848
|
+
target: existingLink.getAttribute("target") || "_self",
|
|
3849
|
+
rel: existingLink.getAttribute("rel") || "",
|
|
3850
|
+
title: existingLink.getAttribute("title") || "",
|
|
3851
|
+
pageRef: existingLink.getAttribute("data-page-ref") || "",
|
|
3852
|
+
urlExtra: existingLink.getAttribute("data-url-extra") || "",
|
|
3853
|
+
});
|
|
3854
|
+
}
|
|
3855
|
+
else {
|
|
3856
|
+
setIsEditing(false);
|
|
3857
|
+
setLinkData(EMPTY_LINK);
|
|
3858
|
+
}
|
|
3859
|
+
setShowDialog(true);
|
|
3860
|
+
}, []);
|
|
3861
|
+
const restoreSelection = useCallback(() => {
|
|
3862
|
+
if (savedRangeRef.current) {
|
|
3863
|
+
const sel = document.getSelection();
|
|
3864
|
+
if (sel) {
|
|
3865
|
+
sel.removeAllRanges();
|
|
3866
|
+
sel.addRange(savedRangeRef.current);
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
}, []);
|
|
3870
|
+
const handleSave = useCallback((data) => {
|
|
3871
|
+
setShowDialog(false);
|
|
3872
|
+
restoreSelection();
|
|
3873
|
+
const sel = document.getSelection();
|
|
3874
|
+
if (!sel || sel.rangeCount === 0)
|
|
3875
|
+
return;
|
|
3876
|
+
const range = sel.getRangeAt(0);
|
|
3877
|
+
const container = range.commonAncestorContainer;
|
|
3878
|
+
const element = container.nodeType === Node.TEXT_NODE
|
|
3879
|
+
? container.parentElement
|
|
3880
|
+
: container;
|
|
3881
|
+
const existingLink = element?.closest("a");
|
|
3882
|
+
if (existingLink) {
|
|
3883
|
+
// Update existing link
|
|
3884
|
+
existingLink.setAttribute("href", data.url);
|
|
3885
|
+
if (data.target && data.target !== "_self") {
|
|
3886
|
+
existingLink.setAttribute("target", data.target);
|
|
3887
|
+
}
|
|
3888
|
+
else {
|
|
3889
|
+
existingLink.removeAttribute("target");
|
|
3890
|
+
}
|
|
3891
|
+
if (data.rel) {
|
|
3892
|
+
existingLink.setAttribute("rel", data.rel);
|
|
3893
|
+
}
|
|
3894
|
+
else {
|
|
3895
|
+
existingLink.removeAttribute("rel");
|
|
3896
|
+
}
|
|
3897
|
+
if (data.title) {
|
|
3898
|
+
existingLink.setAttribute("title", data.title);
|
|
3899
|
+
}
|
|
3900
|
+
else {
|
|
3901
|
+
existingLink.removeAttribute("title");
|
|
3902
|
+
}
|
|
3903
|
+
if (data.pageRef) {
|
|
3904
|
+
existingLink.setAttribute("data-page-ref", data.pageRef);
|
|
3905
|
+
}
|
|
3906
|
+
else {
|
|
3907
|
+
existingLink.removeAttribute("data-page-ref");
|
|
3908
|
+
}
|
|
3909
|
+
if (data.urlExtra) {
|
|
3910
|
+
existingLink.setAttribute("data-url-extra", data.urlExtra);
|
|
3911
|
+
}
|
|
3912
|
+
else {
|
|
3913
|
+
existingLink.removeAttribute("data-url-extra");
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
else {
|
|
3917
|
+
// Create new link
|
|
3918
|
+
document.execCommand("createLink", false, data.url);
|
|
3919
|
+
// Now find the newly created link and set extra attributes
|
|
3920
|
+
const newSel = document.getSelection();
|
|
3921
|
+
if (newSel && newSel.rangeCount > 0) {
|
|
3922
|
+
const newRange = newSel.getRangeAt(0);
|
|
3923
|
+
const newContainer = newRange.commonAncestorContainer;
|
|
3924
|
+
const newElement = newContainer.nodeType === Node.TEXT_NODE
|
|
3925
|
+
? newContainer.parentElement
|
|
3926
|
+
: newContainer;
|
|
3927
|
+
const newLink = newElement?.closest("a");
|
|
3928
|
+
if (newLink) {
|
|
3929
|
+
if (data.target && data.target !== "_self") {
|
|
3930
|
+
newLink.setAttribute("target", data.target);
|
|
3931
|
+
}
|
|
3932
|
+
if (data.rel) {
|
|
3933
|
+
newLink.setAttribute("rel", data.rel);
|
|
3934
|
+
}
|
|
3935
|
+
if (data.title) {
|
|
3936
|
+
newLink.setAttribute("title", data.title);
|
|
3937
|
+
}
|
|
3938
|
+
if (data.pageRef) {
|
|
3939
|
+
newLink.setAttribute("data-page-ref", data.pageRef);
|
|
3940
|
+
}
|
|
3941
|
+
if (data.urlExtra) {
|
|
3942
|
+
newLink.setAttribute("data-url-extra", data.urlExtra);
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
}, [restoreSelection]);
|
|
3948
|
+
const handleRemove = useCallback(() => {
|
|
3949
|
+
setShowDialog(false);
|
|
3950
|
+
restoreSelection();
|
|
3951
|
+
const sel = document.getSelection();
|
|
3952
|
+
if (!sel || sel.rangeCount === 0)
|
|
3953
|
+
return;
|
|
3954
|
+
const range = sel.getRangeAt(0);
|
|
3955
|
+
const container = range.commonAncestorContainer;
|
|
3956
|
+
const element = container.nodeType === Node.TEXT_NODE
|
|
3957
|
+
? container.parentElement
|
|
3958
|
+
: container;
|
|
3959
|
+
const existingLink = element?.closest("a");
|
|
3960
|
+
if (existingLink) {
|
|
3961
|
+
const parent = existingLink.parentNode;
|
|
3962
|
+
if (parent) {
|
|
3963
|
+
while (existingLink.firstChild) {
|
|
3964
|
+
parent.insertBefore(existingLink.firstChild, existingLink);
|
|
3965
|
+
}
|
|
3966
|
+
parent.removeChild(existingLink);
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
}, [restoreSelection]);
|
|
3970
|
+
return (jsxs("div", { style: { position: "relative" }, children: [jsx("button", { type: "button", onClick: openDialog, disabled: props.disabled, className: `rte-toolbar-button ${props.isActive ? "rte-toolbar-button-active" : ""}`, title: "Link", "aria-label": "Link", children: jsx(IconWrapper, { icon: "mdi:link", width: 18, height: 18 }) }), showDialog && (jsx(LinkDialog, { initialData: linkData, options: props.options, onSave: handleSave, onRemove: handleRemove, onClose: () => setShowDialog(false), isEditing: isEditing }))] }));
|
|
3971
|
+
};
|
|
3972
|
+
/* ══════════════════════════════════════════════════════════════════════════
|
|
3973
|
+
Plugin factory + export
|
|
3974
|
+
══════════════════════════════════════════════════════════════════════ */
|
|
3975
|
+
/**
|
|
3976
|
+
* Creates an advanced link plugin with a floating dialog.
|
|
3977
|
+
* Supports URL, target, rel, title, page reference, and URL extra.
|
|
3978
|
+
*/
|
|
3979
|
+
function createAdvancedLinkPlugin(options = {}) {
|
|
3980
|
+
const opts = {
|
|
3981
|
+
enablePageRef: false,
|
|
3982
|
+
enableTarget: true,
|
|
3983
|
+
enableRel: true,
|
|
3984
|
+
enableTitle: true,
|
|
3985
|
+
enableUrlExtra: false,
|
|
3986
|
+
...options,
|
|
3987
|
+
};
|
|
3988
|
+
return {
|
|
3989
|
+
name: "advancedLink",
|
|
3990
|
+
type: "inline",
|
|
3991
|
+
renderButton: (props) => (jsx(LinkToolbarButton, { ...props, editorAPI: props.editorAPI, options: opts })),
|
|
3992
|
+
execute: () => {
|
|
3993
|
+
// Handled by the dialog component
|
|
3994
|
+
},
|
|
3995
|
+
isActive: () => {
|
|
3996
|
+
if (typeof document === "undefined")
|
|
3997
|
+
return false;
|
|
3998
|
+
const sel = document.getSelection();
|
|
3999
|
+
if (!sel || sel.rangeCount === 0)
|
|
4000
|
+
return false;
|
|
4001
|
+
const range = sel.getRangeAt(0);
|
|
4002
|
+
const container = range.commonAncestorContainer;
|
|
4003
|
+
const element = container.nodeType === Node.TEXT_NODE
|
|
4004
|
+
? container.parentElement
|
|
4005
|
+
: container;
|
|
4006
|
+
return element?.closest("a") !== null;
|
|
4007
|
+
},
|
|
4008
|
+
canExecute: () => {
|
|
4009
|
+
const sel = document.getSelection();
|
|
4010
|
+
return sel !== null && sel.rangeCount > 0;
|
|
4011
|
+
},
|
|
4012
|
+
};
|
|
4013
|
+
}
|
|
4014
|
+
/** Pre-built advanced link plugin with target + rel + title enabled */
|
|
4015
|
+
const advancedLinkPlugin = createAdvancedLinkPlugin();
|
|
4016
|
+
|
|
4017
|
+
export { Dropdown, Editor, HistoryManager, TableContextMenuProvider, Toolbar, advancedLinkPlugin, alignmentPlugin, blockquotePlugin, boldPlugin, clearFormattingPlugin, codeInlinePlugin, contentToDOM, contentToHTML, createAdvancedLinkPlugin, createAlignmentPlugin, createBackgroundColorPlugin, createBlockFormatPlugin, createEmptyContent, createFontSizePlugin, createHeadingsPlugin, createImagePlugin, createLinkPlugin, createTextColorPlugin, Editor as default, defaultPlugins, domToContent, ensureAllCheckboxes, findClosestCheckboxList, getCurrentBackgroundColor, getCurrentFontSize, getCurrentHeading, getCurrentTextColor, htmlToContent, indentListItem, indentListItemPlugin, isCheckboxList, italicPlugin, linkPlugin, orderedListPlugin, outdentListItem, outdentListItemPlugin, redoPlugin, strikethroughPlugin, subscriptPlugin, superscriptPlugin, tablePlugin, underlinePlugin, undoPlugin, unorderedListPlugin };
|
|
3019
4018
|
//# sourceMappingURL=index.esm.js.map
|