playbook_ui 17.1.0.pre.alpha.tiptapformatting18993 → 17.1.0.pre.alpha.viteupgrade19008
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.
- checksums.yaml +4 -4
- data/app/pb_kits/playbook/pb_rich_text_editor/TipTap/ToolbarDropdown.tsx +2 -9
- data/app/pb_kits/playbook/pb_rich_text_editor/_rich_text_editor.tsx +0 -31
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_playground.json +1 -5
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_playground.overrides.json +1 -4
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/example.yml +2 -2
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/index.js +0 -1
- data/app/pb_kits/playbook/pb_rich_text_editor/kit.schema.json +0 -8
- data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.html.erb +1 -1
- data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.rb +0 -1
- data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor_advanced.test.js +1 -241
- data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor_rails.js +0 -109
- data/dist/chunks/vendor.js +1 -1
- data/lib/playbook/version.rb +1 -1
- metadata +1 -7
- data/app/pb_kits/playbook/pb_rich_text_editor/TipTap/listSelection.ts +0 -59
- data/app/pb_kits/playbook/pb_rich_text_editor/TipTap/markdownPaste.ts +0 -100
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_markdown_support.jsx +0 -31
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_markdown_support.md +0 -5
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_markdown_support.html.erb +0 -9
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_markdown_support.md +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 282aacb7cfa670d2c5136847e61b470192467443c03b981e5d6d332d652d2129
|
|
4
|
+
data.tar.gz: 5c42f2221d5ad12d21e6bd0f8888d10e81c8ac79e0a867946635bc228f688faf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4778aabf77ca48fc677c0b150c95ecdb4e94cf5a0e5106ee0aa0c095555a97ea06d58d9a2ea84f23084e6f9ab80f9d0a9a8a5976610a5c7bac513b7c7f7e6255
|
|
7
|
+
data.tar.gz: 4e0883f5cb07097a6dc77b3a3beaa447ebc6bd01a90cb8da806d1e575eea5d591cb6714bfeb5fcbd9bc3da1db2139b3b65f65003bf75c6255e8c28bd3fb88019
|
|
@@ -8,7 +8,6 @@ import Nav from '../../pb_nav/_nav'
|
|
|
8
8
|
import NavItem from '../../pb_nav/_item'
|
|
9
9
|
|
|
10
10
|
import { ToolbarTypes } from './EditorTypes'
|
|
11
|
-
import { normalizeListSelection } from './listSelection'
|
|
12
11
|
|
|
13
12
|
const ToolbarDropdown = ({editor}: any): React.ReactElement => {
|
|
14
13
|
const [showPopover, setShowPopover] = useState(false)
|
|
@@ -47,20 +46,14 @@ const toolbarDropdownItems = [
|
|
|
47
46
|
icon: "list",
|
|
48
47
|
isActive: editor.isActive("bulletList"),
|
|
49
48
|
text: "Bullet List",
|
|
50
|
-
onclick: () =>
|
|
51
|
-
normalizeListSelection(editor)
|
|
52
|
-
editor.chain().focus().toggleBulletList().run()
|
|
53
|
-
},
|
|
49
|
+
onclick: () => editor.chain().focus().toggleBulletList().run(),
|
|
54
50
|
},
|
|
55
51
|
{
|
|
56
52
|
node: "orderedList",
|
|
57
53
|
icon: "list-ol",
|
|
58
54
|
isActive: editor.isActive("orderedList"),
|
|
59
55
|
text: "Ordered List",
|
|
60
|
-
onclick: () =>
|
|
61
|
-
normalizeListSelection(editor)
|
|
62
|
-
editor.chain().focus().toggleOrderedList().run()
|
|
63
|
-
}
|
|
56
|
+
onclick: () => editor.chain().focus().toggleOrderedList().run()
|
|
64
57
|
,
|
|
65
58
|
},
|
|
66
59
|
{
|
|
@@ -5,11 +5,6 @@ import Caption from '../pb_caption/_caption'
|
|
|
5
5
|
import colors from '../tokens/exports/_colors.module.scss'
|
|
6
6
|
import { globalProps, GlobalProps } from '../utilities/globalProps'
|
|
7
7
|
import { buildAriaProps, buildDataProps, noop, buildHtmlProps } from '../utilities/props'
|
|
8
|
-
import {
|
|
9
|
-
createMarkdownToHTML,
|
|
10
|
-
handleMarkdownPaste,
|
|
11
|
-
} from './TipTap/markdownPaste'
|
|
12
|
-
import type { MarkdownDependencies } from './TipTap/markdownPaste'
|
|
13
8
|
import TipTapEditor from './_tiptap_editor'
|
|
14
9
|
import TrixTextEditor from './_trix_editor'
|
|
15
10
|
|
|
@@ -27,7 +22,6 @@ type RichTextEditorProps = {
|
|
|
27
22
|
id?: string,
|
|
28
23
|
inline?: boolean,
|
|
29
24
|
label?: string,
|
|
30
|
-
markdownSupport?: MarkdownDependencies,
|
|
31
25
|
extensions?: { [key: string]: string }[],
|
|
32
26
|
name?: string,
|
|
33
27
|
onChange: (html: string, text: string) => void,
|
|
@@ -71,7 +65,6 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
|
|
|
71
65
|
maxWidth = "md",
|
|
72
66
|
requiredIndicator = false,
|
|
73
67
|
label,
|
|
74
|
-
markdownSupport,
|
|
75
68
|
TrixEditor,
|
|
76
69
|
trixInstance: trixInstance = undefined,
|
|
77
70
|
} = props
|
|
@@ -96,30 +89,6 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
|
|
|
96
89
|
dom.setAttribute("aria-multiline", "true")
|
|
97
90
|
}, [advancedEditor, fieldId, labelElementId])
|
|
98
91
|
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
if (!markdownSupport) return
|
|
101
|
-
|
|
102
|
-
const editorElement = advancedEditor?.view?.dom
|
|
103
|
-
if (!editorElement) return
|
|
104
|
-
const markdownToHTML = createMarkdownToHTML(
|
|
105
|
-
markdownSupport.parser,
|
|
106
|
-
markdownSupport.serializer
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
const handleMarkdownSupport = (event: ClipboardEvent) => {
|
|
110
|
-
handleMarkdownPaste(
|
|
111
|
-
event,
|
|
112
|
-
(html) => advancedEditor.chain().focus().insertContent(html).run(),
|
|
113
|
-
markdownToHTML
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
|
-
editorElement.addEventListener("paste", handleMarkdownSupport, true)
|
|
117
|
-
|
|
118
|
-
return () => {
|
|
119
|
-
editorElement.removeEventListener("paste", handleMarkdownSupport, true)
|
|
120
|
-
}
|
|
121
|
-
}, [advancedEditor, markdownSupport])
|
|
122
|
-
|
|
123
92
|
//===========focus prop with advanced editor=================
|
|
124
93
|
const isClickInPopover = (event: Event): boolean => {
|
|
125
94
|
return !!(event.target as Element).closest('.pb_tiptap_toolbar_dropdown_popover')
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
"template": "<RichTextEditor{{props}} />",
|
|
3
3
|
"propTargets": {},
|
|
4
4
|
"defaults": {
|
|
5
|
-
"markdownSupport": false,
|
|
6
5
|
"requiredIndicator": false,
|
|
7
6
|
"simple": false,
|
|
8
7
|
"advancedEditorToolbar": true,
|
|
@@ -134,9 +133,7 @@
|
|
|
134
133
|
"trix",
|
|
135
134
|
"@tiptap/react",
|
|
136
135
|
"@tiptap/starter-kit",
|
|
137
|
-
"@tiptap/extension-link"
|
|
138
|
-
"prosemirror-markdown",
|
|
139
|
-
"prosemirror-model"
|
|
136
|
+
"@tiptap/extension-link"
|
|
140
137
|
],
|
|
141
138
|
"optional": true,
|
|
142
139
|
"note": "Default React editor uses Trix (pass TrixEditor from react-trix). Advanced editor uses TipTap. The host app must already have the needed packages installed. Agents must not install packages — tell the user if they are missing. Rails advanced TipTap may load via the kit’s esm.sh script."
|
|
@@ -161,7 +158,6 @@
|
|
|
161
158
|
"advancedEditor",
|
|
162
159
|
"extensions",
|
|
163
160
|
"inputOptions",
|
|
164
|
-
"markdownSupport",
|
|
165
161
|
"onChange",
|
|
166
162
|
"template",
|
|
167
163
|
"TrixEditor",
|
|
@@ -5,9 +5,7 @@
|
|
|
5
5
|
"trix",
|
|
6
6
|
"@tiptap/react",
|
|
7
7
|
"@tiptap/starter-kit",
|
|
8
|
-
"@tiptap/extension-link"
|
|
9
|
-
"prosemirror-markdown",
|
|
10
|
-
"prosemirror-model"
|
|
8
|
+
"@tiptap/extension-link"
|
|
11
9
|
],
|
|
12
10
|
"optional": true,
|
|
13
11
|
"note": "Default React editor uses Trix (pass TrixEditor from react-trix). Advanced editor uses TipTap. The host app must already have the needed packages installed. Agents must not install packages — tell the user if they are missing. Rails advanced TipTap may load via the kit’s esm.sh script."
|
|
@@ -42,7 +40,6 @@
|
|
|
42
40
|
"advancedEditor",
|
|
43
41
|
"extensions",
|
|
44
42
|
"inputOptions",
|
|
45
|
-
"markdownSupport",
|
|
46
43
|
"onChange",
|
|
47
44
|
"template",
|
|
48
45
|
"TrixEditor",
|
|
@@ -2,12 +2,10 @@ examples:
|
|
|
2
2
|
|
|
3
3
|
rails:
|
|
4
4
|
- rich_text_editor_rails_default: "Rails (TipTap)"
|
|
5
|
-
- rich_text_editor_rails_markdown_support: "Rails (Markdown Support)"
|
|
6
5
|
- rich_text_editor_rails_simple: "Rails (TipTap — Simple toolbar)"
|
|
7
6
|
|
|
8
7
|
react:
|
|
9
8
|
- rich_text_editor_advanced_default: Advanced Default
|
|
10
|
-
- rich_text_editor_advanced_markdown_support: Advanced (Markdown Support)
|
|
11
9
|
- rich_text_editor_more_extensions: Advanced (Extra Extensions)
|
|
12
10
|
- rich_text_editor_toolbar_disabled: Advanced (Toolbar disabled)
|
|
13
11
|
- rich_text_editor_advanced_simple: Advanced (Simple)
|
|
@@ -32,3 +30,5 @@ examples:
|
|
|
32
30
|
|
|
33
31
|
|
|
34
32
|
|
|
33
|
+
|
|
34
|
+
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { default as RichTextEditorDefault } from './_rich_text_editor_default.jsx'
|
|
2
2
|
export { default as RichTextEditorAdvancedDefault } from './_rich_text_editor_advanced_default.jsx'
|
|
3
|
-
export { default as RichTextEditorAdvancedMarkdownSupport } from './_rich_text_editor_advanced_markdown_support.jsx'
|
|
4
3
|
export { default as RichTextEditorMoreExtensions } from './_rich_text_editor_more_extensions.jsx'
|
|
5
4
|
export { default as RichTextEditorToolbarDisabled } from './_rich_text_editor_toolbar_disabled.jsx'
|
|
6
5
|
export { default as RichTextEditorAdvancedPreview } from './_rich_text_editor_advanced_preview.jsx'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<%# TipTap loads via full URL dynamic import() in rich_text_editor_rails.js (esm.sh) — no import map, so no clash with Vite/host maps. %>
|
|
2
|
-
<%= pb_content_tag(:div, id: object.container_id, class: object.classname, data: { pb_rte_tiptap: true, input_id: object.input_id, initial_html: object.initial_html,
|
|
2
|
+
<%= pb_content_tag(:div, id: object.container_id, class: object.classname, data: { pb_rte_tiptap: true, input_id: object.input_id, initial_html: object.initial_html, rte_simple: object.simple }) do %>
|
|
3
3
|
<div class="pb_rich_text_editor_kit">
|
|
4
4
|
<% if object.label.present? %>
|
|
5
5
|
<label for="<%= object.input_id %>">
|
|
@@ -9,7 +9,6 @@ module Playbook
|
|
|
9
9
|
prop :input_options, type: Playbook::Props::HashProp, default: {}
|
|
10
10
|
prop :label
|
|
11
11
|
prop :required_indicator, type: Playbook::Props::Boolean, default: false
|
|
12
|
-
prop :markdown_support, type: Playbook::Props::Boolean, default: false
|
|
13
12
|
# When true, TipTap toolbar matches React `simple`: Bold + Italic only (no block-style Popover).
|
|
14
13
|
# Use in modals or narrow layouts where the block dropdown misbehaves.
|
|
15
14
|
prop :simple, type: Playbook::Props::Boolean, default: false
|
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
3
|
-
import { Editor } from "@tiptap/core";
|
|
2
|
+
import { render } from "../utilities/test-utils";
|
|
4
3
|
import { useEditor, EditorContent } from "@tiptap/react";
|
|
5
4
|
import StarterKit from "@tiptap/starter-kit";
|
|
6
5
|
import Link from "@tiptap/extension-link";
|
|
7
|
-
import { defaultMarkdownParser } from "prosemirror-markdown";
|
|
8
|
-
import { DOMSerializer } from "prosemirror-model";
|
|
9
6
|
|
|
10
7
|
import RichTextEditor from "./_rich_text_editor";
|
|
11
|
-
import { normalizeListSelection } from "./TipTap/listSelection";
|
|
12
|
-
import { createMarkdownToHTML, handleMarkdownPaste } from "./TipTap/markdownPaste";
|
|
13
8
|
|
|
14
9
|
const kitClass = "pb_rich_text_editor_advanced_container";
|
|
15
|
-
const markdownToHTML = createMarkdownToHTML(defaultMarkdownParser, DOMSerializer);
|
|
16
10
|
|
|
17
11
|
const EditorTest = (props) => {
|
|
18
12
|
const editor = useEditor({
|
|
@@ -51,237 +45,3 @@ test("doesn't returns toolbar class name", () => {
|
|
|
51
45
|
container.getElementsByClassName(`${kitClass} toolbar-active`).length
|
|
52
46
|
).toBe(0);
|
|
53
47
|
});
|
|
54
|
-
|
|
55
|
-
describe("TipTap list selection", () => {
|
|
56
|
-
const content = "<h2>How should it work?</h2><p>First paragraph</p><p>Second paragraph</p>";
|
|
57
|
-
|
|
58
|
-
test.each([
|
|
59
|
-
["bulletList", "ul"],
|
|
60
|
-
["orderedList", "ol"],
|
|
61
|
-
])("excludes a heading touched only at its end boundary from a %s", (listType, tag) => {
|
|
62
|
-
const editor = new Editor({ extensions: [StarterKit], content });
|
|
63
|
-
const headingEnd = editor.state.doc.child(0).nodeSize - 1;
|
|
64
|
-
const paragraphEnd = headingEnd + editor.state.doc.child(1).nodeSize;
|
|
65
|
-
|
|
66
|
-
editor.commands.setTextSelection({ from: headingEnd, to: paragraphEnd });
|
|
67
|
-
normalizeListSelection(editor);
|
|
68
|
-
if (listType === "bulletList") editor.chain().toggleBulletList().run();
|
|
69
|
-
else editor.chain().toggleOrderedList().run();
|
|
70
|
-
|
|
71
|
-
expect(editor.getHTML()).toBe(
|
|
72
|
-
`<h2>How should it work?</h2><${tag}><li><p>First paragraph</p></li></${tag}><p>Second paragraph</p>`
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
editor.destroy();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("excludes a paragraph touched only at its start boundary", () => {
|
|
79
|
-
const editor = new Editor({ extensions: [StarterKit], content });
|
|
80
|
-
const firstParagraphStart = editor.state.doc.child(0).nodeSize + 1;
|
|
81
|
-
const secondParagraphStart =
|
|
82
|
-
editor.state.doc.child(0).nodeSize + editor.state.doc.child(1).nodeSize + 1;
|
|
83
|
-
|
|
84
|
-
editor.commands.setTextSelection({ from: firstParagraphStart, to: secondParagraphStart });
|
|
85
|
-
normalizeListSelection(editor);
|
|
86
|
-
editor.chain().toggleBulletList().run();
|
|
87
|
-
|
|
88
|
-
expect(editor.getHTML()).toBe(
|
|
89
|
-
"<h2>How should it work?</h2><ul><li><p>First paragraph</p></li></ul><p>Second paragraph</p>"
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
editor.destroy();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("separates a pasted heading and body joined by a hard break", () => {
|
|
96
|
-
const editor = new Editor({
|
|
97
|
-
extensions: [StarterKit],
|
|
98
|
-
content: "<p><strong>How should it work?</strong><br>Keep React and Rails visible.</p>",
|
|
99
|
-
});
|
|
100
|
-
const hardBreakEnd = editor.state.doc.child(0).child(1).nodeSize +
|
|
101
|
-
editor.state.doc.child(0).child(0).nodeSize + 1;
|
|
102
|
-
const paragraphEnd = editor.state.doc.child(0).nodeSize - 1;
|
|
103
|
-
|
|
104
|
-
editor.commands.setTextSelection({ from: hardBreakEnd, to: paragraphEnd });
|
|
105
|
-
normalizeListSelection(editor);
|
|
106
|
-
editor.chain().toggleBulletList().run();
|
|
107
|
-
|
|
108
|
-
expect(editor.getHTML()).toBe(
|
|
109
|
-
"<p><strong>How should it work?</strong></p><ul><li><p>Keep React and Rails visible.</p></li></ul>"
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
editor.destroy();
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe("TipTap Markdown paste", () => {
|
|
117
|
-
const createClipboardData = (text, html = "") => ({
|
|
118
|
-
files: [],
|
|
119
|
-
getData: (format) => {
|
|
120
|
-
if (format === "text/plain") return text;
|
|
121
|
-
if (format === "text/html") return html;
|
|
122
|
-
return "";
|
|
123
|
-
},
|
|
124
|
-
types: html ? ["text/plain", "text/html"] : ["text/plain"],
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
const createPasteEvent = (text, html = "") => {
|
|
128
|
-
const event = new Event("paste", { bubbles: true, cancelable: true });
|
|
129
|
-
Object.defineProperty(event, "clipboardData", {
|
|
130
|
-
value: createClipboardData(text, html),
|
|
131
|
-
});
|
|
132
|
-
return event;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
test("converts common Markdown blocks and inline formatting to HTML", () => {
|
|
136
|
-
const markdown = [
|
|
137
|
-
"## How should it work?",
|
|
138
|
-
"",
|
|
139
|
-
"Keep **React** and [Rails](https://example.com) visible.",
|
|
140
|
-
"",
|
|
141
|
-
"- React Playground",
|
|
142
|
-
"- Rails Playground",
|
|
143
|
-
].join("\n");
|
|
144
|
-
|
|
145
|
-
expect(markdownToHTML(markdown)).toBe(
|
|
146
|
-
'<h2>How should it work?</h2><p>Keep <strong>React</strong> and <a href="https://example.com">Rails</a> visible.</p><ul><li><p>React Playground</p></li><li><p>Rails Playground</p></li></ul>'
|
|
147
|
-
);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test("leaves ordinary plain text to TipTap's default paste handling", () => {
|
|
151
|
-
expect(markdownToHTML("Keep React and Rails visible.")).toBeNull();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("does not treat intraword underscores as Markdown", () => {
|
|
155
|
-
expect(markdownToHTML("foo_bar_baz")).toBeNull();
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test.each(["*Italic*", "_Italic_"])(
|
|
159
|
-
"converts italic-only Markdown using %s delimiters",
|
|
160
|
-
(markdown) => {
|
|
161
|
-
expect(markdownToHTML(markdown)).toBe("<p><em>Italic</em></p>");
|
|
162
|
-
}
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
test("is disabled by default", () => {
|
|
166
|
-
const addEventListener = jest.spyOn(HTMLElement.prototype, "addEventListener");
|
|
167
|
-
|
|
168
|
-
render(<EditorTest />);
|
|
169
|
-
|
|
170
|
-
const markdownListeners = addEventListener.mock.calls.filter(
|
|
171
|
-
(call, index) => {
|
|
172
|
-
const [eventName, , capture] = call;
|
|
173
|
-
const eventTarget = addEventListener.mock.instances[index];
|
|
174
|
-
return eventName === "paste" &&
|
|
175
|
-
capture === true &&
|
|
176
|
-
eventTarget.classList?.contains("ProseMirror");
|
|
177
|
-
}
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
expect(markdownListeners).toHaveLength(0);
|
|
181
|
-
addEventListener.mockRestore();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test("formats Markdown when markdownSupport is enabled", async () => {
|
|
185
|
-
const addEventListener = jest.spyOn(HTMLElement.prototype, "addEventListener");
|
|
186
|
-
const { container } = render(
|
|
187
|
-
<EditorTest
|
|
188
|
-
markdownSupport={{
|
|
189
|
-
parser: defaultMarkdownParser,
|
|
190
|
-
serializer: DOMSerializer,
|
|
191
|
-
}}
|
|
192
|
-
/>
|
|
193
|
-
);
|
|
194
|
-
const editorElement = container.querySelector(".ProseMirror");
|
|
195
|
-
|
|
196
|
-
await waitFor(() => {
|
|
197
|
-
const markdownListeners = addEventListener.mock.calls.filter(
|
|
198
|
-
(call, index) => {
|
|
199
|
-
const [eventName, , capture] = call;
|
|
200
|
-
const eventTarget = addEventListener.mock.instances[index];
|
|
201
|
-
return eventName === "paste" &&
|
|
202
|
-
capture === true &&
|
|
203
|
-
eventTarget.classList?.contains("ProseMirror");
|
|
204
|
-
}
|
|
205
|
-
);
|
|
206
|
-
expect(markdownListeners).toHaveLength(1);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
fireEvent.paste(editorElement, { clipboardData: createClipboardData("**Markdown**") });
|
|
210
|
-
|
|
211
|
-
await waitFor(() => expect(editorElement.innerHTML).toContain("<strong>Markdown</strong>"));
|
|
212
|
-
addEventListener.mockRestore();
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test("stops TipTap's paste handler after converting Markdown", () => {
|
|
216
|
-
const pasteEvent = createPasteEvent("**Markdown**");
|
|
217
|
-
const stopImmediatePropagation = jest.spyOn(pasteEvent, "stopImmediatePropagation");
|
|
218
|
-
const insertHTML = jest.fn();
|
|
219
|
-
|
|
220
|
-
expect(handleMarkdownPaste(pasteEvent, insertHTML, markdownToHTML)).toBe(true);
|
|
221
|
-
expect(pasteEvent.defaultPrevented).toBe(true);
|
|
222
|
-
expect(stopImmediatePropagation).toHaveBeenCalled();
|
|
223
|
-
expect(insertHTML).toHaveBeenCalledWith("<p><strong>Markdown</strong></p>");
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
test("converts manually copied Markdown wrapped in clipboard HTML", () => {
|
|
227
|
-
const pasteEvent = createPasteEvent(
|
|
228
|
-
"**Markdown**",
|
|
229
|
-
"<div><span>**Markdown**</span></div>"
|
|
230
|
-
);
|
|
231
|
-
const stopImmediatePropagation = jest.spyOn(pasteEvent, "stopImmediatePropagation");
|
|
232
|
-
const insertHTML = jest.fn();
|
|
233
|
-
|
|
234
|
-
expect(handleMarkdownPaste(pasteEvent, insertHTML, markdownToHTML)).toBe(true);
|
|
235
|
-
expect(stopImmediatePropagation).toHaveBeenCalled();
|
|
236
|
-
expect(insertHTML).toHaveBeenCalledWith("<p><strong>Markdown</strong></p>");
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
test("converts a partial selection copied from a Markdown source block", () => {
|
|
240
|
-
const pasteEvent = createPasteEvent(
|
|
241
|
-
"*selected Markdown*",
|
|
242
|
-
"<pre><code>*selected Markdown*</code></pre>"
|
|
243
|
-
);
|
|
244
|
-
const stopImmediatePropagation = jest.spyOn(pasteEvent, "stopImmediatePropagation");
|
|
245
|
-
const insertHTML = jest.fn();
|
|
246
|
-
|
|
247
|
-
expect(handleMarkdownPaste(pasteEvent, insertHTML, markdownToHTML)).toBe(true);
|
|
248
|
-
expect(stopImmediatePropagation).toHaveBeenCalled();
|
|
249
|
-
expect(insertHTML).toHaveBeenCalledWith("<p><em>selected Markdown</em></p>");
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
test("leaves spreadsheet HTML paste handling to TipTap", () => {
|
|
253
|
-
const pasteEvent = createPasteEvent(
|
|
254
|
-
"1. Q1",
|
|
255
|
-
"<table><tbody><tr><td>1. Q1</td></tr></tbody></table>"
|
|
256
|
-
);
|
|
257
|
-
const stopImmediatePropagation = jest.spyOn(pasteEvent, "stopImmediatePropagation");
|
|
258
|
-
const insertHTML = jest.fn();
|
|
259
|
-
|
|
260
|
-
expect(handleMarkdownPaste(pasteEvent, insertHTML, markdownToHTML)).toBe(false);
|
|
261
|
-
expect(stopImmediatePropagation).not.toHaveBeenCalled();
|
|
262
|
-
expect(insertHTML).not.toHaveBeenCalled();
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
test("leaves rich HTML paste handling to TipTap", async () => {
|
|
266
|
-
const { container } = render(
|
|
267
|
-
<EditorTest
|
|
268
|
-
markdownSupport={{
|
|
269
|
-
parser: defaultMarkdownParser,
|
|
270
|
-
serializer: DOMSerializer,
|
|
271
|
-
}}
|
|
272
|
-
/>
|
|
273
|
-
);
|
|
274
|
-
const editorElement = container.querySelector(".ProseMirror");
|
|
275
|
-
const pasteEvent = createPasteEvent(
|
|
276
|
-
"**Markdown**",
|
|
277
|
-
"<p><strong>Rendered HTML</strong></p>"
|
|
278
|
-
);
|
|
279
|
-
const stopImmediatePropagation = jest.spyOn(pasteEvent, "stopImmediatePropagation");
|
|
280
|
-
|
|
281
|
-
fireEvent(editorElement, pasteEvent);
|
|
282
|
-
|
|
283
|
-
await waitFor(() => expect(editorElement.innerHTML).toContain("<strong>Rendered HTML</strong>"));
|
|
284
|
-
expect(stopImmediatePropagation).not.toHaveBeenCalled();
|
|
285
|
-
stopImmediatePropagation.mockRestore();
|
|
286
|
-
});
|
|
287
|
-
});
|
|
@@ -18,7 +18,6 @@ async function initPlaybookRichTextEditorRails(container) {
|
|
|
18
18
|
const editorNode = document.getElementById(`${containerId}-editor`);
|
|
19
19
|
const toolbar = document.getElementById(`${containerId}-toolbar`);
|
|
20
20
|
const rteSimple = container.dataset.rteSimple === "true";
|
|
21
|
-
const markdownSupport = container.dataset.markdownSupport === "true";
|
|
22
21
|
const blockTooltipId = `${containerId}-toolbar-block-tooltip`;
|
|
23
22
|
const iconTemplatesRoot = rteSimple
|
|
24
23
|
? null
|
|
@@ -39,38 +38,6 @@ async function initPlaybookRichTextEditorRails(container) {
|
|
|
39
38
|
const { Editor } = await import(RTE_TIPTAP_ESM("@tiptap/core"));
|
|
40
39
|
const { default: StarterKit } = await import(RTE_TIPTAP_ESM("@tiptap/starter-kit"));
|
|
41
40
|
const { default: Link } = await import(RTE_TIPTAP_ESM("@tiptap/extension-link"));
|
|
42
|
-
const neutralClipboardTags = new Set(["html", "head", "body", "meta", "div", "span", "p"]);
|
|
43
|
-
const markdownSourceTags = new Set([...neutralClipboardTags, "pre", "code"]);
|
|
44
|
-
let markdownToHtml;
|
|
45
|
-
|
|
46
|
-
if (markdownSupport) {
|
|
47
|
-
try {
|
|
48
|
-
const [{ defaultMarkdownParser }, { DOMSerializer }] = await Promise.all([
|
|
49
|
-
import("https://esm.sh/prosemirror-markdown@1.13.2"),
|
|
50
|
-
import("https://esm.sh/prosemirror-model@1.25.0"),
|
|
51
|
-
]);
|
|
52
|
-
|
|
53
|
-
markdownToHtml = (text) => {
|
|
54
|
-
const documentNode = defaultMarkdownParser.parse(text);
|
|
55
|
-
if (!documentNode) return null;
|
|
56
|
-
const hasMarkdownFormatting = (node) => {
|
|
57
|
-
const isFormattedNode = !["doc", "paragraph", "text"].includes(node.type);
|
|
58
|
-
if (isFormattedNode || node.marks?.length) return true;
|
|
59
|
-
|
|
60
|
-
return node.content?.some(hasMarkdownFormatting) || false;
|
|
61
|
-
};
|
|
62
|
-
if (!hasMarkdownFormatting(documentNode.toJSON())) return null;
|
|
63
|
-
|
|
64
|
-
const wrapper = document.createElement("div");
|
|
65
|
-
const serializer = DOMSerializer.fromSchema(documentNode.type.schema);
|
|
66
|
-
wrapper.appendChild(serializer.serializeFragment(documentNode.content));
|
|
67
|
-
wrapper.querySelectorAll("[data-tight]").forEach((node) => node.removeAttribute("data-tight"));
|
|
68
|
-
return wrapper.innerHTML;
|
|
69
|
-
};
|
|
70
|
-
} catch (_error) {
|
|
71
|
-
markdownToHtml = undefined;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
41
|
|
|
75
42
|
const editor = new Editor({
|
|
76
43
|
element: editorNode,
|
|
@@ -83,38 +50,6 @@ async function initPlaybookRichTextEditorRails(container) {
|
|
|
83
50
|
onUpdate: ({ editor: ed }) => syncToHiddenInput(ed),
|
|
84
51
|
});
|
|
85
52
|
|
|
86
|
-
if (markdownSupport && markdownToHtml) {
|
|
87
|
-
editorNode.addEventListener("paste", (event) => {
|
|
88
|
-
const text = event.clipboardData?.getData("text/plain");
|
|
89
|
-
const richHtml = event.clipboardData?.getData("text/html");
|
|
90
|
-
const clipboardDocument = new DOMParser().parseFromString(richHtml || "", "text/html");
|
|
91
|
-
const elements = [...clipboardDocument.querySelectorAll("*")];
|
|
92
|
-
const hasStyledElement = elements.some((element) => element.hasAttribute("style"));
|
|
93
|
-
const hasOnlyNeutralTags = elements.every((element) =>
|
|
94
|
-
neutralClipboardTags.has(element.tagName.toLowerCase())
|
|
95
|
-
);
|
|
96
|
-
const hasOnlyMarkdownSourceTags = elements.every((element) =>
|
|
97
|
-
markdownSourceTags.has(element.tagName.toLowerCase())
|
|
98
|
-
);
|
|
99
|
-
const sourceTextMatches = clipboardDocument.body.textContent?.trim() === text?.trim();
|
|
100
|
-
const hasRichTextFormatting = hasStyledElement ||
|
|
101
|
-
(!hasOnlyNeutralTags && !(hasOnlyMarkdownSourceTags && sourceTextMatches));
|
|
102
|
-
if (hasRichTextFormatting || !text || !markdownToHtml) return;
|
|
103
|
-
|
|
104
|
-
let html;
|
|
105
|
-
try {
|
|
106
|
-
html = markdownToHtml(text);
|
|
107
|
-
} catch (_error) {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
if (!html) return;
|
|
111
|
-
|
|
112
|
-
event.preventDefault();
|
|
113
|
-
event.stopImmediatePropagation();
|
|
114
|
-
editor.chain().focus().insertContent(html).run();
|
|
115
|
-
}, true);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
53
|
syncToHiddenInput(editor);
|
|
119
54
|
|
|
120
55
|
const actionToChain = {
|
|
@@ -166,50 +101,6 @@ async function initPlaybookRichTextEditorRails(container) {
|
|
|
166
101
|
};
|
|
167
102
|
|
|
168
103
|
const applyBlockType = (value) => {
|
|
169
|
-
let { selection } = editor.state;
|
|
170
|
-
|
|
171
|
-
if ((value === "bulletList" || value === "orderedList") && !selection.empty) {
|
|
172
|
-
if (selection.$from.nodeBefore?.type.name === "hardBreak") {
|
|
173
|
-
const selectedSize = selection.to - selection.from;
|
|
174
|
-
const breakPosition = selection.from - 1;
|
|
175
|
-
|
|
176
|
-
editor.chain()
|
|
177
|
-
.deleteRange({ from: breakPosition, to: selection.from })
|
|
178
|
-
.setTextSelection(breakPosition)
|
|
179
|
-
.splitBlock()
|
|
180
|
-
.run();
|
|
181
|
-
|
|
182
|
-
const from = editor.state.selection.from;
|
|
183
|
-
editor.commands.setTextSelection({ from, to: from + selectedSize });
|
|
184
|
-
selection = editor.state.selection;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const { $from, $to } = selection;
|
|
188
|
-
let { from, to } = selection;
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
$from.depth > 0 &&
|
|
192
|
-
$from.parent.isTextblock &&
|
|
193
|
-
$from.parentOffset === $from.parent.content.size
|
|
194
|
-
) {
|
|
195
|
-
const nextBlockStart = $from.after($from.depth) + 1;
|
|
196
|
-
if (nextBlockStart < to) from = nextBlockStart;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (
|
|
200
|
-
$to.depth > 0 &&
|
|
201
|
-
$to.parent.isTextblock &&
|
|
202
|
-
$to.parentOffset === 0
|
|
203
|
-
) {
|
|
204
|
-
const previousBlockEnd = $to.before($to.depth) - 1;
|
|
205
|
-
if (previousBlockEnd > from) to = previousBlockEnd;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (from !== selection.from || to !== selection.to) {
|
|
209
|
-
editor.commands.setTextSelection({ from, to });
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
104
|
const chain = editor.chain().focus();
|
|
214
105
|
if (value === "paragraph") chain.setParagraph().run();
|
|
215
106
|
else if (value === "heading-1") chain.toggleHeading({ level: 1 }).run();
|