@lukso/web-components 1.156.0 → 1.156.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,9 +48,11 @@ let LuksoMarkdownEditor = class extends TailwindStyledElement(style) {
48
48
  this.isHeadingDropdownOpen = false;
49
49
  this.isColorDropdownOpen = false;
50
50
  this.isListDropdownOpen = false;
51
+ this.isAlignmentDropdownOpen = false;
51
52
  this.headingTriggerId = "heading-dropdown-trigger";
52
53
  this.colorTriggerId = "color-dropdown-trigger";
53
54
  this.listTriggerId = "list-dropdown-trigger";
55
+ this.alignmentTriggerId = "alignment-dropdown-trigger";
54
56
  this.currentSelection = { start: 0, end: 0 };
55
57
  this.savedSelection = null;
56
58
  this.defaultColor = "#374151";
@@ -61,10 +63,12 @@ let LuksoMarkdownEditor = class extends TailwindStyledElement(style) {
61
63
  h1: false,
62
64
  h2: false,
63
65
  h3: false,
66
+ h4: false,
64
67
  color: false,
65
68
  activeColor: this.defaultColor,
66
69
  unorderedList: false,
67
- orderedList: false
70
+ orderedList: false,
71
+ alignment: "left"
68
72
  };
69
73
  // Undo/Redo state
70
74
  this.undoStack = [];
@@ -87,14 +91,19 @@ let LuksoMarkdownEditor = class extends TailwindStyledElement(style) {
87
91
  if (this.isListDropdownOpen) {
88
92
  this.isListDropdownOpen = false;
89
93
  }
94
+ if (this.isAlignmentDropdownOpen) {
95
+ this.isAlignmentDropdownOpen = false;
96
+ }
90
97
  return;
91
98
  }
92
99
  const isInsideHeadingDropdown = this.shadowRoot?.getElementById("headingDropdown")?.contains(target);
93
100
  const isInsideColorDropdown = this.shadowRoot?.getElementById("colorDropdown")?.contains(target);
94
101
  const isInsideListDropdown = this.shadowRoot?.getElementById("listDropdown")?.contains(target);
102
+ const isInsideAlignmentDropdown = this.shadowRoot?.getElementById("alignmentDropdown")?.contains(target);
95
103
  const isHeadingTrigger = this.shadowRoot?.getElementById(this.headingTriggerId)?.contains(target);
96
104
  const isColorTrigger = this.shadowRoot?.getElementById(this.colorTriggerId)?.contains(target);
97
105
  const isListTrigger = this.shadowRoot?.getElementById(this.listTriggerId)?.contains(target);
106
+ const isAlignmentTrigger = this.shadowRoot?.getElementById(this.alignmentTriggerId)?.contains(target);
98
107
  if (!isInsideHeadingDropdown && !isHeadingTrigger && this.isHeadingDropdownOpen) {
99
108
  this.isHeadingDropdownOpen = false;
100
109
  }
@@ -104,6 +113,9 @@ let LuksoMarkdownEditor = class extends TailwindStyledElement(style) {
104
113
  if (!isInsideListDropdown && !isListTrigger && this.isListDropdownOpen) {
105
114
  this.isListDropdownOpen = false;
106
115
  }
116
+ if (!isInsideAlignmentDropdown && !isAlignmentTrigger && this.isAlignmentDropdownOpen) {
117
+ this.isAlignmentDropdownOpen = false;
118
+ }
107
119
  };
108
120
  this.styles = ce({
109
121
  slots: {
@@ -116,6 +128,7 @@ let LuksoMarkdownEditor = class extends TailwindStyledElement(style) {
116
128
  colorMenu: "relative",
117
129
  headingMenu: "relative",
118
130
  listMenu: "relative",
131
+ alignmentMenu: "relative",
119
132
  label: "heading-inter-14-bold text-neutral-20",
120
133
  description: "paragraph-inter-12-regular text-neutral-20",
121
134
  divider: "w-[1px] h-4 bg-neutral-90"
@@ -328,7 +341,7 @@ let LuksoMarkdownEditor = class extends TailwindStyledElement(style) {
328
341
  /**
329
342
  * Apply or toggle heading formatting for the current line(s).
330
343
  *
331
- * @param level - 0 to remove heading, 1-3 for heading levels
344
+ * @param level - 0 to remove heading, 1-4 for heading levels
332
345
  */
333
346
  applyHeading(level) {
334
347
  if (this.isReadonly || this.isDisabled) return;
@@ -519,6 +532,156 @@ let LuksoMarkdownEditor = class extends TailwindStyledElement(style) {
519
532
  if (this.activeFormats.orderedList) return "ordered";
520
533
  return "none";
521
534
  }
535
+ /**
536
+ * Get the current alignment icon name based on activeFormats.
537
+ */
538
+ getAlignmentIcon() {
539
+ switch (this.activeFormats.alignment) {
540
+ case "center":
541
+ return "textalign-center";
542
+ case "right":
543
+ return "textalign-right";
544
+ default:
545
+ return "textalign-left";
546
+ }
547
+ }
548
+ /**
549
+ * Apply or toggle text alignment for the current line(s).
550
+ *
551
+ * @param alignment - 'left', 'center', or 'right'
552
+ */
553
+ applyAlignment(alignment) {
554
+ if (this.isReadonly || this.isDisabled) return;
555
+ this.saveUndoStateBeforeChange();
556
+ this.withSelection((textarea, start, end, value) => {
557
+ const lineStart = value.lastIndexOf("\n", start - 1) + 1;
558
+ let lineEnd = value.indexOf("\n", end);
559
+ if (lineEnd === -1) lineEnd = value.length;
560
+ const before = value.slice(0, lineStart);
561
+ const selected = value.slice(lineStart, lineEnd);
562
+ const after = value.slice(lineEnd);
563
+ let transformed;
564
+ const currentAlignmentRegex = /<div style="text-align: (left|center|right);">(.*?)<\/div>/s;
565
+ const existingMatch = selected.match(currentAlignmentRegex);
566
+ if (existingMatch || this.hasNestedAlignment(selected)) {
567
+ if (existingMatch) {
568
+ const existingAlignment = existingMatch[1];
569
+ const innerContent = existingMatch[2];
570
+ if (existingAlignment === alignment) {
571
+ transformed = innerContent;
572
+ } else {
573
+ transformed = `<div style="text-align: ${alignment};">${innerContent}</div>`;
574
+ }
575
+ } else {
576
+ if (this.getNestedAlignment(selected) === alignment) {
577
+ transformed = this.removeNestedAlignment(selected);
578
+ } else {
579
+ transformed = this.replaceNestedAlignment(selected, alignment);
580
+ }
581
+ }
582
+ } else {
583
+ if (alignment === "left") {
584
+ transformed = selected;
585
+ } else {
586
+ transformed = this.wrapContentWithAlignment(selected, alignment);
587
+ }
588
+ }
589
+ this.value = before + transformed + after;
590
+ textarea.value = before + transformed + after;
591
+ const cursorPosition = before.length + transformed.length;
592
+ requestAnimationFrame(() => {
593
+ textarea.setSelectionRange(cursorPosition, cursorPosition);
594
+ this.updateActiveFormats();
595
+ });
596
+ this.dispatchChange();
597
+ });
598
+ }
599
+ /**
600
+ * Check if content has nested alignment divs inside formatting markers.
601
+ */
602
+ hasNestedAlignment(content) {
603
+ const alignmentRegex = /<div style="text-align: (left|center|right);">/;
604
+ return alignmentRegex.test(content);
605
+ }
606
+ /**
607
+ * Get the alignment from nested alignment divs.
608
+ */
609
+ getNestedAlignment(content) {
610
+ const alignmentMatch = content.match(
611
+ /<div style="text-align: (left|center|right);">/
612
+ );
613
+ return alignmentMatch ? alignmentMatch[1] : null;
614
+ }
615
+ /**
616
+ * Remove nested alignment divs from content.
617
+ */
618
+ removeNestedAlignment(content) {
619
+ return content.replace(
620
+ /<div style="text-align: (left|center|right);">([^<]*?)<\/div>/g,
621
+ "$2"
622
+ );
623
+ }
624
+ /**
625
+ * Replace nested alignment with a new alignment.
626
+ */
627
+ replaceNestedAlignment(content, newAlignment) {
628
+ return content.replace(
629
+ /<div style="text-align: (left|center|right);">([^<]*?)<\/div>/g,
630
+ `<div style="text-align: ${newAlignment};">$2</div>`
631
+ );
632
+ }
633
+ /**
634
+ * Wrap content with alignment div, ensuring proper nesting inside formatting markers.
635
+ * Examples:
636
+ * - **text** becomes **<div style="text-align: center;">text</div>**
637
+ * - *text* becomes *<div style="text-align: center;">text</div>*
638
+ * - # text becomes # <div style="text-align: center;">text</div>
639
+ *
640
+ * @param content - the content to wrap
641
+ * @param alignment - 'left', 'center' or 'right'
642
+ */
643
+ wrapContentWithAlignment(content, alignment) {
644
+ const alignmentDiv = (innerContent) => `<div style="text-align: ${alignment};">${innerContent}</div>`;
645
+ const headingMatch = content.match(/^(#{1,6}\s+)(.*)$/);
646
+ if (headingMatch) {
647
+ const headingPrefix = headingMatch[1];
648
+ const headingText = headingMatch[2];
649
+ return headingPrefix + alignmentDiv(headingText);
650
+ }
651
+ const boldMatch = content.match(/^(\*\*)(.+?)(\*\*)$/s);
652
+ if (boldMatch) {
653
+ const innerText = boldMatch[2];
654
+ return `**${alignmentDiv(innerText)}**`;
655
+ }
656
+ const italicMatch = content.match(/^(\*)(.+?)(\*)$/s);
657
+ if (italicMatch) {
658
+ const innerText = italicMatch[2];
659
+ return `*${alignmentDiv(innerText)}*`;
660
+ }
661
+ const linkMatch = content.match(/^(\[)(.+?)(\]\([^)]+\))$/s);
662
+ if (linkMatch) {
663
+ const linkStart = linkMatch[1];
664
+ const linkText = linkMatch[2];
665
+ const linkEnd = linkMatch[3];
666
+ return linkStart + alignmentDiv(linkText) + linkEnd;
667
+ }
668
+ const colorMatch = content.match(
669
+ /^(<span style="color: [^"]+;">)(.+?)(<\/span>)$/s
670
+ );
671
+ if (colorMatch) {
672
+ const colorStart = colorMatch[1];
673
+ const colorText = colorMatch[2];
674
+ const colorEnd = colorMatch[3];
675
+ return colorStart + alignmentDiv(colorText) + colorEnd;
676
+ }
677
+ const listMatch = content.match(/^(\s*(?:[-*+]|\d+\.)\s+)(.*)$/);
678
+ if (listMatch) {
679
+ const listPrefix = listMatch[1];
680
+ const listText = listMatch[2];
681
+ return listPrefix + alignmentDiv(listText);
682
+ }
683
+ return alignmentDiv(content);
684
+ }
522
685
  /**
523
686
  * Toggle inline formatting by wrapping/unwrapping selection or current word.
524
687
  *
@@ -752,6 +915,23 @@ let LuksoMarkdownEditor = class extends TailwindStyledElement(style) {
752
915
  );
753
916
  activeColor = beforeColorMatch?.[1] || selectedColorMatch?.[1] || "";
754
917
  }
918
+ const alignmentRegex = /<div style="text-align: (left|center|right);">/;
919
+ let alignment = "left";
920
+ const alignmentMatch = currentLine.match(alignmentRegex);
921
+ if (alignmentMatch) {
922
+ alignment = alignmentMatch[1];
923
+ } else {
924
+ const beforeCurrentLine = this.value.slice(0, lineStart);
925
+ const alignmentStartMatch = beforeCurrentLine.match(
926
+ /.*<div style="text-align: (left|center|right);">[^<]*$/s
927
+ );
928
+ if (alignmentStartMatch) {
929
+ const afterCurrentLine = this.value.slice(lineEnd);
930
+ if (afterCurrentLine.includes("</div>")) {
931
+ alignment = alignmentStartMatch[1];
932
+ }
933
+ }
934
+ }
755
935
  this.activeFormats = {
756
936
  bold: hasBoldWrap,
757
937
  italic: hasItalicWrap,
@@ -759,10 +939,12 @@ let LuksoMarkdownEditor = class extends TailwindStyledElement(style) {
759
939
  h1: headingLevel === 1,
760
940
  h2: headingLevel === 2,
761
941
  h3: headingLevel === 3,
942
+ h4: headingLevel === 4,
762
943
  color: hasColorWrap,
763
944
  activeColor,
764
945
  unorderedList: hasUnorderedList,
765
- orderedList: hasOrderedList
946
+ orderedList: hasOrderedList,
947
+ alignment
766
948
  };
767
949
  }
768
950
  /**
@@ -964,54 +1146,59 @@ let LuksoMarkdownEditor = class extends TailwindStyledElement(style) {
964
1146
  return;
965
1147
  }
966
1148
  const { start: s, end: e } = this.expandSelectionToWord(start, end, value);
967
- const before = value.slice(0, s);
968
- let selected = value.slice(s, e);
969
- const after = value.slice(e);
970
- const colorRegex = /<span style="color: ([^"]+)">(.*?)<\/span>/gs;
971
- selected = selected.replace(colorRegex, "$2");
972
- const fullColorRegex = /<span style="color: ([^"]+)">(.*?)<\/span>/g;
1149
+ const colorRegex = /<span style="color: ([^"]+)">(.*?)<\/span>/g;
973
1150
  let match;
974
- let foundMatch = false;
975
- const searchText = value.slice(
976
- Math.max(0, s - 100),
977
- Math.min(value.length, e + 100)
978
- );
979
- const searchOffset = Math.max(0, s - 100);
980
- match = fullColorRegex.exec(searchText);
1151
+ let newValue = value;
1152
+ let foundSpan = false;
1153
+ colorRegex.lastIndex = 0;
1154
+ match = colorRegex.exec(value);
981
1155
  while (match !== null) {
982
- const matchStart = searchOffset + match.index;
983
- const matchEnd = searchOffset + match.index + match[0].length;
1156
+ const fullMatchStart = match.index;
1157
+ const fullMatchEnd = match.index + match[0].length;
984
1158
  const spanOpenTag = `<span style="color: ${match[1]}">`;
985
- const contentStart = searchOffset + match.index + spanOpenTag.length;
986
- const contentEnd = matchEnd - 7;
987
- if (contentStart <= s && e <= contentEnd) {
988
- const beforeContent = value.slice(contentStart, s);
989
- const afterContent = value.slice(e, contentEnd);
990
- const newContent = beforeContent + selected + afterContent;
991
- this.value = value.slice(0, matchStart) + newContent + value.slice(matchEnd);
992
- textarea.value = value.slice(0, matchStart) + newContent + value.slice(matchEnd);
993
- const selStart = matchStart + beforeContent.length;
994
- const selEnd = selStart + selected.length;
1159
+ const contentStart = fullMatchStart + spanOpenTag.length;
1160
+ const contentEnd = fullMatchEnd - 7;
1161
+ const innerContent = match[2];
1162
+ match = colorRegex.exec(value);
1163
+ const selectionOverlaps = s >= contentStart && s < contentEnd || e > contentStart && e <= contentEnd || s <= contentStart && e >= contentEnd;
1164
+ if (selectionOverlaps) {
1165
+ newValue = value.slice(0, fullMatchStart) + innerContent + value.slice(fullMatchEnd);
1166
+ foundSpan = true;
1167
+ this.value = newValue;
1168
+ textarea.value = newValue;
1169
+ let newStart = s;
1170
+ let newEnd = e;
1171
+ if (s >= fullMatchStart) {
1172
+ const spanOpenTagLength = spanOpenTag.length;
1173
+ if (s >= contentStart) {
1174
+ newStart = s - spanOpenTagLength;
1175
+ } else if (s >= fullMatchStart) {
1176
+ newStart = fullMatchStart;
1177
+ }
1178
+ }
1179
+ if (e >= fullMatchStart) {
1180
+ const spanOpenTagLength = spanOpenTag.length;
1181
+ if (e <= contentEnd) {
1182
+ newEnd = e - spanOpenTagLength;
1183
+ } else {
1184
+ newEnd = e - spanOpenTagLength - 7;
1185
+ }
1186
+ }
995
1187
  requestAnimationFrame(() => {
996
- textarea.setSelectionRange(selStart, selEnd);
1188
+ textarea.setSelectionRange(
1189
+ Math.max(0, newStart),
1190
+ Math.max(0, newEnd)
1191
+ );
997
1192
  this.updateActiveFormats();
998
1193
  });
999
1194
  this.dispatchChange();
1000
- foundMatch = true;
1001
- break;
1195
+ return;
1002
1196
  }
1003
- match = fullColorRegex.exec(searchText);
1004
1197
  }
1005
- if (!foundMatch) {
1006
- this.value = before + selected + after;
1007
- textarea.value = before + selected + after;
1008
- const selStart = before.length;
1009
- const selEnd = selStart + selected.length;
1198
+ if (!foundSpan) {
1010
1199
  requestAnimationFrame(() => {
1011
- textarea.setSelectionRange(selStart, selEnd);
1012
1200
  this.updateActiveFormats();
1013
1201
  });
1014
- this.dispatchChange();
1015
1202
  }
1016
1203
  });
1017
1204
  }
@@ -1022,6 +1209,7 @@ let LuksoMarkdownEditor = class extends TailwindStyledElement(style) {
1022
1209
  if (this.activeFormats.h1) return 1;
1023
1210
  if (this.activeFormats.h2) return 2;
1024
1211
  if (this.activeFormats.h3) return 3;
1212
+ if (this.activeFormats.h4) return 4;
1025
1213
  return 0;
1026
1214
  }
1027
1215
  /**
@@ -1089,10 +1277,14 @@ let LuksoMarkdownEditor = class extends TailwindStyledElement(style) {
1089
1277
  const textarea = this.textareaEl?.shadowRoot?.querySelector(
1090
1278
  "textarea"
1091
1279
  );
1092
- if (!textarea) return false;
1280
+ if (!textarea) {
1281
+ return false;
1282
+ }
1093
1283
  const start = textarea.selectionStart ?? 0;
1094
1284
  const end = textarea.selectionEnd ?? 0;
1095
- if (start !== end) return false;
1285
+ if (start !== end) {
1286
+ return false;
1287
+ }
1096
1288
  const value = this.value;
1097
1289
  const lineStart = value.lastIndexOf("\n", start - 1) + 1;
1098
1290
  let lineEnd = value.indexOf("\n", start);
@@ -1130,10 +1322,8 @@ ${indent}${marker} `;
1130
1322
  this.value = newValue;
1131
1323
  textarea.value = newValue;
1132
1324
  const newCursor = start + prefix.length;
1133
- requestAnimationFrame(() => {
1134
- textarea.setSelectionRange(newCursor, newCursor);
1135
- this.updateActiveFormats();
1136
- });
1325
+ textarea.setSelectionRange(newCursor, newCursor);
1326
+ this.updateActiveFormats();
1137
1327
  this.dispatchChange();
1138
1328
  return true;
1139
1329
  }
@@ -1155,24 +1345,35 @@ ${indent}${marker} `;
1155
1345
  this.dispatchChange();
1156
1346
  return true;
1157
1347
  }
1158
- const nextNumber = parseInt(numberStr, 10) + 1;
1348
+ const currentNumber = parseInt(numberStr, 10);
1349
+ const nextNumber = currentNumber + 1;
1159
1350
  const before = value.slice(0, start);
1160
1351
  const after = value.slice(start);
1161
1352
  const prefix = `
1162
1353
  ${indent}${nextNumber}. `;
1163
- const newValue = before + prefix + after;
1164
- const renumberedValue = this.renumberOrderedListItems(
1165
- newValue,
1166
- start + prefix.length,
1167
- indent
1168
- );
1169
- this.value = renumberedValue;
1170
- textarea.value = renumberedValue;
1354
+ let newValue = before + prefix + after;
1355
+ const lines = newValue.split("\n");
1356
+ const insertLineIndex = before.split("\n").length;
1357
+ const renumberStartIndex = insertLineIndex + 1;
1358
+ let currentNum = nextNumber + 1;
1359
+ const orderedRegex = /^(\s*)(\d+)\.\s+(.*)$/;
1360
+ for (let i = renumberStartIndex; i < lines.length; i++) {
1361
+ const line = lines[i];
1362
+ const match = line.match(orderedRegex);
1363
+ if (match && match[1] === indent) {
1364
+ const content2 = match[3];
1365
+ lines[i] = `${indent}${currentNum}. ${content2}`;
1366
+ currentNum++;
1367
+ } else if (match && match[1].length < indent.length) {
1368
+ break;
1369
+ }
1370
+ }
1371
+ newValue = lines.join("\n");
1372
+ this.value = newValue;
1373
+ textarea.value = newValue;
1171
1374
  const newCursor = start + prefix.length;
1172
- requestAnimationFrame(() => {
1173
- textarea.setSelectionRange(newCursor, newCursor);
1174
- this.updateActiveFormats();
1175
- });
1375
+ textarea.setSelectionRange(newCursor, newCursor);
1376
+ this.updateActiveFormats();
1176
1377
  this.dispatchChange();
1177
1378
  return true;
1178
1379
  }
@@ -1186,10 +1387,14 @@ ${indent}${nextNumber}. `;
1186
1387
  const textarea = this.textareaEl?.shadowRoot?.querySelector(
1187
1388
  "textarea"
1188
1389
  );
1189
- if (!textarea) return false;
1390
+ if (!textarea) {
1391
+ return false;
1392
+ }
1190
1393
  const start = textarea.selectionStart ?? 0;
1191
1394
  const end = textarea.selectionEnd ?? 0;
1192
- if (start !== end) return false;
1395
+ if (start !== end) {
1396
+ return false;
1397
+ }
1193
1398
  const value = this.value;
1194
1399
  const lineStart = value.lastIndexOf("\n", start - 1) + 1;
1195
1400
  let lineEnd = value.indexOf("\n", start);
@@ -1215,20 +1420,86 @@ ${indent}${nextNumber}. `;
1215
1420
  } else if (orderedMatch) {
1216
1421
  const currentIndent = orderedMatch[1] ?? "";
1217
1422
  const content = orderedMatch[3] ?? "";
1218
- newLine = `${currentIndent}${indent}1. ${content}`;
1219
- newCursorOffset = indent.length;
1423
+ if (content.trim() === "") {
1424
+ newLine = currentLine;
1425
+ const nestedLine = `${currentIndent}${indent}1. `;
1426
+ let newValue2 = before + newLine + "\n" + nestedLine + after;
1427
+ const parentIndent = currentIndent;
1428
+ const lines = newValue2.split("\n");
1429
+ const currentLineIndex = Math.floor(lineStart / (newValue2.indexOf("\n") + 1)) || newValue2.slice(0, lineStart).split("\n").length - 1;
1430
+ const currentLineMatch = lines[currentLineIndex]?.match(
1431
+ /^(\s*)(\d+)\.\s*(.*)$/
1432
+ );
1433
+ const nextExpectedNumber = currentLineMatch ? parseInt(currentLineMatch[2], 10) : 1;
1434
+ const orderedRegex = /^(\s*)(\d+)\.\s*(.*)$/;
1435
+ let currentNumber = nextExpectedNumber;
1436
+ for (let i = currentLineIndex + 2; i < lines.length; i++) {
1437
+ const line = lines[i];
1438
+ const orderedMatch2 = line.match(orderedRegex);
1439
+ if (orderedMatch2 && orderedMatch2[1] === parentIndent) {
1440
+ const content2 = orderedMatch2[3];
1441
+ lines[i] = `${parentIndent}${currentNumber}. ${content2}`;
1442
+ currentNumber++;
1443
+ } else if (orderedMatch2 && orderedMatch2[1].length < parentIndent.length) {
1444
+ break;
1445
+ }
1446
+ }
1447
+ newValue2 = lines.join("\n");
1448
+ const newCursor2 = lineStart + newLine.length + 1 + nestedLine.length;
1449
+ this.value = newValue2;
1450
+ textarea.value = newValue2;
1451
+ requestAnimationFrame(() => {
1452
+ textarea.setSelectionRange(newCursor2, newCursor2);
1453
+ this.updateActiveFormats();
1454
+ });
1455
+ this.dispatchChange();
1456
+ return true;
1457
+ } else {
1458
+ const newIndent = currentIndent + indent;
1459
+ let newNumber = 1;
1460
+ const beforeText = value.slice(0, lineStart);
1461
+ const lines = beforeText.split("\n");
1462
+ for (let i = lines.length - 1; i >= 0; i--) {
1463
+ const line = lines[i];
1464
+ const match = line.match(/^(\s*)(\d+)\.\s*(.*)$/);
1465
+ if (match && match[1] === newIndent) {
1466
+ newNumber = parseInt(match[2], 10) + 1;
1467
+ break;
1468
+ } else if (match && match[1].length < newIndent.length) {
1469
+ break;
1470
+ }
1471
+ }
1472
+ newLine = `${newIndent}${newNumber}. ${content}`;
1473
+ newCursorOffset = indent.length;
1474
+ }
1220
1475
  } else {
1221
1476
  return false;
1222
1477
  }
1223
- const newValue = before + newLine + after;
1478
+ let newValue = before + newLine + after;
1479
+ if (orderedMatch) {
1480
+ const newIndent = (orderedMatch[1] ?? "") + indent;
1481
+ newValue = this.renumberOrderedListItems(
1482
+ newValue,
1483
+ lineStart + newLine.length,
1484
+ // Start renumbering after the current line
1485
+ newIndent
1486
+ // Use the new indentation level
1487
+ );
1488
+ const parentIndent = orderedMatch[1] ?? "";
1489
+ newValue = this.renumberOrderedListItems(
1490
+ newValue,
1491
+ lineStart,
1492
+ // Start from this line for parent level
1493
+ parentIndent
1494
+ // Use the parent level indentation
1495
+ );
1496
+ }
1224
1497
  this.value = newValue;
1225
1498
  textarea.value = newValue;
1226
1499
  const cursorInLine = start - lineStart;
1227
1500
  const newCursor = lineStart + cursorInLine + newCursorOffset;
1228
- requestAnimationFrame(() => {
1229
- textarea.setSelectionRange(newCursor, newCursor);
1230
- this.updateActiveFormats();
1231
- });
1501
+ textarea.setSelectionRange(newCursor, newCursor);
1502
+ this.updateActiveFormats();
1232
1503
  this.dispatchChange();
1233
1504
  return true;
1234
1505
  }
@@ -1437,25 +1708,46 @@ ${indent}${nextNumber}. `;
1437
1708
  markerEndPosition = indent.length + numberStr.length + 2;
1438
1709
  hasContent = content.trim().length > 0;
1439
1710
  }
1440
- if (cursorPositionInLine === markerEndPosition && !hasContent) {
1711
+ const isAtOrAfterMarker = cursorPositionInLine >= markerEndPosition;
1712
+ if (isAtOrAfterMarker && !hasContent) {
1441
1713
  this.saveUndoStateBeforeChange();
1442
1714
  const before = value.slice(0, lineStart);
1443
- const after = value.slice(
1444
- lineEnd === value.length ? lineEnd : lineEnd + 1
1445
- );
1446
- let newValue = before + after;
1715
+ const after = value.slice(lineEnd);
1716
+ let newValue;
1717
+ if (lineEnd === value.length) {
1718
+ newValue = before.endsWith("\n") ? before.slice(0, -1) : before;
1719
+ } else {
1720
+ newValue = before + after.slice(1);
1721
+ }
1447
1722
  if (orderedMatch) {
1448
1723
  const indent = orderedMatch[1] ?? "";
1449
- newValue = this.renumberOrderedListItems(
1450
- newValue,
1451
- before.length,
1452
- indent
1453
- );
1724
+ const lines = newValue.split("\n");
1725
+ const startLineIndex = Math.max(0, before.split("\n").length - 1);
1726
+ let nextNumber = 1;
1727
+ for (let i = startLineIndex - 1; i >= 0; i--) {
1728
+ const line = lines[i];
1729
+ const match = line.match(/^(\s*)(\d+)\.\s*(.*)$/);
1730
+ if (match && match[1] === indent) {
1731
+ nextNumber = parseInt(match[2], 10) + 1;
1732
+ break;
1733
+ }
1734
+ }
1735
+ for (let i = startLineIndex; i < lines.length; i++) {
1736
+ const line = lines[i];
1737
+ const match = line.match(/^(\s*)(\d+)\.\s*(.*)$/);
1738
+ if (match && match[1] === indent) {
1739
+ lines[i] = `${indent}${nextNumber}. ${match[3]}`;
1740
+ nextNumber++;
1741
+ } else if (match && match[1].length < indent.length) {
1742
+ break;
1743
+ }
1744
+ }
1745
+ newValue = lines.join("\n");
1454
1746
  }
1455
1747
  this.value = newValue;
1456
1748
  textarea.value = newValue;
1457
1749
  let newCursor = before.length;
1458
- if (before.endsWith("\n") && before.length > 1) {
1750
+ if (newCursor > 0 && before.endsWith("\n")) {
1459
1751
  newCursor = before.length - 1;
1460
1752
  }
1461
1753
  requestAnimationFrame(() => {
@@ -1615,6 +1907,7 @@ ${indent}${nextNumber}. `;
1615
1907
  e.stopPropagation();
1616
1908
  this.isColorDropdownOpen = false;
1617
1909
  this.isListDropdownOpen = false;
1910
+ this.isAlignmentDropdownOpen = false;
1618
1911
  this.isHeadingDropdownOpen = !this.isHeadingDropdownOpen;
1619
1912
  }}
1620
1913
  aria-expanded=${this.isHeadingDropdownOpen ? "true" : "false"}
@@ -1688,6 +1981,18 @@ ${indent}${nextNumber}. `;
1688
1981
  >
1689
1982
  Heading 3
1690
1983
  </lukso-dropdown-option>
1984
+ <lukso-dropdown-option
1985
+ ?is-selected=${this.getActiveHeadingLevel() === 4}
1986
+ @click=${(e) => {
1987
+ e.stopPropagation();
1988
+ this.restoreFocusAndSelection();
1989
+ this.applyHeading(4);
1990
+ this.isHeadingDropdownOpen = false;
1991
+ }}
1992
+ size="medium"
1993
+ >
1994
+ Heading 4
1995
+ </lukso-dropdown-option>
1691
1996
  </lukso-dropdown>
1692
1997
  </div>
1693
1998
 
@@ -1717,6 +2022,7 @@ ${indent}${nextNumber}. `;
1717
2022
  this.restoreFocusAndSelection();
1718
2023
  this.isHeadingDropdownOpen = false;
1719
2024
  this.isColorDropdownOpen = false;
2025
+ this.isAlignmentDropdownOpen = false;
1720
2026
  this.isListDropdownOpen = !this.isListDropdownOpen;
1721
2027
  }}
1722
2028
  aria-expanded=${this.isListDropdownOpen ? "true" : "false"}
@@ -1789,6 +2095,108 @@ ${indent}${nextNumber}. `;
1789
2095
  this.activeFormats.link
1790
2096
  )}
1791
2097
 
2098
+ <!-- Text Alignment -->
2099
+ <div class=${this.styles().alignmentMenu()}>
2100
+ <lukso-tooltip text="Text alignment" placement="top">
2101
+ <lukso-button
2102
+ id=${this.alignmentTriggerId}
2103
+ @click=${(e) => {
2104
+ e.stopPropagation();
2105
+ this.restoreFocusAndSelection();
2106
+ this.isHeadingDropdownOpen = false;
2107
+ this.isColorDropdownOpen = false;
2108
+ this.isListDropdownOpen = false;
2109
+ this.isAlignmentDropdownOpen = !this.isAlignmentDropdownOpen;
2110
+ }}
2111
+ aria-expanded=${this.isAlignmentDropdownOpen ? "true" : "false"}
2112
+ aria-label="Text alignment"
2113
+ variant="secondary"
2114
+ size="small"
2115
+ custom-class=${this.toolbarButton({
2116
+ active: this.activeFormats.alignment !== "left"
2117
+ })}
2118
+ is-icon
2119
+ >
2120
+ <lukso-icon
2121
+ name=${this.getAlignmentIcon()}
2122
+ size="small"
2123
+ pack="vuesax"
2124
+ variant="linear"
2125
+ ></lukso-icon>
2126
+ </lukso-button>
2127
+ </lukso-tooltip>
2128
+ <lukso-dropdown
2129
+ id="alignmentDropdown"
2130
+ trigger-id=""
2131
+ size="medium"
2132
+ ?is-open=${this.isAlignmentDropdownOpen}
2133
+ >
2134
+ <lukso-dropdown-option
2135
+ ?is-selected=${this.activeFormats.alignment === "left"}
2136
+ @click=${(e) => {
2137
+ e.stopPropagation();
2138
+ this.restoreFocusAndSelection();
2139
+ this.applyAlignment("left");
2140
+ this.isAlignmentDropdownOpen = false;
2141
+ }}
2142
+ size="medium"
2143
+ aria-label="Align left"
2144
+ >
2145
+ <div style="display: flex; align-items: center; gap: 8px;">
2146
+ <lukso-icon
2147
+ name="textalign-left"
2148
+ size="small"
2149
+ pack="vuesax"
2150
+ variant="linear"
2151
+ ></lukso-icon>
2152
+ Left
2153
+ </div>
2154
+ </lukso-dropdown-option>
2155
+ <lukso-dropdown-option
2156
+ ?is-selected=${this.activeFormats.alignment === "center"}
2157
+ @click=${(e) => {
2158
+ e.stopPropagation();
2159
+ this.restoreFocusAndSelection();
2160
+ this.applyAlignment("center");
2161
+ this.isAlignmentDropdownOpen = false;
2162
+ }}
2163
+ size="medium"
2164
+ aria-label="Align center"
2165
+ >
2166
+ <div style="display: flex; align-items: center; gap: 8px;">
2167
+ <lukso-icon
2168
+ name="textalign-center"
2169
+ size="small"
2170
+ pack="vuesax"
2171
+ variant="linear"
2172
+ ></lukso-icon>
2173
+ Center
2174
+ </div>
2175
+ </lukso-dropdown-option>
2176
+ <lukso-dropdown-option
2177
+ ?is-selected=${this.activeFormats.alignment === "right"}
2178
+ @click=${(e) => {
2179
+ e.stopPropagation();
2180
+ this.restoreFocusAndSelection();
2181
+ this.applyAlignment("right");
2182
+ this.isAlignmentDropdownOpen = false;
2183
+ }}
2184
+ size="medium"
2185
+ aria-label="Align right"
2186
+ >
2187
+ <div style="display: flex; align-items: center; gap: 8px;">
2188
+ <lukso-icon
2189
+ name="textalign-right"
2190
+ size="small"
2191
+ pack="vuesax"
2192
+ variant="linear"
2193
+ ></lukso-icon>
2194
+ Right
2195
+ </div>
2196
+ </lukso-dropdown-option>
2197
+ </lukso-dropdown>
2198
+ </div>
2199
+
1792
2200
  <!-- Color -->
1793
2201
  <div class=${this.styles().colorMenu()}>
1794
2202
  <lukso-tooltip text="Text color" placement="top">
@@ -1799,6 +2207,7 @@ ${indent}${nextNumber}. `;
1799
2207
  this.restoreFocusAndSelection();
1800
2208
  this.isHeadingDropdownOpen = false;
1801
2209
  this.isListDropdownOpen = false;
2210
+ this.isAlignmentDropdownOpen = false;
1802
2211
  if (!this.isColorDropdownOpen) {
1803
2212
  const ta = this.textareaEl?.shadowRoot?.querySelector("textarea");
1804
2213
  if (ta) {
@@ -1844,6 +2253,7 @@ ${indent}${nextNumber}. `;
1844
2253
  this.isColorDropdownOpen = false;
1845
2254
  }}
1846
2255
  type="button"
2256
+ aria-label="Clear color"
1847
2257
  >
1848
2258
  Clear
1849
2259
  </button>` : E}
@@ -1855,6 +2265,7 @@ ${indent}${nextNumber}. `;
1855
2265
  style="background-color: ${color}"
1856
2266
  title=${color}
1857
2267
  aria-pressed=${this.activeFormats.activeColor === color ? "true" : "false"}
2268
+ aria-label="Color ${color}"
1858
2269
  @click=${(e) => {
1859
2270
  e.stopPropagation();
1860
2271
  this.selectColor(color);
@@ -1921,7 +2332,7 @@ __decorateClass([
1921
2332
  n({ type: String })
1922
2333
  ], LuksoMarkdownEditor.prototype, "value", 2);
1923
2334
  __decorateClass([
1924
- n({ type: String })
2335
+ n({ type: String, reflect: true })
1925
2336
  ], LuksoMarkdownEditor.prototype, "name", 2);
1926
2337
  __decorateClass([
1927
2338
  n({ type: String })
@@ -1971,6 +2382,9 @@ __decorateClass([
1971
2382
  __decorateClass([
1972
2383
  r()
1973
2384
  ], LuksoMarkdownEditor.prototype, "isListDropdownOpen", 2);
2385
+ __decorateClass([
2386
+ r()
2387
+ ], LuksoMarkdownEditor.prototype, "isAlignmentDropdownOpen", 2);
1974
2388
  __decorateClass([
1975
2389
  r()
1976
2390
  ], LuksoMarkdownEditor.prototype, "currentSelection", 2);