@dbcdk/react-components 0.0.208 → 0.0.210

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/client.cjs +10 -0
  2. package/dist/client.d.ts +4 -0
  3. package/dist/client.js +2 -0
  4. package/dist/components/forms/markdown-editor/MarkdownEditor.cjs +475 -0
  5. package/dist/components/forms/markdown-editor/MarkdownEditor.d.ts +68 -0
  6. package/dist/components/forms/markdown-editor/MarkdownEditor.js +468 -0
  7. package/dist/components/forms/markdown-editor/MarkdownEditor.module.css +202 -0
  8. package/dist/components/forms/markdown-editor/MarkdownViewer.cjs +29 -0
  9. package/dist/components/forms/markdown-editor/MarkdownViewer.d.ts +27 -0
  10. package/dist/components/forms/markdown-editor/MarkdownViewer.js +23 -0
  11. package/dist/components/forms/markdown-editor/MarkdownViewer.module.css +97 -0
  12. package/dist/components/forms/markdown-editor/markdown.utils.cjs +325 -0
  13. package/dist/components/forms/markdown-editor/markdown.utils.d.ts +66 -0
  14. package/dist/components/forms/markdown-editor/markdown.utils.js +310 -0
  15. package/dist/components/listbox/Listbox.cjs +2 -2
  16. package/dist/components/listbox/Listbox.d.ts +2 -2
  17. package/dist/components/listbox/Listbox.js +2 -2
  18. package/dist/components/meta-bar/MetaBar.cjs +2 -2
  19. package/dist/components/meta-bar/MetaBar.d.ts +2 -1
  20. package/dist/components/meta-bar/MetaBar.js +2 -2
  21. package/dist/components/meta-bar/MetaBar.module.css +7 -0
  22. package/dist/components/overlay/side-panel/SidePanel.cjs +80 -2
  23. package/dist/components/overlay/side-panel/SidePanel.js +80 -2
  24. package/dist/components/overlay/tooltip/TooltipProvider.cjs +10 -1
  25. package/dist/components/overlay/tooltip/TooltipProvider.js +11 -2
  26. package/dist/components/overlay/tooltip/useTooltipTrigger.cjs +9 -2
  27. package/dist/components/overlay/tooltip/useTooltipTrigger.js +9 -2
  28. package/dist/components/sidebar/components/sidebar-container/SidebarContainer.cjs +1 -1
  29. package/dist/components/sidebar/components/sidebar-container/SidebarContainer.js +1 -1
  30. package/dist/components/split-pane/SplitPane.cjs +22 -7
  31. package/dist/components/split-pane/SplitPane.d.ts +5 -2
  32. package/dist/components/split-pane/SplitPane.js +22 -7
  33. package/dist/components/split-pane/SplitPane.module.css +9 -0
  34. package/dist/components/split-pane/provider/SplitPaneContext.cjs +5 -2
  35. package/dist/components/split-pane/provider/SplitPaneContext.d.ts +3 -1
  36. package/dist/components/split-pane/provider/SplitPaneContext.js +5 -2
  37. package/dist/components/tabs/Tabs.cjs +83 -69
  38. package/dist/components/tabs/Tabs.d.ts +5 -1
  39. package/dist/components/tabs/Tabs.js +83 -69
  40. package/dist/components/tabs/Tabs.module.css +39 -0
  41. package/dist/components/tree-view/TreeNodeRow.cjs +70 -30
  42. package/dist/components/tree-view/TreeNodeRow.d.ts +6 -1
  43. package/dist/components/tree-view/TreeNodeRow.js +70 -30
  44. package/dist/components/tree-view/TreeView.cjs +107 -19
  45. package/dist/components/tree-view/TreeView.d.ts +1 -1
  46. package/dist/components/tree-view/TreeView.js +107 -19
  47. package/dist/components/tree-view/TreeView.module.css +47 -3
  48. package/dist/components/tree-view/types.d.ts +6 -0
  49. package/dist/index.cjs +213 -208
  50. package/dist/index.css +2 -0
  51. package/dist/index.d.ts +2 -0
  52. package/dist/index.js +1 -0
  53. package/dist/styles/css-helper-classes/typography.css +4 -0
  54. package/package.json +4 -1
