@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/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
- attributes.href = href;
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(attributes).length > 0
1339
- ? attributes
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, suppressContentEditableWarning: true })] }));
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
- export { Dropdown, Editor, HistoryManager, Toolbar, blockquotePlugin, boldPlugin, clearFormattingPlugin, contentToDOM, contentToHTML, 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, underlinePlugin, undoPlugin, unorderedListPlugin };
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