kempo-ui 0.3.6 → 0.3.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.
Files changed (271) hide show
  1. package/dist/components/Accordion.js +5 -5
  2. package/dist/components/Aside.js +16 -16
  3. package/dist/components/Card.js +3 -3
  4. package/dist/components/ColorPicker.js +4 -4
  5. package/dist/components/ContentSlider.js +3 -3
  6. package/dist/components/Context.js +1 -1
  7. package/dist/components/Dialog.js +7 -7
  8. package/dist/components/Dropdown.js +7 -4
  9. package/dist/components/FilterItem.js +2 -2
  10. package/dist/components/FilterList.js +2 -2
  11. package/dist/components/FocusCapture.js +2 -2
  12. package/dist/components/HtmlEditor.js +62 -39
  13. package/dist/components/HybridComponent.js +1 -1
  14. package/dist/components/Icon.js +2 -2
  15. package/dist/components/Import.js +1 -1
  16. package/dist/components/LightComponent.js +1 -1
  17. package/dist/components/Main.js +3 -3
  18. package/dist/components/Nav.js +2 -2
  19. package/dist/components/NavSpacer.js +6 -0
  20. package/dist/components/PhotoViewer.js +3 -3
  21. package/dist/components/Resize.js +3 -3
  22. package/dist/components/ShadowComponent.js +1 -1
  23. package/dist/components/ShowMore.js +3 -3
  24. package/dist/components/Sortable.js +3 -3
  25. package/dist/components/Spinner.js +2 -2
  26. package/dist/components/Split.js +3 -3
  27. package/dist/components/Table.js +27 -27
  28. package/dist/components/Tabs.js +7 -7
  29. package/dist/components/Tags.js +4 -4
  30. package/dist/components/ThemeSelect.js +4 -4
  31. package/dist/components/ThemeSwitcher.js +5 -5
  32. package/dist/components/Timestamp.js +1 -1
  33. package/dist/components/Toast.js +3 -3
  34. package/dist/components/Toggle.js +3 -3
  35. package/dist/components/Tree.js +10 -10
  36. package/dist/components/htmlEditorControls/AlignCenter.js +4 -4
  37. package/dist/components/htmlEditorControls/AlignJustify.js +4 -4
  38. package/dist/components/htmlEditorControls/AlignLeft.js +4 -4
  39. package/dist/components/htmlEditorControls/AlignRight.js +4 -4
  40. package/dist/components/htmlEditorControls/Bold.js +4 -4
  41. package/dist/components/htmlEditorControls/BulletList.js +4 -4
  42. package/dist/components/htmlEditorControls/CharacterCount.js +3 -3
  43. package/dist/components/htmlEditorControls/ClearFormatting.js +4 -4
  44. package/dist/components/htmlEditorControls/CodeBlock.js +3 -3
  45. package/dist/components/htmlEditorControls/ControlGroup.js +2 -2
  46. package/dist/components/htmlEditorControls/ControlSpacer.js +2 -2
  47. package/dist/components/htmlEditorControls/CreateLink.js +3 -3
  48. package/dist/components/htmlEditorControls/DropdownControl.js +3 -3
  49. package/dist/components/htmlEditorControls/FormatBlock.js +4 -4
  50. package/dist/components/htmlEditorControls/HtmlEditorControl.js +2 -2
  51. package/dist/components/htmlEditorControls/InlineCode.js +3 -3
  52. package/dist/components/htmlEditorControls/InsertTable.js +3 -3
  53. package/dist/components/htmlEditorControls/Italic.js +4 -4
  54. package/dist/components/htmlEditorControls/Mode.js +3 -3
  55. package/dist/components/htmlEditorControls/NumberList.js +4 -4
  56. package/dist/components/htmlEditorControls/Strikethrough.js +4 -4
  57. package/dist/components/htmlEditorControls/TextBackgroundColor.js +6 -6
  58. package/dist/components/htmlEditorControls/TextColor.js +7 -7
  59. package/dist/components/htmlEditorControls/Underline.js +4 -4
  60. package/dist/components/htmlEditorControls/WordCount.js +3 -3
  61. package/dist/components/htmlEditorNodes/HtmlComment.js +24 -0
  62. package/dist/components/tableControls/DeleteRecord.js +2 -2
  63. package/dist/components/tableControls/DeleteSelected.js +2 -2
  64. package/dist/components/tableControls/Edit.js +4 -4
  65. package/dist/components/tableControls/ExportCSV.js +2 -2
  66. package/dist/components/tableControls/ExportJson.js +2 -2
  67. package/dist/components/tableControls/FieldSortHide.js +4 -4
  68. package/dist/components/tableControls/Filters.js +17 -17
  69. package/dist/components/tableControls/FirstPage.js +2 -2
  70. package/dist/components/tableControls/HiddenCount.js +2 -2
  71. package/dist/components/tableControls/Hide.js +2 -2
  72. package/dist/components/tableControls/LastPage.js +2 -2
  73. package/dist/components/tableControls/NextPage.js +2 -2
  74. package/dist/components/tableControls/PageSelect.js +4 -4
  75. package/dist/components/tableControls/PageSize.js +3 -3
  76. package/dist/components/tableControls/PrevPage.js +2 -2
  77. package/dist/components/tableControls/Search.js +2 -2
  78. package/dist/components/tableControls/ShowAll.js +2 -2
  79. package/dist/components/tableControls/TableControl.js +2 -2
  80. package/dist/utils/context.js +1 -1
  81. package/dist/utils/debounce.js +1 -1
  82. package/dist/utils/object.js +1 -1
  83. package/dist/utils/theme.js +1 -1
  84. package/dist/utils/wait.js +0 -1
  85. package/docs/components/accordion.html +2 -0
  86. package/docs/components/aside.html +55 -111
  87. package/docs/components/card.html +2 -0
  88. package/docs/components/color-picker.html +2 -0
  89. package/docs/components/content-slider.html +2 -0
  90. package/docs/components/context.html +2 -0
  91. package/docs/components/dialog.html +2 -0
  92. package/docs/components/dropdown.html +2 -0
  93. package/docs/components/filter-list.html +2 -0
  94. package/docs/components/focus-capture.html +2 -0
  95. package/docs/components/html-editor.html +54 -6
  96. package/docs/components/hybrid-component.html +2 -0
  97. package/docs/components/icon.html +2 -0
  98. package/docs/components/import.html +2 -0
  99. package/docs/components/light-component.html +2 -0
  100. package/docs/components/nav-spacer.html +315 -0
  101. package/docs/components/nav.html +2 -0
  102. package/docs/components/photo-viewer.html +2 -0
  103. package/docs/components/resize.html +2 -0
  104. package/docs/components/shadow-component.html +2 -0
  105. package/docs/components/show-more.html +2 -0
  106. package/docs/components/sortable.html +2 -0
  107. package/docs/components/spinner.html +2 -0
  108. package/docs/components/split.html +2 -0
  109. package/docs/components/table.html +2 -0
  110. package/docs/components/tableControls.html +2 -0
  111. package/docs/components/tableCustomFields.html +2 -0
  112. package/docs/components/tableFetchRecords.html +2 -0
  113. package/docs/components/tableFieldSortHide.html +2 -0
  114. package/docs/components/tablePagination.html +2 -0
  115. package/docs/components/tablePlaceholder.html +2 -0
  116. package/docs/components/tableRecordEditing.html +2 -0
  117. package/docs/components/tableRecordFiltering.html +2 -0
  118. package/docs/components/tableRecordHiding.html +2 -0
  119. package/docs/components/tableRecordSearching.html +2 -0
  120. package/docs/components/tableRecordSelection.html +2 -0
  121. package/docs/components/tableRowControls.html +2 -0
  122. package/docs/components/tableServerSync.html +2 -0
  123. package/docs/components/tableSorting.html +2 -0
  124. package/docs/components/tabs.html +2 -0
  125. package/docs/components/tags.html +2 -0
  126. package/docs/components/theme-select.html +2 -0
  127. package/docs/components/theme-switcher.html +2 -0
  128. package/docs/components/timestamp.html +2 -0
  129. package/docs/components/toast.html +2 -0
  130. package/docs/components/toggle.html +2 -0
  131. package/docs/components/tree.html +2 -0
  132. package/docs/icons/comment.svg +1 -0
  133. package/docs/index.html +8 -0
  134. package/docs/src/components/Accordion.js +5 -5
  135. package/docs/src/components/Aside.js +16 -16
  136. package/docs/src/components/Card.js +3 -3
  137. package/docs/src/components/ColorPicker.js +4 -4
  138. package/docs/src/components/ContentSlider.js +3 -3
  139. package/docs/src/components/Context.js +1 -1
  140. package/docs/src/components/Dialog.js +7 -7
  141. package/docs/src/components/Dropdown.js +7 -4
  142. package/docs/src/components/FilterItem.js +2 -2
  143. package/docs/src/components/FilterList.js +2 -2
  144. package/docs/src/components/FocusCapture.js +2 -2
  145. package/docs/src/components/HtmlEditor.js +62 -39
  146. package/docs/src/components/HybridComponent.js +1 -1
  147. package/docs/src/components/Icon.js +2 -2
  148. package/docs/src/components/Import.js +1 -1
  149. package/docs/src/components/LightComponent.js +1 -1
  150. package/docs/src/components/Main.js +3 -3
  151. package/docs/src/components/Nav.js +2 -2
  152. package/docs/src/components/NavSpacer.js +6 -0
  153. package/docs/src/components/PhotoViewer.js +3 -3
  154. package/docs/src/components/Resize.js +3 -3
  155. package/docs/src/components/ShadowComponent.js +1 -1
  156. package/docs/src/components/ShowMore.js +3 -3
  157. package/docs/src/components/Sortable.js +3 -3
  158. package/docs/src/components/Spinner.js +2 -2
  159. package/docs/src/components/Split.js +3 -3
  160. package/docs/src/components/Table.js +27 -27
  161. package/docs/src/components/Tabs.js +7 -7
  162. package/docs/src/components/Tags.js +4 -4
  163. package/docs/src/components/ThemeSelect.js +4 -4
  164. package/docs/src/components/ThemeSwitcher.js +5 -5
  165. package/docs/src/components/Timestamp.js +1 -1
  166. package/docs/src/components/Toast.js +3 -3
  167. package/docs/src/components/Toggle.js +3 -3
  168. package/docs/src/components/Tree.js +10 -10
  169. package/docs/src/components/htmlEditorControls/AlignCenter.js +4 -4
  170. package/docs/src/components/htmlEditorControls/AlignJustify.js +4 -4
  171. package/docs/src/components/htmlEditorControls/AlignLeft.js +4 -4
  172. package/docs/src/components/htmlEditorControls/AlignRight.js +4 -4
  173. package/docs/src/components/htmlEditorControls/Bold.js +4 -4
  174. package/docs/src/components/htmlEditorControls/BulletList.js +4 -4
  175. package/docs/src/components/htmlEditorControls/CharacterCount.js +3 -3
  176. package/docs/src/components/htmlEditorControls/ClearFormatting.js +4 -4
  177. package/docs/src/components/htmlEditorControls/CodeBlock.js +3 -3
  178. package/docs/src/components/htmlEditorControls/ControlGroup.js +2 -2
  179. package/docs/src/components/htmlEditorControls/ControlSpacer.js +2 -2
  180. package/docs/src/components/htmlEditorControls/CreateLink.js +3 -3
  181. package/docs/src/components/htmlEditorControls/DropdownControl.js +3 -3
  182. package/docs/src/components/htmlEditorControls/FormatBlock.js +4 -4
  183. package/docs/src/components/htmlEditorControls/HtmlEditorControl.js +2 -2
  184. package/docs/src/components/htmlEditorControls/InlineCode.js +3 -3
  185. package/docs/src/components/htmlEditorControls/InsertTable.js +3 -3
  186. package/docs/src/components/htmlEditorControls/Italic.js +4 -4
  187. package/docs/src/components/htmlEditorControls/Mode.js +3 -3
  188. package/docs/src/components/htmlEditorControls/NumberList.js +4 -4
  189. package/docs/src/components/htmlEditorControls/Strikethrough.js +4 -4
  190. package/docs/src/components/htmlEditorControls/TextBackgroundColor.js +6 -6
  191. package/docs/src/components/htmlEditorControls/TextColor.js +7 -7
  192. package/docs/src/components/htmlEditorControls/Underline.js +4 -4
  193. package/docs/src/components/htmlEditorControls/WordCount.js +3 -3
  194. package/docs/src/components/htmlEditorNodes/HtmlComment.js +24 -0
  195. package/docs/src/components/tableControls/DeleteRecord.js +2 -2
  196. package/docs/src/components/tableControls/DeleteSelected.js +2 -2
  197. package/docs/src/components/tableControls/Edit.js +4 -4
  198. package/docs/src/components/tableControls/ExportCSV.js +2 -2
  199. package/docs/src/components/tableControls/ExportJson.js +2 -2
  200. package/docs/src/components/tableControls/FieldSortHide.js +4 -4
  201. package/docs/src/components/tableControls/Filters.js +17 -17
  202. package/docs/src/components/tableControls/FirstPage.js +2 -2
  203. package/docs/src/components/tableControls/HiddenCount.js +2 -2
  204. package/docs/src/components/tableControls/Hide.js +2 -2
  205. package/docs/src/components/tableControls/LastPage.js +2 -2
  206. package/docs/src/components/tableControls/NextPage.js +2 -2
  207. package/docs/src/components/tableControls/PageSelect.js +4 -4
  208. package/docs/src/components/tableControls/PageSize.js +3 -3
  209. package/docs/src/components/tableControls/PrevPage.js +2 -2
  210. package/docs/src/components/tableControls/Search.js +2 -2
  211. package/docs/src/components/tableControls/ShowAll.js +2 -2
  212. package/docs/src/components/tableControls/TableControl.js +2 -2
  213. package/docs/src/utils/context.js +1 -1
  214. package/docs/src/utils/debounce.js +1 -1
  215. package/docs/src/utils/object.js +1 -1
  216. package/docs/src/utils/theme.js +1 -1
  217. package/docs/src/utils/wait.js +0 -1
  218. package/docs/utils/context.html +2 -0
  219. package/docs/utils/cookie.html +2 -0
  220. package/docs/utils/debounce.html +2 -0
  221. package/docs/utils/drag.html +2 -0
  222. package/docs/utils/elevation.html +2 -0
  223. package/docs/utils/formatTimestamp.html +8 -6
  224. package/docs/utils/object.html +2 -0
  225. package/docs/utils/propConverters.html +2 -0
  226. package/docs/utils/string.html +2 -0
  227. package/docs/utils/theme.html +2 -0
  228. package/docs/utils/toTitleCase.html +8 -6
  229. package/docs/utils/type.html +2 -0
  230. package/docs/utils/wait.html +2 -0
  231. package/docs-src/components/aside.page.html +53 -111
  232. package/docs-src/components/html-editor.page.html +52 -6
  233. package/docs-src/components/nav-spacer.page.html +65 -0
  234. package/docs-src/index.page.html +6 -0
  235. package/docs-src/nav.fragment.html +2 -0
  236. package/docs-src/utils/formatTimestamp.page.html +6 -6
  237. package/docs-src/utils/toTitleCase.page.html +6 -6
  238. package/icons/comment.svg +1 -0
  239. package/llm.txt +1 -0
  240. package/package.json +3 -3
  241. package/scripts/build.js +1 -1
  242. package/src/components/Accordion.js +1 -1
  243. package/src/components/Aside.js +12 -1
  244. package/src/components/Dropdown.js +7 -3
  245. package/src/components/HtmlEditor.js +699 -1206
  246. package/src/components/Main.js +1 -1
  247. package/src/components/NavSpacer.js +55 -0
  248. package/src/components/htmlEditorControls/AlignCenter.js +5 -5
  249. package/src/components/htmlEditorControls/AlignJustify.js +5 -5
  250. package/src/components/htmlEditorControls/AlignLeft.js +5 -5
  251. package/src/components/htmlEditorControls/AlignRight.js +5 -5
  252. package/src/components/htmlEditorControls/Bold.js +5 -5
  253. package/src/components/htmlEditorControls/BulletList.js +5 -5
  254. package/src/components/htmlEditorControls/ClearFormatting.js +5 -5
  255. package/src/components/htmlEditorControls/CodeBlock.js +16 -8
  256. package/src/components/htmlEditorControls/CreateLink.js +3 -7
  257. package/src/components/htmlEditorControls/FormatBlock.js +5 -5
  258. package/src/components/htmlEditorControls/InlineCode.js +1 -13
  259. package/src/components/htmlEditorControls/InsertTable.js +43 -44
  260. package/src/components/htmlEditorControls/Italic.js +5 -5
  261. package/src/components/htmlEditorControls/Mode.js +1 -1
  262. package/src/components/htmlEditorControls/NumberList.js +5 -5
  263. package/src/components/htmlEditorControls/Strikethrough.js +5 -5
  264. package/src/components/htmlEditorControls/Underline.js +5 -5
  265. package/src/components/htmlEditorNodes/HtmlComment.js +118 -0
  266. package/tests/components/Accordion.browser-test.js +154 -0
  267. package/tests/components/Aside.browser-test.js +75 -0
  268. package/tests/components/Dropdown.browser-test.js +85 -0
  269. package/tests/components/HtmlComment.browser-test.js +115 -0
  270. package/tests/components/HtmlEditor.browser-test.js +379 -0
  271. package/tests/components/NavSpacer.browser-test.js +81 -0