package/dist/client.cjs CHANGED
@@ -45,6 +45,8 @@ var TableSettings = require('./components/table/components/table-settings/TableS
45
45
  var useTableSettings = require('./hooks/useTableSettings');
46
46
  var chartColors = require('./constants/chart-colors');
47
47
  var Textarea = require('./components/forms/text-area/Textarea');
48
+ var MarkdownEditor = require('./components/forms/markdown-editor/MarkdownEditor');
49
+ var MarkdownViewer = require('./components/forms/markdown-editor/MarkdownViewer');
48
50
  var AppHeader = require('./components/app-header/AppHeader');
49
51
  var Sidebar = require('./components/sidebar/Sidebar');
50
52
  var TreeView = require('./components/tree-view/TreeView');
@@ -306,6 +308,14 @@ Object.defineProperty(exports, "Textarea", {
306
308
  enumerable: true,
307
309
  get: function () { return Textarea.Textarea; }
308
310
  });
311
+ Object.defineProperty(exports, "MarkdownEditor", {
312
+ enumerable: true,
313
+ get: function () { return MarkdownEditor.MarkdownEditor; }
314
+ });
315
+ Object.defineProperty(exports, "MarkdownViewer", {
316
+ enumerable: true,
317
+ get: function () { return MarkdownViewer.MarkdownViewer; }
318
+ });
309
319
  Object.defineProperty(exports, "AppHeader", {
310
320
  enumerable: true,
311
321
  get: function () { return AppHeader.AppHeader; }
package/dist/client.d.ts CHANGED
@@ -76,6 +76,10 @@ export type { ChartSemanticSeverity, ChartSemanticColor } from './constants/char
76
76
  export type { Severity } from './constants/severity.types';
77
77
  export { Textarea } from './components/forms/text-area/Textarea';
78
78
  export type { TextareaProps } from './components/forms/text-area/Textarea';
79
+ export { MarkdownEditor } from './components/forms/markdown-editor/MarkdownEditor';
80
+ export type { MarkdownEditorLabels, MarkdownEditorProps, } from './components/forms/markdown-editor/MarkdownEditor';
81
+ export { MarkdownViewer } from './components/forms/markdown-editor/MarkdownViewer';
82
+ export type { MarkdownViewerProps } from './components/forms/markdown-editor/MarkdownViewer';
79
83
  export { AppHeader } from './components/app-header/AppHeader';
80
84
  export type { AppHeaderSize } from './components/app-header/AppHeader';
81
85
  export { Sidebar } from './components/sidebar/Sidebar';
package/dist/client.js CHANGED
@@ -43,6 +43,8 @@ export { TableSettings, TableSettingsMenuSection } from './components/table/comp
43
43
  export { localStorageTableSettingsStorage, useTableSettings } from './hooks/useTableSettings';
44
44
  export { ChartSemanticColorVar } from './constants/chart-colors';
45
45
  export { Textarea } from './components/forms/text-area/Textarea';
46
+ export { MarkdownEditor } from './components/forms/markdown-editor/MarkdownEditor';
47
+ export { MarkdownViewer } from './components/forms/markdown-editor/MarkdownViewer';
46
48
  export { AppHeader } from './components/app-header/AppHeader';
47
49
  export { Sidebar } from './components/sidebar/Sidebar';
48
50
  export { TreeView, collectExpandableIds, findAncestorIds } from './components/tree-view/TreeView';
@@ -0,0 +1,475 @@
1
+ 'use client';
2
+ 'use strict';
3
+
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var lucideReact = require('lucide-react');
6
+ var React = require('react');
7
+ var reactDom = require('react-dom');
8
+ var Button = require('../../../components/button/Button');
9
+ var InputContainer = require('../../../components/forms/input-container/InputContainer');
10
+ var messageIds = require('../../../components/forms/input-container/messageIds');
11
+ var Switch = require('../../../components/forms/switch/Switch');
12
+ var Modal = require('../../../components/overlay/modal/Modal');
13
+ var refs_utils = require('../../../utils/refs.utils');
14
+ var markdown_utils = require('./markdown.utils');
15
+ var styles = require('./MarkdownEditor.module.css');
16
+ var MarkdownViewer = require('./MarkdownViewer');
17
+
18
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
19
+
20
+ var React__default = /*#__PURE__*/_interopDefault(React);
21
+ var styles__default = /*#__PURE__*/_interopDefault(styles);
22
+
23
+ const DEFAULT_LABELS = {
24
+ editor: "Markdown-editor",
25
+ editTab: "Rediger",
26
+ previewTab: "Forh\xE5ndsvisning",
27
+ toolbar: "Markdown-formatering",
28
+ bold: "Fed",
29
+ italic: "Kursiv",
30
+ link: "Inds\xE6t link",
31
+ linkTextPlaceholder: "linktekst",
32
+ linkUrlPlaceholder: "inds\xE6t url",
33
+ heading: "Overskrift",
34
+ bulletList: "Punktopstilling",
35
+ literal: "Bogstavelig tekst",
36
+ plainText: "Ren tekst",
37
+ emptyPreview: "Intet at forh\xE5ndsvise endnu.",
38
+ newTab: "(\xE5bner i en ny fane)",
39
+ plainTextConfirmTitle: "Skift til ren tekst?",
40
+ plainTextConfirmBody: "Teksten indeholder formatering. I ren tekst vises formateringstegnene som almindelig tekst, og formateringen g\xE5r tabt.",
41
+ plainTextTooLongBody: "I ren tekst gemmes specialtegn med ekstra tegn, s\xE5 teksten bliver l\xE6ngere end tilladt og skal forkortes.",
42
+ plainTextConfirmAction: "Skift til ren tekst",
43
+ cancel: "Annuller"
44
+ };
45
+ const TOOLBAR_GROUPS = [
46
+ [
47
+ {
48
+ label: "bold",
49
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Bold, { "aria-hidden": "true" }),
50
+ format: { type: "wrap", marker: "**" },
51
+ shortcut: "b"
52
+ },
53
+ {
54
+ label: "italic",
55
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Italic, { "aria-hidden": "true" }),
56
+ format: { type: "wrap", marker: "*" },
57
+ shortcut: "i"
58
+ },
59
+ { label: "link", icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link, { "aria-hidden": "true" }), format: { type: "link" }, shortcut: "k" }
60
+ ],
61
+ [
62
+ {
63
+ label: "heading",
64
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Heading, { "aria-hidden": "true" }),
65
+ format: { type: "line", prefix: "# ", toggle: true }
66
+ }
67
+ ],
68
+ [
69
+ {
70
+ label: "bulletList",
71
+ icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.List, { "aria-hidden": "true" }),
72
+ format: { type: "line", prefix: "- ", toggle: true }
73
+ }
74
+ ],
75
+ [{ label: "literal", icon: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.RemoveFormatting, { "aria-hidden": "true" }), format: { type: "escape" } }]
76
+ ];
77
+ const TOOLBAR_ITEMS = TOOLBAR_GROUPS.flat();
78
+ const subscribeToNothing = () => () => {
79
+ };
80
+ const isApplePlatform = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);
81
+ function replaceWithUndo(textarea, next) {
82
+ const current = textarea.value;
83
+ let start = 0;
84
+ while (start < current.length && start < next.length && current[start] === next[start]) start++;
85
+ let end = 0;
86
+ while (end < current.length - start && end < next.length - start && current[current.length - 1 - end] === next[next.length - 1 - end]) {
87
+ end++;
88
+ }
89
+ const inserted = next.slice(start, next.length - end);
90
+ if (typeof document.execCommand !== "function") return false;
91
+ textarea.focus();
92
+ textarea.setSelectionRange(start, current.length - end);
93
+ const applied = inserted ? document.execCommand("insertText", false, inserted) : document.execCommand("delete", false);
94
+ return applied && textarea.value === next;
95
+ }
96
+ function MarkdownEditor({
97
+ value,
98
+ onChange,
99
+ onBlur,
100
+ onFocus,
101
+ ref,
102
+ name,
103
+ placeholder,
104
+ rows = 8,
105
+ maxLength,
106
+ disabled = false,
107
+ readOnly = false,
108
+ id,
109
+ className,
110
+ defaultPlainText = false,
111
+ onPlainTextChange,
112
+ headingOffset,
113
+ labels: labelOverrides,
114
+ label,
115
+ error,
116
+ helpText,
117
+ helpTextAddition,
118
+ fullWidth,
119
+ required,
120
+ orientation = "vertical",
121
+ modified,
122
+ modifiedLabel,
123
+ labelAction,
124
+ size,
125
+ labelWidth,
126
+ labelAlignment
127
+ }) {
128
+ const labels = { ...DEFAULT_LABELS, ...labelOverrides };
129
+ const generatedId = React.useId();
130
+ const editorId = id != null ? id : `markdown-editor-${generatedId}`;
131
+ const panelId = `${editorId}-panel`;
132
+ const tabRefs = React.useRef({
133
+ edit: null,
134
+ preview: null
135
+ });
136
+ const toolbarRefs = React.useRef([]);
137
+ const textareaRef = React.useRef(null);
138
+ const mergedTextareaRef = React.useMemo(() => refs_utils.mergeRefs(textareaRef, ref), [ref]);
139
+ const pendingSelection = React.useRef(null);
140
+ const [activeTab, setActiveTab] = React.useState("edit");
141
+ const [selection, setSelection] = React.useState([0, 0]);
142
+ const [plainText, setPlainText] = React.useState(() => defaultPlainText && markdown_utils.isPlainMarkdown(value));
143
+ const [checkedValue, setCheckedValue] = React.useState(value);
144
+ if (plainText && value !== checkedValue) {
145
+ setCheckedValue(value);
146
+ if (!markdown_utils.isPlainMarkdown(value)) setPlainText(false);
147
+ }
148
+ const [confirmPlainText, setConfirmPlainText] = React.useState(null);
149
+ const [toolbarFocusIndex, setToolbarFocusIndex] = React.useState(0);
150
+ const shortcutModifier = React.useSyncExternalStore(
151
+ subscribeToNothing,
152
+ () => isApplePlatform() ? "\u2318" : "Ctrl+",
153
+ () => "Ctrl+"
154
+ );
155
+ const hideToolbar = activeTab === "preview";
156
+ const formattingDisabled = disabled || readOnly;
157
+ const enterPlainText = () => {
158
+ setConfirmPlainText(null);
159
+ setPlainText(true);
160
+ setSelection([0, 0]);
161
+ onPlainTextChange == null ? void 0 : onPlainTextChange(true);
162
+ const converted = markdown_utils.plainTextToMarkdown(markdown_utils.markdownToPlainText(value));
163
+ if (converted !== value) onChange(converted);
164
+ };
165
+ const handlePlainTextChange = (nextPlainText) => {
166
+ if (!nextPlainText) {
167
+ setPlainText(false);
168
+ setSelection([0, 0]);
169
+ onPlainTextChange == null ? void 0 : onPlainTextChange(false);
170
+ return;
171
+ }
172
+ const formatting = !markdown_utils.isPlainMarkdown(value);
173
+ const tooLong = maxLength !== void 0 && markdown_utils.plainTextToMarkdown(markdown_utils.markdownToPlainText(value)).length > maxLength;
174
+ if (formatting || tooLong) setConfirmPlainText({ formatting, tooLong });
175
+ else enterPlainText();
176
+ };
177
+ React.useEffect(() => {
178
+ const textarea = textareaRef.current;
179
+ if (activeTab !== "preview" || !(textarea == null ? void 0 : textarea.labels)) return;
180
+ const labelElements = [...textarea.labels];
181
+ const reveal = () => {
182
+ var _a;
183
+ reactDom.flushSync(() => setActiveTab("edit"));
184
+ (_a = textareaRef.current) == null ? void 0 : _a.focus();
185
+ };
186
+ labelElements.forEach((element) => element.addEventListener("click", reveal));
187
+ return () => labelElements.forEach((element) => element.removeEventListener("click", reveal));
188
+ }, [activeTab]);
189
+ React.useEffect(() => {
190
+ const textarea = textareaRef.current;
191
+ if (!textarea) return;
192
+ const sync = () => setSelection(
193
+ (current) => current[0] === textarea.selectionStart && current[1] === textarea.selectionEnd ? current : [textarea.selectionStart, textarea.selectionEnd]
194
+ );
195
+ textarea.addEventListener("selectionchange", sync);
196
+ document.addEventListener("selectionchange", sync);
197
+ return () => {
198
+ textarea.removeEventListener("selectionchange", sync);
199
+ document.removeEventListener("selectionchange", sync);
200
+ };
201
+ }, []);
202
+ const handleTabKeyDown = (event) => {
203
+ var _a;
204
+ const keys = {
205
+ ArrowRight: activeTab === "edit" ? "preview" : "edit",
206
+ ArrowDown: activeTab === "edit" ? "preview" : "edit",
207
+ ArrowLeft: activeTab === "edit" ? "preview" : "edit",
208
+ ArrowUp: activeTab === "edit" ? "preview" : "edit",
209
+ Home: "edit",
210
+ End: "preview"
211
+ };
212
+ const nextTab = keys[event.key];
213
+ if (!nextTab) return;
214
+ event.preventDefault();
215
+ setActiveTab(nextTab);
216
+ (_a = tabRefs.current[nextTab]) == null ? void 0 : _a.focus();
217
+ };
218
+ React.useLayoutEffect(() => {
219
+ const selection2 = pendingSelection.current;
220
+ const textarea = textareaRef.current;
221
+ if (!selection2 || !textarea) return;
222
+ textarea.focus();
223
+ textarea.setSelectionRange(selection2[0], selection2[1]);
224
+ setSelection(selection2);
225
+ pendingSelection.current = null;
226
+ }, [value]);
227
+ const withPlaceholders = (format) => format.type === "link" ? {
228
+ ...format,
229
+ textPlaceholder: labels.linkTextPlaceholder,
230
+ urlPlaceholder: labels.linkUrlPlaceholder
231
+ } : format;
232
+ const applyFormat = (itemFormat) => {
233
+ const format = withPlaceholders(itemFormat);
234
+ const textarea = textareaRef.current;
235
+ if (!textarea || formattingDisabled) return;
236
+ const formatted = markdown_utils.formatMarkdownSelection(
237
+ value,
238
+ textarea.selectionStart,
239
+ textarea.selectionEnd,
240
+ format
241
+ );
242
+ if (formatted.value === value) {
243
+ textarea.focus();
244
+ textarea.setSelectionRange(formatted.selectionStart, formatted.selectionEnd);
245
+ return;
246
+ }
247
+ pendingSelection.current = [formatted.selectionStart, formatted.selectionEnd];
248
+ if (!replaceWithUndo(textarea, formatted.value)) onChange(formatted.value);
249
+ };
250
+ const isItemDisabled = (item) => formattingDisabled || item.format.type === "escape" && selection[0] === selection[1];
251
+ const enabledIndexes = TOOLBAR_ITEMS.flatMap(
252
+ (item, index) => isItemDisabled(item) ? [] : [index]
253
+ );
254
+ const tabStopIndex = enabledIndexes.includes(toolbarFocusIndex) ? toolbarFocusIndex : enabledIndexes[0];
255
+ const handleToolbarKeyDown = (event) => {
256
+ var _a;
257
+ const position = enabledIndexes.indexOf(tabStopIndex);
258
+ const targets = {
259
+ ArrowRight: enabledIndexes[(position + 1) % enabledIndexes.length],
260
+ ArrowLeft: enabledIndexes[(position - 1 + enabledIndexes.length) % enabledIndexes.length],
261
+ Home: enabledIndexes[0],
262
+ End: enabledIndexes[enabledIndexes.length - 1]
263
+ };
264
+ const target = targets[event.key];
265
+ if (target === void 0) return;
266
+ event.preventDefault();
267
+ setToolbarFocusIndex(target);
268
+ (_a = toolbarRefs.current[target]) == null ? void 0 : _a.focus();
269
+ };
270
+ const handleTextareaKeyDown = (event) => {
271
+ if (plainText || !(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return;
272
+ const item = TOOLBAR_ITEMS.find((item2) => item2.shortcut === event.key.toLowerCase());
273
+ if (!item || isItemDisabled(item)) return;
274
+ event.preventDefault();
275
+ applyFormat(item.format);
276
+ };
277
+ let itemIndex = 0;
278
+ const toolbar = /* @__PURE__ */ jsxRuntime.jsx(
279
+ "div",
280
+ {
281
+ className: [styles__default.default.toolbar, hideToolbar ? styles__default.default.toolbarHidden : ""].filter(Boolean).join(" "),
282
+ role: "toolbar",
283
+ "aria-label": labels.toolbar,
284
+ "aria-hidden": hideToolbar || void 0,
285
+ inert: hideToolbar,
286
+ onKeyDown: handleToolbarKeyDown,
287
+ children: TOOLBAR_GROUPS.map((group, groupIndex) => /* @__PURE__ */ jsxRuntime.jsxs(React__default.default.Fragment, { children: [
288
+ groupIndex > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: styles__default.default.separator, "aria-hidden": "true" }),
289
+ group.map((item) => {
290
+ var _a;
291
+ const index = itemIndex++;
292
+ const shortcut = (_a = item.shortcut) == null ? void 0 : _a.toUpperCase();
293
+ return /* @__PURE__ */ jsxRuntime.jsx(
294
+ Button.Button,
295
+ {
296
+ ref: (element) => {
297
+ toolbarRefs.current[index] = element;
298
+ },
299
+ "aria-label": labels[item.label],
300
+ title: shortcut ? `${labels[item.label]} (${shortcutModifier}${shortcut})` : labels[item.label],
301
+ "aria-keyshortcuts": shortcut ? `Control+${shortcut} Meta+${shortcut}` : void 0,
302
+ "aria-pressed": markdown_utils.isMarkdownFormatActive(
303
+ value,
304
+ selection[0],
305
+ selection[1],
306
+ item.format
307
+ ),
308
+ className: styles__default.default.formatButton,
309
+ variant: "inline",
310
+ size: "sm",
311
+ icon: item.icon,
312
+ disabled: isItemDisabled(item),
313
+ tabIndex: index === tabStopIndex ? 0 : -1,
314
+ onFocus: () => setToolbarFocusIndex(index),
315
+ onMouseDown: (event) => event.preventDefault(),
316
+ onClick: () => applyFormat(item.format)
317
+ },
318
+ item.label
319
+ );
320
+ })
321
+ ] }, groupIndex))
322
+ }
323
+ );
324
+ return /* @__PURE__ */ jsxRuntime.jsxs(
325
+ InputContainer.InputContainer,
326
+ {
327
+ label,
328
+ htmlFor: editorId,
329
+ error,
330
+ helpText,
331
+ helpTextAddition,
332
+ fullWidth,
333
+ required,
334
+ orientation,
335
+ modified,
336
+ modifiedLabel,
337
+ labelAction,
338
+ size,
339
+ labelWidth,
340
+ labelAlignment,
341
+ children: [
342
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: [styles__default.default.field, className].filter(Boolean).join(" "), children: /* @__PURE__ */ jsxRuntime.jsxs(
343
+ "div",
344
+ {
345
+ className: styles__default.default.frame,
346
+ "data-invalid": error ? "true" : void 0,
347
+ style: { "--markdown-editor-rows": rows },
348
+ children: [
349
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: styles__default.default.header, children: [
350
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles__default.default.tabList, role: "tablist", "aria-label": label != null ? label : labels.editor, children: ["edit", "preview"].map((tab) => /* @__PURE__ */ jsxRuntime.jsx(
351
+ "button",
352
+ {
353
+ ref: (element) => {
354
+ tabRefs.current[tab] = element;
355
+ },
356
+ id: `${editorId}-tab-${tab}`,
357
+ className: styles__default.default.tabButton,
358
+ type: "button",
359
+ role: "tab",
360
+ "aria-selected": activeTab === tab,
361
+ "aria-controls": panelId,
362
+ tabIndex: activeTab === tab ? 0 : -1,
363
+ onClick: () => setActiveTab(tab),
364
+ onKeyDown: handleTabKeyDown,
365
+ children: tab === "edit" ? labels.editTab : labels.previewTab
366
+ },
367
+ tab
368
+ )) }),
369
+ !plainText && toolbar,
370
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles__default.default.modeSwitch, children: /* @__PURE__ */ jsxRuntime.jsx(
371
+ Switch.Switch,
372
+ {
373
+ noContainer: true,
374
+ size: "sm",
375
+ label: labels.plainText,
376
+ checked: plainText,
377
+ disabled: formattingDisabled,
378
+ onChange: handlePlainTextChange
379
+ }
380
+ ) })
381
+ ] }),
382
+ /* @__PURE__ */ jsxRuntime.jsxs(
383
+ "div",
384
+ {
385
+ id: panelId,
386
+ className: styles__default.default.panel,
387
+ role: "tabpanel",
388
+ "aria-labelledby": `${editorId}-tab-${activeTab}`,
389
+ children: [
390
+ name && /* @__PURE__ */ jsxRuntime.jsx("input", { type: "hidden", name, value, disabled }),
391
+ /* @__PURE__ */ jsxRuntime.jsx(
392
+ "textarea",
393
+ {
394
+ ref: mergedTextareaRef,
395
+ id: editorId,
396
+ className: styles__default.default.textarea,
397
+ value: plainText ? markdown_utils.markdownToPlainText(value) : value,
398
+ onChange: (event) => {
399
+ const textarea = event.currentTarget;
400
+ if (!plainText) {
401
+ onChange(textarea.value);
402
+ return;
403
+ }
404
+ const limited = markdown_utils.limitPlainText(
405
+ markdown_utils.markdownToPlainText(value),
406
+ textarea.value,
407
+ maxLength
408
+ );
409
+ if (!limited) {
410
+ const caret = textarea.selectionStart - (textarea.value.length - markdown_utils.markdownToPlainText(value).length);
411
+ queueMicrotask(() => textarea.setSelectionRange(caret, caret));
412
+ return;
413
+ }
414
+ if (limited.caret >= 0) pendingSelection.current = [limited.caret, limited.caret];
415
+ onChange(markdown_utils.plainTextToMarkdown(limited.text));
416
+ },
417
+ onSelect: (event) => setSelection([event.currentTarget.selectionStart, event.currentTarget.selectionEnd]),
418
+ onKeyDown: handleTextareaKeyDown,
419
+ onBlur,
420
+ onFocus,
421
+ placeholder,
422
+ rows,
423
+ maxLength: plainText ? void 0 : maxLength,
424
+ disabled,
425
+ readOnly,
426
+ required,
427
+ hidden: activeTab !== "edit",
428
+ onInvalid: () => {
429
+ if (activeTab === "preview") {
430
+ reactDom.flushSync(() => setActiveTab("edit"));
431
+ }
432
+ },
433
+ "aria-invalid": Boolean(error) || void 0,
434
+ "aria-describedby": messageIds.getDescribedBy(editorId, { error, helpText })
435
+ }
436
+ ),
437
+ activeTab === "preview" && /* @__PURE__ */ jsxRuntime.jsx(
438
+ MarkdownViewer.MarkdownViewer,
439
+ {
440
+ className: styles__default.default.preview,
441
+ markdown: value,
442
+ headingOffset,
443
+ openLinksInNewTab: true,
444
+ newTabLabel: labels.newTab,
445
+ emptyMessage: labels.emptyPreview
446
+ }
447
+ )
448
+ ]
449
+ }
450
+ )
451
+ ]
452
+ }
453
+ ) }),
454
+ /* @__PURE__ */ jsxRuntime.jsx(
455
+ Modal.Modal,
456
+ {
457
+ isOpen: confirmPlainText !== null,
458
+ onRequestClose: () => setConfirmPlainText(null),
459
+ header: labels.plainTextConfirmTitle,
460
+ severity: "warning",
461
+ primaryAction: { label: labels.plainTextConfirmAction, onClick: enterPlainText },
462
+ secondaryAction: { label: labels.cancel, onClick: () => setConfirmPlainText(null) },
463
+ children: [
464
+ (confirmPlainText == null ? void 0 : confirmPlainText.formatting) && labels.plainTextConfirmBody,
465
+ (confirmPlainText == null ? void 0 : confirmPlainText.tooLong) && labels.plainTextTooLongBody
466
+ ].filter(Boolean).join(" ")
467
+ }
468
+ )
469
+ ]
470
+ }
471
+ );
472
+ }
473
+ MarkdownEditor.displayName = "MarkdownEditor";
474
+
475
+ exports.MarkdownEditor = MarkdownEditor;
@@ -0,0 +1,68 @@
1
+ import React from 'react';
2
+ import type { InputContainerProps } from '../../../components/forms/input-container/InputContainer';
3
+ export interface MarkdownEditorLabels {
4
+ editor: string;
5
+ editTab: string;
6
+ previewTab: string;
7
+ toolbar: string;
8
+ bold: string;
9
+ italic: string;
10
+ link: string;
11
+ linkTextPlaceholder: string;
12
+ /** Must contain a space, so an unfinished link is never rendered as a link. */
13
+ linkUrlPlaceholder: string;
14
+ heading: string;
15
+ bulletList: string;
16
+ literal: string;
17
+ plainText: string;
18
+ emptyPreview: string;
19
+ newTab: string;
20
+ plainTextConfirmTitle: string;
21
+ plainTextConfirmBody: string;
22
+ plainTextTooLongBody: string;
23
+ plainTextConfirmAction: string;
24
+ cancel: string;
25
+ }
26
+ export interface MarkdownEditorProps extends Omit<InputContainerProps, 'children' | 'htmlFor'> {
27
+ /** Raw Markdown, including visible backslash escapes. */
28
+ value: string;
29
+ onChange: (value: string) => void;
30
+ onBlur?: React.FocusEventHandler<HTMLTextAreaElement>;
31
+ onFocus?: React.FocusEventHandler<HTMLTextAreaElement>;
32
+ ref?: React.Ref<HTMLTextAreaElement>;
33
+ /** Submits the Markdown `value` with native forms, also in plain text mode. */
34
+ name?: string;
35
+ placeholder?: string;
36
+ /** Visible text rows; the preview keeps at least the same height. */
37
+ rows?: number;
38
+ /**
39
+ * Maximum length of the stored Markdown `value`. In plain text mode escapes count too, so
40
+ * slightly fewer characters can be typed when the text contains Markdown characters.
41
+ */
42
+ maxLength?: number;
43
+ disabled?: boolean;
44
+ readOnly?: boolean;
45
+ id?: string;
46
+ className?: string;
47
+ /**
48
+ * Start in plain text mode, where the formatting toolbar is hidden and the user types plain
49
+ * text. `value` stays Markdown: syntax characters and line breaks are escaped automatically, so
50
+ * it renders exactly as typed. Ignored when `value` already contains formatting, so it is never
51
+ * lost silently; formatting set from outside later also switches back to Markdown. The user can
52
+ * switch modes with the "Ren tekst" switch.
53
+ */
54
+ defaultPlainText?: boolean;
55
+ /**
56
+ * Called when the user switches between Markdown and plain text, e.g. to remember the choice. Not
57
+ * called when formatting set from outside switches the editor back to Markdown.
58
+ */
59
+ onPlainTextChange?: (plainText: boolean) => void;
60
+ /** Added to heading levels in the preview, e.g. `1` previews `#` as `<h2>`. */
61
+ headingOffset?: number;
62
+ /** Overrides for the built-in Danish texts. */
63
+ labels?: Partial<MarkdownEditorLabels>;
64
+ }
65
+ export declare function MarkdownEditor({ value, onChange, onBlur, onFocus, ref, name, placeholder, rows, maxLength, disabled, readOnly, id, className, defaultPlainText, onPlainTextChange, headingOffset, labels: labelOverrides, label, error, helpText, helpTextAddition, fullWidth, required, orientation, modified, modifiedLabel, labelAction, size, labelWidth, labelAlignment, }: MarkdownEditorProps): React.ReactElement;
66
+ export declare namespace MarkdownEditor {
67
+ var displayName: string;
68
+ }