playbook_ui 17.2.0.pre.rc.2 → 18.0.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.
- checksums.yaml +4 -4
- data/app/pb_kits/playbook/pb_advanced_table/kit.schema.json +8 -2
- data/app/pb_kits/playbook/pb_background/kit.schema.json +16 -4
- data/app/pb_kits/playbook/pb_card/kit.schema.json +4 -1
- data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_time.jsx +14 -18
- data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_time.md +3 -1
- data/app/pb_kits/playbook/pb_date_picker/kit.schema.json +4 -1
- data/app/pb_kits/playbook/pb_dropdown/kit.schema.json +8 -2
- data/app/pb_kits/playbook/pb_file_upload/kit.schema.json +4 -1
- data/app/pb_kits/playbook/pb_filter/kit.schema.json +4 -1
- data/app/pb_kits/playbook/pb_home_address_street/kit.schema.json +4 -1
- data/app/pb_kits/playbook/pb_icon/kit.schema.json +8 -2
- data/app/pb_kits/playbook/pb_layout/docs/_playground.json +1 -4
- data/app/pb_kits/playbook/pb_message/kit.schema.json +4 -1
- data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.tsx +31 -1
- data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_format_selected_display.jsx +191 -0
- data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_format_selected_display.md +1 -0
- data/app/pb_kits/playbook/pb_multi_level_select/docs/_playground.json +31 -2
- data/app/pb_kits/playbook/pb_multi_level_select/docs/_playground.overrides.json +31 -2
- data/app/pb_kits/playbook/pb_multi_level_select/docs/example.yml +1 -0
- data/app/pb_kits/playbook/pb_multi_level_select/docs/index.js +1 -0
- data/app/pb_kits/playbook/pb_multi_level_select/kit.schema.json +14 -2
- data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.test.jsx +39 -1
- data/app/pb_kits/playbook/pb_overlay/kit.schema.json +4 -1
- data/app/pb_kits/playbook/pb_pagination/pagination.html.erb +1 -1
- data/app/pb_kits/playbook/pb_progress_step/kit.schema.json +8 -2
- data/app/pb_kits/playbook/pb_radio/kit.schema.json +4 -1
- data/app/pb_kits/playbook/pb_rich_text_editor/TipTap/ToolbarDropdown.tsx +9 -2
- data/app/pb_kits/playbook/pb_rich_text_editor/TipTap/listSelection.ts +59 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/TipTap/markdownPaste.ts +100 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/_rich_text_editor.tsx +31 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_playground.json +5 -1
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_playground.overrides.json +4 -1
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_markdown_support.jsx +31 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_markdown_support.md +5 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_markdown_support.html.erb +9 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_markdown_support.md +5 -0
- 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 +1 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/kit.schema.json +11 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.html.erb +2 -2
- data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.rb +1 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor_advanced.test.js +241 -1
- data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor_rails.js +109 -0
- data/app/pb_kits/playbook/pb_selectable_card_icon/kit.schema.json +4 -1
- data/app/pb_kits/playbook/pb_selectable_icon/kit.schema.json +4 -1
- data/app/pb_kits/playbook/pb_skeleton_loading/kit.schema.json +8 -2
- data/app/pb_kits/playbook/pb_source/kit.schema.json +4 -1
- data/app/pb_kits/playbook/pb_stat_change/kit.schema.json +4 -1
- data/app/pb_kits/playbook/pb_stat_value/kit.schema.json +4 -1
- data/app/pb_kits/playbook/pb_table/_table.tsx +9 -0
- data/app/pb_kits/playbook/pb_table/docs/_playground.json +29 -1
- data/app/pb_kits/playbook/pb_table/docs/_playground.overrides.json +23 -0
- data/app/pb_kits/playbook/pb_table/docs/_sections.yml +2 -1
- data/app/pb_kits/playbook/pb_table/docs/_table_action_middle.html.erb +38 -32
- data/app/pb_kits/playbook/pb_table/docs/_table_action_middle.jsx +35 -35
- data/app/pb_kits/playbook/pb_table/docs/_table_as_wrapper.html.erb +34 -0
- data/app/pb_kits/playbook/pb_table/docs/_table_as_wrapper.jsx +46 -0
- data/app/pb_kits/playbook/pb_table/docs/_table_as_wrapper.md +2 -0
- data/app/pb_kits/playbook/pb_table/docs/_table_colspan.html.erb +32 -0
- data/app/pb_kits/playbook/pb_table/docs/_table_colspan.jsx +52 -0
- data/app/pb_kits/playbook/pb_table/docs/_table_colspan.md +5 -0
- data/app/pb_kits/playbook/pb_table/docs/_table_container.html.erb +32 -32
- data/app/pb_kits/playbook/pb_table/docs/_table_container.jsx +32 -32
- data/app/pb_kits/playbook/pb_table/docs/_table_contrast_border.html.erb +64 -64
- data/app/pb_kits/playbook/pb_table/docs/_table_contrast_border.jsx +64 -65
- data/app/pb_kits/playbook/pb_table/docs/_table_data_table.html.erb +32 -32
- data/app/pb_kits/playbook/pb_table/docs/_table_data_table.jsx +32 -32
- data/app/pb_kits/playbook/pb_table/docs/_table_disable_hover.html.erb +33 -33
- data/app/pb_kits/playbook/pb_table/docs/_table_disable_hover.jsx +32 -32
- data/app/pb_kits/playbook/pb_table/docs/_table_header.html.erb +8 -8
- data/app/pb_kits/playbook/pb_table/docs/_table_icon_buttons.html.erb +35 -35
- data/app/pb_kits/playbook/pb_table/docs/_table_icon_buttons.jsx +37 -37
- data/app/pb_kits/playbook/pb_table/docs/_table_lg.html.erb +33 -33
- data/app/pb_kits/playbook/pb_table/docs/_table_lg.jsx +32 -32
- data/app/pb_kits/playbook/pb_table/docs/_table_md.html.erb +32 -32
- data/app/pb_kits/playbook/pb_table/docs/_table_md.jsx +32 -32
- data/app/pb_kits/playbook/pb_table/docs/_table_multiline.html.erb +54 -33
- data/app/pb_kits/playbook/pb_table/docs/_table_multiline.jsx +38 -38
- data/app/pb_kits/playbook/pb_table/docs/_table_one_action.html.erb +38 -32
- data/app/pb_kits/playbook/pb_table/docs/_table_one_action.jsx +35 -35
- data/app/pb_kits/playbook/pb_table/docs/_table_outer_padding.html.erb +38 -32
- data/app/pb_kits/playbook/pb_table/docs/_table_outer_padding.jsx +35 -41
- data/app/pb_kits/playbook/pb_table/docs/_table_responsive_table.html.erb +72 -74
- data/app/pb_kits/playbook/pb_table/docs/_table_responsive_table.jsx +72 -72
- data/app/pb_kits/playbook/pb_table/docs/_table_side_highlight.html.erb +99 -99
- data/app/pb_kits/playbook/pb_table/docs/_table_side_highlight.jsx +93 -93
- data/app/pb_kits/playbook/pb_table/docs/_table_single_line.html.erb +53 -46
- data/app/pb_kits/playbook/pb_table/docs/_table_single_line.jsx +38 -38
- data/app/pb_kits/playbook/pb_table/docs/_table_sm.html.erb +33 -33
- data/app/pb_kits/playbook/pb_table/docs/_table_sm.jsx +32 -32
- data/app/pb_kits/playbook/pb_table/docs/_table_sticky.html.erb +89 -82
- data/app/pb_kits/playbook/pb_table/docs/_table_sticky.jsx +82 -81
- data/app/pb_kits/playbook/pb_table/docs/_table_sticky.md +3 -3
- data/app/pb_kits/playbook/pb_table/docs/_table_sticky_columns.html.erb +73 -72
- data/app/pb_kits/playbook/pb_table/docs/_table_sticky_columns.jsx +72 -72
- data/app/pb_kits/playbook/pb_table/docs/_table_sticky_left_columns.html.erb +72 -75
- data/app/pb_kits/playbook/pb_table/docs/_table_sticky_left_columns.jsx +73 -73
- data/app/pb_kits/playbook/pb_table/docs/_table_sticky_right_columns.html.erb +72 -72
- data/app/pb_kits/playbook/pb_table/docs/_table_sticky_right_columns.jsx +73 -73
- data/app/pb_kits/playbook/pb_table/docs/_table_two_actions.html.erb +41 -41
- data/app/pb_kits/playbook/pb_table/docs/_table_two_actions.jsx +37 -37
- data/app/pb_kits/playbook/pb_table/docs/_table_two_plus_actions.html.erb +44 -32
- data/app/pb_kits/playbook/pb_table/docs/_table_two_plus_actions.jsx +57 -54
- data/app/pb_kits/playbook/pb_table/docs/_table_vertical_border.html.erb +32 -32
- data/app/pb_kits/playbook/pb_table/docs/_table_vertical_border.jsx +32 -32
- data/app/pb_kits/playbook/pb_table/docs/_table_with_background_kit.html.erb +64 -62
- data/app/pb_kits/playbook/pb_table/docs/_table_with_background_kit.jsx +69 -79
- data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_rails.md +1 -1
- data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_react.md +1 -1
- data/app/pb_kits/playbook/pb_table/docs/_table_with_header_style_borderless.html.erb +32 -32
- data/app/pb_kits/playbook/pb_table/docs/_table_with_header_style_borderless.jsx +32 -32
- data/app/pb_kits/playbook/pb_table/docs/example.yml +5 -1
- data/app/pb_kits/playbook/pb_table/docs/index.js +3 -1
- data/app/pb_kits/playbook/pb_table/kit.schema.json +6 -0
- data/app/pb_kits/playbook/pb_table/subcomponents/_table_cell.tsx +6 -1
- data/app/pb_kits/playbook/pb_table/subcomponents/_table_header.tsx +6 -1
- data/app/pb_kits/playbook/pb_table/table.test.js +62 -0
- data/app/pb_kits/playbook/pb_table/table_cell.html.erb +1 -1
- data/app/pb_kits/playbook/pb_table/table_cell.rb +1 -0
- data/app/pb_kits/playbook/pb_text_input/kit.schema.json +4 -1
- data/app/pb_kits/playbook/pb_timeline/kit.schema.json +4 -1
- data/app/pb_kits/playbook/pb_user_badge/user_badge.rb +1 -5
- data/dist/chunks/{_table-SGw4kUye.js → _table-CUU7TisO.js} +1 -1
- data/dist/chunks/vendor.js +2 -2
- data/lib/playbook/changelog_generator.rb +204 -23
- data/lib/playbook/engine.rb +0 -2
- data/lib/playbook/version.rb +1 -1
- metadata +27 -13
|
@@ -24,6 +24,7 @@ examples:
|
|
|
24
24
|
- multi_level_select_default: Default
|
|
25
25
|
- multi_level_select_react_hook: React Hook
|
|
26
26
|
- multi_level_select_single: Single Select
|
|
27
|
+
- multi_level_select_format_selected_display: Custom Single Select Display
|
|
27
28
|
- multi_level_select_single_children_only: Single Select w/ Hidden Radios
|
|
28
29
|
- multi_level_select_return_all_selected: Return All Selected
|
|
29
30
|
- multi_level_select_input_display: With Input Display None
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { default as MultiLevelSelectDefault } from "./_multi_level_select_default.jsx"
|
|
2
2
|
export { default as MultiLevelSelectSingle } from "./_multi_level_select_single.jsx"
|
|
3
|
+
export { default as MultiLevelSelectFormatSelectedDisplay } from "./_multi_level_select_format_selected_display.jsx"
|
|
3
4
|
export { default as MultiLevelSelectSingleChildrenOnly } from "./_multi_level_select_single_children_only.jsx"
|
|
4
5
|
export { default as MultiLevelSelectReturnAllSelected } from "./_multi_level_select_return_all_selected.jsx"
|
|
5
6
|
export { default as MultiLevelSelectSelectedIdsReact } from "./_multi_level_select_selected_ids_react.jsx"
|
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
],
|
|
24
24
|
"default": ""
|
|
25
25
|
},
|
|
26
|
+
"formatSelectedDisplay": {
|
|
27
|
+
"type": "function",
|
|
28
|
+
"platforms": [
|
|
29
|
+
"react"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
26
32
|
"inputDisplay": {
|
|
27
33
|
"type": "enum",
|
|
28
34
|
"platforms": [
|
|
@@ -91,7 +97,10 @@
|
|
|
91
97
|
"default": true
|
|
92
98
|
},
|
|
93
99
|
"treeData": {
|
|
94
|
-
"type":
|
|
100
|
+
"type": {
|
|
101
|
+
"react": "GenericObject",
|
|
102
|
+
"rails": "array"
|
|
103
|
+
},
|
|
95
104
|
"platforms": [
|
|
96
105
|
"react",
|
|
97
106
|
"rails"
|
|
@@ -118,7 +127,10 @@
|
|
|
118
127
|
"default": "Start typing..."
|
|
119
128
|
},
|
|
120
129
|
"selectedIds": {
|
|
121
|
-
"type":
|
|
130
|
+
"type": {
|
|
131
|
+
"react": "string[] | any",
|
|
132
|
+
"rails": "array"
|
|
133
|
+
},
|
|
122
134
|
"platforms": [
|
|
123
135
|
"react",
|
|
124
136
|
"rails"
|
|
@@ -384,8 +384,47 @@ describe('MultiLevelSelect single variant', () => {
|
|
|
384
384
|
|
|
385
385
|
expect(input.value).toBe('Power Home Remodeling')
|
|
386
386
|
})
|
|
387
|
+
|
|
388
|
+
test('should format the selected input display with hierarchy context', () => {
|
|
389
|
+
render(
|
|
390
|
+
<MultiLevelSelect
|
|
391
|
+
data={{ testid: testId }}
|
|
392
|
+
formatSelectedDisplay={(item, { path }) =>
|
|
393
|
+
path.map(({ label }) => label).join(' / ')
|
|
394
|
+
}
|
|
395
|
+
treeData={treeData}
|
|
396
|
+
variant="single"
|
|
397
|
+
/>
|
|
398
|
+
)
|
|
399
|
+
const kit = screen.getByTestId(testId)
|
|
400
|
+
const input = kit.querySelector('#multiselect_input')
|
|
401
|
+
fireEvent.click(input)
|
|
402
|
+
fireEvent.click(kit.querySelector('#people1'))
|
|
403
|
+
|
|
404
|
+
expect(input.value).toBe('Power Home Remodeling / People')
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
test('should format the selected input display from selectedIds', () => {
|
|
408
|
+
render(
|
|
409
|
+
<MultiLevelSelect
|
|
410
|
+
data={{ testid: testId }}
|
|
411
|
+
formatSelectedDisplay={(item, { ancestors }) => {
|
|
412
|
+
const parent = ancestors[ancestors.length - 1]
|
|
413
|
+
return `${parent.label}: ${item.label}`
|
|
414
|
+
}}
|
|
415
|
+
selectedIds={['talent1']}
|
|
416
|
+
treeData={treeData}
|
|
417
|
+
variant="single"
|
|
418
|
+
/>
|
|
419
|
+
)
|
|
420
|
+
const kit = screen.getByTestId(testId)
|
|
421
|
+
const input = kit.querySelector('#multiselect_input')
|
|
422
|
+
|
|
423
|
+
expect(input.value).toBe('People: Talent Acquisition')
|
|
424
|
+
})
|
|
387
425
|
})
|
|
388
426
|
|
|
427
|
+
|
|
389
428
|
describe('MultiLevelSelect disabled parent behavior', () => {
|
|
390
429
|
test('children of disabled parent should also be disabled in single variant', () => {
|
|
391
430
|
render(
|
|
@@ -833,4 +872,3 @@ describe('PbMultiLevelSelect Rails enhanced element', () => {
|
|
|
833
872
|
expect(element.querySelectorAll('.dropdown_item').length).toBeGreaterThan(0)
|
|
834
873
|
})
|
|
835
874
|
})
|
|
836
|
-
|
|
@@ -36,7 +36,10 @@
|
|
|
36
36
|
"default": false
|
|
37
37
|
},
|
|
38
38
|
"variant": {
|
|
39
|
-
"type":
|
|
39
|
+
"type": {
|
|
40
|
+
"react": "string",
|
|
41
|
+
"rails": "enum"
|
|
42
|
+
},
|
|
40
43
|
"platforms": [
|
|
41
44
|
"react",
|
|
42
45
|
"rails"
|
|
@@ -48,7 +51,10 @@
|
|
|
48
51
|
"default": "default"
|
|
49
52
|
},
|
|
50
53
|
"color": {
|
|
51
|
-
"type":
|
|
54
|
+
"type": {
|
|
55
|
+
"react": "string",
|
|
56
|
+
"rails": "enum"
|
|
57
|
+
},
|
|
52
58
|
"platforms": [
|
|
53
59
|
"react",
|
|
54
60
|
"rails"
|
|
@@ -8,6 +8,7 @@ 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'
|
|
11
12
|
|
|
12
13
|
const ToolbarDropdown = ({editor}: any): React.ReactElement => {
|
|
13
14
|
const [showPopover, setShowPopover] = useState(false)
|
|
@@ -46,14 +47,20 @@ const toolbarDropdownItems = [
|
|
|
46
47
|
icon: "list",
|
|
47
48
|
isActive: editor.isActive("bulletList"),
|
|
48
49
|
text: "Bullet List",
|
|
49
|
-
onclick: () =>
|
|
50
|
+
onclick: () => {
|
|
51
|
+
normalizeListSelection(editor)
|
|
52
|
+
editor.chain().focus().toggleBulletList().run()
|
|
53
|
+
},
|
|
50
54
|
},
|
|
51
55
|
{
|
|
52
56
|
node: "orderedList",
|
|
53
57
|
icon: "list-ol",
|
|
54
58
|
isActive: editor.isActive("orderedList"),
|
|
55
59
|
text: "Ordered List",
|
|
56
|
-
onclick: () =>
|
|
60
|
+
onclick: () => {
|
|
61
|
+
normalizeListSelection(editor)
|
|
62
|
+
editor.chain().focus().toggleOrderedList().run()
|
|
63
|
+
}
|
|
57
64
|
,
|
|
58
65
|
},
|
|
59
66
|
{
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Editor } from "@tiptap/core"
|
|
2
|
+
|
|
3
|
+
type ListSelection = { from: number, to: number } | null
|
|
4
|
+
|
|
5
|
+
const getNormalizedListSelection = (editor: Editor): ListSelection => {
|
|
6
|
+
const { selection } = editor.state
|
|
7
|
+
|
|
8
|
+
if (selection.empty) return null
|
|
9
|
+
|
|
10
|
+
const { $from, $to } = selection
|
|
11
|
+
let { from, to } = selection
|
|
12
|
+
|
|
13
|
+
if (
|
|
14
|
+
$from.depth > 0 &&
|
|
15
|
+
$from.parent.isTextblock &&
|
|
16
|
+
$from.parentOffset === $from.parent.content.size
|
|
17
|
+
) {
|
|
18
|
+
const nextBlockStart = $from.after($from.depth) + 1
|
|
19
|
+
if (nextBlockStart < to) from = nextBlockStart
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (
|
|
23
|
+
$to.depth > 0 &&
|
|
24
|
+
$to.parent.isTextblock &&
|
|
25
|
+
$to.parentOffset === 0
|
|
26
|
+
) {
|
|
27
|
+
const previousBlockEnd = $to.before($to.depth) - 1
|
|
28
|
+
if (previousBlockEnd > from) to = previousBlockEnd
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return from === selection.from && to === selection.to ? null : { from, to }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const normalizeListSelection = (editor: Editor): void => {
|
|
35
|
+
const initialSelection = editor.state.selection
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
!initialSelection.empty &&
|
|
39
|
+
initialSelection.$from.nodeBefore?.type.name === "hardBreak"
|
|
40
|
+
) {
|
|
41
|
+
const selectedSize = initialSelection.to - initialSelection.from
|
|
42
|
+
const breakPosition = initialSelection.from - 1
|
|
43
|
+
|
|
44
|
+
editor.chain()
|
|
45
|
+
.deleteRange({ from: breakPosition, to: initialSelection.from })
|
|
46
|
+
.setTextSelection(breakPosition)
|
|
47
|
+
.splitBlock()
|
|
48
|
+
.run()
|
|
49
|
+
|
|
50
|
+
const from = editor.state.selection.from
|
|
51
|
+
editor.commands.setTextSelection({ from, to: from + selectedSize })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const selection = getNormalizedListSelection(editor)
|
|
55
|
+
|
|
56
|
+
if (selection) editor.commands.setTextSelection(selection)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { getNormalizedListSelection, normalizeListSelection }
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const NEUTRAL_CLIPBOARD_TAGS = new Set(["html", "head", "body", "meta", "div", "span", "p"])
|
|
2
|
+
const MARKDOWN_SOURCE_TAGS = new Set([...NEUTRAL_CLIPBOARD_TAGS, "pre", "code"])
|
|
3
|
+
|
|
4
|
+
type MarkdownJSONNode = {
|
|
5
|
+
content?: MarkdownJSONNode[],
|
|
6
|
+
marks?: unknown[],
|
|
7
|
+
type: string,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type MarkdownDocumentNode = {
|
|
11
|
+
content: unknown,
|
|
12
|
+
toJSON: () => MarkdownJSONNode,
|
|
13
|
+
type: { schema: unknown },
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type MarkdownParser = {
|
|
17
|
+
parse: (text: string) => MarkdownDocumentNode | null,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type MarkdownDOMSerializer = {
|
|
21
|
+
fromSchema: (schema: unknown) => {
|
|
22
|
+
serializeFragment: (content: unknown) => DocumentFragment,
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type MarkdownDependencies = {
|
|
27
|
+
parser: MarkdownParser,
|
|
28
|
+
serializer: MarkdownDOMSerializer,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const hasRichTextFormatting = (html: string, text: string): boolean => {
|
|
32
|
+
if (!html) return false
|
|
33
|
+
|
|
34
|
+
const clipboardDocument = new DOMParser().parseFromString(html, "text/html")
|
|
35
|
+
const elements = [...clipboardDocument.querySelectorAll("*")]
|
|
36
|
+
const hasStyledElement = elements.some((element) => element.hasAttribute("style"))
|
|
37
|
+
if (hasStyledElement) return true
|
|
38
|
+
|
|
39
|
+
const hasOnlyNeutralTags = elements.every((element) =>
|
|
40
|
+
NEUTRAL_CLIPBOARD_TAGS.has(element.tagName.toLowerCase())
|
|
41
|
+
)
|
|
42
|
+
if (hasOnlyNeutralTags) return false
|
|
43
|
+
|
|
44
|
+
const hasOnlyMarkdownSourceTags = elements.every((element) =>
|
|
45
|
+
MARKDOWN_SOURCE_TAGS.has(element.tagName.toLowerCase())
|
|
46
|
+
)
|
|
47
|
+
const sourceTextMatches = clipboardDocument.body.textContent?.trim() === text.trim()
|
|
48
|
+
return !(hasOnlyMarkdownSourceTags && sourceTextMatches)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const hasMarkdownFormatting = (node: MarkdownJSONNode): boolean => {
|
|
52
|
+
const isFormattedNode = !["doc", "paragraph", "text"].includes(node.type)
|
|
53
|
+
if (isFormattedNode || node.marks?.length) return true
|
|
54
|
+
|
|
55
|
+
return node.content?.some(hasMarkdownFormatting) || false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const createMarkdownToHTML = (
|
|
59
|
+
markdownParser: MarkdownParser,
|
|
60
|
+
domSerializer: MarkdownDOMSerializer
|
|
61
|
+
) => (text: string): string | null => {
|
|
62
|
+
const documentNode = markdownParser.parse(text)
|
|
63
|
+
if (!documentNode || !hasMarkdownFormatting(documentNode.toJSON())) return null
|
|
64
|
+
|
|
65
|
+
const container = document.createElement("div")
|
|
66
|
+
const serializer = domSerializer.fromSchema(documentNode.type.schema)
|
|
67
|
+
container.appendChild(serializer.serializeFragment(documentNode.content))
|
|
68
|
+
container.querySelectorAll("[data-tight]").forEach((node) => node.removeAttribute("data-tight"))
|
|
69
|
+
|
|
70
|
+
return container.innerHTML
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const handleMarkdownPaste = (
|
|
74
|
+
event: ClipboardEvent,
|
|
75
|
+
insertHTML: (html: string) => void,
|
|
76
|
+
markdownToHTML: (text: string) => string | null
|
|
77
|
+
): boolean => {
|
|
78
|
+
const text = event.clipboardData?.getData("text/plain")
|
|
79
|
+
const richHtml = event.clipboardData?.getData("text/html")
|
|
80
|
+
if (!text || hasRichTextFormatting(richHtml, text)) return false
|
|
81
|
+
|
|
82
|
+
let html
|
|
83
|
+
try {
|
|
84
|
+
html = markdownToHTML(text)
|
|
85
|
+
} catch (_error) {
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
if (!html) return false
|
|
89
|
+
|
|
90
|
+
event.preventDefault()
|
|
91
|
+
event.stopImmediatePropagation()
|
|
92
|
+
insertHTML(html)
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export {
|
|
97
|
+
createMarkdownToHTML,
|
|
98
|
+
handleMarkdownPaste,
|
|
99
|
+
}
|
|
100
|
+
export type { MarkdownDependencies }
|
|
@@ -5,6 +5,11 @@ 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'
|
|
8
13
|
import TipTapEditor from './_tiptap_editor'
|
|
9
14
|
import TrixTextEditor from './_trix_editor'
|
|
10
15
|
|
|
@@ -22,6 +27,7 @@ type RichTextEditorProps = {
|
|
|
22
27
|
id?: string,
|
|
23
28
|
inline?: boolean,
|
|
24
29
|
label?: string,
|
|
30
|
+
markdownSupport?: MarkdownDependencies,
|
|
25
31
|
extensions?: { [key: string]: string }[],
|
|
26
32
|
name?: string,
|
|
27
33
|
onChange: (html: string, text: string) => void,
|
|
@@ -65,6 +71,7 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
|
|
|
65
71
|
maxWidth = "md",
|
|
66
72
|
requiredIndicator = false,
|
|
67
73
|
label,
|
|
74
|
+
markdownSupport,
|
|
68
75
|
TrixEditor,
|
|
69
76
|
trixInstance: trixInstance = undefined,
|
|
70
77
|
} = props
|
|
@@ -89,6 +96,30 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
|
|
|
89
96
|
dom.setAttribute("aria-multiline", "true")
|
|
90
97
|
}, [advancedEditor, fieldId, labelElementId])
|
|
91
98
|
|
|
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
|
+
|
|
92
123
|
//===========focus prop with advanced editor=================
|
|
93
124
|
const isClickInPopover = (event: Event): boolean => {
|
|
94
125
|
return !!(event.target as Element).closest('.pb_tiptap_toolbar_dropdown_popover')
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"template": "<RichTextEditor{{props}} />",
|
|
3
3
|
"propTargets": {},
|
|
4
4
|
"defaults": {
|
|
5
|
+
"markdownSupport": false,
|
|
5
6
|
"requiredIndicator": false,
|
|
6
7
|
"simple": false,
|
|
7
8
|
"advancedEditorToolbar": true,
|
|
@@ -133,7 +134,9 @@
|
|
|
133
134
|
"trix",
|
|
134
135
|
"@tiptap/react",
|
|
135
136
|
"@tiptap/starter-kit",
|
|
136
|
-
"@tiptap/extension-link"
|
|
137
|
+
"@tiptap/extension-link",
|
|
138
|
+
"prosemirror-markdown",
|
|
139
|
+
"prosemirror-model"
|
|
137
140
|
],
|
|
138
141
|
"optional": true,
|
|
139
142
|
"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."
|
|
@@ -158,6 +161,7 @@
|
|
|
158
161
|
"advancedEditor",
|
|
159
162
|
"extensions",
|
|
160
163
|
"inputOptions",
|
|
164
|
+
"markdownSupport",
|
|
161
165
|
"onChange",
|
|
162
166
|
"template",
|
|
163
167
|
"TrixEditor",
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
"trix",
|
|
6
6
|
"@tiptap/react",
|
|
7
7
|
"@tiptap/starter-kit",
|
|
8
|
-
"@tiptap/extension-link"
|
|
8
|
+
"@tiptap/extension-link",
|
|
9
|
+
"prosemirror-markdown",
|
|
10
|
+
"prosemirror-model"
|
|
9
11
|
],
|
|
10
12
|
"optional": true,
|
|
11
13
|
"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."
|
|
@@ -40,6 +42,7 @@
|
|
|
40
42
|
"advancedEditor",
|
|
41
43
|
"extensions",
|
|
42
44
|
"inputOptions",
|
|
45
|
+
"markdownSupport",
|
|
43
46
|
"onChange",
|
|
44
47
|
"template",
|
|
45
48
|
"TrixEditor",
|
data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_markdown_support.jsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import RichTextEditor from '../../pb_rich_text_editor/_rich_text_editor'
|
|
3
|
+
import { useEditor, EditorContent } from '@tiptap/react'
|
|
4
|
+
import StarterKit from '@tiptap/starter-kit'
|
|
5
|
+
import Link from '@tiptap/extension-link'
|
|
6
|
+
import { defaultMarkdownParser } from 'prosemirror-markdown'
|
|
7
|
+
import { DOMSerializer } from 'prosemirror-model'
|
|
8
|
+
|
|
9
|
+
const RichTextEditorAdvancedMarkdownSupport = (props) => {
|
|
10
|
+
const editor = useEditor({
|
|
11
|
+
extensions: [StarterKit, Link],
|
|
12
|
+
content: 'Paste Markdown here to preserve headings, emphasis, links, lists, quotes, and code blocks.',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
if (!editor) return null
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<RichTextEditor
|
|
19
|
+
advancedEditor={editor}
|
|
20
|
+
markdownSupport={{
|
|
21
|
+
parser: defaultMarkdownParser,
|
|
22
|
+
serializer: DOMSerializer,
|
|
23
|
+
}}
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
<EditorContent editor={editor} />
|
|
27
|
+
</RichTextEditor>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default RichTextEditorAdvancedMarkdownSupport
|
data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_markdown_support.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Use `markdownSupport` to convert recognizable Markdown from the clipboard into formatted TipTap content when it is pasted into the editor. Pass the parser and serializer together through `markdownSupport`; Playbook owns the conversion, safe clipboard detection, and TipTap insertion while the consuming application supplies the libraries.
|
|
2
|
+
|
|
3
|
+
Markdown support is opt-in for backward compatibility. Omit `markdownSupport` to keep TipTap's standard paste behavior. Rich HTML and ordinary plain text also continue through the standard paste path when Markdown syntax is not detected.
|
|
4
|
+
|
|
5
|
+
The example uses `prosemirror-markdown` and `prosemirror-model`. These parser packages are not included in the `playbook-ui` runtime bundle.
|
data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_markdown_support.html.erb
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<%= pb_rails("rich_text_editor", props: {
|
|
2
|
+
label: "Markdown-enabled description",
|
|
3
|
+
markdown_support: true,
|
|
4
|
+
input_options: {
|
|
5
|
+
id: "rails_rte_markdown_support_demo",
|
|
6
|
+
name: "description",
|
|
7
|
+
},
|
|
8
|
+
value: "<p>Paste Markdown here to preserve its formatting.</p>",
|
|
9
|
+
}) %>
|
data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_markdown_support.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Set `markdown_support: true` to convert recognizable Markdown from the clipboard into formatted TipTap content. Supported formatting includes headings, bold and italic text, links, ordered and unordered lists, blockquotes, and fenced code blocks.
|
|
2
|
+
|
|
3
|
+
The prop is opt-in and defaults to `false`, so existing Rails Rich Text Editors retain TipTap's standard paste behavior. Rich HTML, tables, styled content, media, and ordinary plain text also remain on TipTap's native paste path.
|
|
4
|
+
|
|
5
|
+
When Markdown support is enabled, the Rails editor loads `prosemirror-markdown` and `prosemirror-model` from [esm.sh](https://esm.sh) before initializing. Applications with a strict Content Security Policy must allow esm.sh and its redirected module URLs.
|
|
@@ -2,10 +2,12 @@ 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)"
|
|
5
6
|
- rich_text_editor_rails_simple: "Rails (TipTap — Simple toolbar)"
|
|
6
7
|
|
|
7
8
|
react:
|
|
8
9
|
- rich_text_editor_advanced_default: Advanced Default
|
|
10
|
+
- rich_text_editor_advanced_markdown_support: Advanced (Markdown Support)
|
|
9
11
|
- rich_text_editor_more_extensions: Advanced (Extra Extensions)
|
|
10
12
|
- rich_text_editor_toolbar_disabled: Advanced (Toolbar disabled)
|
|
11
13
|
- rich_text_editor_advanced_simple: Advanced (Simple)
|
|
@@ -30,5 +32,3 @@ examples:
|
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
@@ -1,5 +1,6 @@
|
|
|
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'
|
|
3
4
|
export { default as RichTextEditorMoreExtensions } from './_rich_text_editor_more_extensions.jsx'
|
|
4
5
|
export { default as RichTextEditorToolbarDisabled } from './_rich_text_editor_toolbar_disabled.jsx'
|
|
5
6
|
export { default as RichTextEditorAdvancedPreview } from './_rich_text_editor_advanced_preview.jsx'
|
|
@@ -51,6 +51,17 @@
|
|
|
51
51
|
"rails"
|
|
52
52
|
]
|
|
53
53
|
},
|
|
54
|
+
"markdownSupport": {
|
|
55
|
+
"type": {
|
|
56
|
+
"react": "MarkdownDependencies",
|
|
57
|
+
"rails": "boolean"
|
|
58
|
+
},
|
|
59
|
+
"platforms": [
|
|
60
|
+
"react",
|
|
61
|
+
"rails"
|
|
62
|
+
],
|
|
63
|
+
"default": false
|
|
64
|
+
},
|
|
54
65
|
"extensions": {
|
|
55
66
|
"type": "GenericObject",
|
|
56
67
|
"platforms": [
|
|
@@ -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, rte_simple: object.simple }) do %>
|
|
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, markdown_support: object.markdown_support, 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 %>">
|
|
@@ -158,5 +158,5 @@
|
|
|
158
158
|
|
|
159
159
|
<%# Module script: rich_text_editor_rails.js %>
|
|
160
160
|
<script type="module">
|
|
161
|
-
<%= File.read(Playbook.kit_path("rich_text_editor", "rich_text_editor_rails.js")).html_safe %>
|
|
161
|
+
<%= ::File.read(Playbook.kit_path("rich_text_editor", "rich_text_editor_rails.js")).html_safe %>
|
|
162
162
|
</script>
|
|
@@ -9,6 +9,7 @@ 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
|
|
12
13
|
# When true, TipTap toolbar matches React `simple`: Bold + Italic only (no block-style Popover).
|
|
13
14
|
# Use in modals or narrow layouts where the block dropdown misbehaves.
|
|
14
15
|
prop :simple, type: Playbook::Props::Boolean, default: false
|