@knime/kds-rich-text-editor 0.30.4

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 (35) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +710 -0
  3. package/README.md +116 -0
  4. package/dist/index.css +282 -0
  5. package/dist/index.js +917 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/src/KdsRichTextEditor.vue.d.ts +35 -0
  8. package/dist/src/KdsRichTextEditor.vue.d.ts.map +1 -0
  9. package/dist/src/index.d.ts +6 -0
  10. package/dist/src/index.d.ts.map +1 -0
  11. package/dist/src/toolbar/KdsCreateLinkModal.vue.d.ts +459 -0
  12. package/dist/src/toolbar/KdsCreateLinkModal.vue.d.ts.map +1 -0
  13. package/dist/src/toolbar/RichTextEditorBaseToolbar.vue.d.ts +48 -0
  14. package/dist/src/toolbar/RichTextEditorBaseToolbar.vue.d.ts.map +1 -0
  15. package/dist/src/toolbar/RichTextEditorToolbar.vue.d.ts +10 -0
  16. package/dist/src/toolbar/RichTextEditorToolbar.vue.d.ts.map +1 -0
  17. package/dist/src/toolbar/types.d.ts +29 -0
  18. package/dist/src/toolbar/types.d.ts.map +1 -0
  19. package/dist/src/toolbar/useLinkTool.d.ts +22 -0
  20. package/dist/src/toolbar/useLinkTool.d.ts.map +1 -0
  21. package/dist/src/types.d.ts +112 -0
  22. package/dist/src/types.d.ts.map +1 -0
  23. package/dist/src/utils/custom-escape.d.ts +15 -0
  24. package/dist/src/utils/custom-escape.d.ts.map +1 -0
  25. package/dist/src/utils/custom-hard-break.d.ts +2 -0
  26. package/dist/src/utils/custom-hard-break.d.ts.map +1 -0
  27. package/dist/src/utils/custom-link.d.ts +16 -0
  28. package/dist/src/utils/custom-link.d.ts.map +1 -0
  29. package/dist/src/utils/custom-text-align.d.ts +2 -0
  30. package/dist/src/utils/custom-text-align.d.ts.map +1 -0
  31. package/dist/src/utils/paragraphTextStyle/extension.d.ts +17 -0
  32. package/dist/src/utils/paragraphTextStyle/extension.d.ts.map +1 -0
  33. package/dist/src/utils/paragraphTextStyle/index.d.ts +4 -0
  34. package/dist/src/utils/paragraphTextStyle/index.d.ts.map +1 -0
  35. package/package.json +76 -0