@@ -2,35 +2,32 @@ import { html, css } from '../lit-all.min.js';
2
2
  import ShadowComponent from './ShadowComponent.js';
3
3
  import formatCode from '../utils/formatCode.js';
4
4
  import debounce from '../utils/debounce.js';
5
+ import { getCalculatedTheme, subscribeToTheme } from '../utils/theme.js';
6
+ import Dialog from './Dialog.js';
7
+
8
+ /*
9
+ Default CDN URLs
10
+ */
11
+ const LEXICAL_VERSION = '0.43.0';
12
+ const MONACO_VERSION = '0.52.2';
13
+ const DEFAULT_LEXICAL_BASE = 'https://esm.sh';
14
+ const DEFAULT_MONACO_SRC = `https://cdn.jsdelivr.net/npm/monaco-editor@${MONACO_VERSION}/min`;
15
+
16
+ const lexicalUrl = (base, pkg) => `${base}/${pkg}@${LEXICAL_VERSION}`;
5
17
 
6
18
  export default class HtmlEditor extends ShadowComponent {
7
19
  static formAssociated = true;
8
20
 
9
21
  static properties = {
10
- name: {
11
- type: String,
12
- reflect: true
13
- },
14
- value: {
15
- type: String,
16
- reflect: true
17
- },
18
- selection: {
19
- type: Object,
20
- state: true
21
- },
22
- mode: {
23
- type: String,
24
- reflect: true
25
- },
26
- hasTopToolbar: {
27
- type: Boolean,
28
- state: true
29
- },
30
- hasBottomToolbar: {
31
- type: Boolean,
32
- state: true
33
- }
22
+ name: { type: String, reflect: true },
23
+ value: { type: String, reflect: true },
24
+ selection: { type: Object, state: true },
25
+ mode: { type: String, reflect: true },
26
+ lexicalSrc: { type: String, attribute: 'lexical-src' },
27
+ monacoSrc: { type: String, attribute: 'monaco-src' },
28
+ nodes: { type: String },
29
+ hasTopToolbar: { type: Boolean, state: true },
30
+ hasBottomToolbar: { type: Boolean, state: true }
34
31
  };
35
32
 
36
33
  constructor() {
@@ -41,14 +38,18 @@ export default class HtmlEditor extends ShadowComponent {
41
38
  this.selection = null;
42
39
  this.cursor = null;
43
40
  this.mode = 'visual';
41
+ this.lexicalSrc = '';
42
+ this.monacoSrc = '';
43
+ this.nodes = '';
44
44
  this.hasTopToolbar = false;
45
45
  this.hasBottomToolbar = false;
46
- this.skipSync = false;
46
+ this.skipValueSync = false;
47
+ this.lexicalValueSync = false;
47
48
  this.savedSelection = null;
48
-
49
- this.debouncedDispatchChange = debounce(() => {
50
- this.value = this.editorEl.innerHTML;
51
- }, 300);
49
+ this.lexicalEditor = null;
50
+ this.monacoEditor = null;
51
+ this.lx = {};
52
+ this.debouncedSyncValue = debounce(() => this.syncValueFromLexical(), 300);
52
53
  }
53
54
 
54
55
  /*
@@ -56,64 +57,49 @@ export default class HtmlEditor extends ShadowComponent {
56
57
  */
57
58
  connectedCallback() {
58
59
  super.connectedCallback();
59
-
60
60
  if(this.hasAttribute('value')){
61
61
  this.value = this.getAttribute('value');
62
62
  }
63
-
64
- this.slotObserver = new MutationObserver(() => {
65
- this.updateToolbarVisibility();
66
- });
67
-
68
- this.slotObserver.observe(this, {
69
- childList: true,
70
- subtree: true
71
- });
72
-
63
+ this.slotObserver = new MutationObserver(() => this.updateToolbarVisibility());
64
+ this.slotObserver.observe(this, { childList: true, subtree: true });
73
65
  this.updateToolbarVisibility();
74
66
  }
75
67
 
76
68
  disconnectedCallback() {
77
69
  super.disconnectedCallback();
78
70
  this.slotObserver?.disconnect();
79
-
80
- if(this.boundUpdateSelection){
81
- this.shadowRoot.removeEventListener('selectionchange', this.boundUpdateSelection);
82
-
83
- if(this.editorEl){
84
- this.editorEl.removeEventListener('mouseup', this.boundUpdateSelection);
85
- this.editorEl.removeEventListener('keyup', this.boundUpdateSelection);
86
- this.editorEl.removeEventListener('focus', this.boundUpdateSelection);
87
- }
88
- }
71
+ this.cleanupFns?.forEach(fn => fn?.());
72
+ this.monacoEditor?.dispose();
73
+ this.unsubscribeTheme?.();
74
+ if(this.syncShadowSelection) document.removeEventListener('selectionchange', this.syncShadowSelection);
89
75
  }
90
76
 
91
77
  updateToolbarVisibility() {
92
- const topSlotContent = Array.from(this.children).filter(child =>
93
- child.getAttribute('slot') === 'toolbar-top'
94
- );
95
- const bottomSlotContent = Array.from(this.children).filter(child =>
96
- child.getAttribute('slot') === 'toolbar-bottom'
97
- );
98
-
99
- this.hasTopToolbar = topSlotContent.length > 0;
100
- this.hasBottomToolbar = bottomSlotContent.length > 0;
78
+ this.hasTopToolbar = Array.from(this.children).some(c => c.getAttribute('slot') === 'toolbar-top');
79
+ this.hasBottomToolbar = Array.from(this.children).some(c => c.getAttribute('slot') === 'toolbar-bottom');
101
80
  }
102
81
 
103
82
  updated(changedProperties) {
104
83
  super.updated(changedProperties);
105
-
106
- if(changedProperties.has('value')){
107
- this.syncContent();
84
+ if(changedProperties.has('value') && !this.skipValueSync){
85
+ if(this.lexicalValueSync){
86
+ this.lexicalValueSync = false;
87
+ } else {
88
+ if(this.lexicalEditor && this.mode === 'visual' && !this.isVisualCompatible(this.value)){
89
+ this.skipLexicalExport = true;
90
+ this.mode = 'code';
91
+ } else {
92
+ this.syncContentToEditors();
93
+ }
94
+ }
108
95
  this.updateFormValue();
109
96
  this.dispatchEvent(new CustomEvent('change', {
110
97
  detail: { value: this.value },
111
98
  bubbles: true
112
99
  }));
113
100
  }
114
-
115
101
  if(changedProperties.has('mode')){
116
- this.syncContent();
102
+ this.handleModeSwitch(changedProperties.get('mode'));
117
103
  this.dispatchEvent(new CustomEvent('mode-changed', {
118
104
  detail: { mode: this.mode },
119
105
  bubbles: true
@@ -121,22 +107,10 @@ export default class HtmlEditor extends ShadowComponent {
121
107
  }
122
108
  }
123
109
 
124
- firstUpdated() {
125
- this.editorEl = this.shadowRoot.querySelector('.editor');
126
- this.textareaEl = this.shadowRoot.querySelector('textarea');
127
-
128
- if(this.value){
129
- this.syncContent();
130
- }
131
-
132
- this.boundUpdateSelection = this.updateSelection.bind(this);
133
-
134
- this.shadowRoot.addEventListener('selectionchange', this.boundUpdateSelection);
135
- this.editorEl.addEventListener('mouseup', this.boundUpdateSelection);
136
- this.editorEl.addEventListener('keyup', this.boundUpdateSelection);
137
- this.editorEl.addEventListener('focus', this.boundUpdateSelection);
138
- this.editorEl.addEventListener('keydown', this.handleTableNavigation);
139
-
110
+ async firstUpdated() {
111
+ this.lexicalContainer = this.shadowRoot.querySelector('.lexical-editor');
112
+ this.monacoContainer = this.shadowRoot.querySelector('.monaco-editor-container');
113
+ await this.initLexical();
140
114
  this.dispatchEvent(new CustomEvent('ready', {
141
115
  detail: { value: this.value },
142
116
  bubbles: true
@@ -144,385 +118,406 @@ export default class HtmlEditor extends ShadowComponent {
144
118
  }
145
119
 
146
120
  /*
147
- Form Integration
121
+ Module Loading
148
122
  */
149
- updateFormValue() {
150
- this.internals.setFormValue(this.getValue());
123
+ async loadNodeModules() {
124
+ if(!this.nodes?.trim()) return [];
125
+ const base = new URL('./htmlEditorNodes/', import.meta.url).href;
126
+ const modules = await Promise.all(
127
+ this.nodes.split(',').map(n => n.trim()).filter(Boolean).map(n => import(/* @vite-ignore */ `${base}${n}.js`))
128
+ );
129
+ return modules.map(m => m.default?.lexicalNode).filter(Boolean);
151
130
  }
152
131
 
153
- formResetCallback() {
154
- this.value = '';
132
+ async loadLexicalModules() {
133
+ const base = this.lexicalSrc || DEFAULT_LEXICAL_BASE;
134
+ const url = pkg => lexicalUrl(base, pkg);
135
+ const [lexical, richText, lexicalHtml, history, list, link, selection, table, code] = await Promise.all([
136
+ import(/* @vite-ignore */ url('lexical')),
137
+ import(/* @vite-ignore */ url('@lexical/rich-text')),
138
+ import(/* @vite-ignore */ url('@lexical/html')),
139
+ import(/* @vite-ignore */ url('@lexical/history')),
140
+ import(/* @vite-ignore */ url('@lexical/list')),
141
+ import(/* @vite-ignore */ url('@lexical/link')),
142
+ import(/* @vite-ignore */ url('@lexical/selection')),
143
+ import(/* @vite-ignore */ url('@lexical/table')),
144
+ import(/* @vite-ignore */ url('@lexical/code'))
145
+ ]);
146
+ this.lx = { lexical, richText, lexicalHtml, history, list, link, selection, table, code };
147
+ this.StyledTextNode = class extends lexical.TextNode {
148
+ static getType() { return 'styled-text'; }
149
+ static clone(node) { return new this(node.__text, node.__key); }
150
+ static importDOM() {
151
+ return {
152
+ span: () => ({
153
+ conversion: domNode => {
154
+ const style = domNode.getAttribute('style');
155
+ if(!style) return null;
156
+ const node = lexical.$createTextNode(domNode.textContent);
157
+ node.setStyle(style);
158
+ return { node };
159
+ },
160
+ priority: 1
161
+ })
162
+ };
163
+ }
164
+ static importJSON(json) { return lexical.$createTextNode(json.text); }
165
+ exportJSON() { return { ...super.exportJSON(), type: 'styled-text' }; }
166
+ };
155
167
  }
156
168
 
157
- formStateRestoreCallback(state) {
158
- this.value = state;
159
- }
169
+ async initLexical() {
170
+ await this.loadLexicalModules();
171
+ this.customNodes = await this.loadNodeModules();
172
+ this.nodeCompatCheckers = this.customNodes.filter(n => typeof n.isVisualCompatible === 'function').map(n => n.isVisualCompatible);
173
+ this.nodePreprocessors = this.customNodes.filter(n => typeof n.preprocessHtml === 'function').map(n => n.preprocessHtml);
174
+ const { lexical, richText, history, list, link, table, code } = this.lx;
175
+
176
+ const editorConfig = {
177
+ namespace: 'KempoHtmlEditor',
178
+ theme: {
179
+ paragraph: 'k-editor-p',
180
+ heading: { h1: 'k-editor-h1', h2: 'k-editor-h2', h3: 'k-editor-h3', h4: 'k-editor-h4', h5: 'k-editor-h5', h6: 'k-editor-h6' },
181
+ text: { underline: 'td-u', strikethrough: 'td-lt' },
182
+ list: { ul: 'k-editor-ul', ol: 'k-editor-ol', listitem: 'k-editor-li' },
183
+ link: 'k-editor-link',
184
+ quote: 'k-editor-quote',
185
+ code: 'k-editor-code-block',
186
+ codeHighlight: {},
187
+ table: 'k-editor-table',
188
+ tableCell: 'k-editor-table-cell',
189
+ tableCellHeader: 'k-editor-table-cell-header'
190
+ },
191
+ nodes: [richText.HeadingNode, richText.QuoteNode, list.ListNode, list.ListItemNode, link.LinkNode, table.TableNode, table.TableCellNode, table.TableRowNode, code.CodeNode, code.CodeHighlightNode, this.StyledTextNode, ...this.customNodes],
192
+ onError: console.error,
193
+ editorState: null
194
+ };
160
195
 
161
- /*
162
- Event Handlers
163
- */
164
- handleTableNavigation = (e) => {
165
- if(e.key !== 'Tab' && e.key !== 'Enter') return;
166
- if(this.mode !== 'visual') return;
167
-
168
- const sel = this.shadowRoot.getSelection() || window.getSelection();
169
- if(!sel.rangeCount) return;
170
-
171
- let node = sel.getRangeAt(0).startContainer;
172
-
173
- if(node.nodeType === Node.TEXT_NODE){
174
- node = node.parentElement;
175
- }
176
-
177
- const cell = node.closest('td, th');
178
- if(!cell) return;
179
-
180
- e.preventDefault();
181
-
182
- if(e.key === 'Tab'){
183
- const row = cell.parentElement;
184
- const cells = Array.from(row.cells);
185
- const currentIndex = cells.indexOf(cell);
186
-
187
- if(e.shiftKey){
188
- // Shift+Tab: Go to previous cell
189
- let prevCell = cells[currentIndex - 1];
190
-
191
- if(!prevCell){
192
- const prevRow = row.previousElementSibling;
193
- if(prevRow){
194
- prevCell = prevRow.cells[prevRow.cells.length - 1];
195
- }
196
- }
197
-
198
- if(prevCell){
199
- const range = document.createRange();
200
- range.selectNodeContents(prevCell);
201
- range.collapse(false);
202
- sel.removeAllRanges();
203
- sel.addRange(range);
204
- prevCell.focus();
205
- }
196
+ this.lexicalEditor = lexical.createEditor(editorConfig);
197
+ this.lexicalEditor.setRootElement(this.lexicalContainer);
198
+
199
+ this.cleanupFns = [
200
+ richText.registerRichText(this.lexicalEditor),
201
+ history.registerHistory(this.lexicalEditor, history.createEmptyHistoryState(), 300)
202
+ ];
203
+ if(list.registerList) this.cleanupFns.push(list.registerList(this.lexicalEditor));
204
+ if(table.registerTable) this.cleanupFns.push(table.registerTable(this.lexicalEditor));
205
+ if(code.registerCodeHighlighting) this.cleanupFns.push(code.registerCodeHighlighting(this.lexicalEditor));
206
+ if(link.registerLink) this.cleanupFns.push(link.registerLink(this.lexicalEditor, {
207
+ validateUrl: url => { try { new URL(url); return true; } catch { return false; } }
208
+ }));
209
+
210
+ if(this.value){
211
+ if(this.isVisualCompatible(this.value)){
212
+ this.importHtmlToLexical(this.value);
206
213
  } else {
207
- // Tab: Go to next cell
208
- let nextCell = cells[currentIndex + 1];
209
-
210
- if(!nextCell){
211
- const nextRow = row.nextElementSibling;
212
- if(nextRow){
213
- nextCell = nextRow.cells[0];
214
- }
215
- }
216
-
217
- if(nextCell){
218
- const range = document.createRange();
219
- range.selectNodeContents(nextCell);
220
- range.collapse(false);
221
- sel.removeAllRanges();
222
- sel.addRange(range);
223
- nextCell.focus();
224
- }
225
- }
226
- } else if(e.key === 'Enter'){
227
- const row = cell.parentElement;
228
- const cells = Array.from(row.cells);
229
- const currentIndex = cells.indexOf(cell);
230
-
231
- if(e.shiftKey){
232
- // Shift+Enter: Go to cell above
233
- const prevRow = row.previousElementSibling;
234
- if(prevRow && prevRow.cells[currentIndex]){
235
- const prevCell = prevRow.cells[currentIndex];
236
- const range = document.createRange();
237
- range.selectNodeContents(prevCell);
238
- range.collapse(false);
239
- sel.removeAllRanges();
240
- sel.addRange(range);
241
- prevCell.focus();
242
- }
243
- } else {
244
- // Enter: Go to cell below or create paragraph after table
245
- const nextRow = row.nextElementSibling;
246
- if(nextRow && nextRow.cells[currentIndex]){
247
- const nextCell = nextRow.cells[currentIndex];
248
- const range = document.createRange();
249
- range.selectNodeContents(nextCell);
250
- range.collapse(false);
251
- sel.removeAllRanges();
252
- sel.addRange(range);
253
- nextCell.focus();
254
- } else {
255
- // Last row - create paragraph after table
256
- const table = cell.closest('table');
257
- if(table){
258
- const p = document.createElement('p');
259
- p.innerHTML = '<br>';
260
-
261
- if(table.nextSibling){
262
- table.parentNode.insertBefore(p, table.nextSibling);
263
- } else {
264
- table.parentNode.appendChild(p);
265
- }
266
-
267
- const range = document.createRange();
268
- range.setStart(p, 0);
269
- range.setEnd(p, 0);
270
- sel.removeAllRanges();
271
- sel.addRange(range);
272
- p.focus();
273
- }
274
- }
214
+ this.skipLexicalExport = true;
215
+ this.mode = 'code';
275
216
  }
276
217
  }
277
- };
278
218
 
279
- updateSelection = () => {
280
- if(this.mode !== 'visual'){
281
- this.selection = null;
282
- return;
283
- }
219
+ this.lexicalEditor.registerUpdateListener(({ dirtyElements, dirtyLeaves }) => {
220
+ if(dirtyElements.size === 0 && dirtyLeaves.size === 0) return;
221
+ this.debouncedSyncValue();
222
+ this.dispatchEvent(new CustomEvent('input', {
223
+ detail: { value: this.exportHtmlFromLexical() },
224
+ bubbles: true
225
+ }));
226
+ });
284
227
 
285
- const sel = this.shadowRoot.getSelection() || window.getSelection();
286
-
287
- if(!sel.rangeCount){
288
- this.selection = null;
289
- return;
228
+ this.lexicalEditor.registerCommand(
229
+ lexical.SELECTION_CHANGE_COMMAND,
230
+ () => { this.updateSelection(); return false; },
231
+ lexical.COMMAND_PRIORITY_LOW
232
+ );
233
+
234
+ this.syncShadowSelection = () => {
235
+ if(this.mode !== 'visual' || !this.lexicalEditor) return;
236
+ const shadowSel = this.shadowRoot.getSelection?.();
237
+ if(!shadowSel || shadowSel.rangeCount === 0) return;
238
+ const range = shadowSel.getRangeAt(0);
239
+ if(!this.lexicalContainer.contains(range.startContainer)) return;
240
+ this.lexicalEditor.update(() => {
241
+ const lexSel = lexical.$createRangeSelectionFromDom(shadowSel, this.lexicalEditor);
242
+ if(lexSel) lexical.$setSelection(lexSel);
243
+ }, { discrete: true });
244
+ this.updateSelection();
245
+ };
246
+ document.addEventListener('selectionchange', this.syncShadowSelection);
247
+ }
248
+
249
+ async initMonaco() {
250
+ if(this.monacoEditor) return;
251
+ if(this.monacoInitPromise) return this.monacoInitPromise;
252
+ this.monacoInitPromise = this._initMonaco();
253
+ await this.monacoInitPromise;
254
+ this.monacoInitPromise = null;
255
+ }
256
+
257
+ async _initMonaco() {
258
+ const src = this.monacoSrc || DEFAULT_MONACO_SRC;
259
+
260
+ await new Promise((resolve, reject) => {
261
+ if(window.monaco){ resolve(); return; }
262
+ if(window.require?.defined?.('vs/editor/editor.main')){ resolve(); return; }
263
+ const existing = document.querySelector(`script[src="${src}/vs/loader.js"]`);
264
+ if(existing){
265
+ existing.addEventListener('load', () => {
266
+ window.require.config({ paths: { vs: `${src}/vs` } });
267
+ window.require(['vs/editor/editor.main'], () => resolve(), reject);
268
+ });
269
+ return;
270
+ }
271
+ const script = document.createElement('script');
272
+ script.src = `${src}/vs/loader.js`;
273
+ script.onload = () => {
274
+ window.require.config({ paths: { vs: `${src}/vs` } });
275
+ window.require(['vs/editor/editor.main'], () => resolve(), reject);
276
+ };
277
+ script.onerror = reject;
278
+ document.head.appendChild(script);
279
+ });
280
+
281
+ this.monacoEditor = window.monaco.editor.create(this.monacoContainer, {
282
+ value: formatCode(this.value),
283
+ language: 'html',
284
+ theme: getCalculatedTheme() === 'dark' ? 'vs-dark' : 'vs',
285
+ minimap: { enabled: false },
286
+ wordWrap: 'on',
287
+ fontSize: 14,
288
+ scrollBeyondLastLine: false,
289
+ automaticLayout: true,
290
+ tabSize: 2
291
+ });
292
+
293
+ const monacoCSS = document.querySelector('link[href*="monaco"][href*="editor.main.css"]');
294
+ if(monacoCSS){
295
+ const link = document.createElement('link');
296
+ link.rel = 'stylesheet';
297
+ link.href = monacoCSS.href;
298
+ this.shadowRoot.appendChild(link);
290
299
  }
291
300
 
292
- const range = sel.getRangeAt(0);
293
-
294
- const isStartInEditor = this.editorEl.contains(range.startContainer) || this.editorEl === range.startContainer;
295
- const isEndInEditor = this.editorEl.contains(range.endContainer) || this.editorEl === range.endContainer;
296
-
297
- if(!isStartInEditor || !isEndInEditor){
298
- this.selection = null;
299
- return;
301
+ this.unsubscribeTheme = subscribeToTheme(() => {
302
+ if(this.monacoEditor) window.monaco.editor.setTheme(getCalculatedTheme() === 'dark' ? 'vs-dark' : 'vs');
303
+ });
304
+
305
+ this.monacoEditor.onDidChangeModelContent(() => {
306
+ this.skipValueSync = true;
307
+ this.value = this.monacoEditor.getValue();
308
+ this.skipValueSync = false;
309
+ this.updateFormValue();
310
+ this.dispatchEvent(new CustomEvent('change', { detail: { value: this.value }, bubbles: true }));
311
+ this.dispatchEvent(new CustomEvent('input', { detail: { value: this.value }, bubbles: true }));
312
+ });
313
+ }
314
+
315
+ /*
316
+ Content Sync
317
+ */
318
+ importHtmlToLexical(htmlString) {
319
+ if(!this.lexicalEditor || !this.lx.lexicalHtml) return;
320
+ const { lexical, lexicalHtml } = this.lx;
321
+ const processed = (this.nodePreprocessors || []).reduce((str, fn) => fn(str), htmlString);
322
+ this.lexicalEditor.update(() => {
323
+ const root = lexical.$getRoot();
324
+ root.clear();
325
+ if(!processed?.trim()) return;
326
+ const dom = new DOMParser().parseFromString(processed, 'text/html');
327
+ const nodes = lexicalHtml.$generateNodesFromDOM(this.lexicalEditor, dom);
328
+ if(nodes.length > 0) lexical.$insertNodes(nodes);
329
+ }, { discrete: true });
330
+ }
331
+
332
+ exportHtmlFromLexical() {
333
+ if(!this.lexicalEditor || !this.lx.lexicalHtml) return this.value;
334
+ let result = '';
335
+ this.lexicalEditor.getEditorState().read(() => {
336
+ result = this.lx.lexicalHtml.$generateHtmlFromNodes(this.lexicalEditor, null);
337
+ });
338
+ return this.cleanExportedHtml(result);
339
+ }
340
+
341
+ isVisualCompatible(htmlStr) {
342
+ if(!htmlStr?.trim()) return true;
343
+ const incompatibleTags = new Set(['script', 'style', 'meta', 'link', 'head', 'iframe', 'object', 'embed', 'canvas', 'video', 'audio', 'form', 'input', 'button', 'select', 'textarea', 'fieldset', 'label', 'noscript', 'template', 'slot', 'svg', 'math']);
344
+ const checkers = this.nodeCompatCheckers || [];
345
+ const doc = new DOMParser().parseFromString(htmlStr, 'text/html');
346
+ const walker = document.createTreeWalker(doc.body, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT);
347
+ let node;
348
+ while((node = walker.nextNode())){
349
+ if(checkers.some(check => check(node))) continue;
350
+ if(node.nodeType === Node.COMMENT_NODE) return false;
351
+ if(incompatibleTags.has(node.tagName?.toLowerCase())) return false;
300
352
  }
353
+ return true;
354
+ }
301
355
 
302
- const selectedText = range.toString();
356
+ cleanExportedHtml(html) {
357
+ const doc = new DOMParser().parseFromString(html, 'text/html');
358
+ doc.body.querySelectorAll('[class]').forEach(el => {
359
+ const classes = Array.from(el.classList).filter(c => !c.startsWith('k-editor-') && c !== 'td-u' && c !== 'td-lt');
360
+ if(classes.length === 0) el.removeAttribute('class');
361
+ else el.className = classes.join(' ');
362
+ });
363
+ doc.body.querySelectorAll('[style]').forEach(el => {
364
+ const style = el.style.cssText.replace(/white-space:\s*pre-wrap;?\s*/g, '').trim();
365
+ if(!style) el.removeAttribute('style');
366
+ else el.style.cssText = style;
367
+ });
368
+ doc.body.querySelectorAll('span:not([class]):not([style]):not([id])').forEach(span => {
369
+ if(!span.attributes.length) span.replaceWith(...span.childNodes);
370
+ });
371
+ doc.body.querySelectorAll('b > strong, i > em, b > b, strong > strong, i > i, em > em').forEach(inner => {
372
+ inner.replaceWith(...inner.childNodes);
373
+ });
374
+ doc.body.querySelectorAll('pre[data-highlight-language], code[data-highlight-language]').forEach(el => {
375
+ el.removeAttribute('data-highlight-language');
376
+ el.removeAttribute('data-language');
377
+ });
378
+ return doc.body.innerHTML;
379
+ }
303
380
 
304
- if(range.collapsed){
305
- this.selection = null;
306
- this.cursor = {
307
- container: range.startContainer,
308
- offset: range.startOffset
309
- };
310
- } else {
311
- this.cursor = null;
312
- this.selection = {
313
- startContainer: range.startContainer,
314
- startOffset: range.startOffset,
315
- endContainer: range.endContainer,
316
- endOffset: range.endOffset,
317
- text: selectedText,
318
- collapsed: false
319
- };
381
+ syncValueFromLexical() {
382
+ if(!this.lexicalEditor) return;
383
+ this.lexicalValueSync = true;
384
+ this.value = this.exportHtmlFromLexical();
385
+ this.updateFormValue();
386
+ }
387
+
388
+ syncContentToEditors() {
389
+ if(this.mode === 'visual' && this.lexicalEditor){
390
+ this.importHtmlToLexical(this.value);
391
+ }
320
392
  }
321
- }; handleEditorInput = () => {
322
- // Don't update this.value on every keystroke - it triggers Lit's reactive system
323
- // which calls syncContent() and destroys the cursor position.
324
- // Use debounce to only update after user stops typing for 300ms.
325
- this.debouncedDispatchChange();
326
- this.dispatchEvent(new CustomEvent('input', { detail: { value: this.editorEl.innerHTML } }));
327
- };
328
393
 
329
- handleTextareaInput = (e) => {
394
+ async handleModeSwitch(previousMode) {
330
395
  if(this.mode === 'code'){
331
- this.value = e.target.value;
396
+ if(this.lexicalEditor && !this.skipLexicalExport) this.value = this.exportHtmlFromLexical();
397
+ this.skipLexicalExport = false;
398
+ await this.initMonaco();
399
+ if(this.monacoEditor){
400
+ this.monacoEditor.setValue(formatCode(this.value));
401
+ this.monacoEditor.layout();
402
+ }
403
+ } else if(this.mode === 'visual'){
404
+ if(this.monacoEditor) this.value = this.monacoEditor.getValue();
405
+ if(this.lexicalEditor) this.importHtmlToLexical(this.value);
332
406
  }
333
- };
407
+ this.requestUpdate();
408
+ }
334
409
 
335
- handleEditorPaste = (e) => {
336
- e.preventDefault();
337
- const text = e.clipboardData.getData('text/plain');
338
- document.execCommand('insertText', false, text);
339
- };
410
+ /*
411
+ Form Integration
412
+ */
413
+ updateFormValue() {
414
+ this.internals.setFormValue(this.getValue());
415
+ }
340
416
 
341
- handleEditorBlur = () => {
342
- if(this.mode === 'visual'){
343
- // Save selection before it's cleared
344
- const selection = this.shadowRoot.getSelection() || window.getSelection();
345
- if(selection.rangeCount > 0){
346
- const range = selection.getRangeAt(0);
347
- if(this.editorEl.contains(range.commonAncestorContainer)){
348
- this.savedSelection = {
349
- startContainer: range.startContainer,
350
- startOffset: range.startOffset,
351
- endContainer: range.endContainer,
352
- endOffset: range.endOffset,
353
- collapsed: range.collapsed
354
- };
355
- }
356
- }
357
-
358
- const cleanedValue = this.cleanupHtml(this.editorEl.innerHTML);
359
- if(cleanedValue !== this.value){
360
- this.value = cleanedValue;
361
- this.syncContent();
362
- }
417
+ formResetCallback() {
418
+ this.value = '';
419
+ }
420
+
421
+ formStateRestoreCallback(state) {
422
+ this.value = state;
423
+ }
424
+
425
+ /*
426
+ Selection Management
427
+ */
428
+ updateSelection = () => {
429
+ if(this.mode !== 'visual' || !this.lexicalEditor){
430
+ this.selection = null;
431
+ return;
363
432
  }
433
+ const { lexical } = this.lx;
434
+ this.lexicalEditor.getEditorState().read(() => {
435
+ const sel = lexical.$getSelection();
436
+ if(lexical.$isRangeSelection(sel) && !sel.isCollapsed()){
437
+ this.selection = { text: sel.getTextContent(), collapsed: false };
438
+ } else {
439
+ this.selection = null;
440
+ this.cursor = sel ? { anchor: sel.anchor, focus: sel.focus } : null;
441
+ }
442
+ });
364
443
  };
365
444
 
366
445
  /*
367
446
  Public Methods - Mode Control
368
447
  */
369
448
  setMode(mode) {
370
- if(['visual', 'code'].includes(mode)){
371
- if(this.mode === 'visual' && mode === 'code'){
372
- this.value = this.cleanupHtml(this.editorEl.innerHTML);
373
- }
374
- this.mode = mode;
449
+ if(!['visual', 'code'].includes(mode)) return this;
450
+ if(mode === 'visual' && !this.isVisualCompatible(this.getValue())){
451
+ Dialog.confirm(
452
+ 'This html contains code that is not compatible with the visual editor, the incompatible code will be lost',
453
+ response => { if(response) this.mode = mode; },
454
+ { title: 'Warning', confirmText: 'Change Anyways' }
455
+ );
456
+ return this;
375
457
  }
458
+ this.mode = mode;
376
459
  return this;
377
460
  }
378
461
 
379
462
  toggleMode() {
380
- const newMode = this.mode === 'visual' ? 'code' : 'visual';
381
- this.setMode(newMode);
382
- return this;
463
+ return this.setMode(this.mode === 'visual' ? 'code' : 'visual');
383
464
  }
384
465
 
385
466
  /*
386
467
  Public Methods - Content Management
387
468
  */
388
469
  getValue() {
389
- if(this.mode === 'visual'){
390
- const content = this.editorEl ? this.editorEl.innerHTML : this.value;
391
- return content.replace(/\u200B/g, '');
470
+ if(this.mode === 'visual' && this.lexicalEditor){
471
+ this.skipValueSync = true;
472
+ this.value = this.exportHtmlFromLexical();
473
+ this.skipValueSync = false;
474
+ } else if(this.mode === 'code' && this.monacoEditor){
475
+ return this.monacoEditor.getValue();
392
476
  }
393
477
  return this.value;
394
478
  }
395
479
 
396
- setValue(html) {
397
- this.value = html;
398
- this.syncContent();
480
+ setValue(htmlStr) {
481
+ if(this.lexicalEditor && this.mode === 'visual' && !this.isVisualCompatible(htmlStr)){
482
+ this.value = htmlStr;
483
+ this.skipLexicalExport = true;
484
+ this.mode = 'code';
485
+ this.updateFormValue();
486
+ return this;
487
+ }
488
+ this.skipValueSync = true;
489
+ this.value = htmlStr;
490
+ if(this.mode === 'visual') this.syncContentToEditors();
491
+ else if(this.mode === 'code' && this.monacoEditor) this.monacoEditor.setValue(formatCode(htmlStr));
492
+ this.updateFormValue();
493
+ this.skipValueSync = false;
399
494
  return this;
400
495
  }
401
496
 
402
497
  clear() {
403
- this.value = '';
404
- this.syncContent();
405
- return this;
406
- }
407
-
408
- cleanupHtml(html){
409
- const tempDiv = document.createElement('div');
410
- tempDiv.innerHTML = html;
411
-
412
- const inlineTags = ['code', 'a', 'span', 'strong', 'em', 'b', 'i', 'u', 's'];
413
- const blockTags = ['pre', 'div', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote'];
414
-
415
- const unwrapNested = (parent, tagName) => {
416
- const elements = parent.querySelectorAll(tagName);
417
- elements.forEach(el => {
418
- let ancestor = el.parentElement;
419
- while(ancestor && ancestor !== parent){
420
- if(ancestor.tagName.toLowerCase() === tagName){
421
- while(el.firstChild){
422
- el.parentNode.insertBefore(el.firstChild, el);
423
- }
424
- el.parentNode.removeChild(el);
425
- break;
426
- }
427
- ancestor = ancestor.parentElement;
428
- }
429
- });
430
- };
431
-
432
- const removeUselessStyles = (parent) => {
433
- const allElements = parent.querySelectorAll('*');
434
- allElements.forEach(el => {
435
- if(!el.hasAttribute('style')) return;
436
-
437
- const style = el.style;
438
- const uselessProps = [
439
- 'font-size',
440
- 'font-family',
441
- 'background-color',
442
- 'line-height',
443
- 'font-weight',
444
- 'font-style'
445
- ];
446
-
447
- const uselessValues = [
448
- 'inherit',
449
- 'rgba(0, 0, 0, 0)',
450
- 'transparent',
451
- 'initial',
452
- 'unset'
453
- ];
454
-
455
- uselessProps.forEach(prop => {
456
- const value = style.getPropertyValue(prop);
457
- if(value && uselessValues.some(uv => value.includes(uv))){
458
- style.removeProperty(prop);
459
- }
460
- });
461
-
462
- if(style.length === 0){
463
- el.removeAttribute('style');
464
- }
465
- });
466
- };
467
-
468
- const unwrapEmptySpans = (parent) => {
469
- const spans = parent.querySelectorAll('span');
470
- spans.forEach(span => {
471
- if(!span.className && !span.id && !span.hasAttribute('style')){
472
- while(span.firstChild){
473
- span.parentNode.insertBefore(span.firstChild, span);
474
- }
475
- span.parentNode.removeChild(span);
476
- }
477
- });
478
- };
479
-
480
- const normalizeWhitespace = (parent) => {
481
- const walker = document.createTreeWalker(
482
- parent,
483
- NodeFilter.SHOW_TEXT,
484
- null
485
- );
486
-
487
- const textNodes = [];
488
- while(walker.nextNode()){
489
- textNodes.push(walker.currentNode);
490
- }
491
-
492
- textNodes.forEach(node => {
493
- node.textContent = node.textContent.replace(/\u00A0/g, ' ');
494
- });
495
- };
496
-
497
- inlineTags.forEach(tag => unwrapNested(tempDiv, tag));
498
- blockTags.forEach(tag => unwrapNested(tempDiv, tag));
499
- removeUselessStyles(tempDiv);
500
- unwrapEmptySpans(tempDiv);
501
- normalizeWhitespace(tempDiv);
502
-
503
- return tempDiv.innerHTML;
498
+ return this.setValue('');
504
499
  }
505
500
 
506
501
  /*
507
502
  Public Methods - Text Formatting
508
503
  */
509
504
  bold() {
510
- this.execCommand('bold');
505
+ this.lexicalFormat('bold');
511
506
  return this;
512
507
  }
513
508
 
514
509
  italic() {
515
- this.execCommand('italic');
510
+ this.lexicalFormat('italic');
516
511
  return this;
517
512
  }
518
513
 
519
514
  underline() {
520
- this.execCommand('underline');
515
+ this.lexicalFormat('underline');
521
516
  return this;
522
517
  }
523
518
 
524
519
  strikethrough() {
525
- this.execCommand('strikeThrough');
520
+ this.lexicalFormat('strikethrough');
526
521
  return this;
527
522
  }
528
523
 
@@ -530,12 +525,18 @@ export default class HtmlEditor extends ShadowComponent {
530
525
  Public Methods - Lists
531
526
  */
532
527
  orderedList() {
533
- this.execCommand('insertOrderedList');
528
+ if(this.mode !== 'visual' || !this.lexicalEditor) return this;
529
+ this.lexicalEditor.update(() => {
530
+ this.lx.list.$insertList('number');
531
+ }, { discrete: true });
534
532
  return this;
535
533
  }
536
534
 
537
535
  unorderedList() {
538
- this.execCommand('insertUnorderedList');
536
+ if(this.mode !== 'visual' || !this.lexicalEditor) return this;
537
+ this.lexicalEditor.update(() => {
538
+ this.lx.list.$insertList('bullet');
539
+ }, { discrete: true });
539
540
  return this;
540
541
  }
541
542
 
@@ -543,22 +544,22 @@ export default class HtmlEditor extends ShadowComponent {
543
544
  Public Methods - Text Alignment
544
545
  */
545
546
  alignLeft() {
546
- this.execCommand('justifyLeft');
547
+ this.lexicalFormatElement('left');
547
548
  return this;
548
549
  }
549
550
 
550
551
  alignCenter() {
551
- this.execCommand('justifyCenter');
552
+ this.lexicalFormatElement('center');
552
553
  return this;
553
554
  }
554
555
 
555
556
  alignRight() {
556
- this.execCommand('justifyRight');
557
+ this.lexicalFormatElement('right');
557
558
  return this;
558
559
  }
559
560
 
560
561
  alignJustify() {
561
- this.execCommand('justifyFull');
562
+ this.lexicalFormatElement('justify');
562
563
  return this;
563
564
  }
564
565
 
@@ -566,52 +567,46 @@ export default class HtmlEditor extends ShadowComponent {
566
567
  Public Methods - Text Color
567
568
  */
568
569
  setTextColor(color) {
569
- this.execCommand('foreColor', color);
570
+ if(this.mode !== 'visual' || !this.lexicalEditor) return this;
571
+ const { lexical, selection } = this.lx;
572
+ this.lexicalEditor.update(() => {
573
+ const sel = lexical.$getSelection();
574
+ if(!lexical.$isRangeSelection(sel)) return;
575
+ selection.$patchStyleText(sel, { color });
576
+ }, { discrete: true });
570
577
  return this;
571
578
  }
572
579
 
573
580
  removeTextColor() {
574
- this.execCommand('removeFormat');
581
+ if(this.mode !== 'visual' || !this.lexicalEditor) return this;
582
+ const { lexical, selection } = this.lx;
583
+ this.lexicalEditor.update(() => {
584
+ const sel = lexical.$getSelection();
585
+ if(!lexical.$isRangeSelection(sel)) return;
586
+ selection.$patchStyleText(sel, { color: null });
587
+ }, { discrete: true });
575
588
  return this;
576
589
  }
577
590
 
578
591
  setTextBackgroundColor(color) {
579
- this.execCommand('backColor', color);
592
+ if(this.mode !== 'visual' || !this.lexicalEditor) return this;
593
+ const { lexical, selection } = this.lx;
594
+ this.lexicalEditor.update(() => {
595
+ const sel = lexical.$getSelection();
596
+ if(!lexical.$isRangeSelection(sel)) return;
597
+ selection.$patchStyleText(sel, { 'background-color': color });
598
+ }, { discrete: true });
580
599
  return this;
581
600
  }
582
601
 
583
602
  removeTextBackgroundColor() {
584
- const selection = this.shadowRoot.getSelection();
585
- if(!selection || selection.rangeCount === 0) return this;
586
-
587
- const range = selection.getRangeAt(0);
588
- if(range.collapsed) return this;
589
-
590
- const fragment = range.extractContents();
591
- const walker = document.createTreeWalker(
592
- fragment,
593
- NodeFilter.SHOW_ELEMENT,
594
- null
595
- );
596
-
597
- const removeBackground = (el) => {
598
- if(el.style){
599
- el.style.backgroundColor = '';
600
- if(!el.style.cssText){
601
- el.removeAttribute('style');
602
- }
603
- }
604
- };
605
-
606
- let node;
607
- while(node = walker.nextNode()){
608
- removeBackground(node);
609
- }
610
-
611
- removeBackground(fragment.firstChild);
612
- range.insertNode(fragment);
613
-
614
- this.editorEl.dispatchEvent(new Event('input', {bubbles: true}));
603
+ if(this.mode !== 'visual' || !this.lexicalEditor) return this;
604
+ const { lexical, selection } = this.lx;
605
+ this.lexicalEditor.update(() => {
606
+ const sel = lexical.$getSelection();
607
+ if(!lexical.$isRangeSelection(sel)) return;
608
+ selection.$patchStyleText(sel, { 'background-color': null });
609
+ }, { discrete: true });
615
610
  return this;
616
611
  }
617
612
 
@@ -619,830 +614,305 @@ export default class HtmlEditor extends ShadowComponent {
619
614
  Public Methods - Formatting Control
620
615
  */
621
616
  removeFormat() {
622
- if(this.mode !== 'visual') return this;
623
-
624
- // If there's a selection, use the standard removeFormat command
625
- if(this.selection){
626
- this.execCommand('removeFormat');
627
- return this;
628
- }
629
-
630
- // No selection - check if cursor is inside a formatting tag
631
- const sel = this.shadowRoot.getSelection() || window.getSelection();
632
- if(!sel.rangeCount) return this;
633
-
634
- let node = sel.getRangeAt(0).startContainer;
635
- if(node.nodeType === Node.TEXT_NODE){
636
- node = node.parentElement;
637
- }
638
-
639
- // Find the closest formatting element
640
- const formattingTags = ['B', 'I', 'U', 'S', 'STRONG', 'EM', 'CODE', 'MARK', 'SUB', 'SUP', 'SPAN'];
641
- const formattingElement = node.closest(formattingTags.map(tag => tag.toLowerCase()).join(','));
642
-
643
- if(formattingElement && this.editorEl.contains(formattingElement)){
644
- // Get the text content
645
- const textContent = formattingElement.textContent;
646
-
647
- // Create a text node to replace the formatting element
648
- const textNode = document.createTextNode(textContent);
649
- formattingElement.replaceWith(textNode);
650
-
651
- // Place cursor at the end of the text
652
- const range = document.createRange();
653
- range.setStart(textNode, textContent.length);
654
- range.setEnd(textNode, textContent.length);
655
- sel.removeAllRanges();
656
- sel.addRange(range);
657
-
658
- // Update value
659
- this.value = this.editorEl.innerHTML;
660
- }
661
-
617
+ if(this.mode !== 'visual' || !this.lexicalEditor) return this;
618
+ const { lexical, selection } = this.lx;
619
+ this.lexicalEditor.update(() => {
620
+ const sel = lexical.$getSelection();
621
+ if(!lexical.$isRangeSelection(sel)) return;
622
+ sel.getNodes().forEach(node => {
623
+ if(lexical.$isTextNode(node)) node.setFormat(0);
624
+ });
625
+ selection.$patchStyleText(sel, { color: null, 'background-color': null });
626
+ }, { discrete: true });
662
627
  return this;
663
628
  }
664
629
 
665
630
  formatBlock(tag) {
666
- this.execCommand('formatBlock', tag);
631
+ if(this.mode !== 'visual' || !this.lexicalEditor) return this;
632
+ const { lexical, richText, code } = this.lx;
633
+ this.lexicalEditor.update(() => {
634
+ const sel = lexical.$getSelection();
635
+ if(!lexical.$isRangeSelection(sel)) return;
636
+ const anchor = sel.anchor.getNode();
637
+ const topLevel = anchor.getTopLevelElementOrThrow();
638
+ const isCodeNode = code.$isCodeNode(topLevel);
639
+
640
+ let newBlock;
641
+ if(tag === 'p'){
642
+ newBlock = lexical.$createParagraphNode();
643
+ } else if(tag.match(/^h[1-6]$/)){
644
+ newBlock = richText.$createHeadingNode(tag);
645
+ } else if(tag === 'blockquote'){
646
+ newBlock = richText.$createQuoteNode();
647
+ } else if(tag === 'pre'){
648
+ newBlock = code.$createCodeNode();
649
+ } else {
650
+ return;
651
+ }
652
+
653
+ if(isCodeNode && tag !== 'pre'){
654
+ const text = topLevel.getTextContent();
655
+ topLevel.replace(newBlock);
656
+ newBlock.append(lexical.$createTextNode(text));
657
+ } else {
658
+ const children = topLevel.getChildren();
659
+ topLevel.replace(newBlock);
660
+ children.forEach(child => newBlock.append(child));
661
+ }
662
+ newBlock.selectEnd();
663
+ }, { discrete: true });
667
664
  return this;
668
665
  }
669
666
 
670
- insertHTML(html) {
671
- this.execCommand('insertHTML', html);
667
+ insertHTML(htmlStr) {
668
+ if(this.mode !== 'visual' || !this.lexicalEditor) return this;
669
+ const { lexical, lexicalHtml } = this.lx;
670
+ this.lexicalEditor.update(() => {
671
+ const dom = new DOMParser().parseFromString(htmlStr, 'text/html');
672
+ const nodes = lexicalHtml.$generateNodesFromDOM(this.lexicalEditor, dom);
673
+ lexical.$insertNodes(nodes);
674
+ }, { discrete: true });
672
675
  return this;
673
676
  }
674
677
 
675
- /**
676
- * Insert or edit a table at the cursor position
677
- * @param {number} rows - Number of rows (excluding header row if includeHeaders is true)
678
- * @param {number} columns - Number of columns
679
- * @param {boolean} includeHeaders - Whether to include a header row
680
- * @param {Array<Array<string>>|null} cellData - Optional 2D array of cell contents for editing existing tables
681
- * @returns {HtmlEditor} Returns this for chaining
682
- */
678
+ insertAtCursor(htmlStr) {
679
+ return this.insertHTML(htmlStr);
680
+ }
681
+
683
682
  insertTable(rows, columns, includeHeaders = false, cellData = null) {
684
- if(this.mode !== 'visual') return this;
685
-
686
- const CURSOR_MARKER = '\uFFF0';
687
- const markerData = this.getValueWithSelectionMarkers();
688
- let html = markerData.html;
689
-
690
- // Create table element
691
- const table = document.createElement('table');
692
- table.style.cssText = 'border-collapse: collapse; width: 100%;';
693
-
694
- // Create header if needed
695
- if(includeHeaders){
696
- const thead = document.createElement('thead');
697
- const headerRow = document.createElement('tr');
698
-
699
- for(let c = 0; c < columns; c++){
700
- const th = document.createElement('th');
701
- th.style.cssText = 'border: 1px solid var(--border-color, #ccc); padding: 0.5rem; text-align: left; background-color: var(--background-secondary, #f5f5f5);';
702
-
703
- if(cellData && cellData[0] && cellData[0][c] !== undefined){
704
- th.innerHTML = cellData[0][c];
705
- } else {
706
- th.innerHTML = `Header ${c + 1}`;
683
+ if(this.mode !== 'visual' || !this.lexicalEditor) return this;
684
+ const { lexical, table } = this.lx;
685
+ this.lexicalEditor.update(() => {
686
+ const totalRows = rows + (includeHeaders ? 1 : 0);
687
+ const rowNodes = [];
688
+ for(let r = 0; r < totalRows; r++){
689
+ const cells = [];
690
+ for(let c = 0; c < columns; c++){
691
+ const isHeader = includeHeaders && r === 0;
692
+ const headerState = isHeader ? table.TableCellHeaderStates.ROW : table.TableCellHeaderStates.NO_STATUS;
693
+ const cell = table.$createTableCellNode(headerState);
694
+ const content = cellData?.[r]?.[c] ?? (isHeader ? `Header ${c + 1}` : '');
695
+ const p = lexical.$createParagraphNode();
696
+ p.append(lexical.$createTextNode(content || '\u00A0'));
697
+ cell.append(p);
698
+ cells.push(cell);
707
699
  }
708
-
709
- headerRow.appendChild(th);
700
+ rowNodes.push(table.$createTableRowNode().append(...cells));
710
701
  }
711
-
712
- thead.appendChild(headerRow);
713
- table.appendChild(thead);
714
- }
715
-
716
- // Create body rows
717
- const tbody = document.createElement('tbody');
718
- const startRow = includeHeaders && cellData ? 1 : 0;
719
- const dataStartRow = includeHeaders ? 1 : 0;
720
-
721
- for(let r = 0; r < rows; r++){
722
- const row = document.createElement('tr');
723
-
724
- for(let c = 0; c < columns; c++){
725
- const td = document.createElement('td');
726
- td.style.cssText = 'border: 1px solid var(--border-color, #ccc); padding: 0.5rem;';
727
-
728
- if(cellData && cellData[r + dataStartRow] && cellData[r + dataStartRow][c] !== undefined){
729
- td.innerHTML = cellData[r + dataStartRow][c];
730
- } else {
731
- td.innerHTML = '\u200B';
732
- }
733
-
734
- row.appendChild(td);
702
+ const tableNode = table.$createTableNode().append(...rowNodes);
703
+ const sel = lexical.$getSelection();
704
+ if(lexical.$isRangeSelection(sel)){
705
+ const anchor = sel.anchor.getNode();
706
+ const topLevel = anchor.getTopLevelElementOrThrow();
707
+ topLevel.insertAfter(tableNode);
708
+ const trailing = lexical.$createParagraphNode();
709
+ tableNode.insertAfter(trailing);
710
+ trailing.selectEnd();
711
+ } else {
712
+ const root = lexical.$getRoot();
713
+ root.append(tableNode);
714
+ root.append(lexical.$createParagraphNode());
735
715
  }
736
-
737
- tbody.appendChild(row);
738
- }
739
-
740
- table.appendChild(tbody);
741
-
742
- // Convert to HTML
743
- const tempDiv = document.createElement('div');
744
- tempDiv.appendChild(table);
745
- const tableHTML = tempDiv.innerHTML;
746
-
747
- const pAfter = document.createElement('p');
748
- pAfter.innerHTML = '<br>';
749
- const fullHTML = tableHTML + pAfter.outerHTML;
750
-
751
- // Insert or append table
752
- if(markerData.hasCursor){
753
- html = html.replace(CURSOR_MARKER, fullHTML + CURSOR_MARKER);
754
- } else if(markerData.hasSelection){
755
- // Replace selection with table
756
- html = html.replace(markerData.selectionStart, '').replace(markerData.selectionEnd, '');
757
- html = html + fullHTML;
758
- } else {
759
- // No cursor or selection - append to end
760
- html = html + fullHTML;
761
- }
762
-
763
- // Restore cursor position after table
764
- const markers = {
765
- hasCursor: markerData.hasCursor,
766
- hasSelection: false,
767
- cursorMarker: CURSOR_MARKER
768
- };
769
-
770
- this.setValueFromSelectionMarkers(html, markers);
771
-
716
+ }, { discrete: true });
772
717
  return this;
773
718
  }
774
719
 
775
- insertElementAtCursor(element, selectAfter = true) {
776
- if(this.mode !== 'visual') return this;
777
-
778
- const CURSOR_MARKER = '\uFFF0';
779
- const SELECT_START = '\uFFF1';
780
- const SELECT_END = '\uFFF2';
781
-
782
- const markerData = this.getValueWithSelectionMarkers();
783
- let html = markerData.html;
784
-
785
- if(selectAfter){
786
- element.textContent = SELECT_START + (element.textContent || '') + SELECT_END;
787
- }
788
- const elementHtml = element.outerHTML;
789
-
790
- if(markerData.hasCursor){
791
- html = html.replace(CURSOR_MARKER, elementHtml);
792
- } else {
793
- // No cursor - append at the very end of content
794
- // Wrap inline elements in a paragraph
795
- const inlineElements = ['code', 'a', 'span', 'strong', 'em', 'b', 'i', 'u', 's', 'mark', 'sub', 'sup'];
796
- const tagName = element.tagName.toLowerCase();
797
-
798
- if(inlineElements.includes(tagName)){
799
- html = html + '<p>' + elementHtml + '</p>';
800
- } else {
801
- html = html + elementHtml;
802
- }
803
- }
804
-
805
- const markers = {
806
- hasCursor: false,
807
- hasSelection: selectAfter,
808
- selectionStart: SELECT_START,
809
- selectionEnd: SELECT_END,
810
- selectedText: element.textContent || ''
811
- };
812
-
813
- this.setValueFromSelectionMarkers(html, markers);
814
-
815
- return this;
720
+ insertElementAtCursor(element) {
721
+ return this.insertHTML(element.outerHTML);
816
722
  }
817
723
 
818
- replaceSelectionWithElement(element, selectAfter = true) {
819
- if(this.mode !== 'visual') return this;
820
-
821
- const SELECT_START = '\uFFF1';
822
- const SELECT_END = '\uFFF2';
823
-
824
- const markerData = this.getValueWithSelectionMarkers();
825
-
826
- if(!markerData.hasSelection) return this;
827
-
828
- let html = markerData.html;
829
-
830
- if(selectAfter){
831
- element.textContent = SELECT_START + element.textContent + SELECT_END;
832
- }
833
- const elementHtml = element.outerHTML;
834
-
835
- const selectedPattern = SELECT_START + markerData.selectedText + SELECT_END;
836
- html = html.replace(selectedPattern, elementHtml);
837
-
838
- const markers = {
839
- hasCursor: false,
840
- hasSelection: selectAfter,
841
- selectionStart: SELECT_START,
842
- selectionEnd: SELECT_END,
843
- selectedText: markerData.selectedText
844
- };
845
-
846
- this.setValueFromSelectionMarkers(html, markers);
847
-
724
+ replaceSelectionWithElement(element) {
725
+ if(this.mode !== 'visual' || !this.lexicalEditor) return this;
726
+ const { lexical, lexicalHtml } = this.lx;
727
+ this.lexicalEditor.update(() => {
728
+ const sel = lexical.$getSelection();
729
+ if(!lexical.$isRangeSelection(sel)) return;
730
+ sel.removeText();
731
+ const dom = new DOMParser().parseFromString(element.outerHTML, 'text/html');
732
+ const nodes = lexicalHtml.$generateNodesFromDOM(this.lexicalEditor, dom);
733
+ lexical.$insertNodes(nodes);
734
+ }, { discrete: true });
848
735
  return this;
849
736
  }
850
737
 
738
+ wrapSelection(before, after, savedSelection = null) {
739
+ if(this.mode !== 'visual' || !this.lexicalEditor) return this;
740
+ const text = savedSelection || this.getSelectedText();
741
+ if(!text) return this;
742
+ return this.insertHTML(before + text + after);
743
+ }
744
+
851
745
  /*
852
746
  Public Methods - Selection Management
853
747
  */
854
748
  getSelection() {
855
- if(this.mode !== 'visual') return null;
856
-
857
- this.editorEl.focus();
858
- const selection = window.getSelection();
859
-
860
- if(!selection.rangeCount) return null;
861
-
862
- const range = selection.getRangeAt(0);
863
- const isInEditor = this.editorEl.contains(range.commonAncestorContainer);
864
-
865
- if(!isInEditor) return null;
866
-
867
- return {
868
- text: selection.toString(),
869
- html: range.cloneContents().textContent ? new XMLSerializer().serializeToString(range.cloneContents()) : '',
870
- range: range,
871
- selection: selection
872
- };
749
+ if(this.mode !== 'visual' || !this.lexicalEditor) return null;
750
+ let result = null;
751
+ const { lexical } = this.lx;
752
+ this.lexicalEditor.getEditorState().read(() => {
753
+ const sel = lexical.$getSelection();
754
+ if(!lexical.$isRangeSelection(sel) || sel.isCollapsed()) return;
755
+ result = { text: sel.getTextContent(), html: sel.getTextContent(), selection: sel };
756
+ });
757
+ return result;
873
758
  }
874
759
 
875
760
  getSelectedText() {
876
- const sel = this.getSelection();
877
- return sel ? sel.text : '';
761
+ let text = '';
762
+ if(this.mode !== 'visual' || !this.lexicalEditor) return text;
763
+ const { lexical } = this.lx;
764
+ this.lexicalEditor.getEditorState().read(() => {
765
+ const sel = lexical.$getSelection();
766
+ if(lexical.$isRangeSelection(sel)) text = sel.getTextContent();
767
+ });
768
+ return text;
878
769
  }
879
770
 
880
771
  getSelectedHTML() {
881
- const sel = this.getSelection();
882
- return sel ? sel.html : '';
883
- }
884
-
885
- setSelection(startNode, startOffset, endNode, endOffset) {
886
- if(this.mode !== 'visual') return this;
887
-
888
- this.editorEl.focus();
889
- const selection = window.getSelection();
890
- const range = document.createRange();
891
-
892
- range.setStart(startNode, startOffset);
893
- range.setEnd(endNode, endOffset);
894
-
895
- selection.removeAllRanges();
896
- selection.addRange(range);
897
-
898
- return this;
772
+ return this.getSelectedText();
899
773
  }
900
774
 
901
775
  selectAll() {
902
- if(this.mode === 'visual'){
903
- this.execCommand('selectAll');
904
- } else {
905
- this.textareaEl.select();
776
+ if(this.mode === 'visual' && this.lexicalEditor){
777
+ const { lexical } = this.lx;
778
+ this.lexicalEditor.update(() => {
779
+ lexical.$selectAll();
780
+ }, { discrete: true });
781
+ } else if(this.monacoEditor){
782
+ const model = this.monacoEditor.getModel();
783
+ if(model) this.monacoEditor.setSelection(model.getFullModelRange());
906
784
  }
907
785
  return this;
908
786
  }
909
787
 
910
- replaceSelection(html) {
911
- if(this.mode !== 'visual') return this;
912
-
913
- const sel = this.getSelection();
914
- if(!sel) return this;
915
-
916
- sel.range.deleteContents();
917
- const fragment = sel.range.createContextualFragment(html);
918
- sel.range.insertNode(fragment);
919
-
920
- this.value = this.editorEl.innerHTML;
921
- return this;
788
+ replaceSelection(htmlStr) {
789
+ return this.insertHTML(htmlStr);
922
790
  }
923
791
 
924
792
  deleteSelection() {
925
793
  if(this.mode === 'visual'){
926
- this.execCommand('delete');
927
- } else {
928
- const start = this.textareaEl.selectionStart;
929
- const end = this.textareaEl.selectionEnd;
930
- this.textareaEl.value = this.textareaEl.value.substring(0, start) + this.textareaEl.value.substring(end);
931
- this.value = this.textareaEl.value;
794
+ this.lexicalCmd('DELETE_CHARACTER_COMMAND', false);
795
+ } else if(this.monacoEditor){
796
+ this.monacoEditor.trigger('keyboard', 'deleteAllLeft', null);
932
797
  }
933
798
  return this;
934
799
  }
935
800
 
936
- getTextPosition(root, targetNode){
937
- const walker = document.createTreeWalker(
938
- root,
939
- NodeFilter.SHOW_TEXT,
940
- null
941
- );
942
-
943
- let position = 0;
944
-
945
- while(walker.nextNode()){
946
- const node = walker.currentNode;
947
-
948
- if(targetNode.contains(node)){
949
- return position;
950
- }
951
-
952
- position += node.textContent.length;
953
- }
954
-
955
- return position;
956
- }
957
-
958
- wrapSelection(before, after, savedSelection = null){
959
- if(this.mode !== 'visual') return this;
960
-
961
- let selectedText;
962
-
963
- if(savedSelection){
964
- selectedText = savedSelection;
965
- } else {
966
- const selection = window.getSelection();
967
-
968
- if(!selection.rangeCount || selection.isCollapsed){
969
- return this;
970
- }
971
-
972
- const range = selection.getRangeAt(0);
973
- const isInEditor = this.editorEl.contains(range.commonAncestorContainer);
974
-
975
- if(!isInEditor){
976
- return this;
977
- }
978
-
979
- selectedText = selection.toString();
980
- }
981
-
982
- if(!selectedText) return this;
983
-
984
- this.editorEl.focus();
985
-
986
- const openingTagMatch = before.match(/<([a-zA-Z]+)/);
987
- if(!openingTagMatch) return this;
988
- const outerTagName = openingTagMatch[1];
989
-
990
- const tempDiv = document.createElement('div');
991
- tempDiv.innerHTML = this.getValue();
992
-
993
- const textContent = tempDiv.textContent;
994
- const textIndex = textContent.indexOf(selectedText);
995
-
996
- if(textIndex === -1) return this;
997
-
998
- const walker = document.createTreeWalker(
999
- tempDiv,
1000
- NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
1001
- null
1002
- );
1003
-
1004
- let currentPos = 0;
1005
- let startNode = null;
1006
- let startOffset = 0;
1007
- let endNode = null;
1008
- let endOffset = 0;
1009
- const overlappingWrappers = [];
1010
-
1011
- while(walker.nextNode()){
1012
- const node = walker.currentNode;
1013
-
1014
- if(node.nodeType === Node.ELEMENT_NODE){
1015
- if(node.tagName.toLowerCase() === outerTagName){
1016
- const nodeTextStart = this.getTextPosition(tempDiv, node);
1017
- const nodeTextEnd = nodeTextStart + node.textContent.length;
1018
-
1019
- const selectionEnd = textIndex + selectedText.length;
1020
- const overlaps = !(nodeTextEnd <= textIndex || nodeTextStart >= selectionEnd);
1021
-
1022
- if(overlaps){
1023
- overlappingWrappers.push(node);
1024
- }
1025
- }
1026
- continue;
1027
- }
1028
-
1029
- const nodeLength = node.textContent.length;
1030
- const nodeStart = currentPos;
1031
- const nodeEnd = currentPos + nodeLength;
1032
-
1033
- if(!startNode && nodeEnd > textIndex){
1034
- startNode = node;
1035
- startOffset = textIndex - nodeStart;
1036
- }
1037
-
1038
- if(nodeEnd >= textIndex + selectedText.length){
1039
- endNode = node;
1040
- endOffset = (textIndex + selectedText.length) - nodeStart;
1041
- break;
1042
- }
1043
-
1044
- currentPos += nodeLength;
801
+ getValueWithSelectionMarkers() {
802
+ if(this.mode !== 'visual' || !this.lexicalEditor){
803
+ return { html: this.value, hasCursor: false, hasSelection: false, selectedText: '' };
1045
804
  }
1046
-
1047
- if(overlappingWrappers.length > 0){
1048
- overlappingWrappers.forEach(wrapper => {
1049
- const parent = wrapper.parentNode;
1050
- const isPreTag = wrapper.tagName.toLowerCase() === 'pre';
1051
-
1052
- const allTags = [];
1053
- const collectTags = (el) => {
1054
- allTags.push(el);
1055
- Array.from(el.children).forEach(child => collectTags(child));
1056
- };
1057
- collectTags(wrapper);
1058
-
1059
- if(isPreTag){
1060
- const p = document.createElement('p');
1061
- while(wrapper.firstChild){
1062
- p.appendChild(wrapper.firstChild);
1063
- }
1064
- parent.replaceChild(p, wrapper);
1065
- } else {
1066
- allTags.reverse().forEach(tag => {
1067
- if(tag.parentNode){
1068
- while(tag.firstChild){
1069
- tag.parentNode.insertBefore(tag.firstChild, tag);
1070
- }
1071
- tag.parentNode.removeChild(tag);
1072
- }
1073
- });
1074
- }
1075
- });
1076
- } else if(startNode && endNode){
1077
- const range = document.createRange();
1078
- range.setStart(startNode, startOffset);
1079
- range.setEnd(endNode, endOffset);
1080
-
1081
- const fragment = range.extractContents();
1082
-
1083
- const wrapperDiv = document.createElement('div');
1084
- wrapperDiv.innerHTML = before + after;
1085
- const wrapper = wrapperDiv.firstChild;
1086
-
1087
- let innermost = wrapper;
1088
- while(innermost.firstChild && innermost.firstChild.nodeType === Node.ELEMENT_NODE){
1089
- innermost = innermost.firstChild;
1090
- }
1091
-
1092
- innermost.appendChild(fragment);
1093
- range.insertNode(wrapper);
1094
-
1095
- if(outerTagName === 'pre'){
1096
- let parent = wrapper.parentNode;
1097
- while(parent && parent !== tempDiv){
1098
- if(parent.tagName && parent.tagName.toLowerCase() === 'p'){
1099
- const afterContent = [];
1100
- let sibling = wrapper.nextSibling;
1101
- while(sibling){
1102
- const next = sibling.nextSibling;
1103
- afterContent.push(sibling);
1104
- sibling = next;
1105
- }
1106
-
1107
- parent.parentNode.insertBefore(wrapper, parent.nextSibling);
1108
-
1109
- if(afterContent.length > 0 && afterContent.some(n => n.textContent.trim())){
1110
- const newP = document.createElement('p');
1111
- afterContent.forEach(node => newP.appendChild(node));
1112
- wrapper.parentNode.insertBefore(newP, wrapper.nextSibling);
1113
- }
1114
-
1115
- break;
1116
- }
1117
- parent = parent.parentNode;
1118
- }
1119
- }
1120
- }
1121
-
1122
- this.setValue(tempDiv.innerHTML);
1123
-
1124
- this.editorEl.focus();
1125
-
1126
- setTimeout(() => {
1127
- const textContent = this.editorEl.textContent;
1128
- const textIndex = textContent.indexOf(selectedText);
1129
-
1130
- if(textIndex === -1) return;
1131
-
1132
- const walker = document.createTreeWalker(
1133
- this.editorEl,
1134
- NodeFilter.SHOW_TEXT,
1135
- null
1136
- );
1137
-
1138
- let currentPos = 0;
1139
- let startNode = null;
1140
- let startOffset = 0;
1141
- let endNode = null;
1142
- let endOffset = 0;
1143
-
1144
- while(walker.nextNode()){
1145
- const node = walker.currentNode;
1146
- const nodeLength = node.textContent.length;
1147
- const nodeStart = currentPos;
1148
- const nodeEnd = currentPos + nodeLength;
1149
-
1150
- if(startNode === null && nodeEnd > textIndex){
1151
- startNode = node;
1152
- startOffset = textIndex - nodeStart;
1153
- }
1154
-
1155
- if(nodeEnd >= textIndex + selectedText.length){
1156
- endNode = node;
1157
- endOffset = (textIndex + selectedText.length) - nodeStart;
1158
- break;
1159
- }
1160
-
1161
- currentPos += nodeLength;
1162
- }
1163
-
1164
- if(startNode && endNode){
1165
- const range = document.createRange();
1166
- range.setStart(startNode, startOffset);
1167
- range.setEnd(endNode, endOffset);
1168
-
1169
- const selection = window.getSelection();
1170
- selection.removeAllRanges();
1171
- selection.addRange(range);
805
+ let result = { html: this.exportHtmlFromLexical(), hasCursor: false, hasSelection: false, selectedText: '' };
806
+ const { lexical } = this.lx;
807
+ this.lexicalEditor.getEditorState().read(() => {
808
+ const sel = lexical.$getSelection();
809
+ if(!lexical.$isRangeSelection(sel)) return;
810
+ if(sel.isCollapsed()){
811
+ result.hasCursor = true;
812
+ } else {
813
+ result.hasSelection = true;
814
+ result.selectedText = sel.getTextContent();
1172
815
  }
1173
- }, 0);
1174
-
1175
- return this;
816
+ });
817
+ return result;
1176
818
  }
1177
819
 
1178
- insertAtCursor(html) {
1179
- this.insertHTML(html);
1180
- return this;
820
+ setValueFromSelectionMarkers(htmlStr) {
821
+ return this.setValue(htmlStr);
822
+ }
823
+
824
+ captureSelection() {
825
+ return this.selection;
826
+ }
827
+
828
+ restoreSavedSelection() {
829
+ return false;
830
+ }
831
+
832
+ clearSavedSelection() {
833
+ this.savedSelection = null;
1181
834
  }
1182
835
 
1183
836
  /*
1184
837
  Public Methods - Links and Media
1185
838
  */
1186
839
  createLink(url) {
1187
- this.execCommand('createLink', url);
840
+ if(this.mode !== 'visual' || !this.lexicalEditor) return this;
841
+ const { link } = this.lx;
842
+ this.lexicalEditor.update(() => {
843
+ link.$toggleLink(url);
844
+ }, { discrete: true });
845
+ return this;
846
+ }
847
+
848
+ createLinkWithText(url, text) {
849
+ if(this.mode !== 'visual' || !this.lexicalEditor) return this;
850
+ const { lexical, link } = this.lx;
851
+ this.lexicalEditor.update(() => {
852
+ const sel = lexical.$getSelection();
853
+ if(lexical.$isRangeSelection(sel) && !sel.isCollapsed()) sel.removeText();
854
+ const linkNode = link.$createLinkNode(url);
855
+ linkNode.append(lexical.$createTextNode(text));
856
+ lexical.$insertNodes([linkNode]);
857
+ }, { discrete: true });
1188
858
  return this;
1189
859
  }
1190
860
 
1191
861
  unlink() {
1192
- this.execCommand('unlink');
862
+ if(this.mode !== 'visual' || !this.lexicalEditor) return this;
863
+ const { lexical, link } = this.lx;
864
+ this.lexicalEditor.update(() => {
865
+ const sel = lexical.$getSelection();
866
+ if(!lexical.$isRangeSelection(sel)) return;
867
+ sel.getNodes().forEach(node => {
868
+ const parent = node.getParent();
869
+ if(parent && link.$isLinkNode?.(parent)){
870
+ parent.getChildren().forEach(child => parent.insertBefore(child));
871
+ parent.remove();
872
+ }
873
+ });
874
+ }, { discrete: true });
1193
875
  return this;
1194
876
  }
1195
877
 
1196
878
  insertImage(url) {
1197
- this.execCommand('insertImage', url);
1198
- return this;
879
+ return this.insertHTML(`<img src="${encodeURI(url)}" />`);
1199
880
  }
1200
881
 
1201
882
  /*
1202
883
  Public Methods - History
1203
884
  */
1204
885
  undo() {
1205
- this.execCommand('undo');
886
+ this.lexicalCmd('UNDO_COMMAND', undefined);
1206
887
  return this;
1207
888
  }
1208
889
 
1209
890
  redo() {
1210
- this.execCommand('redo');
891
+ this.lexicalCmd('REDO_COMMAND', undefined);
1211
892
  return this;
1212
893
  }
1213
894
 
1214
895
  /*
1215
896
  Utility Methods
1216
897
  */
1217
- execCommand(command, value = null) {
1218
- if(this.mode !== 'visual') return;
1219
-
1220
- this.editorEl.focus();
1221
- document.execCommand(command, false, value);
1222
- this.value = this.editorEl.innerHTML;
1223
- }
1224
-
1225
- captureSelection(){
1226
- if(this.mode !== 'visual') return null;
1227
-
1228
- const selection = window.getSelection();
1229
-
1230
- if(!selection.rangeCount){
1231
- return null;
1232
- }
1233
-
1234
- const range = selection.getRangeAt(0);
1235
- const isInEditor = this.editorEl.contains(range.commonAncestorContainer);
1236
-
1237
- if(!isInEditor){
1238
- return null;
1239
- }
1240
-
1241
- const selectedText = range.toString();
1242
-
1243
- if(range.collapsed){
1244
- return null;
1245
- }
1246
-
1247
- return {
1248
- startContainer: range.startContainer,
1249
- startOffset: range.startOffset,
1250
- endContainer: range.endContainer,
1251
- endOffset: range.endOffset,
1252
- text: selectedText,
1253
- collapsed: range.collapsed
1254
- };
1255
- }
1256
-
1257
- restoreSavedSelection(){
1258
- if(!this.savedSelection) return false;
1259
-
1260
- const selection = this.shadowRoot.getSelection() || window.getSelection();
1261
- const range = document.createRange();
1262
-
1263
- try {
1264
- range.setStart(this.savedSelection.startContainer, this.savedSelection.startOffset);
1265
- range.setEnd(this.savedSelection.endContainer, this.savedSelection.endOffset);
1266
- selection.removeAllRanges();
1267
- selection.addRange(range);
1268
- return true;
1269
- } catch(e) {
1270
- this.savedSelection = null;
1271
- return false;
1272
- }
898
+ lexicalCmd(commandName, payload) {
899
+ if(this.mode !== 'visual' || !this.lexicalEditor) return;
900
+ const cmd = this.lx.lexical?.[commandName];
901
+ if(cmd) this.lexicalEditor.dispatchCommand(cmd, payload);
1273
902
  }
1274
903
 
1275
- clearSavedSelection(){
1276
- this.savedSelection = null;
1277
- }
1278
-
1279
- getValueWithSelectionMarkers(){
1280
- if(this.mode !== 'visual') return { html: this.value, hasCursor: false, hasSelection: false };
1281
-
1282
- const CURSOR_MARKER = '\uFFF0';
1283
- const SELECTION_START = '\uFFF1';
1284
- const SELECTION_END = '\uFFF2';
1285
-
1286
- const selection = this.shadowRoot.getSelection() || window.getSelection();
1287
-
1288
- // If no current selection, try to restore saved selection
1289
- if(!selection.rangeCount){
1290
- if(!this.restoreSavedSelection()){
1291
- return { html: this.editorEl.innerHTML, hasCursor: false, hasSelection: false };
1292
- }
1293
- }
1294
-
1295
- if(!selection.rangeCount){
1296
- return { html: this.editorEl.innerHTML, hasCursor: false, hasSelection: false };
1297
- }
1298
-
1299
- const range = selection.getRangeAt(0);
1300
- const isInEditor = this.editorEl.contains(range.commonAncestorContainer);
1301
-
1302
- if(!isInEditor){
1303
- return { html: this.editorEl.innerHTML, hasCursor: false, hasSelection: false };
1304
- }
1305
-
1306
- const selectedText = range.toString();
1307
-
1308
- if(range.collapsed){
1309
- const cursorMarker = document.createTextNode(CURSOR_MARKER);
1310
- const tempRange = document.createRange();
1311
- tempRange.setStart(range.startContainer, range.startOffset);
1312
- tempRange.insertNode(cursorMarker);
1313
-
1314
- const htmlWithMarker = this.editorEl.innerHTML;
1315
- cursorMarker.remove();
1316
-
1317
- return {
1318
- html: htmlWithMarker,
1319
- hasCursor: true,
1320
- hasSelection: false,
1321
- cursorMarker: CURSOR_MARKER,
1322
- selectedText: ''
1323
- };
1324
- } else {
1325
- const startMarker = document.createTextNode(SELECTION_START);
1326
- const endMarker = document.createTextNode(SELECTION_END);
1327
-
1328
- const tempRange = document.createRange();
1329
- tempRange.setStart(range.endContainer, range.endOffset);
1330
- tempRange.insertNode(endMarker);
1331
-
1332
- tempRange.setStart(range.startContainer, range.startOffset);
1333
- tempRange.insertNode(startMarker);
1334
-
1335
- const htmlWithMarkers = this.editorEl.innerHTML;
1336
-
1337
- startMarker.remove();
1338
- endMarker.remove();
1339
-
1340
- return {
1341
- html: htmlWithMarkers,
1342
- hasCursor: false,
1343
- hasSelection: true,
1344
- selectionStart: SELECTION_START,
1345
- selectionEnd: SELECTION_END,
1346
- selectedText: selectedText
1347
- };
1348
- }
1349
- }
1350
-
1351
- setValueFromSelectionMarkers(html, markers){
1352
- if(this.mode !== 'visual') return this;
1353
-
1354
- const CURSOR_MARKER = markers.cursorMarker || '\uFFF0';
1355
- const SELECTION_START = markers.selectionStart || '\uFFF1';
1356
- const SELECTION_END = markers.selectionEnd || '\uFFF2';
1357
-
1358
- this.editorEl.innerHTML = html;
1359
-
1360
- if(markers.hasCursor){
1361
- const walker = document.createTreeWalker(
1362
- this.editorEl,
1363
- NodeFilter.SHOW_TEXT,
1364
- null
1365
- );
1366
-
1367
- while(walker.nextNode()){
1368
- const node = walker.currentNode;
1369
- const index = node.textContent.indexOf(CURSOR_MARKER);
1370
-
1371
- if(index !== -1){
1372
- node.textContent = node.textContent.replace(CURSOR_MARKER, '');
1373
-
1374
- const range = document.createRange();
1375
- range.setStart(node, index);
1376
- range.setEnd(node, index);
1377
-
1378
- const selection = this.shadowRoot.getSelection() || window.getSelection();
1379
- selection.removeAllRanges();
1380
- selection.addRange(range);
1381
-
1382
- break;
1383
- }
1384
- }
1385
- } else if(markers.hasSelection){
1386
- const walker = document.createTreeWalker(
1387
- this.editorEl,
1388
- NodeFilter.SHOW_TEXT,
1389
- null
1390
- );
1391
-
1392
- let startNode = null;
1393
- let startOffset = 0;
1394
- let endNode = null;
1395
- let endOffset = 0;
1396
-
1397
- while(walker.nextNode()){
1398
- const node = walker.currentNode;
1399
-
1400
- const startIndex = node.textContent.indexOf(SELECTION_START);
1401
- if(startIndex !== -1){
1402
- startNode = node;
1403
- startOffset = startIndex;
1404
- node.textContent = node.textContent.replace(SELECTION_START, '');
1405
- }
1406
-
1407
- const endIndex = node.textContent.indexOf(SELECTION_END);
1408
- if(endIndex !== -1){
1409
- endNode = node;
1410
- endOffset = endIndex;
1411
- node.textContent = node.textContent.replace(SELECTION_END, '');
1412
- }
1413
-
1414
- if(startNode && endNode) break;
1415
- }
1416
-
1417
- if(startNode && endNode){
1418
- const range = document.createRange();
1419
- range.setStart(startNode, startOffset);
1420
- range.setEnd(endNode, endOffset);
1421
-
1422
- const selection = this.shadowRoot.getSelection() || window.getSelection();
1423
- selection.removeAllRanges();
1424
- selection.addRange(range);
1425
- }
1426
- }
1427
-
1428
- this.editorEl.focus();
1429
-
1430
- return this;
904
+ lexicalFormat(format) {
905
+ if(this.mode !== 'visual' || !this.lexicalEditor) return;
906
+ const { lexical } = this.lx;
907
+ this.lexicalEditor.update(() => {
908
+ const sel = lexical.$getSelection();
909
+ if(lexical.$isRangeSelection(sel)) sel.formatText(format);
910
+ }, { discrete: true });
1431
911
  }
1432
912
 
1433
- syncContent() {
1434
- if(!this.editorEl || !this.textareaEl || this.skipSync) return;
1435
-
1436
- if(this.mode === 'visual'){
1437
- if(this.editorEl.innerHTML !== this.value){
1438
- this.editorEl.innerHTML = this.value;
1439
- }
1440
- } else {
1441
- const formattedValue = formatCode(this.value);
1442
- if(this.textareaEl.value !== formattedValue){
1443
- this.textareaEl.value = formattedValue;
1444
- }
1445
- }
913
+ lexicalFormatElement(alignment) {
914
+ if(this.mode !== 'visual' || !this.lexicalEditor) return;
915
+ this.lexicalEditor.dispatchCommand(this.lx.lexical.FORMAT_ELEMENT_COMMAND, alignment);
1446
916
  }
1447
917
 
1448
918
  /*
@@ -1455,7 +925,6 @@ export default class HtmlEditor extends ShadowComponent {
1455
925
  gap: 0;
1456
926
  height: 300px;
1457
927
  }
1458
-
1459
928
  .toolbar-top,
1460
929
  .toolbar-bottom {
1461
930
  display: flex;
@@ -1465,18 +934,20 @@ export default class HtmlEditor extends ShadowComponent {
1465
934
  background: var(--bg-secondary);
1466
935
  min-height: 40px;
1467
936
  }
1468
-
1469
937
  .editor-container {
1470
938
  position: relative;
1471
939
  flex: 1;
1472
940
  min-height: 0;
1473
941
  overflow: hidden;
1474
942
  }
1475
-
1476
- .editor,
1477
- textarea {
943
+ .lexical-editor,
944
+ .monaco-editor-container {
1478
945
  width: 100%;
1479
946
  height: 100%;
947
+ box-sizing: border-box;
948
+ overflow: auto;
949
+ }
950
+ .lexical-editor {
1480
951
  padding: 1rem;
1481
952
  border: 1px solid var(--border-color);
1482
953
  background: var(--bg-primary);
@@ -1484,31 +955,59 @@ export default class HtmlEditor extends ShadowComponent {
1484
955
  font-family: inherit;
1485
956
  font-size: inherit;
1486
957
  line-height: 1.5;
1487
- overflow: auto;
1488
- box-sizing: border-box;
1489
- }
1490
-
1491
- .editor {
1492
958
  outline: none;
1493
959
  }
1494
-
1495
- .editor:focus {
960
+ .lexical-editor:focus {
1496
961
  border-color: var(--primary-color);
1497
962
  }
1498
-
1499
- textarea {
1500
- resize: none;
963
+ .lexical-editor p { margin: 0 0 0.5em 0; }
964
+ .lexical-editor h1,
965
+ .lexical-editor h2,
966
+ .lexical-editor h3,
967
+ .lexical-editor h4,
968
+ .lexical-editor h5,
969
+ .lexical-editor h6 { margin: 0.5em 0; }
970
+ .lexical-editor ul,
971
+ .lexical-editor ol { margin: 0.5em 0; padding-left: 1.5em; }
972
+ .lexical-editor blockquote {
973
+ margin: 0.5em 0;
974
+ padding-left: 1em;
975
+ border-left: 3px solid var(--border-color, #ccc);
976
+ color: var(--text-secondary, #666);
977
+ }
978
+ .lexical-editor a { color: var(--primary-color, #007acc); }
979
+ .lexical-editor code {
980
+ background: var(--bg-secondary, #f5f5f5);
981
+ padding: 0.125em 0.25em;
982
+ border-radius: 3px;
1501
983
  font-family: monospace;
1502
984
  }
1503
-
1504
- textarea:focus {
1505
- outline: none;
1506
- border-color: var(--primary-color);
985
+ .lexical-editor pre {
986
+ background: var(--bg-secondary, #f5f5f5);
987
+ padding: 1em;
988
+ border-radius: 4px;
989
+ font-family: monospace;
990
+ overflow-x: auto;
991
+ white-space: pre-wrap;
1507
992
  }
1508
-
1509
- .editor[hidden],
1510
- textarea[hidden] {
1511
- display: none;
993
+ .lexical-editor table {
994
+ border-collapse: collapse;
995
+ width: 100%;
996
+ }
997
+ .lexical-editor th,
998
+ .lexical-editor td {
999
+ border: 1px solid var(--border-color, #ccc);
1000
+ padding: 0.5rem;
1001
+ }
1002
+ .lexical-editor th {
1003
+ background-color: var(--bg-secondary, #f5f5f5);
1004
+ text-align: left;
1005
+ }
1006
+ .monaco-editor-container {
1007
+ border: 1px solid var(--border-color);
1008
+ }
1009
+ [hidden] {
1010
+ display: none !important;
1512
1011
  }
1513
1012
  `;
1514
1013
 
@@ -1519,23 +1018,17 @@ export default class HtmlEditor extends ShadowComponent {
1519
1018
  <slot name="toolbar-top"></slot>
1520
1019
  </div>
1521
1020
  ` : ''}
1522
-
1523
1021
  <div class="editor-container">
1524
- <div
1525
- class="editor"
1022
+ <div
1023
+ class="lexical-editor"
1526
1024
  contenteditable="true"
1527
1025
  ?hidden=${this.mode !== 'visual'}
1528
- @input=${this.handleEditorInput}
1529
- @paste=${this.handleEditorPaste}
1530
- @blur=${this.handleEditorBlur}
1531
1026
  ></div>
1532
-
1533
- <textarea
1027
+ <div
1028
+ class="monaco-editor-container"
1534
1029
  ?hidden=${this.mode !== 'code'}
1535
- @input=${this.handleTextareaInput}
1536
- ></textarea>
1030
+ ></div>
1537
1031
  </div>
1538
-
1539
1032
  ${this.hasBottomToolbar ? html`
1540
1033
  <div class="toolbar-bottom bt">
1541
1034
  <slot name="toolbar-bottom"></slot>