@lukso/web-components 1.154.0 → 1.155.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/components/index.cjs +14 -4
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +6 -4
- package/dist/components/lukso-button/index.cjs +3 -3
- package/dist/components/lukso-button/index.js +3 -3
- package/dist/components/lukso-card/index.cjs +7 -7
- package/dist/components/lukso-card/index.js +7 -7
- package/dist/components/lukso-checkbox/index.cjs +3 -3
- package/dist/components/lukso-checkbox/index.js +3 -3
- package/dist/components/lukso-collapse/index.cjs +5 -11
- package/dist/components/lukso-collapse/index.js +4 -10
- package/dist/components/lukso-color-picker/index.cjs +4 -4
- package/dist/components/lukso-color-picker/index.js +4 -4
- package/dist/components/lukso-dropdown/index.cjs +11 -7
- package/dist/components/lukso-dropdown/index.d.ts +1 -0
- package/dist/components/lukso-dropdown/index.d.ts.map +1 -1
- package/dist/components/lukso-dropdown/index.js +11 -7
- package/dist/components/lukso-dropdown-option/index.cjs +2 -2
- package/dist/components/lukso-dropdown-option/index.js +2 -2
- package/dist/components/lukso-footer/index.cjs +2 -2
- package/dist/components/lukso-footer/index.js +2 -2
- package/dist/components/lukso-icon/index.cjs +283 -18
- package/dist/components/lukso-icon/index.d.ts +21 -0
- package/dist/components/lukso-icon/index.d.ts.map +1 -1
- package/dist/components/lukso-icon/index.js +282 -18
- package/dist/components/lukso-icon/lukso-icon.stories.d.ts +5 -0
- package/dist/components/lukso-icon/lukso-icon.stories.d.ts.map +1 -1
- package/dist/components/lukso-icon/vuesax/bold/attach-square.svg +3 -0
- package/dist/components/lukso-icon/vuesax/bold/eye-slash.svg +7 -0
- package/dist/components/lukso-icon/vuesax/bold/eye.svg +4 -0
- package/dist/components/lukso-icon/vuesax/bold/link.svg +5 -0
- package/dist/components/lukso-icon/vuesax/bold/smallcaps.svg +3 -0
- package/dist/components/lukso-icon/vuesax/bold/task.svg +8 -0
- package/dist/components/lukso-icon/vuesax/bold/text-bold.svg +5 -0
- package/dist/components/lukso-icon/vuesax/bold/text-italic.svg +3 -0
- package/dist/components/lukso-icon/vuesax/bold/textalign-center.svg +6 -0
- package/dist/components/lukso-icon/vuesax/bold/textalign-justifycenter.svg +6 -0
- package/dist/components/lukso-icon/vuesax/bold/textalign-left.svg +6 -0
- package/dist/components/lukso-icon/vuesax/bold/textalign-right.svg +6 -0
- package/dist/components/lukso-icon/vuesax/broken/attach-square.svg +4 -0
- package/dist/components/lukso-icon/vuesax/broken/eye-slash.svg +9 -0
- package/dist/components/lukso-icon/vuesax/broken/eye.svg +4 -0
- package/dist/components/lukso-icon/vuesax/broken/link.svg +6 -0
- package/dist/components/lukso-icon/vuesax/broken/smallcaps.svg +9 -0
- package/dist/components/lukso-icon/vuesax/broken/task.svg +9 -0
- package/dist/components/lukso-icon/vuesax/broken/text-bold.svg +4 -0
- package/dist/components/lukso-icon/vuesax/broken/text-italic.svg +6 -0
- package/dist/components/lukso-icon/vuesax/broken/textalign-center.svg +7 -0
- package/dist/components/lukso-icon/vuesax/broken/textalign-justifycenter.svg +7 -0
- package/dist/components/lukso-icon/vuesax/broken/textalign-left.svg +7 -0
- package/dist/components/lukso-icon/vuesax/broken/textalign-right.svg +7 -0
- package/dist/components/lukso-icon/vuesax/bulk/attach-square.svg +4 -0
- package/dist/components/lukso-icon/vuesax/bulk/eye-slash.svg +7 -0
- package/dist/components/lukso-icon/vuesax/bulk/frame.svg +4 -0
- package/dist/components/lukso-icon/vuesax/bulk/link.svg +5 -0
- package/dist/components/lukso-icon/vuesax/bulk/smallcaps.svg +5 -0
- package/dist/components/lukso-icon/vuesax/bulk/task.svg +8 -0
- package/dist/components/lukso-icon/vuesax/bulk/text-bold.svg +4 -0
- package/dist/components/lukso-icon/vuesax/bulk/text-italic.svg +4 -0
- package/dist/components/lukso-icon/vuesax/bulk/textalign-center.svg +6 -0
- package/dist/components/lukso-icon/vuesax/bulk/textalign-justifycenter.svg +6 -0
- package/dist/components/lukso-icon/vuesax/bulk/textalign-left.svg +6 -0
- package/dist/components/lukso-icon/vuesax/bulk/textalign-right.svg +6 -0
- package/dist/components/lukso-icon/vuesax/linear/attach-square.svg +4 -0
- package/dist/components/lukso-icon/vuesax/linear/eye-slash.svg +8 -0
- package/dist/components/lukso-icon/vuesax/linear/eye.svg +4 -0
- package/dist/components/lukso-icon/vuesax/linear/link.svg +5 -0
- package/dist/components/lukso-icon/vuesax/linear/smallcaps.svg +8 -0
- package/dist/components/lukso-icon/vuesax/linear/task.svg +8 -0
- package/dist/components/lukso-icon/vuesax/linear/text-bold.svg +4 -0
- package/dist/components/lukso-icon/vuesax/linear/text-italic.svg +5 -0
- package/dist/components/lukso-icon/vuesax/linear/textalign-center.svg +6 -0
- package/dist/components/lukso-icon/vuesax/linear/textalign-justifycenter.svg +6 -0
- package/dist/components/lukso-icon/vuesax/linear/textalign-left.svg +6 -0
- package/dist/components/lukso-icon/vuesax/linear/textalign-right.svg +6 -0
- package/dist/components/lukso-icon/vuesax/outline/attach-square.svg +4 -0
- package/dist/components/lukso-icon/vuesax/outline/eye-slash.svg +8 -0
- package/dist/components/lukso-icon/vuesax/outline/eye.svg +4 -0
- package/dist/components/lukso-icon/vuesax/outline/link.svg +5 -0
- package/dist/components/lukso-icon/vuesax/outline/smallcaps.svg +8 -0
- package/dist/components/lukso-icon/vuesax/outline/task.svg +8 -0
- package/dist/components/lukso-icon/vuesax/outline/text-bold.svg +4 -0
- package/dist/components/lukso-icon/vuesax/outline/text-italic.svg +5 -0
- package/dist/components/lukso-icon/vuesax/outline/textalign-center.svg +6 -0
- package/dist/components/lukso-icon/vuesax/outline/textalign-justifycenter.svg +6 -0
- package/dist/components/lukso-icon/vuesax/outline/textalign-left.svg +6 -0
- package/dist/components/lukso-icon/vuesax/outline/textalign-right.svg +6 -0
- package/dist/components/lukso-icon/vuesax/twotone/attach-square.svg +4 -0
- package/dist/components/lukso-icon/vuesax/twotone/eye-slash.svg +8 -0
- package/dist/components/lukso-icon/vuesax/twotone/eye.svg +4 -0
- package/dist/components/lukso-icon/vuesax/twotone/link.svg +5 -0
- package/dist/components/lukso-icon/vuesax/twotone/smallcaps.svg +10 -0
- package/dist/components/lukso-icon/vuesax/twotone/task.svg +8 -0
- package/dist/components/lukso-icon/vuesax/twotone/text-bold.svg +4 -0
- package/dist/components/lukso-icon/vuesax/twotone/text-italic.svg +5 -0
- package/dist/components/lukso-icon/vuesax/twotone/textalign-center.svg +6 -0
- package/dist/components/lukso-icon/vuesax/twotone/textalign-justifycenter.svg +6 -0
- package/dist/components/lukso-icon/vuesax/twotone/textalign-left.svg +6 -0
- package/dist/components/lukso-icon/vuesax/twotone/textalign-right.svg +6 -0
- package/dist/components/lukso-image/index.cjs +4 -4
- package/dist/components/lukso-image/index.js +4 -4
- package/dist/components/lukso-input/index.cjs +3 -3
- package/dist/components/lukso-input/index.js +3 -3
- package/dist/components/lukso-markdown/index.cjs +168 -0
- package/dist/components/lukso-markdown/index.d.ts +22 -0
- package/dist/components/lukso-markdown/index.d.ts.map +1 -0
- package/dist/components/lukso-markdown/index.js +166 -0
- package/dist/components/lukso-markdown/lukso-markdown.stories.d.ts +13 -0
- package/dist/components/lukso-markdown/lukso-markdown.stories.d.ts.map +1 -0
- package/dist/components/lukso-markdown-editor/index.cjs +1996 -0
- package/dist/components/lukso-markdown-editor/index.d.ts +255 -0
- package/dist/components/lukso-markdown-editor/index.d.ts.map +1 -0
- package/dist/components/lukso-markdown-editor/index.js +1994 -0
- package/dist/components/lukso-markdown-editor/lukso-markdown-editor.stories.d.ts +27 -0
- package/dist/components/lukso-markdown-editor/lukso-markdown-editor.stories.d.ts.map +1 -0
- package/dist/components/lukso-modal/index.cjs +2 -2
- package/dist/components/lukso-modal/index.js +2 -2
- package/dist/components/lukso-navbar/index.cjs +3 -3
- package/dist/components/lukso-navbar/index.js +3 -3
- package/dist/components/lukso-pagination/index.cjs +3 -3
- package/dist/components/lukso-pagination/index.js +3 -3
- package/dist/components/lukso-profile/index.cjs +3 -3
- package/dist/components/lukso-profile/index.js +3 -3
- package/dist/components/lukso-progress/index.cjs +3 -3
- package/dist/components/lukso-progress/index.js +3 -3
- package/dist/components/lukso-radio/index.cjs +3 -3
- package/dist/components/lukso-radio/index.js +3 -3
- package/dist/components/lukso-radio-group/index.cjs +3 -3
- package/dist/components/lukso-radio-group/index.js +3 -3
- package/dist/components/lukso-sanitize/index.cjs +4 -10
- package/dist/components/lukso-sanitize/index.js +4 -10
- package/dist/components/lukso-search/index.cjs +6 -6
- package/dist/components/lukso-search/index.d.ts.map +1 -1
- package/dist/components/lukso-search/index.js +6 -6
- package/dist/components/lukso-select/index.cjs +5 -5
- package/dist/components/lukso-select/index.js +5 -5
- package/dist/components/lukso-share/index.cjs +2 -2
- package/dist/components/lukso-share/index.js +2 -2
- package/dist/components/lukso-switch/index.cjs +3 -3
- package/dist/components/lukso-switch/index.js +3 -3
- package/dist/components/lukso-tag/index.cjs +3 -3
- package/dist/components/lukso-tag/index.js +3 -3
- package/dist/components/lukso-terms/index.cjs +3 -3
- package/dist/components/lukso-terms/index.js +3 -3
- package/dist/components/lukso-textarea/index.cjs +3 -3
- package/dist/components/lukso-textarea/index.js +3 -3
- package/dist/components/lukso-tooltip/index.cjs +4 -4
- package/dist/components/lukso-tooltip/index.js +4 -4
- package/dist/components/lukso-username/index.cjs +5 -5
- package/dist/components/lukso-username/index.js +5 -5
- package/dist/components/lukso-wizard/index.cjs +2 -2
- package/dist/components/lukso-wizard/index.js +2 -2
- package/dist/docs/VuesaxPack.stories.d.ts +6 -0
- package/dist/docs/VuesaxPack.stories.d.ts.map +1 -0
- package/dist/{index-6Bz9XVC3.js → index-1cnTXnRX.js} +4 -4
- package/dist/{index-DUwutUFV.js → index-B5_11hN3.js} +5 -5
- package/dist/index-BWp0TAbf.js +41 -0
- package/dist/{index-Dg9hcDqR.cjs → index-CO57mpzm.cjs} +5 -5
- package/dist/{index-gJTlTDGh.js → index-CsepkLbs.js} +1 -1
- package/dist/{index-m3XGqZFz.cjs → index-D-a6OWyj.cjs} +1 -1
- package/dist/{index-U4Y7KwZv.cjs → index-D4HqZcbk.cjs} +2 -2
- package/dist/{index-fSZGRljb.cjs → index-D9mdD_OM.cjs} +4 -4
- package/dist/{index-CvWyP3CS.js → index-DFculCCQ.js} +2 -2
- package/dist/index-KrWvJ44l.cjs +50 -0
- package/dist/index.cjs +14 -4
- package/dist/index.js +6 -4
- package/dist/{isAddress-B3b91Jxf.cjs → isAddress-7c5mkwjj.cjs} +1 -1
- package/dist/{isAddress-Dq9UN07g.js → isAddress-DyEmEF7O.js} +1 -1
- package/dist/{property-BXmHod5d.cjs → property-CLMAG2Rj.cjs} +1 -1
- package/dist/{property-B4XYi6Sk.js → property-DkFGx5Fg.js} +1 -1
- package/dist/query-CHb9Ft_d.js +9 -0
- package/dist/query-EFiHeHdi.cjs +11 -0
- package/dist/shared/tailwind-element/index.cjs +1 -1
- package/dist/shared/tailwind-element/index.js +1 -1
- package/dist/{state-CkvZ4IP8.js → state-DPXXwNMf.js} +1 -1
- package/dist/{state-CW2YmM3f.cjs → state-DXBdLH-W.cjs} +1 -1
- package/dist/{style-map-c2S85yDu.cjs → style-map-BFG88xg5.cjs} +1 -1
- package/dist/{style-map-D1R4wi4h.js → style-map-CXXmrGLe.js} +1 -1
- package/dist/unsafe-html-C4RqiLkE.js +10 -0
- package/dist/unsafe-html-CxwvRvgp.cjs +12 -0
- package/dist/vite.full.config.d.ts.map +1 -1
- package/package.json +11 -1
- package/tailwind.config.cjs +1 -0
- package/tools/copy-assets.cjs +16 -16
- package/tools/copy-assets.js +2 -2
- package/tools/index.cjs +1 -1
- package/tools/index.js +1 -1
- package/dist/index-C9vH8YlV.js +0 -41
- package/dist/index-DkfODalz.cjs +0 -50
|
@@ -0,0 +1,1994 @@
|
|
|
1
|
+
import { T as TailwindStyledElement, E, x } from '../../index-BWp0TAbf.js';
|
|
2
|
+
import { n, t } from '../../property-DkFGx5Fg.js';
|
|
3
|
+
import { r } from '../../state-DPXXwNMf.js';
|
|
4
|
+
import { e } from '../../query-CHb9Ft_d.js';
|
|
5
|
+
import { c as ce } from '../../index-B9iart53.js';
|
|
6
|
+
import '../../tailwind-config.js';
|
|
7
|
+
import { c as cn } from '../../cn-Cu9aP49j.js';
|
|
8
|
+
import '../lukso-textarea/index.js';
|
|
9
|
+
import '../lukso-markdown/index.js';
|
|
10
|
+
import '../lukso-switch/index.js';
|
|
11
|
+
import '../lukso-button/index.js';
|
|
12
|
+
import '../lukso-icon/index.js';
|
|
13
|
+
import '../lukso-sanitize/index.js';
|
|
14
|
+
import '../lukso-dropdown/index.js';
|
|
15
|
+
import '../lukso-dropdown-option/index.js';
|
|
16
|
+
import '../lukso-tooltip/index.js';
|
|
17
|
+
|
|
18
|
+
const style = ":host {\n display: flex\n}";
|
|
19
|
+
|
|
20
|
+
var __defProp = Object.defineProperty;
|
|
21
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
22
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
23
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
24
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
25
|
+
if (decorator = decorators[i])
|
|
26
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
27
|
+
if (kind && result) __defProp(target, key, result);
|
|
28
|
+
return result;
|
|
29
|
+
};
|
|
30
|
+
let LuksoMarkdownEditor = class extends TailwindStyledElement(style) {
|
|
31
|
+
constructor() {
|
|
32
|
+
super(...arguments);
|
|
33
|
+
this.value = "";
|
|
34
|
+
this.name = "";
|
|
35
|
+
this.label = "";
|
|
36
|
+
this.description = "";
|
|
37
|
+
this.error = "";
|
|
38
|
+
this.isFullWidth = false;
|
|
39
|
+
this.isReadonly = false;
|
|
40
|
+
this.isDisabled = false;
|
|
41
|
+
this.isNonResizable = false;
|
|
42
|
+
this.autofocus = false;
|
|
43
|
+
this.size = "large";
|
|
44
|
+
this.isPreview = false;
|
|
45
|
+
this.rows = 6;
|
|
46
|
+
this.placeholder = "";
|
|
47
|
+
this.savedSelectionForPreview = null;
|
|
48
|
+
this.isHeadingDropdownOpen = false;
|
|
49
|
+
this.isColorDropdownOpen = false;
|
|
50
|
+
this.isListDropdownOpen = false;
|
|
51
|
+
this.headingTriggerId = "heading-dropdown-trigger";
|
|
52
|
+
this.colorTriggerId = "color-dropdown-trigger";
|
|
53
|
+
this.listTriggerId = "list-dropdown-trigger";
|
|
54
|
+
this.currentSelection = { start: 0, end: 0 };
|
|
55
|
+
this.savedSelection = null;
|
|
56
|
+
this.activeFormats = {
|
|
57
|
+
bold: false,
|
|
58
|
+
italic: false,
|
|
59
|
+
link: false,
|
|
60
|
+
h1: false,
|
|
61
|
+
h2: false,
|
|
62
|
+
h3: false,
|
|
63
|
+
color: false,
|
|
64
|
+
activeColor: "",
|
|
65
|
+
unorderedList: false,
|
|
66
|
+
orderedList: false
|
|
67
|
+
};
|
|
68
|
+
// Undo/Redo state
|
|
69
|
+
this.undoStack = [];
|
|
70
|
+
this.redoStack = [];
|
|
71
|
+
this.isUndoRedoAction = false;
|
|
72
|
+
this.lastSavedValue = "";
|
|
73
|
+
this.undoTimeout = null;
|
|
74
|
+
this.MAX_UNDO_STACK_SIZE = 100;
|
|
75
|
+
this.UNDO_SAVE_DELAY = 500;
|
|
76
|
+
this.handleOutsideClick = (event) => {
|
|
77
|
+
const target = event.target;
|
|
78
|
+
const isInsideThisComponent = this.contains(target) || this.shadowRoot?.contains(target);
|
|
79
|
+
if (!isInsideThisComponent) {
|
|
80
|
+
if (this.isHeadingDropdownOpen) {
|
|
81
|
+
this.isHeadingDropdownOpen = false;
|
|
82
|
+
}
|
|
83
|
+
if (this.isColorDropdownOpen) {
|
|
84
|
+
this.isColorDropdownOpen = false;
|
|
85
|
+
}
|
|
86
|
+
if (this.isListDropdownOpen) {
|
|
87
|
+
this.isListDropdownOpen = false;
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const isInsideHeadingDropdown = this.shadowRoot?.getElementById("headingDropdown")?.contains(target);
|
|
92
|
+
const isInsideColorDropdown = this.shadowRoot?.getElementById("colorDropdown")?.contains(target);
|
|
93
|
+
const isInsideListDropdown = this.shadowRoot?.getElementById("listDropdown")?.contains(target);
|
|
94
|
+
const isHeadingTrigger = this.shadowRoot?.getElementById(this.headingTriggerId)?.contains(target);
|
|
95
|
+
const isColorTrigger = this.shadowRoot?.getElementById(this.colorTriggerId)?.contains(target);
|
|
96
|
+
const isListTrigger = this.shadowRoot?.getElementById(this.listTriggerId)?.contains(target);
|
|
97
|
+
if (!isInsideHeadingDropdown && !isHeadingTrigger && this.isHeadingDropdownOpen) {
|
|
98
|
+
this.isHeadingDropdownOpen = false;
|
|
99
|
+
}
|
|
100
|
+
if (!isInsideColorDropdown && !isColorTrigger && this.isColorDropdownOpen) {
|
|
101
|
+
this.isColorDropdownOpen = false;
|
|
102
|
+
}
|
|
103
|
+
if (!isInsideListDropdown && !isListTrigger && this.isListDropdownOpen) {
|
|
104
|
+
this.isListDropdownOpen = false;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
this.styles = ce({
|
|
108
|
+
slots: {
|
|
109
|
+
wrapper: "w-[inherit] grid gap-3",
|
|
110
|
+
header: "flex items-center justify-between gap-2 border border-neutral-90 rounded-12 px-3 py-2",
|
|
111
|
+
toolbar: "flex flex-wrap items-center gap-1",
|
|
112
|
+
area: "",
|
|
113
|
+
editor: "",
|
|
114
|
+
preview: "p-3",
|
|
115
|
+
colorMenu: "relative",
|
|
116
|
+
headingMenu: "relative",
|
|
117
|
+
listMenu: "relative",
|
|
118
|
+
label: "heading-inter-14-bold text-neutral-20",
|
|
119
|
+
description: "paragraph-inter-12-regular text-neutral-20",
|
|
120
|
+
divider: "w-[1px] h-4 bg-neutral-90"
|
|
121
|
+
},
|
|
122
|
+
variants: {
|
|
123
|
+
isFullWidth: {
|
|
124
|
+
true: { wrapper: "w-full" },
|
|
125
|
+
false: { wrapper: "w-[350px]" }
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
compoundVariants: [{ isFullWidth: false, class: { wrapper: "w-[500px]" } }]
|
|
129
|
+
});
|
|
130
|
+
this.toolbarButton = ce({
|
|
131
|
+
base: "hover:bg-neutral-95 transition border-0 !shadow-none",
|
|
132
|
+
variants: {
|
|
133
|
+
active: { true: "bg-neutral-95" },
|
|
134
|
+
disabled: { true: "opacity-50 cursor-not-allowed" }
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
this.defaultColor = "#374151";
|
|
138
|
+
// prose gray
|
|
139
|
+
/**
|
|
140
|
+
* Hex color palette for the color picker.
|
|
141
|
+
* A curated collection of colors organized by hue families,
|
|
142
|
+
* ranging from light pastels to bold colors.
|
|
143
|
+
*/
|
|
144
|
+
this.colorSamples = [
|
|
145
|
+
"#FFE5E5",
|
|
146
|
+
// very light red
|
|
147
|
+
"#FFB3B3",
|
|
148
|
+
"#FF8080",
|
|
149
|
+
"#FF6666",
|
|
150
|
+
"#FF4D4D",
|
|
151
|
+
"#E63946",
|
|
152
|
+
// strong pastel red
|
|
153
|
+
"#FFEBD5",
|
|
154
|
+
// very light orange
|
|
155
|
+
"#FFD1A3",
|
|
156
|
+
"#FFB870",
|
|
157
|
+
"#FFA54D",
|
|
158
|
+
"#FF944D",
|
|
159
|
+
"#FF7F11",
|
|
160
|
+
// bold pastel orange
|
|
161
|
+
"#FFFBD5",
|
|
162
|
+
// very light yellow
|
|
163
|
+
"#FFF3A3",
|
|
164
|
+
"#FFE870",
|
|
165
|
+
"#FFDD4D",
|
|
166
|
+
"#FFD633",
|
|
167
|
+
"#F4C430",
|
|
168
|
+
// deep pastel yellow
|
|
169
|
+
"#E6F9EC",
|
|
170
|
+
// very light green
|
|
171
|
+
"#B3E6C1",
|
|
172
|
+
"#80D499",
|
|
173
|
+
"#66CC80",
|
|
174
|
+
"#4DB870",
|
|
175
|
+
"#2D9C5B",
|
|
176
|
+
// bold pastel green
|
|
177
|
+
"#E6F3FF",
|
|
178
|
+
// very light blue
|
|
179
|
+
"#B3DAFF",
|
|
180
|
+
"#80C2FF",
|
|
181
|
+
"#66B2FF",
|
|
182
|
+
"#4DA6FF",
|
|
183
|
+
"#3A86FF",
|
|
184
|
+
// bold pastel blue
|
|
185
|
+
"#F3E6FF",
|
|
186
|
+
// very light purple
|
|
187
|
+
"#D9B3FF",
|
|
188
|
+
"#BF80FF",
|
|
189
|
+
"#A64DFF",
|
|
190
|
+
"#9333FF",
|
|
191
|
+
"#7B2CBF",
|
|
192
|
+
// bold pastel purple
|
|
193
|
+
"#FFE6F3",
|
|
194
|
+
// very light pink
|
|
195
|
+
"#FFB3D9",
|
|
196
|
+
"#FF80BF",
|
|
197
|
+
"#FF66B2",
|
|
198
|
+
"#FF4DA6",
|
|
199
|
+
"#E75480",
|
|
200
|
+
// bold pastel pink
|
|
201
|
+
"#F5E6DA",
|
|
202
|
+
// very light brown
|
|
203
|
+
"#E6CBB3",
|
|
204
|
+
"#D9B38C",
|
|
205
|
+
"#CC9966",
|
|
206
|
+
"#B3773A",
|
|
207
|
+
"#8B5E3C",
|
|
208
|
+
// bold pastel brown
|
|
209
|
+
"#FFFFFF",
|
|
210
|
+
// white
|
|
211
|
+
"#F5F5F5",
|
|
212
|
+
// very light gray
|
|
213
|
+
"#E0E0E0",
|
|
214
|
+
// light gray
|
|
215
|
+
"#BDBDBD",
|
|
216
|
+
// medium gray
|
|
217
|
+
"#757575",
|
|
218
|
+
// dark gray
|
|
219
|
+
"#000000",
|
|
220
|
+
// black
|
|
221
|
+
this.defaultColor
|
|
222
|
+
];
|
|
223
|
+
/**
|
|
224
|
+
* Handle input event from the internal textarea component.
|
|
225
|
+
* @param event
|
|
226
|
+
*/
|
|
227
|
+
this.handleTextareaInput = (event) => {
|
|
228
|
+
const newValue = event.detail?.value || "";
|
|
229
|
+
if (!this.isUndoRedoAction) {
|
|
230
|
+
this.scheduleUndoStateSave();
|
|
231
|
+
}
|
|
232
|
+
this.value = newValue;
|
|
233
|
+
this.updateActiveFormats();
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* Textarea keyup handler.
|
|
237
|
+
* Update active formats when user navigates with keyboard.
|
|
238
|
+
*/
|
|
239
|
+
this.handleTextareaKeyUp = () => {
|
|
240
|
+
requestAnimationFrame(() => this.updateActiveFormats());
|
|
241
|
+
};
|
|
242
|
+
/**
|
|
243
|
+
* Textarea click handler.
|
|
244
|
+
* Update active formats when user clicks to move cursor.
|
|
245
|
+
*/
|
|
246
|
+
this.handleTextareaClick = () => {
|
|
247
|
+
requestAnimationFrame(() => this.updateActiveFormats());
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* Handle keydown events for undo/redo shortcuts and list continuation.
|
|
251
|
+
*/
|
|
252
|
+
this.handleKeyDown = (event) => {
|
|
253
|
+
const isUndo = (event.metaKey || event.ctrlKey) && event.key === "z" && !event.shiftKey;
|
|
254
|
+
const isRedo = (event.metaKey || event.ctrlKey) && (event.key === "y" || event.key === "z" && event.shiftKey);
|
|
255
|
+
if (isUndo) {
|
|
256
|
+
event.preventDefault();
|
|
257
|
+
this.undo();
|
|
258
|
+
} else if (isRedo) {
|
|
259
|
+
event.preventDefault();
|
|
260
|
+
this.redo();
|
|
261
|
+
} else if (event.key === "Enter" && !event.metaKey && !event.ctrlKey && !event.shiftKey) {
|
|
262
|
+
if (this.handleListContinuation()) {
|
|
263
|
+
event.preventDefault();
|
|
264
|
+
}
|
|
265
|
+
} else if (event.key === "Backspace" && !event.metaKey && !event.ctrlKey && !event.shiftKey) {
|
|
266
|
+
if (this.handleListBackspace()) {
|
|
267
|
+
event.preventDefault();
|
|
268
|
+
}
|
|
269
|
+
} else if (event.key === "Tab" && !event.metaKey && !event.ctrlKey) {
|
|
270
|
+
if (event.shiftKey) {
|
|
271
|
+
if (this.handleListOutdent()) {
|
|
272
|
+
event.preventDefault();
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
if (this.handleListIndent()) {
|
|
276
|
+
event.preventDefault();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
dispatchChange(event) {
|
|
283
|
+
this.updateComplete.then(() => {
|
|
284
|
+
const changeEvent = new CustomEvent("on-change", {
|
|
285
|
+
detail: { value: this.value, event },
|
|
286
|
+
bubbles: false,
|
|
287
|
+
composed: true
|
|
288
|
+
});
|
|
289
|
+
this.dispatchEvent(changeEvent);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Utility to perform an operation with the current textarea selection.
|
|
294
|
+
*
|
|
295
|
+
* @param callback -
|
|
296
|
+
*/
|
|
297
|
+
withSelection(callback) {
|
|
298
|
+
const textarea = this.textareaEl?.shadowRoot?.querySelector(
|
|
299
|
+
"textarea"
|
|
300
|
+
);
|
|
301
|
+
if (!textarea) return;
|
|
302
|
+
const start = textarea.selectionStart ?? 0;
|
|
303
|
+
const end = textarea.selectionEnd ?? 0;
|
|
304
|
+
const value = this.value;
|
|
305
|
+
callback(textarea, start, end, value);
|
|
306
|
+
textarea.focus();
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Expand empty selection to current word boundaries
|
|
310
|
+
*
|
|
311
|
+
* @param start - selection start
|
|
312
|
+
* @param end - selection end
|
|
313
|
+
* @param value - full textarea value
|
|
314
|
+
*/
|
|
315
|
+
expandSelectionToWord(start, end, value) {
|
|
316
|
+
if (start !== end) return { start, end };
|
|
317
|
+
const charBefore = start > 0 ? value[start - 1] : "";
|
|
318
|
+
const charAfter = start < value.length ? value[start] : "";
|
|
319
|
+
if (charBefore === "(" && charAfter === ")" || charBefore === "[" && charAfter === "]") {
|
|
320
|
+
return { start, end };
|
|
321
|
+
}
|
|
322
|
+
let _start = start;
|
|
323
|
+
let _end = end;
|
|
324
|
+
const isWord = (ch) => /[\p{L}\p{N}_-]/u.test(ch);
|
|
325
|
+
while (_start > 0 && isWord(value[_start - 1])) _start--;
|
|
326
|
+
while (_end < value.length && isWord(value[_end])) _end++;
|
|
327
|
+
return { start: _start, end: _end };
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Apply or toggle heading formatting for the current line(s).
|
|
331
|
+
*
|
|
332
|
+
* @param level - 0 to remove heading, 1-3 for heading levels
|
|
333
|
+
*/
|
|
334
|
+
applyHeading(level) {
|
|
335
|
+
if (this.isReadonly || this.isDisabled) return;
|
|
336
|
+
this.saveUndoStateBeforeChange();
|
|
337
|
+
const desiredPrefix = level > 0 ? `${"#".repeat(level)} ` : "";
|
|
338
|
+
this.withSelection((textarea, start, end, value) => {
|
|
339
|
+
const lineStart = value.lastIndexOf("\n", start - 1) + 1;
|
|
340
|
+
let lineEnd = value.indexOf("\n", end);
|
|
341
|
+
if (lineEnd === -1) lineEnd = value.length;
|
|
342
|
+
const before = value.slice(0, lineStart);
|
|
343
|
+
const selected = value.slice(lineStart, lineEnd);
|
|
344
|
+
const after = value.slice(lineEnd);
|
|
345
|
+
const lines = selected.split("\n");
|
|
346
|
+
const headingRegex = /^(#{1,6})\s+/;
|
|
347
|
+
const allAlreadyLevel = lines.every((l) => l.startsWith(desiredPrefix));
|
|
348
|
+
let transformed;
|
|
349
|
+
if (level === 0) {
|
|
350
|
+
transformed = lines.map((l) => l.replace(headingRegex, "")).join("\n");
|
|
351
|
+
} else {
|
|
352
|
+
transformed = lines.map((l) => {
|
|
353
|
+
const withoutAny = l.replace(headingRegex, "");
|
|
354
|
+
if (allAlreadyLevel) {
|
|
355
|
+
return withoutAny;
|
|
356
|
+
}
|
|
357
|
+
return desiredPrefix + withoutAny;
|
|
358
|
+
}).join("\n");
|
|
359
|
+
}
|
|
360
|
+
this.value = before + transformed + after;
|
|
361
|
+
textarea.value = before + transformed + after;
|
|
362
|
+
let cursorPosition = before.length;
|
|
363
|
+
if (level > 0 && !allAlreadyLevel) {
|
|
364
|
+
const firstLine = lines[0] || "";
|
|
365
|
+
const contentAfterHeading = firstLine.replace(headingRegex, "");
|
|
366
|
+
if (contentAfterHeading.trim()) {
|
|
367
|
+
const firstLineEnd = transformed.indexOf("\n");
|
|
368
|
+
cursorPosition = before.length + (firstLineEnd === -1 ? transformed.length : firstLineEnd);
|
|
369
|
+
} else {
|
|
370
|
+
cursorPosition = before.length + desiredPrefix.length;
|
|
371
|
+
}
|
|
372
|
+
} else if (level === 0 || allAlreadyLevel) {
|
|
373
|
+
const firstLineEnd = transformed.indexOf("\n");
|
|
374
|
+
cursorPosition = before.length + (firstLineEnd === -1 ? transformed.length : firstLineEnd);
|
|
375
|
+
}
|
|
376
|
+
requestAnimationFrame(() => {
|
|
377
|
+
textarea.setSelectionRange(cursorPosition, cursorPosition);
|
|
378
|
+
this.updateActiveFormats();
|
|
379
|
+
});
|
|
380
|
+
this.dispatchChange();
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Apply or toggle list formatting for the current line(s).
|
|
385
|
+
*
|
|
386
|
+
* @param listType - 'none' to remove list, 'unordered' for bullet list, 'ordered' for numbered list
|
|
387
|
+
*/
|
|
388
|
+
applyList(listType) {
|
|
389
|
+
if (this.isReadonly || this.isDisabled) return;
|
|
390
|
+
this.saveUndoStateBeforeChange();
|
|
391
|
+
this.withSelection((textarea, start, end, value) => {
|
|
392
|
+
const lineStart = value.lastIndexOf("\n", start - 1) + 1;
|
|
393
|
+
let lineEnd = value.indexOf("\n", end);
|
|
394
|
+
if (lineEnd === -1) lineEnd = value.length;
|
|
395
|
+
const before = value.slice(0, lineStart);
|
|
396
|
+
const selected = value.slice(lineStart, lineEnd);
|
|
397
|
+
const after = value.slice(lineEnd);
|
|
398
|
+
const lines = selected.split("\n");
|
|
399
|
+
const unorderedRegex = /^(\s*)[-*+]\s+/;
|
|
400
|
+
const orderedRegex = /^(\s*)\d+\.\s+/;
|
|
401
|
+
let transformed;
|
|
402
|
+
if (listType === "none") {
|
|
403
|
+
transformed = lines.map((l) => {
|
|
404
|
+
if (unorderedRegex.test(l)) {
|
|
405
|
+
return l.replace(unorderedRegex, "$1");
|
|
406
|
+
}
|
|
407
|
+
if (orderedRegex.test(l)) {
|
|
408
|
+
return l.replace(orderedRegex, "$1");
|
|
409
|
+
}
|
|
410
|
+
return l;
|
|
411
|
+
}).join("\n");
|
|
412
|
+
} else if (listType === "unordered") {
|
|
413
|
+
const allAlreadyUnordered = lines.every(
|
|
414
|
+
(l) => l.trim() === "" || unorderedRegex.test(l)
|
|
415
|
+
);
|
|
416
|
+
if (allAlreadyUnordered && lines.some((l) => unorderedRegex.test(l))) {
|
|
417
|
+
transformed = lines.map((l) => {
|
|
418
|
+
if (unorderedRegex.test(l)) {
|
|
419
|
+
return l.replace(unorderedRegex, "$1");
|
|
420
|
+
}
|
|
421
|
+
return l;
|
|
422
|
+
}).join("\n");
|
|
423
|
+
} else {
|
|
424
|
+
transformed = lines.map((l) => {
|
|
425
|
+
if (l.trim() === "") return l;
|
|
426
|
+
let cleaned = l;
|
|
427
|
+
if (unorderedRegex.test(l)) {
|
|
428
|
+
cleaned = l.replace(unorderedRegex, "$1");
|
|
429
|
+
} else if (orderedRegex.test(l)) {
|
|
430
|
+
cleaned = l.replace(orderedRegex, "$1");
|
|
431
|
+
}
|
|
432
|
+
const indentMatch = cleaned.match(/^(\s*)/);
|
|
433
|
+
const indent = indentMatch ? indentMatch[1] : "";
|
|
434
|
+
const content = cleaned.replace(/^\s*/, "");
|
|
435
|
+
return `${indent}- ${content}`;
|
|
436
|
+
}).join("\n");
|
|
437
|
+
}
|
|
438
|
+
} else if (listType === "ordered") {
|
|
439
|
+
const allAlreadyOrdered = lines.every(
|
|
440
|
+
(l) => l.trim() === "" || orderedRegex.test(l)
|
|
441
|
+
);
|
|
442
|
+
if (allAlreadyOrdered && lines.some((l) => orderedRegex.test(l))) {
|
|
443
|
+
transformed = lines.map((l) => {
|
|
444
|
+
if (orderedRegex.test(l)) {
|
|
445
|
+
return l.replace(orderedRegex, "$1");
|
|
446
|
+
}
|
|
447
|
+
return l;
|
|
448
|
+
}).join("\n");
|
|
449
|
+
} else {
|
|
450
|
+
let counter = 1;
|
|
451
|
+
const beforeLines = before.split("\n");
|
|
452
|
+
for (let i = beforeLines.length - 1; i >= 0; i--) {
|
|
453
|
+
const line = beforeLines[i];
|
|
454
|
+
const orderedMatch = line.match(/^(\s*)(\d+)\.\s*(.*)$/);
|
|
455
|
+
if (orderedMatch) {
|
|
456
|
+
counter = parseInt(orderedMatch[2], 10) + 1;
|
|
457
|
+
break;
|
|
458
|
+
} else if (line.trim() !== "" && !line.match(/^\s*[-*+]\s+/)) {
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
transformed = lines.map((l) => {
|
|
463
|
+
if (l.trim() === "") return l;
|
|
464
|
+
let cleaned = l;
|
|
465
|
+
if (unorderedRegex.test(l)) {
|
|
466
|
+
cleaned = l.replace(unorderedRegex, "$1");
|
|
467
|
+
} else if (orderedRegex.test(l)) {
|
|
468
|
+
cleaned = l.replace(orderedRegex, "$1");
|
|
469
|
+
}
|
|
470
|
+
const indentMatch = cleaned.match(/^(\s*)/);
|
|
471
|
+
const indent = indentMatch ? indentMatch[1] : "";
|
|
472
|
+
const content = cleaned.replace(/^\s*/, "");
|
|
473
|
+
return `${indent}${counter++}. ${content}`;
|
|
474
|
+
}).join("\n");
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
let finalValue = before + transformed + after;
|
|
478
|
+
if (listType === "ordered") {
|
|
479
|
+
const allAlreadyOrdered = lines.every(
|
|
480
|
+
(l) => l.trim() === "" || orderedRegex.test(l)
|
|
481
|
+
);
|
|
482
|
+
if (!allAlreadyOrdered || !lines.some((l) => orderedRegex.test(l))) {
|
|
483
|
+
const transformedLines = transformed.split("\n");
|
|
484
|
+
const lastTransformedLine = transformedLines[transformedLines.length - 1];
|
|
485
|
+
const indentMatch = lastTransformedLine.match(/^(\s*)/);
|
|
486
|
+
const indent = indentMatch ? indentMatch[1] : "";
|
|
487
|
+
const endPosition = before.length + transformed.length;
|
|
488
|
+
finalValue = this.renumberOrderedListItems(
|
|
489
|
+
finalValue,
|
|
490
|
+
endPosition,
|
|
491
|
+
indent
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (listType === "none") {
|
|
496
|
+
const removedOrderedItems = lines.some((l) => orderedRegex.test(l));
|
|
497
|
+
if (removedOrderedItems) {
|
|
498
|
+
finalValue = this.renumberSubsequentOrderedLists(
|
|
499
|
+
finalValue,
|
|
500
|
+
before.length + transformed.length
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
this.value = finalValue;
|
|
505
|
+
textarea.value = finalValue;
|
|
506
|
+
const firstLineEnd = transformed.indexOf("\n");
|
|
507
|
+
const cursorPosition = before.length + (firstLineEnd === -1 ? transformed.length : firstLineEnd);
|
|
508
|
+
requestAnimationFrame(() => {
|
|
509
|
+
textarea.setSelectionRange(cursorPosition, cursorPosition);
|
|
510
|
+
this.updateActiveFormats();
|
|
511
|
+
});
|
|
512
|
+
this.dispatchChange();
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Get the current active list type based on activeFormats.
|
|
517
|
+
*/
|
|
518
|
+
getActiveListType() {
|
|
519
|
+
if (this.activeFormats.unorderedList) return "unordered";
|
|
520
|
+
if (this.activeFormats.orderedList) return "ordered";
|
|
521
|
+
return "none";
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Toggle inline formatting by wrapping/unwrapping selection or current word.
|
|
525
|
+
*
|
|
526
|
+
* @param wrapper - the markdown wrapper to toggle, e.g. '**' for bold, '*' for italic
|
|
527
|
+
*/
|
|
528
|
+
toggleWrap(wrapper) {
|
|
529
|
+
if (this.isReadonly || this.isDisabled) return;
|
|
530
|
+
this.saveUndoStateBeforeChange();
|
|
531
|
+
this.withSelection((textarea, start, end, value) => {
|
|
532
|
+
if (this.isSelectionInLinkUrl(start, end, value)) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const { start: s, end: e } = this.expandSelectionToWord(start, end, value);
|
|
536
|
+
let before = value.slice(0, s);
|
|
537
|
+
let selected = value.slice(s, e);
|
|
538
|
+
let after = value.slice(e);
|
|
539
|
+
const hasOuterWrap = before.endsWith(wrapper) && after.startsWith(wrapper);
|
|
540
|
+
if (hasOuterWrap) {
|
|
541
|
+
before = before.slice(0, before.length - wrapper.length);
|
|
542
|
+
after = after.slice(wrapper.length);
|
|
543
|
+
this.value = before + selected + after;
|
|
544
|
+
const selStart2 = before.length;
|
|
545
|
+
const selEnd2 = selStart2 + selected.length;
|
|
546
|
+
requestAnimationFrame(() => {
|
|
547
|
+
textarea.setSelectionRange(selStart2, selEnd2);
|
|
548
|
+
this.updateActiveFormats();
|
|
549
|
+
});
|
|
550
|
+
this.dispatchChange();
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const innerWrapped = selected.startsWith(wrapper) && selected.endsWith(wrapper);
|
|
554
|
+
if (innerWrapped) {
|
|
555
|
+
selected = selected.slice(
|
|
556
|
+
wrapper.length,
|
|
557
|
+
selected.length - wrapper.length
|
|
558
|
+
);
|
|
559
|
+
this.value = before + selected + after;
|
|
560
|
+
const selStart2 = before.length;
|
|
561
|
+
const selEnd2 = selStart2 + selected.length;
|
|
562
|
+
requestAnimationFrame(() => {
|
|
563
|
+
textarea.setSelectionRange(selStart2, selEnd2);
|
|
564
|
+
this.updateActiveFormats();
|
|
565
|
+
});
|
|
566
|
+
this.dispatchChange();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const wrapped = `${wrapper}${selected || ""}${wrapper}`;
|
|
570
|
+
this.value = before + wrapped + after;
|
|
571
|
+
const selStart = before.length + wrapper.length;
|
|
572
|
+
const selEnd = selStart + (selected ? selected.length : 0);
|
|
573
|
+
requestAnimationFrame(() => {
|
|
574
|
+
textarea.setSelectionRange(selStart, selEnd);
|
|
575
|
+
this.updateActiveFormats();
|
|
576
|
+
});
|
|
577
|
+
this.dispatchChange();
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Toggle preview mode on or off.
|
|
582
|
+
*/
|
|
583
|
+
togglePreview() {
|
|
584
|
+
if (this.isPreview) {
|
|
585
|
+
this.exitPreview();
|
|
586
|
+
} else {
|
|
587
|
+
this.enterPreview();
|
|
588
|
+
}
|
|
589
|
+
this.isPreview = !this.isPreview;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Enter preview mode - save current state and remove keyboard listeners from textarea
|
|
593
|
+
*/
|
|
594
|
+
enterPreview() {
|
|
595
|
+
const textarea = this.textareaEl?.shadowRoot?.querySelector("textarea");
|
|
596
|
+
if (textarea) {
|
|
597
|
+
this.savedSelectionForPreview = {
|
|
598
|
+
start: textarea.selectionStart ?? 0,
|
|
599
|
+
end: textarea.selectionEnd ?? 0
|
|
600
|
+
};
|
|
601
|
+
this.removeKeyboardListeners();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Exit preview mode - restore state and reattach keyboard listeners
|
|
606
|
+
*/
|
|
607
|
+
exitPreview() {
|
|
608
|
+
requestAnimationFrame(() => {
|
|
609
|
+
const textarea = this.textareaEl?.shadowRoot?.querySelector("textarea");
|
|
610
|
+
if (textarea && this.savedSelectionForPreview) {
|
|
611
|
+
textarea.focus();
|
|
612
|
+
textarea.setSelectionRange(
|
|
613
|
+
this.savedSelectionForPreview.start,
|
|
614
|
+
this.savedSelectionForPreview.end
|
|
615
|
+
);
|
|
616
|
+
this.currentSelection = this.savedSelectionForPreview;
|
|
617
|
+
this.savedSelectionForPreview = null;
|
|
618
|
+
this.updateActiveFormats();
|
|
619
|
+
}
|
|
620
|
+
this.addKeyboardListeners();
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Insert or edit a markdown link [text](url).
|
|
625
|
+
*/
|
|
626
|
+
insertLink() {
|
|
627
|
+
if (this.isReadonly || this.isDisabled) return;
|
|
628
|
+
this.saveUndoStateBeforeChange();
|
|
629
|
+
const placeholderText = "link text";
|
|
630
|
+
const placeholderUrl = "";
|
|
631
|
+
this.withSelection((textarea, start, end, value) => {
|
|
632
|
+
if (this.isSelectionInLinkUrl(start, end, value) || this.isSelectionInLinkText(start, end, value)) {
|
|
633
|
+
const leftBracket2 = value.lastIndexOf("[", start);
|
|
634
|
+
const rightParen2 = value.indexOf(")", Math.max(start, end));
|
|
635
|
+
const rightBracket2 = value.indexOf("]", leftBracket2);
|
|
636
|
+
const openParen2 = value.indexOf("(", rightBracket2);
|
|
637
|
+
if (leftBracket2 !== -1 && rightBracket2 !== -1 && openParen2 !== -1 && rightParen2 !== -1) {
|
|
638
|
+
const candidate = value.slice(leftBracket2, rightParen2 + 1);
|
|
639
|
+
const linkRegex2 = /^\[([^\]]+)\]\(([^)]*)\)$/;
|
|
640
|
+
const match = candidate.match(linkRegex2);
|
|
641
|
+
if (match) {
|
|
642
|
+
const textOnly = match[1];
|
|
643
|
+
this.value = value.slice(0, leftBracket2) + textOnly + value.slice(rightParen2 + 1);
|
|
644
|
+
const newCursor = leftBracket2 + textOnly.length;
|
|
645
|
+
requestAnimationFrame(() => {
|
|
646
|
+
textarea.setSelectionRange(newCursor, newCursor);
|
|
647
|
+
this.updateActiveFormats();
|
|
648
|
+
});
|
|
649
|
+
this.dispatchChange();
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const { start: s, end: e } = this.expandSelectionToWord(start, end, value);
|
|
656
|
+
const before = value.slice(0, s);
|
|
657
|
+
const selected = value.slice(s, e);
|
|
658
|
+
const after = value.slice(e);
|
|
659
|
+
const linkRegex = /^\[([^\]]+)\]\(([^)]+)\)$/;
|
|
660
|
+
if (linkRegex.test(selected)) {
|
|
661
|
+
const match = selected.match(linkRegex);
|
|
662
|
+
const textOnly = match?.[1] ?? selected;
|
|
663
|
+
this.value = before + textOnly + after;
|
|
664
|
+
const newStart = before.length;
|
|
665
|
+
const newEnd = newStart + textOnly.length;
|
|
666
|
+
requestAnimationFrame(() => {
|
|
667
|
+
textarea.setSelectionRange(newStart, newEnd);
|
|
668
|
+
this.updateActiveFormats();
|
|
669
|
+
});
|
|
670
|
+
this.dispatchChange();
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const leftBracket = value.lastIndexOf("[", s);
|
|
674
|
+
const rightParen = value.indexOf(")", e);
|
|
675
|
+
const rightBracket = value.indexOf("]", e);
|
|
676
|
+
const openParen = value.indexOf("(", e);
|
|
677
|
+
if (leftBracket !== -1 && rightBracket !== -1 && openParen !== -1 && rightParen !== -1 && leftBracket < rightBracket && rightBracket < openParen && openParen < rightParen) {
|
|
678
|
+
const candidate = value.slice(leftBracket, rightParen + 1);
|
|
679
|
+
if (linkRegex.test(candidate)) {
|
|
680
|
+
const match = candidate.match(linkRegex);
|
|
681
|
+
const textOnly = match?.[1] ?? candidate;
|
|
682
|
+
this.value = value.slice(0, leftBracket) + textOnly + value.slice(rightParen + 1);
|
|
683
|
+
const newCursor = leftBracket + textOnly.length;
|
|
684
|
+
requestAnimationFrame(() => {
|
|
685
|
+
textarea.setSelectionRange(newCursor, newCursor);
|
|
686
|
+
this.updateActiveFormats();
|
|
687
|
+
});
|
|
688
|
+
this.dispatchChange();
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const text = selected || placeholderText;
|
|
693
|
+
const md = `[${text}](${placeholderUrl})`;
|
|
694
|
+
this.value = before + md + after;
|
|
695
|
+
const cursorPosition = before.length + 1 + text.length + 2;
|
|
696
|
+
requestAnimationFrame(() => {
|
|
697
|
+
textarea.focus();
|
|
698
|
+
textarea.setSelectionRange(cursorPosition, cursorPosition);
|
|
699
|
+
this.updateActiveFormats();
|
|
700
|
+
});
|
|
701
|
+
this.dispatchChange();
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Update active formatting states based on current selection or cursor position.
|
|
706
|
+
*/
|
|
707
|
+
updateActiveFormats() {
|
|
708
|
+
const textarea = this.textareaEl?.shadowRoot?.querySelector("textarea");
|
|
709
|
+
if (!textarea || !this.value) return;
|
|
710
|
+
const start = textarea.selectionStart ?? 0;
|
|
711
|
+
const end = textarea.selectionEnd ?? 0;
|
|
712
|
+
this.currentSelection = { start, end };
|
|
713
|
+
const { start: s, end: e } = this.expandSelectionToWord(
|
|
714
|
+
start,
|
|
715
|
+
end,
|
|
716
|
+
this.value
|
|
717
|
+
);
|
|
718
|
+
const selectedText = this.value.slice(s, e);
|
|
719
|
+
const beforeSelection = this.value.slice(0, s);
|
|
720
|
+
const afterSelection = this.value.slice(e);
|
|
721
|
+
const hasBoldWrap = beforeSelection.endsWith("**") && afterSelection.startsWith("**") || selectedText.startsWith("**") && selectedText.endsWith("**");
|
|
722
|
+
const hasItalicWrap = beforeSelection.endsWith("*") && afterSelection.startsWith("*") && !beforeSelection.endsWith("**") || selectedText.startsWith("*") && selectedText.endsWith("*") && !selectedText.startsWith("**");
|
|
723
|
+
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/;
|
|
724
|
+
const hasLink = linkRegex.test(selectedText) || this.isWithinLink(start, this.value);
|
|
725
|
+
const lineStart = this.value.lastIndexOf("\n", start - 1) + 1;
|
|
726
|
+
const lineEnd = this.value.indexOf("\n", start);
|
|
727
|
+
const currentLine = this.value.slice(
|
|
728
|
+
lineStart,
|
|
729
|
+
lineEnd === -1 ? this.value.length : lineEnd
|
|
730
|
+
);
|
|
731
|
+
const headingMatch = currentLine.match(/^(#{1,6})\s/);
|
|
732
|
+
const headingLevel = headingMatch ? headingMatch[1].length : 0;
|
|
733
|
+
const unorderedListMatch = currentLine.match(/^\s*[-*+]\s/);
|
|
734
|
+
const orderedListMatch = currentLine.match(/^\s*\d+\.\s/);
|
|
735
|
+
const hasUnorderedList = !!unorderedListMatch;
|
|
736
|
+
const hasOrderedList = !!orderedListMatch;
|
|
737
|
+
const colorRegex = /<span style="color: ([^"]+)">/;
|
|
738
|
+
const hasColorWrap = !!(beforeSelection.match(colorRegex) && afterSelection.includes("</span>") || selectedText.match(/^<span style="color: ([^"]+)">(.*)<\/span>$/s));
|
|
739
|
+
let activeColor = this.defaultColor;
|
|
740
|
+
if (hasColorWrap) {
|
|
741
|
+
const beforeColorMatch = beforeSelection.match(
|
|
742
|
+
/<span style="color: ([^"]+)">([^<]*)$/
|
|
743
|
+
);
|
|
744
|
+
const selectedColorMatch = selectedText.match(
|
|
745
|
+
/^<span style="color: ([^"]+)">/
|
|
746
|
+
);
|
|
747
|
+
activeColor = beforeColorMatch?.[1] || selectedColorMatch?.[1] || "";
|
|
748
|
+
}
|
|
749
|
+
this.activeFormats = {
|
|
750
|
+
bold: hasBoldWrap,
|
|
751
|
+
italic: hasItalicWrap,
|
|
752
|
+
link: hasLink,
|
|
753
|
+
h1: headingLevel === 1,
|
|
754
|
+
h2: headingLevel === 2,
|
|
755
|
+
h3: headingLevel === 3,
|
|
756
|
+
color: hasColorWrap,
|
|
757
|
+
activeColor,
|
|
758
|
+
unorderedList: hasUnorderedList,
|
|
759
|
+
orderedList: hasOrderedList
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Check if a given position is within a markdown link [text](url).
|
|
764
|
+
* @param position - cursor position
|
|
765
|
+
* @param value - full textarea value
|
|
766
|
+
*/
|
|
767
|
+
isWithinLink(position, value) {
|
|
768
|
+
const leftBracket = value.lastIndexOf("[", position);
|
|
769
|
+
const rightParen = value.indexOf(")", position);
|
|
770
|
+
if (leftBracket === -1 || rightParen === -1) return false;
|
|
771
|
+
const candidate = value.slice(leftBracket, rightParen + 1);
|
|
772
|
+
return /^\[([^\]]+)\]\(([^)]+)\)$/.test(candidate);
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* Check if selection is within the text part [] of a markdown link
|
|
776
|
+
* @param start - selection start position
|
|
777
|
+
* @param end - selection end position
|
|
778
|
+
* @param value - full textarea value
|
|
779
|
+
*/
|
|
780
|
+
isSelectionInLinkText(start, end, value) {
|
|
781
|
+
const leftBracket = value.lastIndexOf("[", start);
|
|
782
|
+
const rightParen = value.indexOf(")", Math.max(start, end));
|
|
783
|
+
if (leftBracket === -1 || rightParen === -1) return false;
|
|
784
|
+
const rightBracket = value.indexOf("]", leftBracket);
|
|
785
|
+
const openParen = value.indexOf("(", rightBracket);
|
|
786
|
+
if (rightBracket === -1 || openParen === -1) return false;
|
|
787
|
+
if (!(leftBracket < rightBracket && rightBracket < openParen && openParen < rightParen))
|
|
788
|
+
return false;
|
|
789
|
+
const candidate = value.slice(leftBracket, rightParen + 1);
|
|
790
|
+
if (!/^\[([^\]]+)\]\(([^)]*)\)$/.test(candidate)) return false;
|
|
791
|
+
return start >= leftBracket + 1 && end <= rightBracket;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Check if selection is within the URL part () of a markdown link
|
|
795
|
+
* @param start - selection start position
|
|
796
|
+
* @param end - selection end position
|
|
797
|
+
* @param value - full textarea value
|
|
798
|
+
*/
|
|
799
|
+
isSelectionInLinkUrl(start, end, value) {
|
|
800
|
+
const leftBracket = value.lastIndexOf("[", start);
|
|
801
|
+
const rightParen = value.indexOf(")", Math.max(start, end));
|
|
802
|
+
if (leftBracket === -1 || rightParen === -1) return false;
|
|
803
|
+
const rightBracket = value.indexOf("]", leftBracket);
|
|
804
|
+
const openParen = value.indexOf("(", rightBracket);
|
|
805
|
+
if (rightBracket === -1 || openParen === -1) return false;
|
|
806
|
+
if (!(leftBracket < rightBracket && rightBracket < openParen && openParen < rightParen))
|
|
807
|
+
return false;
|
|
808
|
+
const candidate = value.slice(leftBracket, rightParen + 1);
|
|
809
|
+
if (!/^\[([^\]]+)\]\(([^)]*)\)$/.test(candidate)) return false;
|
|
810
|
+
return start >= openParen + 1 && end <= rightParen;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Add keyboard event listeners to the textarea for undo/redo functionality.
|
|
814
|
+
*/
|
|
815
|
+
addKeyboardListeners() {
|
|
816
|
+
const tryAddListener = (attempts = 0) => {
|
|
817
|
+
const textarea = this.textareaEl?.shadowRoot?.querySelector("textarea");
|
|
818
|
+
if (textarea) {
|
|
819
|
+
textarea.removeEventListener("keydown", this.handleKeyDown);
|
|
820
|
+
textarea.addEventListener("keydown", this.handleKeyDown);
|
|
821
|
+
} else if (attempts < 15) {
|
|
822
|
+
requestAnimationFrame(() => tryAddListener(attempts + 1));
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
tryAddListener();
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Remove keyboard event listeners from the textarea to prevent memory leaks.
|
|
829
|
+
*/
|
|
830
|
+
removeKeyboardListeners() {
|
|
831
|
+
const textarea = this.textareaEl?.shadowRoot?.querySelector("textarea");
|
|
832
|
+
if (textarea) {
|
|
833
|
+
textarea.removeEventListener("keydown", this.handleKeyDown);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Toggle color formatting by wrapping/unwrapping selection or current word.
|
|
838
|
+
*
|
|
839
|
+
* @param color - the hex color code to apply, e.g. '#FF0000' for red
|
|
840
|
+
*/
|
|
841
|
+
toggleColorWrap(color) {
|
|
842
|
+
if (this.isReadonly || this.isDisabled) return;
|
|
843
|
+
this.saveUndoStateBeforeChange();
|
|
844
|
+
this.withSelection((textarea, start, end, value) => {
|
|
845
|
+
if (this.isSelectionInLinkUrl(start, end, value)) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
const { start: s, end: e } = this.expandSelectionToWord(start, end, value);
|
|
849
|
+
let before = value.slice(0, s);
|
|
850
|
+
let selected = value.slice(s, e);
|
|
851
|
+
let after = value.slice(e);
|
|
852
|
+
const colorTagClose = "</span>";
|
|
853
|
+
const anyColorSpanRegex = /<span style="color: ([^"]+)">$/;
|
|
854
|
+
const beforeColorMatch = before.match(anyColorSpanRegex);
|
|
855
|
+
const hasOuterWrap = beforeColorMatch && after.startsWith(colorTagClose);
|
|
856
|
+
if (hasOuterWrap) {
|
|
857
|
+
const existingColor = beforeColorMatch[1];
|
|
858
|
+
let selStart2;
|
|
859
|
+
let selEnd2;
|
|
860
|
+
if (existingColor === color) {
|
|
861
|
+
before = before.slice(0, before.length - beforeColorMatch[0].length);
|
|
862
|
+
after = after.slice(colorTagClose.length);
|
|
863
|
+
this.value = before + selected + after;
|
|
864
|
+
selStart2 = before.length;
|
|
865
|
+
selEnd2 = selStart2 + selected.length;
|
|
866
|
+
} else {
|
|
867
|
+
const newColorTagOpen2 = `<span style="color: ${color}">`;
|
|
868
|
+
before = before.slice(0, before.length - beforeColorMatch[0].length) + newColorTagOpen2;
|
|
869
|
+
this.value = before + selected + after;
|
|
870
|
+
selStart2 = before.length;
|
|
871
|
+
selEnd2 = selStart2 + selected.length;
|
|
872
|
+
}
|
|
873
|
+
requestAnimationFrame(() => {
|
|
874
|
+
textarea.setSelectionRange(selStart2, selEnd2);
|
|
875
|
+
this.updateActiveFormats();
|
|
876
|
+
});
|
|
877
|
+
this.dispatchChange();
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const colorRegex = /^<span style="color: ([^"]+)">(.*)<\/span>$/s;
|
|
881
|
+
const innerMatch = selected.match(colorRegex);
|
|
882
|
+
if (innerMatch) {
|
|
883
|
+
const existingColor = innerMatch[1];
|
|
884
|
+
const innerText = innerMatch[2];
|
|
885
|
+
if (existingColor === color) {
|
|
886
|
+
selected = innerText;
|
|
887
|
+
} else {
|
|
888
|
+
selected = `<span style="color: ${color}">${innerText}</span>`;
|
|
889
|
+
}
|
|
890
|
+
this.value = before + selected + after;
|
|
891
|
+
const selStart2 = before.length;
|
|
892
|
+
const selEnd2 = selStart2 + (existingColor === color ? innerText.length : innerText.length);
|
|
893
|
+
requestAnimationFrame(() => {
|
|
894
|
+
textarea.setSelectionRange(selStart2, selEnd2);
|
|
895
|
+
this.updateActiveFormats();
|
|
896
|
+
});
|
|
897
|
+
this.dispatchChange();
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
const newColorTagOpen = `<span style="color: ${color}">`;
|
|
901
|
+
const wrapped = `${newColorTagOpen}${selected || ""}${colorTagClose}`;
|
|
902
|
+
this.value = before + wrapped + after;
|
|
903
|
+
const selStart = before.length + newColorTagOpen.length;
|
|
904
|
+
const selEnd = selStart + (selected ? selected.length : 4);
|
|
905
|
+
requestAnimationFrame(() => {
|
|
906
|
+
textarea.setSelectionRange(selStart, selEnd);
|
|
907
|
+
this.updateActiveFormats();
|
|
908
|
+
});
|
|
909
|
+
this.dispatchChange();
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Handle color selection from the dropdown.
|
|
914
|
+
*
|
|
915
|
+
* @param color - the selected hex color code
|
|
916
|
+
*/
|
|
917
|
+
selectColor(color) {
|
|
918
|
+
if (this.savedSelection) {
|
|
919
|
+
const textarea = this.textareaEl?.shadowRoot?.querySelector("textarea");
|
|
920
|
+
if (textarea) {
|
|
921
|
+
textarea.focus();
|
|
922
|
+
textarea.setSelectionRange(
|
|
923
|
+
this.savedSelection.start,
|
|
924
|
+
this.savedSelection.end
|
|
925
|
+
);
|
|
926
|
+
this.currentSelection = this.savedSelection;
|
|
927
|
+
this.savedSelection = null;
|
|
928
|
+
this.toggleColorWrap(color);
|
|
929
|
+
}
|
|
930
|
+
} else {
|
|
931
|
+
this.toggleColorWrap(color);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Clear color formatting from the current selection or word.
|
|
936
|
+
*/
|
|
937
|
+
clearColor() {
|
|
938
|
+
if (this.isReadonly || this.isDisabled) return;
|
|
939
|
+
this.saveUndoStateBeforeChange();
|
|
940
|
+
if (this.savedSelection) {
|
|
941
|
+
const textarea = this.textareaEl?.shadowRoot?.querySelector("textarea");
|
|
942
|
+
if (textarea) {
|
|
943
|
+
textarea.focus();
|
|
944
|
+
textarea.setSelectionRange(
|
|
945
|
+
this.savedSelection.start,
|
|
946
|
+
this.savedSelection.end
|
|
947
|
+
);
|
|
948
|
+
this.currentSelection = this.savedSelection;
|
|
949
|
+
this.savedSelection = null;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
this.withSelection((ta, start, end, value) => {
|
|
953
|
+
if (this.isSelectionInLinkUrl(start, end, value)) {
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const { start: s, end: e } = this.expandSelectionToWord(start, end, value);
|
|
957
|
+
const before = value.slice(0, s);
|
|
958
|
+
let selected = value.slice(s, e);
|
|
959
|
+
const after = value.slice(e);
|
|
960
|
+
const colorRegex = /<span style="color: ([^"]+)">(.*?)<\/span>/gs;
|
|
961
|
+
selected = selected.replace(colorRegex, "$2");
|
|
962
|
+
const fullColorRegex = /<span style="color: ([^"]+)">(.*?)<\/span>/g;
|
|
963
|
+
let match;
|
|
964
|
+
let foundMatch = false;
|
|
965
|
+
const searchText = value.slice(
|
|
966
|
+
Math.max(0, s - 100),
|
|
967
|
+
Math.min(value.length, e + 100)
|
|
968
|
+
);
|
|
969
|
+
const searchOffset = Math.max(0, s - 100);
|
|
970
|
+
match = fullColorRegex.exec(searchText);
|
|
971
|
+
while (match !== null) {
|
|
972
|
+
const matchStart = searchOffset + match.index;
|
|
973
|
+
const matchEnd = searchOffset + match.index + match[0].length;
|
|
974
|
+
const spanOpenTag = `<span style="color: ${match[1]}">`;
|
|
975
|
+
const contentStart = searchOffset + match.index + spanOpenTag.length;
|
|
976
|
+
const contentEnd = matchEnd - 7;
|
|
977
|
+
if (contentStart <= s && e <= contentEnd) {
|
|
978
|
+
const beforeContent = value.slice(contentStart, s);
|
|
979
|
+
const afterContent = value.slice(e, contentEnd);
|
|
980
|
+
const newContent = beforeContent + selected + afterContent;
|
|
981
|
+
this.value = value.slice(0, matchStart) + newContent + value.slice(matchEnd);
|
|
982
|
+
const selStart = matchStart + beforeContent.length;
|
|
983
|
+
const selEnd = selStart + selected.length;
|
|
984
|
+
requestAnimationFrame(() => {
|
|
985
|
+
ta.setSelectionRange(selStart, selEnd);
|
|
986
|
+
this.updateActiveFormats();
|
|
987
|
+
});
|
|
988
|
+
this.dispatchChange();
|
|
989
|
+
foundMatch = true;
|
|
990
|
+
break;
|
|
991
|
+
}
|
|
992
|
+
match = fullColorRegex.exec(searchText);
|
|
993
|
+
}
|
|
994
|
+
if (!foundMatch) {
|
|
995
|
+
this.value = before + selected + after;
|
|
996
|
+
const selStart = before.length;
|
|
997
|
+
const selEnd = selStart + selected.length;
|
|
998
|
+
requestAnimationFrame(() => {
|
|
999
|
+
ta.setSelectionRange(selStart, selEnd);
|
|
1000
|
+
this.updateActiveFormats();
|
|
1001
|
+
});
|
|
1002
|
+
this.dispatchChange();
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Determine the current active heading level (1-3) based on activeFormats.
|
|
1008
|
+
*/
|
|
1009
|
+
getActiveHeadingLevel() {
|
|
1010
|
+
if (this.activeFormats.h1) return 1;
|
|
1011
|
+
if (this.activeFormats.h2) return 2;
|
|
1012
|
+
if (this.activeFormats.h3) return 3;
|
|
1013
|
+
return 0;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Save the initial state to the undo stack.
|
|
1017
|
+
*/
|
|
1018
|
+
saveInitialUndoState() {
|
|
1019
|
+
const textarea = this.textareaEl?.shadowRoot?.querySelector("textarea");
|
|
1020
|
+
const initialSelection = textarea ? { start: textarea.selectionStart ?? 0, end: textarea.selectionEnd ?? 0 } : { start: 0, end: 0 };
|
|
1021
|
+
this.undoStack = [{ value: this.value, selection: initialSelection }];
|
|
1022
|
+
this.redoStack = [];
|
|
1023
|
+
this.lastSavedValue = this.value;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Save the current state to the undo stack before making a change.
|
|
1027
|
+
*
|
|
1028
|
+
*/
|
|
1029
|
+
saveUndoStateBeforeChange() {
|
|
1030
|
+
if (this.isUndoRedoAction) return;
|
|
1031
|
+
const textarea = this.textareaEl?.shadowRoot?.querySelector("textarea");
|
|
1032
|
+
const selection = textarea ? { start: textarea.selectionStart ?? 0, end: textarea.selectionEnd ?? 0 } : { start: 0, end: 0 };
|
|
1033
|
+
this.undoStack.push({
|
|
1034
|
+
value: this.value,
|
|
1035
|
+
selection
|
|
1036
|
+
});
|
|
1037
|
+
if (this.undoStack.length > this.MAX_UNDO_STACK_SIZE) {
|
|
1038
|
+
this.undoStack.shift();
|
|
1039
|
+
}
|
|
1040
|
+
this.redoStack = [];
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Save the current state to the undo stack if the value has changed.
|
|
1044
|
+
*/
|
|
1045
|
+
saveUndoState() {
|
|
1046
|
+
if (this.isUndoRedoAction) return;
|
|
1047
|
+
if (this.value !== this.lastSavedValue) {
|
|
1048
|
+
this.undoStack.push({
|
|
1049
|
+
value: this.lastSavedValue,
|
|
1050
|
+
selection: this.currentSelection
|
|
1051
|
+
});
|
|
1052
|
+
if (this.undoStack.length > this.MAX_UNDO_STACK_SIZE) {
|
|
1053
|
+
this.undoStack.shift();
|
|
1054
|
+
}
|
|
1055
|
+
this.redoStack = [];
|
|
1056
|
+
this.lastSavedValue = this.value;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Schedule an undo state save after a short delay.
|
|
1061
|
+
*/
|
|
1062
|
+
scheduleUndoStateSave() {
|
|
1063
|
+
if (this.undoTimeout) {
|
|
1064
|
+
clearTimeout(this.undoTimeout);
|
|
1065
|
+
}
|
|
1066
|
+
this.undoTimeout = window.setTimeout(() => {
|
|
1067
|
+
this.saveUndoState();
|
|
1068
|
+
this.undoTimeout = null;
|
|
1069
|
+
}, this.UNDO_SAVE_DELAY);
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Handle list continuation when Enter is pressed within a list item.
|
|
1073
|
+
* Works against the textarea value/selection.
|
|
1074
|
+
* Returns true if handled.
|
|
1075
|
+
*/
|
|
1076
|
+
handleListContinuation() {
|
|
1077
|
+
const textarea = this.textareaEl?.shadowRoot?.querySelector(
|
|
1078
|
+
"textarea"
|
|
1079
|
+
);
|
|
1080
|
+
if (!textarea) return false;
|
|
1081
|
+
const start = textarea.selectionStart ?? 0;
|
|
1082
|
+
const end = textarea.selectionEnd ?? 0;
|
|
1083
|
+
if (start !== end) return false;
|
|
1084
|
+
const value = this.value;
|
|
1085
|
+
const lineStart = value.lastIndexOf("\n", start - 1) + 1;
|
|
1086
|
+
let lineEnd = value.indexOf("\n", start);
|
|
1087
|
+
if (lineEnd === -1) lineEnd = value.length;
|
|
1088
|
+
const currentLine = value.slice(lineStart, lineEnd);
|
|
1089
|
+
const unorderedMatch = currentLine.match(/^(\s*)([-*+])\s*(.*)$/);
|
|
1090
|
+
const orderedMatch = currentLine.match(/^(\s*)(\d+)\.\s*(.*)$/);
|
|
1091
|
+
if (!unorderedMatch && !orderedMatch) {
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
this.saveUndoStateBeforeChange();
|
|
1095
|
+
if (unorderedMatch) {
|
|
1096
|
+
const indent = unorderedMatch[1] ?? "";
|
|
1097
|
+
const marker = unorderedMatch[2] ?? "-";
|
|
1098
|
+
const content = unorderedMatch[3] ?? "";
|
|
1099
|
+
if (content.trim() === "") {
|
|
1100
|
+
const before2 = value.slice(0, lineStart);
|
|
1101
|
+
const after2 = value.slice(lineEnd);
|
|
1102
|
+
const newValue2 = before2 + (lineEnd === value.length ? "" : "") + after2;
|
|
1103
|
+
this.value = newValue2;
|
|
1104
|
+
textarea.value = newValue2;
|
|
1105
|
+
const newCursor2 = before2.length;
|
|
1106
|
+
requestAnimationFrame(() => {
|
|
1107
|
+
textarea.setSelectionRange(newCursor2, newCursor2);
|
|
1108
|
+
this.updateActiveFormats();
|
|
1109
|
+
});
|
|
1110
|
+
this.dispatchChange();
|
|
1111
|
+
return true;
|
|
1112
|
+
}
|
|
1113
|
+
const before = value.slice(0, start);
|
|
1114
|
+
const after = value.slice(start);
|
|
1115
|
+
const prefix = `
|
|
1116
|
+
${indent}${marker} `;
|
|
1117
|
+
const newValue = before + prefix + after;
|
|
1118
|
+
this.value = newValue;
|
|
1119
|
+
textarea.value = newValue;
|
|
1120
|
+
const newCursor = start + prefix.length;
|
|
1121
|
+
requestAnimationFrame(() => {
|
|
1122
|
+
textarea.setSelectionRange(newCursor, newCursor);
|
|
1123
|
+
this.updateActiveFormats();
|
|
1124
|
+
});
|
|
1125
|
+
this.dispatchChange();
|
|
1126
|
+
return true;
|
|
1127
|
+
}
|
|
1128
|
+
if (orderedMatch) {
|
|
1129
|
+
const indent = orderedMatch[1] ?? "";
|
|
1130
|
+
const numberStr = orderedMatch[2] ?? "1";
|
|
1131
|
+
const content = orderedMatch[3] ?? "";
|
|
1132
|
+
if (content.trim() === "") {
|
|
1133
|
+
const before2 = value.slice(0, lineStart);
|
|
1134
|
+
const after2 = value.slice(lineEnd);
|
|
1135
|
+
const newValue2 = before2 + (lineEnd === value.length ? "" : "") + after2;
|
|
1136
|
+
this.value = newValue2;
|
|
1137
|
+
textarea.value = newValue2;
|
|
1138
|
+
const newCursor2 = before2.length;
|
|
1139
|
+
requestAnimationFrame(() => {
|
|
1140
|
+
textarea.setSelectionRange(newCursor2, newCursor2);
|
|
1141
|
+
this.updateActiveFormats();
|
|
1142
|
+
});
|
|
1143
|
+
this.dispatchChange();
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
let nextNumber = parseInt(numberStr, 10) + 1;
|
|
1147
|
+
const beforeText = value.slice(0, lineStart);
|
|
1148
|
+
const beforeLines = beforeText.split("\n");
|
|
1149
|
+
let lastNumberAtThisLevel = 0;
|
|
1150
|
+
for (let i = beforeLines.length - 1; i >= 0; i--) {
|
|
1151
|
+
const line = beforeLines[i];
|
|
1152
|
+
const match = line.match(/^(\s*)(\d+)\.\s*(.*)$/);
|
|
1153
|
+
if (match && match[1] === indent) {
|
|
1154
|
+
lastNumberAtThisLevel = parseInt(match[2], 10);
|
|
1155
|
+
break;
|
|
1156
|
+
} else if (line.trim() !== "" && !line.match(/^\s*[-*+]\s+/) && !match) {
|
|
1157
|
+
break;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
if (lastNumberAtThisLevel > 0) {
|
|
1161
|
+
nextNumber = lastNumberAtThisLevel + 1;
|
|
1162
|
+
}
|
|
1163
|
+
const before = value.slice(0, start);
|
|
1164
|
+
const after = value.slice(start);
|
|
1165
|
+
const prefix = `
|
|
1166
|
+
${indent}${nextNumber}. `;
|
|
1167
|
+
const newValue = before + prefix + after;
|
|
1168
|
+
const renumberedValue = this.renumberOrderedListItems(
|
|
1169
|
+
newValue,
|
|
1170
|
+
start + prefix.length,
|
|
1171
|
+
indent
|
|
1172
|
+
);
|
|
1173
|
+
this.value = renumberedValue;
|
|
1174
|
+
textarea.value = renumberedValue;
|
|
1175
|
+
const newCursor = start + prefix.length;
|
|
1176
|
+
requestAnimationFrame(() => {
|
|
1177
|
+
textarea.setSelectionRange(newCursor, newCursor);
|
|
1178
|
+
this.updateActiveFormats();
|
|
1179
|
+
});
|
|
1180
|
+
this.dispatchChange();
|
|
1181
|
+
return true;
|
|
1182
|
+
}
|
|
1183
|
+
return false;
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Handle Tab key to indent a list item (increase nesting level).
|
|
1187
|
+
* Returns true if handled.
|
|
1188
|
+
*/
|
|
1189
|
+
handleListIndent() {
|
|
1190
|
+
const textarea = this.textareaEl?.shadowRoot?.querySelector(
|
|
1191
|
+
"textarea"
|
|
1192
|
+
);
|
|
1193
|
+
if (!textarea) return false;
|
|
1194
|
+
const start = textarea.selectionStart ?? 0;
|
|
1195
|
+
const end = textarea.selectionEnd ?? 0;
|
|
1196
|
+
if (start !== end) return false;
|
|
1197
|
+
const value = this.value;
|
|
1198
|
+
const lineStart = value.lastIndexOf("\n", start - 1) + 1;
|
|
1199
|
+
let lineEnd = value.indexOf("\n", start);
|
|
1200
|
+
if (lineEnd === -1) lineEnd = value.length;
|
|
1201
|
+
const currentLine = value.slice(lineStart, lineEnd);
|
|
1202
|
+
const unorderedMatch = currentLine.match(/^(\s*)([-*+])\s*(.*)$/);
|
|
1203
|
+
const orderedMatch = currentLine.match(/^(\s*)(\d+)\.\s*(.*)$/);
|
|
1204
|
+
if (!unorderedMatch && !orderedMatch) {
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
1207
|
+
this.saveUndoStateBeforeChange();
|
|
1208
|
+
const before = value.slice(0, lineStart);
|
|
1209
|
+
const after = value.slice(lineEnd);
|
|
1210
|
+
const indent = " ";
|
|
1211
|
+
let newLine;
|
|
1212
|
+
let newCursorOffset = 0;
|
|
1213
|
+
if (unorderedMatch) {
|
|
1214
|
+
const currentIndent = unorderedMatch[1] ?? "";
|
|
1215
|
+
const marker = unorderedMatch[2] ?? "-";
|
|
1216
|
+
const content = unorderedMatch[3] ?? "";
|
|
1217
|
+
newLine = `${currentIndent}${indent}${marker} ${content}`;
|
|
1218
|
+
newCursorOffset = indent.length;
|
|
1219
|
+
} else if (orderedMatch) {
|
|
1220
|
+
const currentIndent = orderedMatch[1] ?? "";
|
|
1221
|
+
const content = orderedMatch[3] ?? "";
|
|
1222
|
+
newLine = `${currentIndent}${indent}1. ${content}`;
|
|
1223
|
+
newCursorOffset = indent.length;
|
|
1224
|
+
} else {
|
|
1225
|
+
return false;
|
|
1226
|
+
}
|
|
1227
|
+
const newValue = before + newLine + after;
|
|
1228
|
+
this.value = newValue;
|
|
1229
|
+
textarea.value = newValue;
|
|
1230
|
+
const cursorInLine = start - lineStart;
|
|
1231
|
+
const newCursor = lineStart + cursorInLine + newCursorOffset;
|
|
1232
|
+
requestAnimationFrame(() => {
|
|
1233
|
+
textarea.setSelectionRange(newCursor, newCursor);
|
|
1234
|
+
this.updateActiveFormats();
|
|
1235
|
+
});
|
|
1236
|
+
this.dispatchChange();
|
|
1237
|
+
return true;
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Handle Shift+Tab key to outdent a list item (decrease nesting level).
|
|
1241
|
+
* Returns true if handled.
|
|
1242
|
+
*/
|
|
1243
|
+
handleListOutdent() {
|
|
1244
|
+
const textarea = this.textareaEl?.shadowRoot?.querySelector(
|
|
1245
|
+
"textarea"
|
|
1246
|
+
);
|
|
1247
|
+
if (!textarea) return false;
|
|
1248
|
+
const start = textarea.selectionStart ?? 0;
|
|
1249
|
+
const end = textarea.selectionEnd ?? 0;
|
|
1250
|
+
if (start !== end) return false;
|
|
1251
|
+
const value = this.value;
|
|
1252
|
+
const lineStart = value.lastIndexOf("\n", start - 1) + 1;
|
|
1253
|
+
let lineEnd = value.indexOf("\n", start);
|
|
1254
|
+
if (lineEnd === -1) lineEnd = value.length;
|
|
1255
|
+
const currentLine = value.slice(lineStart, lineEnd);
|
|
1256
|
+
const unorderedMatch = currentLine.match(/^(\s*)([-*+])\s*(.*)$/);
|
|
1257
|
+
const orderedMatch = currentLine.match(/^(\s*)(\d+)\.\s*(.*)$/);
|
|
1258
|
+
if (!unorderedMatch && !orderedMatch) {
|
|
1259
|
+
return false;
|
|
1260
|
+
}
|
|
1261
|
+
const currentIndent = (unorderedMatch || orderedMatch)?.[1] ?? "";
|
|
1262
|
+
if (currentIndent.length < 4) {
|
|
1263
|
+
return false;
|
|
1264
|
+
}
|
|
1265
|
+
this.saveUndoStateBeforeChange();
|
|
1266
|
+
const before = value.slice(0, lineStart);
|
|
1267
|
+
const after = value.slice(lineEnd);
|
|
1268
|
+
const outdent = " ";
|
|
1269
|
+
let newLine;
|
|
1270
|
+
let newCursorOffset = 0;
|
|
1271
|
+
if (unorderedMatch) {
|
|
1272
|
+
const marker = unorderedMatch[2] ?? "-";
|
|
1273
|
+
const content = unorderedMatch[3] ?? "";
|
|
1274
|
+
const newIndent = currentIndent.slice(outdent.length);
|
|
1275
|
+
newLine = `${newIndent}${marker} ${content}`;
|
|
1276
|
+
newCursorOffset = -outdent.length;
|
|
1277
|
+
} else if (orderedMatch) {
|
|
1278
|
+
const content = orderedMatch[3] ?? "";
|
|
1279
|
+
const newIndent = currentIndent.slice(outdent.length);
|
|
1280
|
+
let newNumber = 1;
|
|
1281
|
+
const beforeLines = before.split("\n");
|
|
1282
|
+
for (let i = beforeLines.length - 1; i >= 0; i--) {
|
|
1283
|
+
const line = beforeLines[i];
|
|
1284
|
+
const match = line.match(/^(\s*)(\d+)\.\s*(.*)$/);
|
|
1285
|
+
if (match && match[1] === newIndent) {
|
|
1286
|
+
newNumber = parseInt(match[2], 10) + 1;
|
|
1287
|
+
break;
|
|
1288
|
+
} else if (line.trim() !== "" && !line.match(/^\s*[-*+]\s+/) && !match) {
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
newLine = `${newIndent}${newNumber}. ${content}`;
|
|
1293
|
+
newCursorOffset = -outdent.length;
|
|
1294
|
+
} else {
|
|
1295
|
+
return false;
|
|
1296
|
+
}
|
|
1297
|
+
let newValue = before + newLine + after;
|
|
1298
|
+
if (orderedMatch) {
|
|
1299
|
+
const newIndent = currentIndent.slice(outdent.length);
|
|
1300
|
+
newValue = this.renumberOrderedListItems(
|
|
1301
|
+
newValue,
|
|
1302
|
+
lineStart + newLine.length,
|
|
1303
|
+
newIndent
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
this.value = newValue;
|
|
1307
|
+
textarea.value = newValue;
|
|
1308
|
+
const cursorInLine = start - lineStart;
|
|
1309
|
+
const newCursor = Math.max(
|
|
1310
|
+
lineStart,
|
|
1311
|
+
lineStart + cursorInLine + newCursorOffset
|
|
1312
|
+
);
|
|
1313
|
+
requestAnimationFrame(() => {
|
|
1314
|
+
textarea.setSelectionRange(newCursor, newCursor);
|
|
1315
|
+
this.updateActiveFormats();
|
|
1316
|
+
});
|
|
1317
|
+
this.dispatchChange();
|
|
1318
|
+
return true;
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Renumber ordered list items that come after the given position with the same indentation level.
|
|
1322
|
+
*
|
|
1323
|
+
* @param value - The text content to process
|
|
1324
|
+
* @param fromPosition - Start looking for list items from this position
|
|
1325
|
+
* @param targetIndent - Only renumber items with this exact indentation
|
|
1326
|
+
* @returns The text with renumbered list items
|
|
1327
|
+
*/
|
|
1328
|
+
renumberOrderedListItems(value, fromPosition, targetIndent) {
|
|
1329
|
+
const lines = value.split("\n");
|
|
1330
|
+
const fromLineIndex = Math.max(
|
|
1331
|
+
0,
|
|
1332
|
+
value.slice(0, fromPosition).split("\n").length - 1
|
|
1333
|
+
);
|
|
1334
|
+
let nextExpectedNumber = 1;
|
|
1335
|
+
const currentLine = lines[fromLineIndex];
|
|
1336
|
+
const currentMatch = currentLine?.match(/^(\s*)(\d+)\.\s*(.*)$/);
|
|
1337
|
+
if (currentMatch && currentMatch[1] === targetIndent) {
|
|
1338
|
+
nextExpectedNumber = parseInt(currentMatch[2], 10) + 1;
|
|
1339
|
+
} else {
|
|
1340
|
+
for (let i = fromLineIndex - 1; i >= 0; i--) {
|
|
1341
|
+
const line = lines[i];
|
|
1342
|
+
const orderedMatch = line.match(/^(\s*)(\d+)\.\s*(.*)$/);
|
|
1343
|
+
if (orderedMatch && orderedMatch[1] === targetIndent) {
|
|
1344
|
+
nextExpectedNumber = parseInt(orderedMatch[2], 10) + 1;
|
|
1345
|
+
break;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
let currentNumber = nextExpectedNumber;
|
|
1350
|
+
const orderedRegex = /^(\s*)(\d+)\.\s*(.*)$/;
|
|
1351
|
+
for (let i = fromLineIndex + 1; i < lines.length; i++) {
|
|
1352
|
+
const line = lines[i];
|
|
1353
|
+
const orderedMatch = line.match(orderedRegex);
|
|
1354
|
+
if (orderedMatch && orderedMatch[1] === targetIndent) {
|
|
1355
|
+
const indent = orderedMatch[1];
|
|
1356
|
+
const content = orderedMatch[3];
|
|
1357
|
+
lines[i] = `${indent}${currentNumber}. ${content}`;
|
|
1358
|
+
currentNumber++;
|
|
1359
|
+
} else if (orderedMatch && orderedMatch[1].length < targetIndent.length) {
|
|
1360
|
+
break;
|
|
1361
|
+
} else if (line.trim() !== "" && !orderedMatch && !line.match(/^\s*[-*+]\s+/)) {
|
|
1362
|
+
if (targetIndent === "") {
|
|
1363
|
+
break;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
return lines.join("\n");
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Renumber any ordered lists that come after the given position, starting each new sequence from 1.
|
|
1371
|
+
* This is used when removing list formatting breaks the continuity of a list.
|
|
1372
|
+
*
|
|
1373
|
+
* @param value - The text content to process
|
|
1374
|
+
* @param fromPosition - Start looking for list items from this position
|
|
1375
|
+
* @returns The text with renumbered list sequences
|
|
1376
|
+
*/
|
|
1377
|
+
renumberSubsequentOrderedLists(value, fromPosition) {
|
|
1378
|
+
const lines = value.split("\n");
|
|
1379
|
+
const fromLineIndex = Math.max(
|
|
1380
|
+
0,
|
|
1381
|
+
value.slice(0, fromPosition).split("\n").length - 1
|
|
1382
|
+
);
|
|
1383
|
+
const orderedRegex = /^(\s*)(\d+)\.\s*(.*)$/;
|
|
1384
|
+
let currentSequenceNumber = 1;
|
|
1385
|
+
let inSequence = false;
|
|
1386
|
+
let currentIndent = "";
|
|
1387
|
+
for (let i = fromLineIndex + 1; i < lines.length; i++) {
|
|
1388
|
+
const line = lines[i];
|
|
1389
|
+
const orderedMatch = line.match(orderedRegex);
|
|
1390
|
+
if (orderedMatch) {
|
|
1391
|
+
const indent = orderedMatch[1];
|
|
1392
|
+
const content = orderedMatch[3];
|
|
1393
|
+
if (!inSequence || indent !== currentIndent) {
|
|
1394
|
+
currentSequenceNumber = 1;
|
|
1395
|
+
inSequence = true;
|
|
1396
|
+
currentIndent = indent;
|
|
1397
|
+
}
|
|
1398
|
+
lines[i] = `${indent}${currentSequenceNumber}. ${content}`;
|
|
1399
|
+
currentSequenceNumber++;
|
|
1400
|
+
} else if (line.trim() !== "" && !line.match(/^\s*[-*+]\s+/)) {
|
|
1401
|
+
inSequence = false;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return lines.join("\n");
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Handle backspace to remove empty list items.
|
|
1408
|
+
* Returns true if handled.
|
|
1409
|
+
*/
|
|
1410
|
+
handleListBackspace() {
|
|
1411
|
+
const textarea = this.textareaEl?.shadowRoot?.querySelector(
|
|
1412
|
+
"textarea"
|
|
1413
|
+
);
|
|
1414
|
+
if (!textarea) return false;
|
|
1415
|
+
const start = textarea.selectionStart ?? 0;
|
|
1416
|
+
const end = textarea.selectionEnd ?? 0;
|
|
1417
|
+
if (start !== end) return false;
|
|
1418
|
+
const value = this.value;
|
|
1419
|
+
const lineStart = value.lastIndexOf("\n", start - 1) + 1;
|
|
1420
|
+
let lineEnd = value.indexOf("\n", start);
|
|
1421
|
+
if (lineEnd === -1) lineEnd = value.length;
|
|
1422
|
+
const currentLine = value.slice(lineStart, lineEnd);
|
|
1423
|
+
const cursorPositionInLine = start - lineStart;
|
|
1424
|
+
const unorderedMatch = currentLine.match(/^(\s*)([-*+])\s*(.*)$/);
|
|
1425
|
+
const orderedMatch = currentLine.match(/^(\s*)(\d+)\.\s*(.*)$/);
|
|
1426
|
+
if (!unorderedMatch && !orderedMatch) {
|
|
1427
|
+
return false;
|
|
1428
|
+
}
|
|
1429
|
+
let markerEndPosition = 0;
|
|
1430
|
+
let hasContent = false;
|
|
1431
|
+
if (unorderedMatch) {
|
|
1432
|
+
const indent = unorderedMatch[1] ?? "";
|
|
1433
|
+
const marker = unorderedMatch[2] ?? "-";
|
|
1434
|
+
const content = unorderedMatch[3] ?? "";
|
|
1435
|
+
markerEndPosition = indent.length + marker.length + 1;
|
|
1436
|
+
hasContent = content.trim().length > 0;
|
|
1437
|
+
} else if (orderedMatch) {
|
|
1438
|
+
const indent = orderedMatch[1] ?? "";
|
|
1439
|
+
const numberStr = orderedMatch[2] ?? "1";
|
|
1440
|
+
const content = orderedMatch[3] ?? "";
|
|
1441
|
+
markerEndPosition = indent.length + numberStr.length + 2;
|
|
1442
|
+
hasContent = content.trim().length > 0;
|
|
1443
|
+
}
|
|
1444
|
+
if (cursorPositionInLine === markerEndPosition && !hasContent) {
|
|
1445
|
+
this.saveUndoStateBeforeChange();
|
|
1446
|
+
const before = value.slice(0, lineStart);
|
|
1447
|
+
const after = value.slice(
|
|
1448
|
+
lineEnd === value.length ? lineEnd : lineEnd + 1
|
|
1449
|
+
);
|
|
1450
|
+
let newValue = before + after;
|
|
1451
|
+
if (orderedMatch) {
|
|
1452
|
+
const indent = orderedMatch[1] ?? "";
|
|
1453
|
+
newValue = this.renumberOrderedListItems(
|
|
1454
|
+
newValue,
|
|
1455
|
+
before.length,
|
|
1456
|
+
indent
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
this.value = newValue;
|
|
1460
|
+
textarea.value = newValue;
|
|
1461
|
+
let newCursor = before.length;
|
|
1462
|
+
if (before.endsWith("\n") && before.length > 1) {
|
|
1463
|
+
newCursor = before.length - 1;
|
|
1464
|
+
}
|
|
1465
|
+
requestAnimationFrame(() => {
|
|
1466
|
+
textarea.setSelectionRange(newCursor, newCursor);
|
|
1467
|
+
this.updateActiveFormats();
|
|
1468
|
+
});
|
|
1469
|
+
this.dispatchChange();
|
|
1470
|
+
return true;
|
|
1471
|
+
}
|
|
1472
|
+
return false;
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
/**
|
|
1476
|
+
* Perform an undo operation, reverting to the previous state.
|
|
1477
|
+
*/
|
|
1478
|
+
undo() {
|
|
1479
|
+
if (this.isReadonly || this.isDisabled || this.undoStack.length <= 1) {
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const textarea = this.textareaEl?.shadowRoot?.querySelector("textarea");
|
|
1483
|
+
const currentSelection = textarea ? { start: textarea.selectionStart ?? 0, end: textarea.selectionEnd ?? 0 } : { start: 0, end: 0 };
|
|
1484
|
+
this.redoStack.push({ value: this.value, selection: currentSelection });
|
|
1485
|
+
const previousState = this.undoStack.pop();
|
|
1486
|
+
if (!previousState) {
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
this.isUndoRedoAction = true;
|
|
1490
|
+
this.value = previousState.value;
|
|
1491
|
+
this.lastSavedValue = previousState.value;
|
|
1492
|
+
if (textarea) {
|
|
1493
|
+
textarea.value = previousState.value;
|
|
1494
|
+
}
|
|
1495
|
+
requestAnimationFrame(() => {
|
|
1496
|
+
if (textarea) {
|
|
1497
|
+
textarea.setSelectionRange(
|
|
1498
|
+
previousState.selection.start,
|
|
1499
|
+
previousState.selection.end
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
this.updateActiveFormats();
|
|
1503
|
+
this.isUndoRedoAction = false;
|
|
1504
|
+
});
|
|
1505
|
+
this.dispatchChange();
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Perform a redo operation, reapplying a previously undone state.
|
|
1509
|
+
*/
|
|
1510
|
+
redo() {
|
|
1511
|
+
if (this.isReadonly || this.isDisabled || this.redoStack.length === 0) {
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
const textarea = this.textareaEl?.shadowRoot?.querySelector("textarea");
|
|
1515
|
+
const currentSelection = textarea ? { start: textarea.selectionStart ?? 0, end: textarea.selectionEnd ?? 0 } : { start: 0, end: 0 };
|
|
1516
|
+
this.undoStack.push({ value: this.value, selection: currentSelection });
|
|
1517
|
+
const nextState = this.redoStack.pop();
|
|
1518
|
+
if (!nextState) {
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
this.isUndoRedoAction = true;
|
|
1522
|
+
this.value = nextState.value;
|
|
1523
|
+
this.lastSavedValue = nextState.value;
|
|
1524
|
+
if (textarea) {
|
|
1525
|
+
textarea.value = nextState.value;
|
|
1526
|
+
}
|
|
1527
|
+
requestAnimationFrame(() => {
|
|
1528
|
+
if (textarea) {
|
|
1529
|
+
textarea.setSelectionRange(
|
|
1530
|
+
nextState.selection.start,
|
|
1531
|
+
nextState.selection.end
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
this.updateActiveFormats();
|
|
1535
|
+
this.isUndoRedoAction = false;
|
|
1536
|
+
});
|
|
1537
|
+
this.dispatchChange();
|
|
1538
|
+
}
|
|
1539
|
+
connectedCallback() {
|
|
1540
|
+
super.connectedCallback();
|
|
1541
|
+
document.addEventListener("click", this.handleOutsideClick);
|
|
1542
|
+
}
|
|
1543
|
+
firstUpdated(changedProperties) {
|
|
1544
|
+
super.firstUpdated(changedProperties);
|
|
1545
|
+
requestAnimationFrame(() => {
|
|
1546
|
+
this.saveInitialUndoState();
|
|
1547
|
+
this.updateActiveFormats();
|
|
1548
|
+
this.addKeyboardListeners();
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
disconnectedCallback() {
|
|
1552
|
+
super.disconnectedCallback();
|
|
1553
|
+
document.removeEventListener("click", this.handleOutsideClick);
|
|
1554
|
+
if (this.undoTimeout) {
|
|
1555
|
+
clearTimeout(this.undoTimeout);
|
|
1556
|
+
}
|
|
1557
|
+
this.removeKeyboardListeners();
|
|
1558
|
+
this.savedSelectionForPreview = null;
|
|
1559
|
+
}
|
|
1560
|
+
labelTemplate() {
|
|
1561
|
+
return this.label ? x`<label class="heading-inter-14-bold text-neutral-20 block"
|
|
1562
|
+
>${this.label}</label
|
|
1563
|
+
>` : E;
|
|
1564
|
+
}
|
|
1565
|
+
descriptionTemplate() {
|
|
1566
|
+
return this.description ? x`<div class="paragraph-inter-12-regular text-neutral-20">
|
|
1567
|
+
<lukso-sanitize html-content=${this.description}></lukso-sanitize>
|
|
1568
|
+
</div>` : E;
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Restore focus and selection to the textarea after toolbar interactions.
|
|
1572
|
+
*/
|
|
1573
|
+
restoreFocusAndSelection() {
|
|
1574
|
+
const ta = this.textareaEl?.shadowRoot?.querySelector(
|
|
1575
|
+
"textarea"
|
|
1576
|
+
);
|
|
1577
|
+
if (ta) {
|
|
1578
|
+
ta.focus();
|
|
1579
|
+
const sel = this.currentSelection;
|
|
1580
|
+
const start = typeof sel.start === "number" ? sel.start : ta.selectionStart ?? 0;
|
|
1581
|
+
const end = typeof sel.end === "number" ? sel.end : ta.selectionEnd ?? 0;
|
|
1582
|
+
ta.setSelectionRange(start, end);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
buttonTemplate(icon, handler, name, isActive = false) {
|
|
1586
|
+
return x`
|
|
1587
|
+
<lukso-tooltip text=${name} placement="top">
|
|
1588
|
+
<lukso-button
|
|
1589
|
+
@click=${() => {
|
|
1590
|
+
this.restoreFocusAndSelection();
|
|
1591
|
+
handler();
|
|
1592
|
+
}}
|
|
1593
|
+
aria-label=${name}
|
|
1594
|
+
aria-pressed=${isActive ? "true" : "false"}
|
|
1595
|
+
type="button"
|
|
1596
|
+
variant="secondary"
|
|
1597
|
+
size="small"
|
|
1598
|
+
custom-class=${this.toolbarButton({ active: isActive })}
|
|
1599
|
+
is-icon
|
|
1600
|
+
>
|
|
1601
|
+
<lukso-icon
|
|
1602
|
+
name=${icon}
|
|
1603
|
+
size="small"
|
|
1604
|
+
pack="vuesax"
|
|
1605
|
+
variant="linear"
|
|
1606
|
+
></lukso-icon></lukso-button
|
|
1607
|
+
></lukso-tooltip>
|
|
1608
|
+
`;
|
|
1609
|
+
}
|
|
1610
|
+
toolbarTemplate() {
|
|
1611
|
+
return x`
|
|
1612
|
+
<div class="flex items-center gap-2">
|
|
1613
|
+
<div class=${cn(this.styles().headingMenu())}>
|
|
1614
|
+
<!-- Heading -->
|
|
1615
|
+
<lukso-tooltip text="Heading options" placement="top">
|
|
1616
|
+
<lukso-button
|
|
1617
|
+
id=${this.headingTriggerId}
|
|
1618
|
+
@click=${(e) => {
|
|
1619
|
+
e.stopPropagation();
|
|
1620
|
+
this.isColorDropdownOpen = false;
|
|
1621
|
+
this.isListDropdownOpen = false;
|
|
1622
|
+
this.isHeadingDropdownOpen = !this.isHeadingDropdownOpen;
|
|
1623
|
+
}}
|
|
1624
|
+
aria-expanded=${this.isHeadingDropdownOpen ? "true" : "false"}
|
|
1625
|
+
aria-label="Heading options"
|
|
1626
|
+
variant="secondary"
|
|
1627
|
+
size="small"
|
|
1628
|
+
custom-class=${this.toolbarButton({
|
|
1629
|
+
active: this.getActiveHeadingLevel() > 0
|
|
1630
|
+
})}
|
|
1631
|
+
is-icon
|
|
1632
|
+
>
|
|
1633
|
+
<lukso-icon
|
|
1634
|
+
name="smallcaps"
|
|
1635
|
+
size="small"
|
|
1636
|
+
pack="vuesax"
|
|
1637
|
+
variant="linear"
|
|
1638
|
+
></lukso-icon>
|
|
1639
|
+
</lukso-button>
|
|
1640
|
+
</lukso-tooltip>
|
|
1641
|
+
<lukso-dropdown
|
|
1642
|
+
id="headingDropdown"
|
|
1643
|
+
trigger-id=""
|
|
1644
|
+
size="medium"
|
|
1645
|
+
?is-open=${this.isHeadingDropdownOpen}
|
|
1646
|
+
>
|
|
1647
|
+
<lukso-dropdown-option
|
|
1648
|
+
?is-selected=${this.getActiveHeadingLevel() === 0}
|
|
1649
|
+
@click=${(e) => {
|
|
1650
|
+
e.stopPropagation();
|
|
1651
|
+
this.restoreFocusAndSelection();
|
|
1652
|
+
this.applyHeading(0);
|
|
1653
|
+
this.isHeadingDropdownOpen = false;
|
|
1654
|
+
}}
|
|
1655
|
+
size="medium"
|
|
1656
|
+
>
|
|
1657
|
+
Normal text
|
|
1658
|
+
</lukso-dropdown-option>
|
|
1659
|
+
<lukso-dropdown-option
|
|
1660
|
+
?is-selected=${this.getActiveHeadingLevel() === 1}
|
|
1661
|
+
@click=${(e) => {
|
|
1662
|
+
e.stopPropagation();
|
|
1663
|
+
this.restoreFocusAndSelection();
|
|
1664
|
+
this.applyHeading(1);
|
|
1665
|
+
this.isHeadingDropdownOpen = false;
|
|
1666
|
+
}}
|
|
1667
|
+
size="medium"
|
|
1668
|
+
>
|
|
1669
|
+
Heading 1
|
|
1670
|
+
</lukso-dropdown-option>
|
|
1671
|
+
<lukso-dropdown-option
|
|
1672
|
+
?is-selected=${this.getActiveHeadingLevel() === 2}
|
|
1673
|
+
@click=${(e) => {
|
|
1674
|
+
e.stopPropagation();
|
|
1675
|
+
this.restoreFocusAndSelection();
|
|
1676
|
+
this.applyHeading(2);
|
|
1677
|
+
this.isHeadingDropdownOpen = false;
|
|
1678
|
+
}}
|
|
1679
|
+
size="medium"
|
|
1680
|
+
>
|
|
1681
|
+
Heading 2
|
|
1682
|
+
</lukso-dropdown-option>
|
|
1683
|
+
<lukso-dropdown-option
|
|
1684
|
+
?is-selected=${this.getActiveHeadingLevel() === 3}
|
|
1685
|
+
@click=${(e) => {
|
|
1686
|
+
e.stopPropagation();
|
|
1687
|
+
this.restoreFocusAndSelection();
|
|
1688
|
+
this.applyHeading(3);
|
|
1689
|
+
this.isHeadingDropdownOpen = false;
|
|
1690
|
+
}}
|
|
1691
|
+
size="medium"
|
|
1692
|
+
>
|
|
1693
|
+
Heading 3
|
|
1694
|
+
</lukso-dropdown-option>
|
|
1695
|
+
</lukso-dropdown>
|
|
1696
|
+
</div>
|
|
1697
|
+
|
|
1698
|
+
<!-- Bold -->
|
|
1699
|
+
${this.buttonTemplate(
|
|
1700
|
+
"text-bold",
|
|
1701
|
+
() => this.toggleWrap("**"),
|
|
1702
|
+
"Bold",
|
|
1703
|
+
this.activeFormats.bold
|
|
1704
|
+
)}
|
|
1705
|
+
|
|
1706
|
+
<!-- Italic -->
|
|
1707
|
+
${this.buttonTemplate(
|
|
1708
|
+
"text-italic",
|
|
1709
|
+
() => this.toggleWrap("*"),
|
|
1710
|
+
"Italic",
|
|
1711
|
+
this.activeFormats.italic
|
|
1712
|
+
)}
|
|
1713
|
+
|
|
1714
|
+
<!-- List -->
|
|
1715
|
+
<div class=${this.styles().listMenu()}>
|
|
1716
|
+
<lukso-tooltip text="List options" placement="top">
|
|
1717
|
+
<lukso-button
|
|
1718
|
+
id=${this.listTriggerId}
|
|
1719
|
+
@click=${(e) => {
|
|
1720
|
+
e.stopPropagation();
|
|
1721
|
+
this.restoreFocusAndSelection();
|
|
1722
|
+
this.isHeadingDropdownOpen = false;
|
|
1723
|
+
this.isColorDropdownOpen = false;
|
|
1724
|
+
this.isListDropdownOpen = !this.isListDropdownOpen;
|
|
1725
|
+
}}
|
|
1726
|
+
aria-expanded=${this.isListDropdownOpen ? "true" : "false"}
|
|
1727
|
+
aria-label="List options"
|
|
1728
|
+
variant="secondary"
|
|
1729
|
+
size="small"
|
|
1730
|
+
custom-class=${this.toolbarButton({
|
|
1731
|
+
active: this.activeFormats.unorderedList || this.activeFormats.orderedList
|
|
1732
|
+
})}
|
|
1733
|
+
is-icon
|
|
1734
|
+
>
|
|
1735
|
+
<lukso-icon
|
|
1736
|
+
name="task"
|
|
1737
|
+
size="small"
|
|
1738
|
+
pack="vuesax"
|
|
1739
|
+
variant="linear"
|
|
1740
|
+
></lukso-icon>
|
|
1741
|
+
</lukso-button>
|
|
1742
|
+
</lukso-tooltip>
|
|
1743
|
+
<lukso-dropdown
|
|
1744
|
+
id="listDropdown"
|
|
1745
|
+
trigger-id=""
|
|
1746
|
+
size="medium"
|
|
1747
|
+
?is-open=${this.isListDropdownOpen}
|
|
1748
|
+
>
|
|
1749
|
+
<lukso-dropdown-option
|
|
1750
|
+
?is-selected=${this.getActiveListType() === "none"}
|
|
1751
|
+
@click=${(e) => {
|
|
1752
|
+
e.stopPropagation();
|
|
1753
|
+
this.restoreFocusAndSelection();
|
|
1754
|
+
this.applyList("none");
|
|
1755
|
+
this.isListDropdownOpen = false;
|
|
1756
|
+
}}
|
|
1757
|
+
size="medium"
|
|
1758
|
+
>
|
|
1759
|
+
No list
|
|
1760
|
+
</lukso-dropdown-option>
|
|
1761
|
+
<lukso-dropdown-option
|
|
1762
|
+
?is-selected=${this.getActiveListType() === "unordered"}
|
|
1763
|
+
@click=${(e) => {
|
|
1764
|
+
e.stopPropagation();
|
|
1765
|
+
this.restoreFocusAndSelection();
|
|
1766
|
+
this.applyList("unordered");
|
|
1767
|
+
this.isListDropdownOpen = false;
|
|
1768
|
+
}}
|
|
1769
|
+
size="medium"
|
|
1770
|
+
>
|
|
1771
|
+
Unordered
|
|
1772
|
+
</lukso-dropdown-option>
|
|
1773
|
+
<lukso-dropdown-option
|
|
1774
|
+
?is-selected=${this.getActiveListType() === "ordered"}
|
|
1775
|
+
@click=${(e) => {
|
|
1776
|
+
e.stopPropagation();
|
|
1777
|
+
this.restoreFocusAndSelection();
|
|
1778
|
+
this.applyList("ordered");
|
|
1779
|
+
this.isListDropdownOpen = false;
|
|
1780
|
+
}}
|
|
1781
|
+
size="medium"
|
|
1782
|
+
>
|
|
1783
|
+
Ordered
|
|
1784
|
+
</lukso-dropdown-option>
|
|
1785
|
+
</lukso-dropdown>
|
|
1786
|
+
</div>
|
|
1787
|
+
|
|
1788
|
+
<!-- Link -->
|
|
1789
|
+
${this.buttonTemplate(
|
|
1790
|
+
"link",
|
|
1791
|
+
() => this.insertLink(),
|
|
1792
|
+
"Link",
|
|
1793
|
+
this.activeFormats.link
|
|
1794
|
+
)}
|
|
1795
|
+
|
|
1796
|
+
<!-- Color -->
|
|
1797
|
+
<div class=${this.styles().colorMenu()}>
|
|
1798
|
+
<lukso-tooltip text="Text color" placement="top">
|
|
1799
|
+
<lukso-button
|
|
1800
|
+
id=${this.colorTriggerId}
|
|
1801
|
+
@click=${(e) => {
|
|
1802
|
+
e.stopPropagation();
|
|
1803
|
+
this.restoreFocusAndSelection();
|
|
1804
|
+
this.isHeadingDropdownOpen = false;
|
|
1805
|
+
this.isListDropdownOpen = false;
|
|
1806
|
+
if (!this.isColorDropdownOpen) {
|
|
1807
|
+
const ta = this.textareaEl?.shadowRoot?.querySelector("textarea");
|
|
1808
|
+
if (ta) {
|
|
1809
|
+
this.savedSelection = {
|
|
1810
|
+
start: ta.selectionStart ?? 0,
|
|
1811
|
+
end: ta.selectionEnd ?? 0
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
this.isColorDropdownOpen = !this.isColorDropdownOpen;
|
|
1816
|
+
}}
|
|
1817
|
+
aria-expanded=${this.isColorDropdownOpen ? "true" : "false"}
|
|
1818
|
+
aria-pressed=${this.activeFormats.color ? "true" : "false"}
|
|
1819
|
+
aria-label="Text color"
|
|
1820
|
+
variant="secondary"
|
|
1821
|
+
size="small"
|
|
1822
|
+
custom-class=${this.toolbarButton({
|
|
1823
|
+
active: this.activeFormats.color
|
|
1824
|
+
})}
|
|
1825
|
+
is-icon
|
|
1826
|
+
>
|
|
1827
|
+
<div
|
|
1828
|
+
class="size-4 rounded-full"
|
|
1829
|
+
style="background-color: ${this.activeFormats.activeColor};"
|
|
1830
|
+
></div>
|
|
1831
|
+
</lukso-button>
|
|
1832
|
+
</lukso-tooltip>
|
|
1833
|
+
<lukso-dropdown
|
|
1834
|
+
id="colorDropdown"
|
|
1835
|
+
trigger-id=""
|
|
1836
|
+
size="medium"
|
|
1837
|
+
max-height="300"
|
|
1838
|
+
?is-open=${this.isColorDropdownOpen}
|
|
1839
|
+
>
|
|
1840
|
+
<div class="grid grid-cols-8 gap-2 p-2 w-[260px]">
|
|
1841
|
+
<div class="col-span-8 mb-2 flex items-center justify-between">
|
|
1842
|
+
<span class="text-xs text-neutral-60">Text Color</span>
|
|
1843
|
+
${this.activeFormats.color ? x`<button
|
|
1844
|
+
class="text-xs text-neutral-60 hover:text-neutral-20 underline"
|
|
1845
|
+
@click=${(e) => {
|
|
1846
|
+
e.stopPropagation();
|
|
1847
|
+
this.clearColor();
|
|
1848
|
+
this.isColorDropdownOpen = false;
|
|
1849
|
+
}}
|
|
1850
|
+
type="button"
|
|
1851
|
+
>
|
|
1852
|
+
Clear
|
|
1853
|
+
</button>` : E}
|
|
1854
|
+
</div>
|
|
1855
|
+
${this.colorSamples.map(
|
|
1856
|
+
(color) => x`
|
|
1857
|
+
<button
|
|
1858
|
+
class="w-6 h-6 rounded-4 border transition-all ${this.activeFormats.activeColor === color ? "border-neutral-20 ring-2 ring-purple-51" : "border-neutral-90 hover:border-neutral-60"}"
|
|
1859
|
+
style="background-color: ${color}"
|
|
1860
|
+
title=${color}
|
|
1861
|
+
aria-pressed=${this.activeFormats.activeColor === color ? "true" : "false"}
|
|
1862
|
+
@click=${(e) => {
|
|
1863
|
+
e.stopPropagation();
|
|
1864
|
+
this.selectColor(color);
|
|
1865
|
+
this.isColorDropdownOpen = false;
|
|
1866
|
+
}}
|
|
1867
|
+
></button>
|
|
1868
|
+
`
|
|
1869
|
+
)}
|
|
1870
|
+
</div>
|
|
1871
|
+
</lukso-dropdown>
|
|
1872
|
+
</div>
|
|
1873
|
+
|
|
1874
|
+
<div class=${this.styles().divider()}></div>
|
|
1875
|
+
</div>
|
|
1876
|
+
`;
|
|
1877
|
+
}
|
|
1878
|
+
render() {
|
|
1879
|
+
const { wrapper, header, toolbar, area, editor, preview } = this.styles({
|
|
1880
|
+
isFullWidth: this.isFullWidth
|
|
1881
|
+
});
|
|
1882
|
+
return x`
|
|
1883
|
+
<div class=${wrapper()}>
|
|
1884
|
+
${this.labelTemplate()} ${this.descriptionTemplate()}
|
|
1885
|
+
|
|
1886
|
+
<div class=${header()}>
|
|
1887
|
+
<div class=${toolbar()}>${this.toolbarTemplate()}</div>
|
|
1888
|
+
${this.buttonTemplate(
|
|
1889
|
+
"eye",
|
|
1890
|
+
() => this.togglePreview(),
|
|
1891
|
+
"Toggle preview",
|
|
1892
|
+
this.isPreview
|
|
1893
|
+
)}
|
|
1894
|
+
</div>
|
|
1895
|
+
|
|
1896
|
+
<div class=${area()}>
|
|
1897
|
+
${!this.isPreview ? x`<div class=${editor()}>
|
|
1898
|
+
<lukso-textarea
|
|
1899
|
+
.value=${this.value}
|
|
1900
|
+
name=${this.name ? this.name : E}
|
|
1901
|
+
size=${this.size ? this.size : E}
|
|
1902
|
+
rows=${this.rows ? this.rows : E}
|
|
1903
|
+
placeholder=${this.placeholder ? this.placeholder : E}
|
|
1904
|
+
error=${this.error ? this.error : E}
|
|
1905
|
+
?is-full-width=${true}
|
|
1906
|
+
?is-disabled=${this.isDisabled}
|
|
1907
|
+
?is-readonly=${this.isReadonly}
|
|
1908
|
+
?is-non-resizable=${this.isNonResizable}
|
|
1909
|
+
@on-input=${this.handleTextareaInput}
|
|
1910
|
+
@on-key-up=${this.handleTextareaKeyUp}
|
|
1911
|
+
@on-input-click=${this.handleTextareaClick}
|
|
1912
|
+
></lukso-textarea>
|
|
1913
|
+
</div>` : x`<div class=${preview()}>
|
|
1914
|
+
<lukso-markdown
|
|
1915
|
+
value=${this.value}
|
|
1916
|
+
prose-classes="prose prose-base prose-gray"
|
|
1917
|
+
></lukso-markdown>
|
|
1918
|
+
</div>`}
|
|
1919
|
+
</div>
|
|
1920
|
+
</div>
|
|
1921
|
+
`;
|
|
1922
|
+
}
|
|
1923
|
+
};
|
|
1924
|
+
__decorateClass([
|
|
1925
|
+
n({ type: String })
|
|
1926
|
+
], LuksoMarkdownEditor.prototype, "value", 2);
|
|
1927
|
+
__decorateClass([
|
|
1928
|
+
n({ type: String })
|
|
1929
|
+
], LuksoMarkdownEditor.prototype, "name", 2);
|
|
1930
|
+
__decorateClass([
|
|
1931
|
+
n({ type: String })
|
|
1932
|
+
], LuksoMarkdownEditor.prototype, "label", 2);
|
|
1933
|
+
__decorateClass([
|
|
1934
|
+
n({ type: String })
|
|
1935
|
+
], LuksoMarkdownEditor.prototype, "description", 2);
|
|
1936
|
+
__decorateClass([
|
|
1937
|
+
n({ type: String })
|
|
1938
|
+
], LuksoMarkdownEditor.prototype, "error", 2);
|
|
1939
|
+
__decorateClass([
|
|
1940
|
+
n({ type: Boolean, attribute: "is-full-width" })
|
|
1941
|
+
], LuksoMarkdownEditor.prototype, "isFullWidth", 2);
|
|
1942
|
+
__decorateClass([
|
|
1943
|
+
n({ type: Boolean, attribute: "is-readonly" })
|
|
1944
|
+
], LuksoMarkdownEditor.prototype, "isReadonly", 2);
|
|
1945
|
+
__decorateClass([
|
|
1946
|
+
n({ type: Boolean, attribute: "is-disabled" })
|
|
1947
|
+
], LuksoMarkdownEditor.prototype, "isDisabled", 2);
|
|
1948
|
+
__decorateClass([
|
|
1949
|
+
n({ type: Boolean, attribute: "is-non-resizable" })
|
|
1950
|
+
], LuksoMarkdownEditor.prototype, "isNonResizable", 2);
|
|
1951
|
+
__decorateClass([
|
|
1952
|
+
n({ type: Boolean })
|
|
1953
|
+
], LuksoMarkdownEditor.prototype, "autofocus", 2);
|
|
1954
|
+
__decorateClass([
|
|
1955
|
+
n({ type: String, reflect: true })
|
|
1956
|
+
], LuksoMarkdownEditor.prototype, "size", 2);
|
|
1957
|
+
__decorateClass([
|
|
1958
|
+
n({ type: Boolean, attribute: "is-preview", reflect: true })
|
|
1959
|
+
], LuksoMarkdownEditor.prototype, "isPreview", 2);
|
|
1960
|
+
__decorateClass([
|
|
1961
|
+
n({ type: Number })
|
|
1962
|
+
], LuksoMarkdownEditor.prototype, "rows", 2);
|
|
1963
|
+
__decorateClass([
|
|
1964
|
+
n({ type: String })
|
|
1965
|
+
], LuksoMarkdownEditor.prototype, "placeholder", 2);
|
|
1966
|
+
__decorateClass([
|
|
1967
|
+
r()
|
|
1968
|
+
], LuksoMarkdownEditor.prototype, "savedSelectionForPreview", 2);
|
|
1969
|
+
__decorateClass([
|
|
1970
|
+
r()
|
|
1971
|
+
], LuksoMarkdownEditor.prototype, "isHeadingDropdownOpen", 2);
|
|
1972
|
+
__decorateClass([
|
|
1973
|
+
r()
|
|
1974
|
+
], LuksoMarkdownEditor.prototype, "isColorDropdownOpen", 2);
|
|
1975
|
+
__decorateClass([
|
|
1976
|
+
r()
|
|
1977
|
+
], LuksoMarkdownEditor.prototype, "isListDropdownOpen", 2);
|
|
1978
|
+
__decorateClass([
|
|
1979
|
+
r()
|
|
1980
|
+
], LuksoMarkdownEditor.prototype, "currentSelection", 2);
|
|
1981
|
+
__decorateClass([
|
|
1982
|
+
r()
|
|
1983
|
+
], LuksoMarkdownEditor.prototype, "savedSelection", 2);
|
|
1984
|
+
__decorateClass([
|
|
1985
|
+
r()
|
|
1986
|
+
], LuksoMarkdownEditor.prototype, "activeFormats", 2);
|
|
1987
|
+
__decorateClass([
|
|
1988
|
+
e("lukso-textarea")
|
|
1989
|
+
], LuksoMarkdownEditor.prototype, "textareaEl", 2);
|
|
1990
|
+
LuksoMarkdownEditor = __decorateClass([
|
|
1991
|
+
t("lukso-markdown-editor")
|
|
1992
|
+
], LuksoMarkdownEditor);
|
|
1993
|
+
|
|
1994
|
+
export { LuksoMarkdownEditor };
|