@synerise/ds-rich-text 0.1.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +173 -0
  4. package/dist/RichText.d.ts +4 -0
  5. package/dist/RichText.js +179 -0
  6. package/dist/RichText.styles.d.ts +31 -0
  7. package/dist/RichText.styles.js +87 -0
  8. package/dist/RichText.types.d.ts +139 -0
  9. package/dist/RichText.types.js +44 -0
  10. package/dist/blockModel/blockModel.types.d.ts +130 -0
  11. package/dist/blockModel/blockModel.types.js +1 -0
  12. package/dist/blockModel/index.d.ts +2 -0
  13. package/dist/blockModel/index.js +14 -0
  14. package/dist/blockModel/serializers/documentToHtml.d.ts +2 -0
  15. package/dist/blockModel/serializers/documentToHtml.js +101 -0
  16. package/dist/blockModel/serializers/documentToMarkdown.d.ts +2 -0
  17. package/dist/blockModel/serializers/documentToMarkdown.js +84 -0
  18. package/dist/blockModel/serializers/documentToTiptap.d.ts +2 -0
  19. package/dist/blockModel/serializers/documentToTiptap.js +133 -0
  20. package/dist/blockModel/serializers/htmlToDocument.d.ts +2 -0
  21. package/dist/blockModel/serializers/htmlToDocument.js +242 -0
  22. package/dist/blockModel/serializers/index.d.ts +6 -0
  23. package/dist/blockModel/serializers/index.js +14 -0
  24. package/dist/blockModel/serializers/markdownToDocument.d.ts +10 -0
  25. package/dist/blockModel/serializers/markdownToDocument.js +11 -0
  26. package/dist/blockModel/serializers/tiptapToDocument.d.ts +2 -0
  27. package/dist/blockModel/serializers/tiptapToDocument.js +189 -0
  28. package/dist/components/ImagePopover.d.ts +3 -0
  29. package/dist/components/ImagePopover.js +89 -0
  30. package/dist/components/LinkPopover.d.ts +3 -0
  31. package/dist/components/LinkPopover.js +57 -0
  32. package/dist/components/Toolbar.d.ts +3 -0
  33. package/dist/components/Toolbar.js +436 -0
  34. package/dist/components/index.d.ts +3 -0
  35. package/dist/components/index.js +8 -0
  36. package/dist/extensions/CodeSnippet.d.ts +19 -0
  37. package/dist/extensions/CodeSnippet.js +76 -0
  38. package/dist/index.d.ts +6 -0
  39. package/dist/index.js +52 -0
  40. package/dist/modules.d.js +1 -0
  41. package/dist/modules.d.ts +4 -0
  42. package/dist/utils/computeVisibleCount.d.ts +6 -0
  43. package/dist/utils/computeVisibleCount.js +19 -0
  44. package/dist/utils/getExtensions.d.ts +13 -0
  45. package/dist/utils/getExtensions.js +80 -0
  46. package/dist/utils/index.d.ts +3 -0
  47. package/dist/utils/index.js +8 -0
  48. package/dist/utils/normalizeUrl.d.ts +7 -0
  49. package/dist/utils/normalizeUrl.js +13 -0
  50. package/dist/utils/sanitizeUrl.d.ts +9 -0
  51. package/dist/utils/sanitizeUrl.js +30 -0
  52. package/package.json +75 -0