package/dist/index.js ADDED
@@ -0,0 +1,917 @@
1
+ import { defineComponent, ref, watch, nextTick, computed, openBlock, createBlock, unref, withCtx, createElementVNode, normalizeClass, createVNode, createCommentVNode, onMounted, onUnmounted, createElementBlock, Fragment, renderSlot, renderList, withModifiers, withKeys, useCssVars, useSlots, useModel, useId, Transition, mergeProps, toHandlers, mergeModels } from 'vue';
2
+ import CharacterCount from '@tiptap/extension-character-count';
3
+ import StarterKit from '@tiptap/starter-kit';
4
+ import { Extension, useEditor, EditorContent } from '@tiptap/vue-3';
5
+ import { KdsModal, KdsButton, KdsTextInput, KdsToggleButton, KdsMenuButton, KdsSubText } from '@knime/kds-components';
6
+ import HardBreak from '@tiptap/extension-hard-break';
7
+ import { getAttributes } from '@tiptap/core';
8
+ import Link from '@tiptap/extension-link';
9
+ import { Plugin, PluginKey } from '@tiptap/pm/state';
10
+ import TextAlign from '@tiptap/extension-text-align';
11
+ import Paragraph from '@tiptap/extension-paragraph';
12
+
13
+ import './index.css';const isMac = () => navigator?.userAgent?.toLowerCase()?.includes("mac");
14
+
15
+ const getMetaOrCtrlKey = () => {
16
+ return isMac() ? "metaKey" : "ctrlKey";
17
+ };
18
+
19
+ const ExternalLinkIconUrl = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='none'%20stroke='currentColor'%20stroke-linecap='round'%20stroke-linejoin='round'%20viewBox='0%200%2012%2012'%3e%3cpath%20d='M5.5%201.5h-3a1%201%200%200%200-1%201v7a1%201%200%200%200%201%201h7a1%201%200%200%200%201-1v-3m0-5L6%205.892M10.5%201.5v2.75m0-2.75H7.75'%20vector-effect='non-scaling-stroke'/%3e%3c/svg%3e";
20
+
21
+ const _hoisted_1$2 = { class: "controls-apply-group" };
22
+ const _sfc_main$3 = /* @__PURE__ */ defineComponent({
23
+ __name: "KdsCreateLinkModal",
24
+ props: {
25
+ text: {},
26
+ url: {},
27
+ isActive: {},
28
+ isEdit: {},
29
+ urlValidator: { type: Function }
30
+ },
31
+ emits: ["addLink", "removeLink", "cancelAddLink"],
32
+ setup(__props, { emit: __emit }) {
33
+ const props = __props;
34
+ const inputRef = ref(null);
35
+ const editedText = ref(props.text.value);
36
+ const editedUrl = ref(props.url.value);
37
+ watch(
38
+ () => props.isActive.value,
39
+ async (isOpen) => {
40
+ if (!isOpen) {
41
+ return;
42
+ }
43
+ editedText.value = props.text.value;
44
+ editedUrl.value = props.url.value;
45
+ await nextTick();
46
+ requestAnimationFrame(() => {
47
+ inputRef.value?.focus();
48
+ });
49
+ }
50
+ );
51
+ const emit = __emit;
52
+ const closeModal = () => {
53
+ emit("cancelAddLink");
54
+ };
55
+ const onSubmit = () => {
56
+ emit("addLink", editedText.value, editedUrl.value);
57
+ };
58
+ const validateUrl = () => {
59
+ const validScheme = props.urlValidator(editedUrl.value);
60
+ return !editedUrl.value || validScheme;
61
+ };
62
+ const isValid = computed(() => {
63
+ const textChanged = editedText.value !== props.text.value;
64
+ const urlChanged = editedUrl.value !== props.url.value;
65
+ const urlNotEmpty = editedUrl.value !== "";
66
+ return (textChanged || urlChanged) && validateUrl() && urlNotEmpty;
67
+ });
68
+ const onkeyup = (keyupEvent) => {
69
+ if (keyupEvent.key === "Enter" && isValid.value) {
70
+ onSubmit();
71
+ }
72
+ };
73
+ return (_ctx, _cache) => {
74
+ return openBlock(), createBlock(unref(KdsModal), {
75
+ active: __props.isActive.value,
76
+ headline: __props.isEdit.value ? "Edit link" : "Insert link",
77
+ class: "modal",
78
+ onClose: closeModal
79
+ }, {
80
+ body: withCtx(() => [
81
+ createVNode(unref(KdsTextInput), {
82
+ ref_key: "inputRef",
83
+ ref: inputRef,
84
+ modelValue: editedText.value,
85
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => editedText.value = $event),
86
+ label: "Link text",
87
+ type: "text",
88
+ onKeyup: onkeyup
89
+ }, null, 8, ["modelValue"]),
90
+ createVNode(unref(KdsTextInput), {
91
+ modelValue: editedUrl.value,
92
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => editedUrl.value = $event),
93
+ label: "URL",
94
+ "sub-text": !validateUrl() ? "Enter a valid URL (e.g. https://example.com)" : "",
95
+ error: !validateUrl(),
96
+ "preserve-sub-text-space": true,
97
+ onKeyup: onkeyup
98
+ }, null, 8, ["modelValue", "sub-text", "error"])
99
+ ]),
100
+ footer: withCtx(() => [
101
+ createElementVNode("div", {
102
+ class: normalizeClass(["modal-footer", { "spread-buttons": __props.isEdit.value }])
103
+ }, [
104
+ createVNode(unref(KdsButton), {
105
+ variant: __props.isEdit.value ? "transparent" : "outlined",
106
+ size: "small",
107
+ label: "Cancel",
108
+ onClick: closeModal
109
+ }, null, 8, ["variant"]),
110
+ createElementVNode("div", _hoisted_1$2, [
111
+ __props.isEdit.value ? (openBlock(), createBlock(unref(KdsButton), {
112
+ key: 0,
113
+ variant: "outlined",
114
+ size: "small",
115
+ label: "Remove URL",
116
+ onClick: _cache[2] || (_cache[2] = ($event) => emit("removeLink"))
117
+ })) : createCommentVNode("", true),
118
+ createVNode(unref(KdsButton), {
119
+ variant: "filled",
120
+ size: "small",
121
+ label: __props.isEdit.value ? "Apply" : "Insert link",
122
+ disabled: !isValid.value,
123
+ onClick: onSubmit
124
+ }, null, 8, ["label", "disabled"])
125
+ ])
126
+ ], 2)
127
+ ]),
128
+ _: 1
129
+ }, 8, ["active", "headline"]);
130
+ };
131
+ }
132
+ });
133
+
134
+ const _export_sfc = (sfc, props) => {
135
+ const target = sfc.__vccOpts || sfc;
136
+ for (const [key, val] of props) {
137
+ target[key] = val;
138
+ }
139
+ return target;
140
+ };
141
+
142
+ const KdsCreateLinkModal = /* @__PURE__ */ _export_sfc(_sfc_main$3, [["__scopeId", "data-v-5b224eb0"]]);
143
+
144
+ const clearHeading = (editor) => {
145
+ const maxLevel = 6;
146
+ for (let i = 1; i <= maxLevel; i++) {
147
+ if (editor.isActive("heading", { level: i })) {
148
+ editor.chain().focus().toggleHeading({ level: i }).run();
149
+ break;
150
+ }
151
+ }
152
+ };
153
+ const getHeadingOptionChild = (level, getEditor) => ({
154
+ id: `${level}`,
155
+ text: `Heading ${level}`,
156
+ hotkey: ["Ctrl", "Alt", `${level}`],
157
+ active: () => getEditor().isActive("heading", { level }),
158
+ handler: () => {
159
+ const editor = getEditor();
160
+ editor.chain().focus().toggleHeading({ level }).run();
161
+ }
162
+ });
163
+ const getParagraphTextStyleTool = (getEditor) => {
164
+ return {
165
+ id: "paragraphTextStyle",
166
+ type: "secondary",
167
+ icon: "textstyles",
168
+ text: "Text style",
169
+ children: [
170
+ {
171
+ id: "standard",
172
+ text: "Standard",
173
+ hotkey: ["Ctrl", "Alt", "0"],
174
+ handler: () => {
175
+ const editor = getEditor();
176
+ clearHeading(editor);
177
+ editor.chain().focus().unsetSmallText().run();
178
+ }
179
+ },
180
+ {
181
+ id: "small",
182
+ text: "Small text",
183
+ hotkey: ["Ctrl", "Alt", "−"],
184
+ // this is a wider minus character for aesthetic reasons
185
+ active: () => getEditor().isActive({ smallText: true }),
186
+ handler: () => {
187
+ const editor = getEditor();
188
+ clearHeading(editor);
189
+ editor.chain().focus().setSmallText().run();
190
+ }
191
+ },
192
+ ...Array.from(
193
+ { length: 6 },
194
+ (_, i) => getHeadingOptionChild(i + 1, getEditor)
195
+ )
196
+ ]
197
+ };
198
+ };
199
+
200
+ const useLinkTool = ({
201
+ editor,
202
+ isRegistered,
203
+ linkToolOptions
204
+ }) => {
205
+ if (!isRegistered) {
206
+ return null;
207
+ }
208
+ const isActive = ref(false);
209
+ const text = ref("");
210
+ const url = ref("");
211
+ const isEdit = computed(() => url.value !== "");
212
+ const isReplacingText = ref(false);
213
+ const onLinkToolClick = () => {
214
+ const { view, state } = editor;
215
+ const { from, to } = view.state.selection;
216
+ const currentLink = editor.getAttributes("link").href;
217
+ url.value = currentLink || "";
218
+ if (currentLink) {
219
+ const hasSelection = from !== to;
220
+ if (hasSelection) {
221
+ const newCursorPosition = from + Math.floor((to - from) / 2);
222
+ editor.chain().focus().setTextSelection(newCursorPosition).run();
223
+ }
224
+ const textBefore = view.state.selection.$from.nodeBefore?.textContent ?? "";
225
+ const textAfter = view.state.selection.$to.nodeAfter?.textContent ?? "";
226
+ text.value = textBefore + textAfter;
227
+ } else {
228
+ text.value = state.doc.textBetween(from, to, "");
229
+ }
230
+ isReplacingText.value = Boolean(currentLink || text.value);
231
+ isActive.value = true;
232
+ };
233
+ const onRemoveLink = () => {
234
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
235
+ isActive.value = false;
236
+ isReplacingText.value = false;
237
+ };
238
+ const addCustomLink = ({
239
+ isEditing,
240
+ url: url2,
241
+ urlText,
242
+ text: text2
243
+ }) => {
244
+ if (isEditing) {
245
+ editor.chain().focus().setLink({ href: url2 }).command(({ tr }) => {
246
+ tr.insertText(text2 || urlText);
247
+ return true;
248
+ }).run();
249
+ } else {
250
+ const { view } = editor;
251
+ const { from: startIndex } = view.state.selection;
252
+ editor.chain().focus().insertContent([{ type: "text", text: " " }]).run();
253
+ editor.chain().focus().insertContentAt(startIndex, [{ type: "text", text: text2 || urlText }]).run();
254
+ const { from: endIndex } = view.state.selection;
255
+ editor.commands.setTextSelection({
256
+ from: startIndex,
257
+ to: endIndex
258
+ });
259
+ editor.chain().focus().setLink({ href: url2 }).run();
260
+ }
261
+ };
262
+ const onAddLink = (text2, urlText) => {
263
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
264
+ if (urlText) {
265
+ const url2 = linkToolOptions.sanitizeUrlText?.(urlText) ?? urlText;
266
+ addCustomLink({
267
+ isEditing: isReplacingText.value,
268
+ urlText,
269
+ url: url2,
270
+ text: text2
271
+ });
272
+ }
273
+ isActive.value = false;
274
+ isReplacingText.value = false;
275
+ };
276
+ const onCancelAddLink = () => {
277
+ isActive.value = false;
278
+ isReplacingText.value = false;
279
+ };
280
+ const onKeyDown = (event) => {
281
+ if (!editor?.isFocused) {
282
+ return;
283
+ }
284
+ const ctrlPressed = event[getMetaOrCtrlKey()];
285
+ if (ctrlPressed && event.key === "k") {
286
+ event.preventDefault();
287
+ onLinkToolClick();
288
+ }
289
+ };
290
+ onMounted(() => {
291
+ window.addEventListener("keydown", onKeyDown);
292
+ });
293
+ onUnmounted(() => {
294
+ window.removeEventListener("keydown", onKeyDown);
295
+ });
296
+ return {
297
+ onLinkToolClick,
298
+ props: {
299
+ url,
300
+ text,
301
+ isActive,
302
+ isEdit,
303
+ urlValidator: linkToolOptions.urlValidator
304
+ },
305
+ events: {
306
+ addLink: onAddLink,
307
+ removeLink: onRemoveLink,
308
+ cancelAddLink: onCancelAddLink
309
+ }
310
+ };
311
+ };
312
+
313
+ const _sfc_main$2 = /* @__PURE__ */ defineComponent({
314
+ __name: "RichTextEditorBaseToolbar",
315
+ props: {
316
+ editor: {},
317
+ baseExtensions: {},
318
+ linkToolOptions: {}
319
+ },
320
+ setup(__props) {
321
+ const props = __props;
322
+ const registerTool = (toolName, tool) => {
323
+ if ("all" in props.baseExtensions) {
324
+ return [tool];
325
+ }
326
+ return props.baseExtensions[toolName] ? [tool] : [];
327
+ };
328
+ const isToolRegistered = (toolName) => {
329
+ if ("all" in props.baseExtensions) {
330
+ return true;
331
+ }
332
+ return Boolean(props.baseExtensions[toolName]);
333
+ };
334
+ const isListActive = () => props.editor.isActive("orderedList") || props.editor.isActive("bulletList");
335
+ const linkTool = useLinkTool({
336
+ editor: props.editor,
337
+ isRegistered: isToolRegistered("link"),
338
+ linkToolOptions: props.linkToolOptions
339
+ });
340
+ const editorTools = [
341
+ ...registerTool("bold", {
342
+ id: "bold",
343
+ type: "primary",
344
+ text: "Bold",
345
+ hotkey: ["Ctrl", "B"],
346
+ icon: "bold",
347
+ active: () => props.editor.isActive("bold"),
348
+ handler: () => props.editor.chain().focus().toggleBold().run()
349
+ }),
350
+ ...registerTool("italic", {
351
+ id: "italic",
352
+ type: "primary",
353
+ text: "Italic",
354
+ icon: "italic",
355
+ hotkey: ["Ctrl", "I"],
356
+ active: () => props.editor.isActive("italic"),
357
+ handler: () => props.editor.chain().focus().toggleItalic().run()
358
+ }),
359
+ ...registerTool("underline", {
360
+ id: "underline",
361
+ type: "primary",
362
+ text: "Underline",
363
+ icon: "underline",
364
+ hotkey: ["Ctrl", "U"],
365
+ active: () => props.editor.isActive("underline"),
366
+ handler: () => props.editor.chain().focus().toggleUnderline().run()
367
+ }),
368
+ ...registerTool("bulletList", {
369
+ id: "bulletList",
370
+ type: "primary",
371
+ text: "Bullet list",
372
+ icon: "list-bulletpoint",
373
+ hotkey: ["Ctrl", "Shift", "8"],
374
+ active: () => props.editor.isActive("bulletList"),
375
+ handler: () => {
376
+ const commandChain = props.editor.chain().focus();
377
+ if (isToolRegistered("textAlign")) {
378
+ commandChain.setTextAlign("left");
379
+ }
380
+ commandChain.toggleBulletList().run();
381
+ }
382
+ }),
383
+ ...registerTool("orderedList", {
384
+ id: "orderedList",
385
+ type: "primary",
386
+ text: "Ordered list",
387
+ icon: "list-number",
388
+ hotkey: ["Ctrl", "Shift", "7"],
389
+ active: () => props.editor.isActive("orderedList"),
390
+ handler: () => {
391
+ const commandChain = props.editor.chain().focus();
392
+ if (isToolRegistered("textAlign")) {
393
+ commandChain.setTextAlign("left");
394
+ }
395
+ commandChain.toggleOrderedList().run();
396
+ }
397
+ }),
398
+ ...registerTool("textAlign", {
399
+ id: "align-left",
400
+ type: "primary",
401
+ icon: "left-aligned",
402
+ text: "Align left",
403
+ hotkey: ["Ctrl", "Shift", "L"],
404
+ active: () => !isListActive() && props.editor.isActive({ textAlign: "left" }),
405
+ handler: () => props.editor.chain().focus().setTextAlign("left").run(),
406
+ disabled: () => isListActive()
407
+ }),
408
+ ...registerTool("textAlign", {
409
+ id: "align-center",
410
+ type: "primary",
411
+ icon: "center-aligned",
412
+ text: "Align center",
413
+ hotkey: ["Ctrl", "Shift", "E"],
414
+ active: () => !isListActive() && props.editor.isActive({ textAlign: "center" }),
415
+ handler: () => props.editor.chain().focus().setTextAlign("center").run(),
416
+ disabled: () => isListActive()
417
+ }),
418
+ ...registerTool("textAlign", {
419
+ id: "align-right",
420
+ type: "primary",
421
+ icon: "right-aligned",
422
+ text: "Align right",
423
+ hotkey: ["Ctrl", "Shift", "R"],
424
+ active: () => !isListActive() && props.editor.isActive({ textAlign: "right" }),
425
+ handler: () => props.editor.chain().focus().setTextAlign("right").run(),
426
+ disabled: () => isListActive()
427
+ }),
428
+ ...registerTool("link", {
429
+ id: "link",
430
+ type: "primary",
431
+ icon: "link",
432
+ text: "Link",
433
+ hotkey: ["Ctrl", "K"],
434
+ active: () => props.editor.isActive("link"),
435
+ handler: () => linkTool?.onLinkToolClick()
436
+ }),
437
+ ...registerTool(
438
+ "paragraphTextStyle",
439
+ getParagraphTextStyleTool(() => props.editor)
440
+ ),
441
+ ...registerTool("blockquote", {
442
+ id: "blockquote",
443
+ type: "secondary",
444
+ icon: "quote",
445
+ text: "Blockquote",
446
+ active: () => props.editor.isActive("blockquote"),
447
+ handler: () => props.editor.chain().focus().toggleBlockquote().run()
448
+ }),
449
+ ...registerTool("codeBlock", {
450
+ id: "codeBlock",
451
+ type: "secondary",
452
+ icon: "code-block",
453
+ text: "Code block",
454
+ hotkey: ["Ctrl", "Alt", "C"],
455
+ active: () => props.editor.isActive("codeBlock"),
456
+ handler: () => props.editor.chain().focus().toggleCodeBlock().run()
457
+ }),
458
+ ...registerTool("strike", {
459
+ id: "strikethrough",
460
+ type: "secondary",
461
+ icon: "strikethrough",
462
+ text: "Strikethrough",
463
+ hotkey: ["Ctrl", "Shift", "X"],
464
+ active: () => props.editor.isActive("strike"),
465
+ handler: () => props.editor.chain().focus().toggleStrike().run()
466
+ }),
467
+ ...registerTool("horizontalRule", {
468
+ id: "horizontalRule",
469
+ type: "secondary",
470
+ icon: "divider",
471
+ text: "Divider",
472
+ active: () => false,
473
+ handler: () => props.editor.chain().focus().setHorizontalRule().run()
474
+ })
475
+ ];
476
+ return (_ctx, _cache) => {
477
+ return openBlock(), createElementBlock(Fragment, null, [
478
+ renderSlot(_ctx.$slots, "default", { tools: editorTools }),
479
+ renderSlot(_ctx.$slots, "linkModal", { linkTool: unref(linkTool) })
480
+ ], 64);
481
+ };
482
+ }
483
+ });
484
+
485
+ const _hoisted_1$1 = { class: "tools" };
486
+ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
487
+ __name: "RichTextEditorToolbar",
488
+ props: {
489
+ editor: {},
490
+ tools: {},
491
+ hotkeyFormatter: { type: Function }
492
+ },
493
+ setup(__props) {
494
+ const props = __props;
495
+ const baseTools = computed(
496
+ () => props.tools.filter(
497
+ ({ type }) => type === "primary"
498
+ )
499
+ );
500
+ const secondaryTools = computed(
501
+ () => props.tools.filter(
502
+ ({ type }) => type === "secondary"
503
+ )
504
+ );
505
+ const secondaryToolsMenuItems = computed(() => {
506
+ const tools = secondaryTools.value.map((tool) => {
507
+ const base = {
508
+ id: tool.id,
509
+ text: tool.text,
510
+ disabled: tool.disabled?.(),
511
+ shortcut: props.hotkeyFormatter(tool.hotkey ?? []),
512
+ accessory: {
513
+ type: "icon",
514
+ name: tool.icon
515
+ }
516
+ };
517
+ if (tool.children) {
518
+ return {
519
+ ...base,
520
+ children: tool.children.map((toolChild) => {
521
+ return {
522
+ id: toolChild.id,
523
+ text: toolChild.text,
524
+ shortcut: props.hotkeyFormatter(toolChild.hotkey ?? []),
525
+ handler: () => toolChild.handler(toolChild.id)
526
+ };
527
+ })
528
+ };
529
+ }
530
+ return {
531
+ ...base,
532
+ handler: () => tool.handler(tool.id)
533
+ };
534
+ });
535
+ return tools;
536
+ });
537
+ return (_ctx, _cache) => {
538
+ return openBlock(), createElementBlock("div", _hoisted_1$1, [
539
+ (openBlock(true), createElementBlock(Fragment, null, renderList(baseTools.value, (tool) => {
540
+ return openBlock(), createBlock(unref(KdsToggleButton), {
541
+ key: tool.id,
542
+ class: "tool",
543
+ ariaLabel: tool.text,
544
+ "data-testid": tool.id,
545
+ disabled: tool.disabled?.(),
546
+ toggled: tool.active?.(),
547
+ "leading-icon": tool.icon,
548
+ title: __props.hotkeyFormatter(tool.hotkey ?? []),
549
+ variant: "transparent",
550
+ size: "small",
551
+ onKeydown: withKeys(withModifiers(($event) => tool.handler?.(tool.id), ["prevent"]), ["enter"]),
552
+ onClick: withModifiers(($event) => tool.handler?.(tool.id), ["stop"])
553
+ }, null, 8, ["ariaLabel", "data-testid", "disabled", "toggled", "leading-icon", "title", "onKeydown", "onClick"]);
554
+ }), 128)),
555
+ secondaryTools.value.length > 0 ? (openBlock(), createBlock(unref(KdsMenuButton), {
556
+ key: 0,
557
+ items: secondaryToolsMenuItems.value,
558
+ "leading-icon": "more-actions",
559
+ ariaLabel: "More actions",
560
+ variant: "transparent",
561
+ size: "small"
562
+ }, null, 8, ["items"])) : createCommentVNode("", true)
563
+ ]);
564
+ };
565
+ }
566
+ });
567
+
568
+ const RichTextEditorToolbar = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-f4d33607"]]);
569
+
570
+ const CustomEscape = Extension.create({
571
+ name: "OverrideEscape",
572
+ addOptions() {
573
+ return {
574
+ handler: () => false
575
+ };
576
+ },
577
+ addKeyboardShortcuts() {
578
+ return {
579
+ Escape: this.options.handler
580
+ };
581
+ }
582
+ });
583
+
584
+ const CustomHardBreak = HardBreak.extend({
585
+ addKeyboardShortcuts() {
586
+ return {
587
+ "Shift-Enter": () => this.editor.commands.setHardBreak()
588
+ };
589
+ }
590
+ });
591
+
592
+ const buildUrlRegex = () => {
593
+ const httpPrefixCheck = "(http://www\\.|https://www\\.|http://|https://)";
594
+ const urlCheck = "[a-z0-9]+([-.]{1}[a-z0-9]+)*\\.[a-z]{2,5}(:[0-9]{1,5})?(/.*)?";
595
+ return new RegExp(`^${httpPrefixCheck}?${urlCheck}$`);
596
+ };
597
+ const URL_REGEX = buildUrlRegex();
598
+ const validateURL = (href) => URL_REGEX.test(href);
599
+ const kdsDefaultLinkToolOptions = {
600
+ urlValidator: validateURL,
601
+ sanitizeUrlText: (urlText) => {
602
+ const containsHttp = ["http://", "https://"].some(
603
+ (protocol) => urlText.includes(protocol)
604
+ );
605
+ return containsHttp ? urlText : `https://${urlText}`;
606
+ }
607
+ };
608
+ const CustomLink = Link.extend({
609
+ addAttributes() {
610
+ const modifier = isMac() ? "⌘" : "Ctrl +";
611
+ return {
612
+ ...this.parent?.(),
613
+ draggable: {
614
+ default: "false"
615
+ },
616
+ title: {
617
+ default: `Use "${modifier} click" to follow link`
618
+ },
619
+ rel: {
620
+ default: "ugc noopener noreferrer nofollow"
621
+ }
622
+ };
623
+ },
624
+ addProseMirrorPlugins() {
625
+ const plugins = this.parent?.() || [];
626
+ const ctrlClickHandler = new Plugin({
627
+ key: new PluginKey("handleControlClick"),
628
+ props: {
629
+ handleClick(view, _pos, event) {
630
+ const attrs = getAttributes(view.state, "link");
631
+ const link = event.target?.closest("a");
632
+ const metaOrCtrlKeyPressed = event[getMetaOrCtrlKey()];
633
+ if (metaOrCtrlKeyPressed && link && attrs.href) {
634
+ window.open(attrs.href, attrs.target);
635
+ }
636
+ return true;
637
+ }
638
+ }
639
+ });
640
+ plugins.push(ctrlClickHandler);
641
+ return plugins;
642
+ }
643
+ });
644
+
645
+ const CustomTextAlign = TextAlign.extend({
646
+ addKeyboardShortcuts() {
647
+ const canActivate = () => {
648
+ return !(this.editor.isActive("bulletList") || this.editor.isActive("orderedList"));
649
+ };
650
+ return {
651
+ "Mod-Shift-l": () => canActivate() && this.editor.commands.setTextAlign("left"),
652
+ "Mod-Shift-e": () => canActivate() && this.editor.commands.setTextAlign("center"),
653
+ "Mod-Shift-r": () => canActivate() && this.editor.commands.setTextAlign("right")
654
+ };
655
+ }
656
+ });
657
+
658
+ const SmallText = Paragraph.extend({
659
+ addAttributes() {
660
+ const smallTextClass = "small-text";
661
+ return {
662
+ ...this.parent?.(),
663
+ smallText: {
664
+ default: false,
665
+ parseHTML: (element) => element.className === smallTextClass,
666
+ renderHTML: (attributes) => {
667
+ return attributes.smallText ? {
668
+ class: smallTextClass
669
+ } : {};
670
+ }
671
+ }
672
+ };
673
+ },
674
+ addCommands() {
675
+ return {
676
+ ...this.parent?.(),
677
+ setSmallText: () => ({ commands }) => {
678
+ return commands.toggleNode(this.name, "paragraph", {
679
+ smallText: true
680
+ });
681
+ },
682
+ unsetSmallText: () => ({ chain }) => {
683
+ return chain().updateAttributes(this.name, { smallText: false }).run();
684
+ }
685
+ };
686
+ },
687
+ addKeyboardShortcuts() {
688
+ return {
689
+ ...this.parent?.(),
690
+ "Mod-Alt--": () => this.editor.commands.setSmallText()
691
+ };
692
+ }
693
+ });
694
+
695
+ const _hoisted_1 = { key: 0 };
696
+ const MIN_CHARACTER_LIMIT = 200;
697
+ const _sfc_main = /* @__PURE__ */ defineComponent({
698
+ __name: "KdsRichTextEditor",
699
+ props: /* @__PURE__ */ mergeModels({
700
+ editable: { type: Boolean, default: true },
701
+ minHeight: { default: null },
702
+ maxHeight: { default: null },
703
+ baseExtensions: { default: () => ({}) },
704
+ hotkeyFormatter: { type: Function, default: (hotkey) => {
705
+ const MacOSkeyMap = {
706
+ Shift: "⇧",
707
+ Ctrl: "⌘",
708
+ Alt: "⌥"
709
+ };
710
+ const mapSymbols = (key) => MacOSkeyMap[key] || key;
711
+ const identityFn = (value) => value;
712
+ return hotkey.map(isMac() ? mapSymbols : identityFn).join(" ");
713
+ } },
714
+ customExtensions: { default: () => [] },
715
+ autofocus: { type: Boolean, default: false },
716
+ withBorder: { type: Boolean, default: true },
717
+ disabled: { type: Boolean, default: false },
718
+ linkToolOptions: { default: () => kdsDefaultLinkToolOptions },
719
+ characterLimit: { default: 1e3 },
720
+ showCharacterCount: { type: Boolean, default: false },
721
+ customEscapeHandler: { type: Function, default: void 0 },
722
+ ariaLabel: { default: () => void 0 },
723
+ ariaLabelledby: { default: () => void 0 }
724
+ }, {
725
+ "modelValue": {},
726
+ "modelModifiers": {}
727
+ }),
728
+ emits: /* @__PURE__ */ mergeModels(["blur"], ["update:modelValue"]),
729
+ setup(__props, { expose: __expose, emit: __emit }) {
730
+ useCssVars((_ctx) => ({
731
+ "f4686d76": maxHeightStyle.value,
732
+ "a618f0d2": minHeightStyle.value,
733
+ "c15f49ba": externalIcon
734
+ }));
735
+ const hardCharacterLimit = Math.max(__props.characterLimit, MIN_CHARACTER_LIMIT);
736
+ const slots = useSlots();
737
+ const externalIcon = `url("${ExternalLinkIconUrl}")`;
738
+ const modelValue = useModel(__props, "modelValue");
739
+ const emit = __emit;
740
+ const isToolEnabled = (extensionName) => {
741
+ if ("all" in __props.baseExtensions) {
742
+ return true;
743
+ }
744
+ return Boolean(__props.baseExtensions[extensionName]);
745
+ };
746
+ const getStarterKitExtensionConfig = (extensionName) => {
747
+ return isToolEnabled(extensionName) ? void 0 : false;
748
+ };
749
+ const extensions = [
750
+ StarterKit.configure({
751
+ bold: getStarterKitExtensionConfig("bold"),
752
+ italic: getStarterKitExtensionConfig("italic"),
753
+ bulletList: getStarterKitExtensionConfig("bulletList"),
754
+ orderedList: getStarterKitExtensionConfig("orderedList"),
755
+ heading: getStarterKitExtensionConfig("heading"),
756
+ blockquote: getStarterKitExtensionConfig("blockquote"),
757
+ code: getStarterKitExtensionConfig("code"),
758
+ codeBlock: getStarterKitExtensionConfig("codeBlock"),
759
+ horizontalRule: getStarterKitExtensionConfig("horizontalRule"),
760
+ strike: getStarterKitExtensionConfig("strike"),
761
+ underline: getStarterKitExtensionConfig("underline"),
762
+ ...isToolEnabled("paragraphTextStyle") && { paragraph: false },
763
+ hardBreak: false,
764
+ // StarterKit v3 bundles a link extension, but we register our own
765
+ // CustomLink (opt-in via baseExtensions) for the custom validation,
766
+ // attributes and ctrl/cmd-click handling, so the bundled one is disabled
767
+ // to avoid a duplicate-extension conflict.
768
+ link: false
769
+ }),
770
+ ...isToolEnabled("paragraphTextStyle") ? [SmallText] : [],
771
+ ...isToolEnabled("textAlign") ? [
772
+ CustomTextAlign.configure({
773
+ types: [
774
+ isToolEnabled("heading") ? "heading" : "",
775
+ "paragraph"
776
+ ].filter(Boolean),
777
+ alignments: ["left", "right", "center"]
778
+ })
779
+ ] : [],
780
+ ...isToolEnabled("link") ? [CustomLink.configure({ validate: validateURL, openOnClick: false })] : [],
781
+ CustomHardBreak,
782
+ CharacterCount.configure({
783
+ limit: hardCharacterLimit
784
+ }),
785
+ ...__props.customEscapeHandler ? [CustomEscape.configure({ handler: __props.customEscapeHandler })] : [],
786
+ ...__props.customExtensions
787
+ ];
788
+ const editor = useEditor({
789
+ content: modelValue.value,
790
+ editable: __props.editable && !__props.disabled,
791
+ autofocus: __props.autofocus,
792
+ extensions,
793
+ editorProps: {
794
+ attributes: {
795
+ ...__props.ariaLabel ? { "aria-label": __props.ariaLabel } : {},
796
+ ...__props.ariaLabelledby ? { "aria-labelledby": __props.ariaLabelledby } : {}
797
+ }
798
+ },
799
+ onUpdate: () => {
800
+ modelValue.value = editor.value?.getHTML() ?? "";
801
+ },
802
+ onBlur: () => emit("blur")
803
+ });
804
+ const minHeightStyle = computed(
805
+ () => __props.minHeight ? `${__props.minHeight}px` : "initial"
806
+ );
807
+ const focus = () => {
808
+ editor.value?.commands?.focus?.();
809
+ };
810
+ __expose({ focus });
811
+ const maxHeightStyle = computed(
812
+ () => __props.maxHeight ? `${__props.maxHeight}px` : "initial"
813
+ );
814
+ watch(modelValue, (newValue) => {
815
+ if (!editor.value) {
816
+ return;
817
+ }
818
+ if (editor.value.getHTML() === newValue) {
819
+ return;
820
+ }
821
+ editor.value.commands.setContent(newValue ?? "");
822
+ });
823
+ watch(
824
+ [() => __props.editable, () => __props.disabled],
825
+ ([newEditable, newDisabled], [oldEditable, oldDisabled]) => {
826
+ if (!editor.value) {
827
+ return;
828
+ }
829
+ const oldValue = oldEditable && !oldDisabled;
830
+ const newValue = newEditable && !newDisabled;
831
+ if (oldValue === newValue) {
832
+ return;
833
+ }
834
+ editor.value.setEditable(newValue);
835
+ if (__props.autofocus) {
836
+ focus();
837
+ }
838
+ }
839
+ );
840
+ onMounted(async () => {
841
+ if (__props.editable && __props.autofocus) {
842
+ await nextTick();
843
+ focus();
844
+ }
845
+ });
846
+ const hasCustomToolbar = slots.customToolbar;
847
+ const hasTools = computed(() => Object.keys(__props.baseExtensions).length);
848
+ const characterCountId = useId();
849
+ return (_ctx, _cache) => {
850
+ return openBlock(), createElementBlock("div", null, [
851
+ createElementVNode("div", {
852
+ class: normalizeClass(["editor-wrapper", { "with-border": __props.withBorder, disabled: __props.disabled }])
853
+ }, [
854
+ hasTools.value ? (openBlock(), createBlock(Transition, {
855
+ key: 0,
856
+ name: "expand",
857
+ css: !unref(hasCustomToolbar)
858
+ }, {
859
+ default: withCtx(() => [
860
+ unref(editor) && __props.editable && !__props.disabled ? (openBlock(), createElementBlock("div", {
861
+ key: 0,
862
+ class: normalizeClass([
863
+ "toolbar-wrapper",
864
+ { "embedded-toolbar": !unref(hasCustomToolbar) }
865
+ ])
866
+ }, [
867
+ createVNode(_sfc_main$2, {
868
+ editor: unref(editor),
869
+ "base-extensions": __props.baseExtensions,
870
+ "link-tool-options": __props.linkToolOptions
871
+ }, {
872
+ default: withCtx(({ tools }) => [
873
+ renderSlot(_ctx.$slots, "customToolbar", {
874
+ editor: unref(editor),
875
+ tools,
876
+ hotkeyFormatter: __props.hotkeyFormatter
877
+ }, () => [
878
+ createVNode(RichTextEditorToolbar, {
879
+ editor: unref(editor),
880
+ tools,
881
+ "hotkey-formatter": __props.hotkeyFormatter
882
+ }, null, 8, ["editor", "tools", "hotkey-formatter"])
883
+ ], true)
884
+ ]),
885
+ linkModal: withCtx(({ linkTool }) => [
886
+ isToolEnabled("link") && linkTool ? (openBlock(), createBlock(KdsCreateLinkModal, mergeProps({ key: 0 }, linkTool.props, toHandlers(linkTool.events)), null, 16)) : createCommentVNode("", true)
887
+ ]),
888
+ _: 3
889
+ }, 8, ["editor", "base-extensions", "link-tool-options"])
890
+ ], 2)) : createCommentVNode("", true)
891
+ ]),
892
+ _: 3
893
+ }, 8, ["css"])) : createCommentVNode("", true),
894
+ createVNode(unref(EditorContent), {
895
+ class: normalizeClass(["rich-text-editor", { editable: __props.editable && !__props.disabled }]),
896
+ editor: unref(editor),
897
+ "aria-disabled": __props.disabled || void 0
898
+ }, null, 8, ["editor", "class", "aria-disabled"])
899
+ ], 2),
900
+ unref(editor) && __props.editable && __props.showCharacterCount ? (openBlock(), createElementBlock("div", _hoisted_1, [
901
+ createVNode(unref(KdsSubText), {
902
+ id: unref(characterCountId),
903
+ "sub-text": `${unref(editor).storage.characterCount.characters()} / ${unref(hardCharacterLimit)} characters`,
904
+ error: false,
905
+ validating: false,
906
+ "preserve-sub-text-space": true
907
+ }, null, 8, ["id", "sub-text"])
908
+ ])) : createCommentVNode("", true)
909
+ ]);
910
+ };
911
+ }
912
+ });
913
+
914
+ const KdsRichTextEditor = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-06a9c7b9"]]);
915
+
916
+ export { KdsCreateLinkModal, KdsRichTextEditor, kdsDefaultLinkToolOptions };
917
+ //# sourceMappingURL=index.js.map