@@ -0,0 +1,242 @@
1
+ import { sanitizeUrl } from "../../utils/sanitizeUrl.js";
2
+ const HEADING_TAGS = /* @__PURE__ */ new Set(["H1", "H2", "H3", "H4", "H5", "H6"]);
3
+ const INLINE_MARK_TAGS = {
4
+ STRONG: "bold",
5
+ B: "bold",
6
+ EM: "italic",
7
+ I: "italic",
8
+ U: "underline",
9
+ S: "strike",
10
+ DEL: "strike",
11
+ CODE: "code"
12
+ };
13
+ const getTextAlign = (el) => {
14
+ const align = el.style.textAlign;
15
+ if (align === "left" || align === "center" || align === "right") {
16
+ return align;
17
+ }
18
+ return void 0;
19
+ };
20
+ const parseInlineNodes = (node, inheritedMarks = []) => {
21
+ const result = [];
22
+ for (const child of Array.from(node.childNodes)) {
23
+ if (child.nodeType === 3) {
24
+ const text = child.textContent || "";
25
+ if (text) {
26
+ result.push({
27
+ type: "text",
28
+ text,
29
+ ...inheritedMarks.length && {
30
+ marks: [...inheritedMarks]
31
+ }
32
+ });
33
+ }
34
+ continue;
35
+ }
36
+ if (child.nodeType !== 1) {
37
+ continue;
38
+ }
39
+ const el = child;
40
+ const tag = el.tagName;
41
+ if (tag === "BR") {
42
+ result.push({
43
+ type: "hardBreak"
44
+ });
45
+ continue;
46
+ }
47
+ if (tag === "A") {
48
+ const linkMark = {
49
+ type: "link",
50
+ // Reject javascript:/data:/… so parsed HTML can't smuggle a script href.
51
+ href: sanitizeUrl(el.getAttribute("href") || ""),
52
+ ...el.getAttribute("target") && {
53
+ target: el.getAttribute("target")
54
+ },
55
+ ...el.getAttribute("rel") && {
56
+ rel: el.getAttribute("rel")
57
+ }
58
+ };
59
+ result.push(...parseInlineNodes(el, [...inheritedMarks, linkMark]));
60
+ continue;
61
+ }
62
+ const markType = INLINE_MARK_TAGS[tag];
63
+ if (markType) {
64
+ const mark = {
65
+ type: markType
66
+ };
67
+ result.push(...parseInlineNodes(el, [...inheritedMarks, mark]));
68
+ continue;
69
+ }
70
+ result.push(...parseInlineNodes(el, inheritedMarks));
71
+ }
72
+ return result;
73
+ };
74
+ const parseBlock = (el) => {
75
+ const tag = el.tagName;
76
+ if (tag === "P") {
77
+ const textAlign = getTextAlign(el);
78
+ return {
79
+ type: "paragraph",
80
+ ...textAlign && {
81
+ attrs: {
82
+ textAlign
83
+ }
84
+ },
85
+ children: parseInlineNodes(el)
86
+ };
87
+ }
88
+ if (HEADING_TAGS.has(tag)) {
89
+ const level = parseInt(tag[1], 10);
90
+ const textAlign = getTextAlign(el);
91
+ return {
92
+ type: "heading",
93
+ attrs: {
94
+ level,
95
+ ...textAlign && {
96
+ textAlign
97
+ }
98
+ },
99
+ children: parseInlineNodes(el)
100
+ };
101
+ }
102
+ if (tag === "UL") {
103
+ return {
104
+ type: "bulletList",
105
+ children: Array.from(el.children).filter((li) => li.tagName === "LI").map((li) => ({
106
+ type: "listItem",
107
+ children: parseListItemChildren(li)
108
+ }))
109
+ };
110
+ }
111
+ if (tag === "OL") {
112
+ const start = el.getAttribute("start");
113
+ return {
114
+ type: "orderedList",
115
+ ...start && parseInt(start, 10) !== 1 && {
116
+ attrs: {
117
+ start: parseInt(start, 10)
118
+ }
119
+ },
120
+ children: Array.from(el.children).filter((li) => li.tagName === "LI").map((li) => ({
121
+ type: "listItem",
122
+ children: parseListItemChildren(li)
123
+ }))
124
+ };
125
+ }
126
+ if (tag === "PRE") {
127
+ const code = el.querySelector("code");
128
+ const text = (code || el).textContent || "";
129
+ const isSnippet = el.getAttribute("data-type") === "code-snippet";
130
+ return {
131
+ type: isSnippet ? "codeSnippet" : "codeBlock",
132
+ children: text ? [{
133
+ type: "text",
134
+ text
135
+ }] : []
136
+ };
137
+ }
138
+ if (tag === "IMG") {
139
+ const src = el.getAttribute("src");
140
+ if (!src) {
141
+ return null;
142
+ }
143
+ return {
144
+ type: "image",
145
+ attrs: {
146
+ src,
147
+ ...el.getAttribute("alt") && {
148
+ alt: el.getAttribute("alt")
149
+ },
150
+ ...el.getAttribute("title") && {
151
+ title: el.getAttribute("title")
152
+ }
153
+ }
154
+ };
155
+ }
156
+ if (tag === "BLOCKQUOTE") {
157
+ return {
158
+ type: "blockquote",
159
+ children: parseChildren(el)
160
+ };
161
+ }
162
+ if (tag === "HR") {
163
+ return {
164
+ type: "horizontalRule"
165
+ };
166
+ }
167
+ if (tag === "TABLE") {
168
+ const rows = Array.from(el.querySelectorAll("tr")).map((tr) => ({
169
+ type: "tableRow",
170
+ children: Array.from(tr.children).filter((c) => c.tagName === "TD" || c.tagName === "TH").map((c) => parseCell(c))
171
+ }));
172
+ return {
173
+ type: "table",
174
+ children: rows
175
+ };
176
+ }
177
+ return null;
178
+ };
179
+ const parseSpan = (el) => {
180
+ const attrs = {};
181
+ const colspan = parseInt(el.getAttribute("colspan") || "", 10);
182
+ const rowspan = parseInt(el.getAttribute("rowspan") || "", 10);
183
+ if (colspan > 1) {
184
+ attrs.colspan = colspan;
185
+ }
186
+ if (rowspan > 1) {
187
+ attrs.rowspan = rowspan;
188
+ }
189
+ return Object.keys(attrs).length ? attrs : void 0;
190
+ };
191
+ const parseCell = (el) => {
192
+ const type = el.tagName === "TH" ? "tableHeader" : "tableCell";
193
+ const span = parseSpan(el);
194
+ const hasBlockChildren = Array.from(el.children).some((c) => ["P", "UL", "OL", "PRE", "BLOCKQUOTE", "H1", "H2", "H3", "H4", "H5", "H6"].includes(c.tagName));
195
+ const children = hasBlockChildren ? parseChildren(el) : [{
196
+ type: "paragraph",
197
+ children: parseInlineNodes(el)
198
+ }];
199
+ return {
200
+ type,
201
+ ...span && {
202
+ attrs: span
203
+ },
204
+ children
205
+ };
206
+ };
207
+ const parseListItemChildren = (li) => {
208
+ const blocks = [];
209
+ const hasBlockChildren = Array.from(li.children).some((c) => c.tagName === "P" || c.tagName === "UL" || c.tagName === "OL");
210
+ if (hasBlockChildren) {
211
+ return parseChildren(li);
212
+ }
213
+ const inlines = parseInlineNodes(li);
214
+ if (inlines.length) {
215
+ blocks.push({
216
+ type: "paragraph",
217
+ children: inlines
218
+ });
219
+ }
220
+ return blocks;
221
+ };
222
+ const parseChildren = (parent) => Array.from(parent.children).map((el) => parseBlock(el)).filter(Boolean);
223
+ const htmlToDocument = (html) => {
224
+ const parser = new DOMParser();
225
+ const dom = parser.parseFromString(`<body>${html}</body>`, "text/html");
226
+ const body = dom.body;
227
+ const children = parseChildren(body);
228
+ return {
229
+ type: "doc",
230
+ version: 1,
231
+ // A doc must have at least one block (schema `block+`). Empty input must still yield an
232
+ // empty paragraph so the editor has a textblock node for the placeholder decoration to
233
+ // attach to — otherwise the placeholder is missing on an untouched empty editor.
234
+ children: children.length ? children : [{
235
+ type: "paragraph",
236
+ children: []
237
+ }]
238
+ };
239
+ };
240
+ export {
241
+ htmlToDocument
242
+ };
@@ -0,0 +1,6 @@
1
+ export { tiptapJsonToDocument } from './tiptapToDocument';
2
+ export { documentToTiptapJson } from './documentToTiptap';
3
+ export { documentToHtml } from './documentToHtml';
4
+ export { documentToMarkdown } from './documentToMarkdown';
5
+ export { markdownToDocument } from './markdownToDocument';
6
+ export { htmlToDocument } from './htmlToDocument';
@@ -0,0 +1,14 @@
1
+ import { tiptapJsonToDocument } from "./tiptapToDocument.js";
2
+ import { documentToTiptapJson } from "./documentToTiptap.js";
3
+ import { documentToHtml } from "./documentToHtml.js";
4
+ import { documentToMarkdown } from "./documentToMarkdown.js";
5
+ import { markdownToDocument } from "./markdownToDocument.js";
6
+ import { htmlToDocument } from "./htmlToDocument.js";
7
+ export {
8
+ documentToHtml,
9
+ documentToMarkdown,
10
+ documentToTiptapJson,
11
+ htmlToDocument,
12
+ markdownToDocument,
13
+ tiptapJsonToDocument
14
+ };
@@ -0,0 +1,10 @@
1
+ import { RichTextDocument } from '../blockModel.types';
2
+ /**
3
+ * Parse a Markdown string into a `RichTextDocument` by rendering it to HTML with
4
+ * markdown-it and reusing the existing `htmlToDocument` parser.
5
+ *
6
+ * Note: Markdown has no single-line code-snippet concept, so a `codeSnippet` block
7
+ * serialized to `` `code` `` parses back as an inline-code paragraph, and a fenced block
8
+ * parses back as a multi-line `codeBlock`.
9
+ */
10
+ export declare const markdownToDocument: (md: string) => RichTextDocument;
@@ -0,0 +1,11 @@
1
+ import MarkdownIt from "markdown-it";
2
+ import { htmlToDocument } from "./htmlToDocument.js";
3
+ const markdown = new MarkdownIt({
4
+ html: true,
5
+ linkify: true,
6
+ breaks: false
7
+ });
8
+ const markdownToDocument = (md) => htmlToDocument(markdown.render(md ?? ""));
9
+ export {
10
+ markdownToDocument
11
+ };
@@ -0,0 +1,2 @@
1
+ import { PMNode, RichTextDocument } from '../blockModel.types';
2
+ export declare const tiptapJsonToDocument: (json: PMNode) => RichTextDocument;
@@ -0,0 +1,189 @@
1
+ const SIMPLE_MARK_TYPES = /* @__PURE__ */ new Set(["bold", "italic", "underline", "strike", "code"]);
2
+ const convertMark = (pm) => {
3
+ if (SIMPLE_MARK_TYPES.has(pm.type)) {
4
+ return {
5
+ type: pm.type
6
+ };
7
+ }
8
+ if (pm.type === "link") {
9
+ const link = {
10
+ type: "link",
11
+ href: pm.attrs?.href
12
+ };
13
+ if (pm.attrs?.target) {
14
+ link.target = pm.attrs.target;
15
+ }
16
+ if (pm.attrs?.rel) {
17
+ link.rel = pm.attrs.rel;
18
+ }
19
+ return link;
20
+ }
21
+ return null;
22
+ };
23
+ const convertInlineNode = (pm) => {
24
+ if (pm.type === "text" && pm.text) {
25
+ const marks = pm.marks?.map(convertMark).filter(Boolean);
26
+ const node = {
27
+ type: "text",
28
+ text: pm.text
29
+ };
30
+ if (marks && marks.length > 0) {
31
+ node.marks = marks;
32
+ }
33
+ return node;
34
+ }
35
+ if (pm.type === "hardBreak") {
36
+ return {
37
+ type: "hardBreak"
38
+ };
39
+ }
40
+ return null;
41
+ };
42
+ const convertBlock = (pm) => {
43
+ switch (pm.type) {
44
+ case "paragraph": {
45
+ const children = (pm.content || []).map(convertInlineNode).filter(Boolean);
46
+ const textAlign = pm.attrs?.textAlign;
47
+ if (textAlign) {
48
+ return {
49
+ type: "paragraph",
50
+ attrs: {
51
+ textAlign
52
+ },
53
+ children
54
+ };
55
+ }
56
+ return {
57
+ type: "paragraph",
58
+ children
59
+ };
60
+ }
61
+ case "heading": {
62
+ const children = (pm.content || []).map(convertInlineNode).filter(Boolean);
63
+ const level = pm.attrs?.level || 1;
64
+ const textAlign = pm.attrs?.textAlign;
65
+ if (textAlign) {
66
+ return {
67
+ type: "heading",
68
+ attrs: {
69
+ level,
70
+ textAlign
71
+ },
72
+ children
73
+ };
74
+ }
75
+ return {
76
+ type: "heading",
77
+ attrs: {
78
+ level
79
+ },
80
+ children
81
+ };
82
+ }
83
+ case "codeBlock": {
84
+ const children = (pm.content || []).map(convertInlineNode).filter(Boolean);
85
+ const language = pm.attrs?.language;
86
+ if (language) {
87
+ return {
88
+ type: "codeBlock",
89
+ attrs: {
90
+ language
91
+ },
92
+ children
93
+ };
94
+ }
95
+ return {
96
+ type: "codeBlock",
97
+ children
98
+ };
99
+ }
100
+ case "codeSnippet": {
101
+ const children = (pm.content || []).map(convertInlineNode).filter(Boolean);
102
+ return {
103
+ type: "codeSnippet",
104
+ children
105
+ };
106
+ }
107
+ case "image": {
108
+ const attrs = {
109
+ src: pm.attrs?.src
110
+ };
111
+ if (pm.attrs?.alt) {
112
+ attrs.alt = pm.attrs.alt;
113
+ }
114
+ if (pm.attrs?.title) {
115
+ attrs.title = pm.attrs.title;
116
+ }
117
+ return {
118
+ type: "image",
119
+ attrs
120
+ };
121
+ }
122
+ case "bulletList":
123
+ return {
124
+ type: "bulletList",
125
+ children: (pm.content || []).filter((n) => n.type === "listItem").map((li) => ({
126
+ type: "listItem",
127
+ children: (li.content || []).map(convertBlock).filter(Boolean)
128
+ }))
129
+ };
130
+ case "orderedList": {
131
+ const start = pm.attrs?.start;
132
+ const block = {
133
+ type: "orderedList",
134
+ children: (pm.content || []).filter((n) => n.type === "listItem").map((li) => ({
135
+ type: "listItem",
136
+ children: (li.content || []).map(convertBlock).filter(Boolean)
137
+ }))
138
+ };
139
+ if (start && start !== 1) {
140
+ block.attrs = {
141
+ start
142
+ };
143
+ }
144
+ return block;
145
+ }
146
+ case "blockquote":
147
+ return {
148
+ type: "blockquote",
149
+ children: (pm.content || []).map(convertBlock).filter(Boolean)
150
+ };
151
+ case "horizontalRule":
152
+ return {
153
+ type: "horizontalRule"
154
+ };
155
+ case "table":
156
+ return {
157
+ type: "table",
158
+ children: (pm.content || []).filter((row) => row.type === "tableRow").map((row) => ({
159
+ type: "tableRow",
160
+ children: (row.content || []).filter((cell) => cell.type === "tableCell" || cell.type === "tableHeader").map((cell) => {
161
+ const span = {};
162
+ if (typeof cell.attrs?.colspan === "number" && cell.attrs.colspan > 1) {
163
+ span.colspan = cell.attrs.colspan;
164
+ }
165
+ if (typeof cell.attrs?.rowspan === "number" && cell.attrs.rowspan > 1) {
166
+ span.rowspan = cell.attrs.rowspan;
167
+ }
168
+ return {
169
+ type: cell.type,
170
+ ...Object.keys(span).length && {
171
+ attrs: span
172
+ },
173
+ children: (cell.content || []).map(convertBlock).filter(Boolean)
174
+ };
175
+ })
176
+ }))
177
+ };
178
+ default:
179
+ return null;
180
+ }
181
+ };
182
+ const tiptapJsonToDocument = (json) => ({
183
+ type: "doc",
184
+ version: 1,
185
+ children: (json.content || []).map(convertBlock).filter(Boolean)
186
+ });
187
+ export {
188
+ tiptapJsonToDocument
189
+ };
@@ -0,0 +1,3 @@
1
+ import { default as React } from 'react';
2
+ import { ImagePopoverProps } from '../RichText.types';
3
+ export declare const ImagePopover: ({ editor, texts, onClose, onImageUpload, }: ImagePopoverProps) => React.JSX.Element;
@@ -0,0 +1,89 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { useRef, useState, useCallback } from "react";
3
+ import Button from "@synerise/ds-button";
4
+ import { Input } from "@synerise/ds-input";
5
+ import { PopoverForm, PopoverActions } from "../RichText.styles.js";
6
+ import "@tiptap/extension-image";
7
+ import "@tiptap/extension-link";
8
+ import "@tiptap/extension-placeholder";
9
+ import "@tiptap/extension-table";
10
+ import "@tiptap/extension-table-cell";
11
+ import "@tiptap/extension-table-header";
12
+ import "@tiptap/extension-table-row";
13
+ import "@tiptap/extension-text-align";
14
+ import "@tiptap/extension-underline";
15
+ import "@tiptap/starter-kit";
16
+ import "../extensions/CodeSnippet.js";
17
+ import { normalizeUrl } from "../utils/normalizeUrl.js";
18
+ const ImagePopover = ({
19
+ editor,
20
+ texts,
21
+ onClose,
22
+ onImageUpload
23
+ }) => {
24
+ const fileInputRef = useRef(null);
25
+ const [url, setUrl] = useState("");
26
+ const [isUploading, setIsUploading] = useState(false);
27
+ const insertImage = useCallback((src) => {
28
+ editor.chain().focus().setImage({
29
+ src
30
+ }).run();
31
+ onClose();
32
+ }, [editor, onClose]);
33
+ const handleApply = useCallback(() => {
34
+ if (url) {
35
+ insertImage(normalizeUrl(url));
36
+ }
37
+ }, [url, insertImage]);
38
+ const handleFileChange = useCallback(async (e) => {
39
+ const file = e.target.files?.[0];
40
+ if (!file) {
41
+ return;
42
+ }
43
+ if (onImageUpload) {
44
+ setIsUploading(true);
45
+ try {
46
+ const src = await onImageUpload(file);
47
+ insertImage(src);
48
+ } catch (error) {
49
+ console.error("RichText: image upload failed", error);
50
+ } finally {
51
+ setIsUploading(false);
52
+ }
53
+ } else {
54
+ const reader = new FileReader();
55
+ reader.onload = () => {
56
+ if (typeof reader.result === "string") {
57
+ insertImage(reader.result);
58
+ }
59
+ };
60
+ reader.onerror = () => {
61
+ console.error("RichText: failed to read image file", reader.error);
62
+ };
63
+ reader.readAsDataURL(file);
64
+ }
65
+ }, [onImageUpload, insertImage]);
66
+ const handleKeyDown = useCallback((e) => {
67
+ if (e.key === "Enter") {
68
+ e.preventDefault();
69
+ handleApply();
70
+ }
71
+ if (e.key === "Escape") {
72
+ e.preventDefault();
73
+ onClose();
74
+ }
75
+ }, [handleApply, onClose]);
76
+ return /* @__PURE__ */ jsxs(PopoverForm, { onKeyDown: handleKeyDown, children: [
77
+ /* @__PURE__ */ jsx(Input, { value: url, onChange: (e) => setUrl(e.target.value), placeholder: texts.imagePlaceholder, autoFocus: true, resetMargin: true }),
78
+ /* @__PURE__ */ jsxs(PopoverActions, { children: [
79
+ /* @__PURE__ */ jsx(Button, { type: "primary", size: "small", onClick: handleApply, disabled: !url, children: texts.imageConfirm }),
80
+ /* @__PURE__ */ jsx(Button, { type: "ghost", size: "small", onClick: () => fileInputRef.current?.click(), disabled: isUploading, children: texts.imageUpload })
81
+ ] }),
82
+ /* @__PURE__ */ jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", onChange: handleFileChange, style: {
83
+ display: "none"
84
+ } })
85
+ ] });
86
+ };
87
+ export {
88
+ ImagePopover
89
+ };
@@ -0,0 +1,3 @@
1
+ import { default as React } from 'react';
2
+ import { LinkPopoverProps } from '../RichText.types';
3
+ export declare const LinkPopover: ({ editor, texts, onClose }: LinkPopoverProps) => React.JSX.Element;
@@ -0,0 +1,57 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { useState, useCallback } from "react";
3
+ import Button from "@synerise/ds-button";
4
+ import { Input } from "@synerise/ds-input";
5
+ import { PopoverForm, PopoverActions } from "../RichText.styles.js";
6
+ import "@tiptap/extension-image";
7
+ import "@tiptap/extension-link";
8
+ import "@tiptap/extension-placeholder";
9
+ import "@tiptap/extension-table";
10
+ import "@tiptap/extension-table-cell";
11
+ import "@tiptap/extension-table-header";
12
+ import "@tiptap/extension-table-row";
13
+ import "@tiptap/extension-text-align";
14
+ import "@tiptap/extension-underline";
15
+ import "@tiptap/starter-kit";
16
+ import "../extensions/CodeSnippet.js";
17
+ import { normalizeUrl } from "../utils/normalizeUrl.js";
18
+ const LinkPopover = ({
19
+ editor,
20
+ texts,
21
+ onClose
22
+ }) => {
23
+ const currentHref = editor.getAttributes("link").href || "";
24
+ const [url, setUrl] = useState(currentHref);
25
+ const handleApply = useCallback(() => {
26
+ if (url) {
27
+ editor.chain().focus().extendMarkRange("link").setLink({
28
+ href: normalizeUrl(url)
29
+ }).run();
30
+ }
31
+ onClose();
32
+ }, [editor, url, onClose]);
33
+ const handleRemove = useCallback(() => {
34
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
35
+ onClose();
36
+ }, [editor, onClose]);
37
+ const handleKeyDown = useCallback((e) => {
38
+ if (e.key === "Enter") {
39
+ e.preventDefault();
40
+ handleApply();
41
+ }
42
+ if (e.key === "Escape") {
43
+ e.preventDefault();
44
+ onClose();
45
+ }
46
+ }, [handleApply, onClose]);
47
+ return /* @__PURE__ */ jsxs(PopoverForm, { onKeyDown: handleKeyDown, children: [
48
+ /* @__PURE__ */ jsx(Input, { value: url, onChange: (e) => setUrl(e.target.value), placeholder: texts.linkPlaceholder, autoFocus: true, resetMargin: true }),
49
+ /* @__PURE__ */ jsxs(PopoverActions, { children: [
50
+ /* @__PURE__ */ jsx(Button, { type: "primary", size: "small", onClick: handleApply, children: texts.linkConfirm }),
51
+ currentHref && /* @__PURE__ */ jsx(Button, { type: "ghost", size: "small", onClick: handleRemove, children: texts.linkRemove })
52
+ ] })
53
+ ] });
54
+ };
55
+ export {
56
+ LinkPopover
57
+ };
@@ -0,0 +1,3 @@
1
+ import { default as React } from 'react';
2
+ import { ToolbarProps } from '../RichText.types';
3
+ export declare const Toolbar: ({ editor, features, headingLevels, texts, disabled, onImageUpload, onEditWithAI, editWithAIOptions, }: ToolbarProps) => React.JSX.Element | null;