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.
Files changed (129) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/kit.schema.json +8 -2
  3. data/app/pb_kits/playbook/pb_background/kit.schema.json +16 -4
  4. data/app/pb_kits/playbook/pb_card/kit.schema.json +4 -1
  5. data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_time.jsx +14 -18
  6. data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_time.md +3 -1
  7. data/app/pb_kits/playbook/pb_date_picker/kit.schema.json +4 -1
  8. data/app/pb_kits/playbook/pb_dropdown/kit.schema.json +8 -2
  9. data/app/pb_kits/playbook/pb_file_upload/kit.schema.json +4 -1
  10. data/app/pb_kits/playbook/pb_filter/kit.schema.json +4 -1
  11. data/app/pb_kits/playbook/pb_home_address_street/kit.schema.json +4 -1
  12. data/app/pb_kits/playbook/pb_icon/kit.schema.json +8 -2
  13. data/app/pb_kits/playbook/pb_layout/docs/_playground.json +1 -4
  14. data/app/pb_kits/playbook/pb_message/kit.schema.json +4 -1
  15. data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.tsx +31 -1
  16. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_format_selected_display.jsx +191 -0
  17. data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_format_selected_display.md +1 -0
  18. data/app/pb_kits/playbook/pb_multi_level_select/docs/_playground.json +31 -2
  19. data/app/pb_kits/playbook/pb_multi_level_select/docs/_playground.overrides.json +31 -2
  20. data/app/pb_kits/playbook/pb_multi_level_select/docs/example.yml +1 -0
  21. data/app/pb_kits/playbook/pb_multi_level_select/docs/index.js +1 -0
  22. data/app/pb_kits/playbook/pb_multi_level_select/kit.schema.json +14 -2
  23. data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.test.jsx +39 -1
  24. data/app/pb_kits/playbook/pb_overlay/kit.schema.json +4 -1
  25. data/app/pb_kits/playbook/pb_pagination/pagination.html.erb +1 -1
  26. data/app/pb_kits/playbook/pb_progress_step/kit.schema.json +8 -2
  27. data/app/pb_kits/playbook/pb_radio/kit.schema.json +4 -1
  28. data/app/pb_kits/playbook/pb_rich_text_editor/TipTap/ToolbarDropdown.tsx +9 -2
  29. data/app/pb_kits/playbook/pb_rich_text_editor/TipTap/listSelection.ts +59 -0
  30. data/app/pb_kits/playbook/pb_rich_text_editor/TipTap/markdownPaste.ts +100 -0
  31. data/app/pb_kits/playbook/pb_rich_text_editor/_rich_text_editor.tsx +31 -0
  32. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_playground.json +5 -1
  33. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_playground.overrides.json +4 -1
  34. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_markdown_support.jsx +31 -0
  35. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_advanced_markdown_support.md +5 -0
  36. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_markdown_support.html.erb +9 -0
  37. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_markdown_support.md +5 -0
  38. data/app/pb_kits/playbook/pb_rich_text_editor/docs/example.yml +2 -2
  39. data/app/pb_kits/playbook/pb_rich_text_editor/docs/index.js +1 -0
  40. data/app/pb_kits/playbook/pb_rich_text_editor/kit.schema.json +11 -0
  41. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.html.erb +2 -2
  42. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.rb +1 -0
  43. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor_advanced.test.js +241 -1
  44. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor_rails.js +109 -0
  45. data/app/pb_kits/playbook/pb_selectable_card_icon/kit.schema.json +4 -1
  46. data/app/pb_kits/playbook/pb_selectable_icon/kit.schema.json +4 -1
  47. data/app/pb_kits/playbook/pb_skeleton_loading/kit.schema.json +8 -2
  48. data/app/pb_kits/playbook/pb_source/kit.schema.json +4 -1
  49. data/app/pb_kits/playbook/pb_stat_change/kit.schema.json +4 -1
  50. data/app/pb_kits/playbook/pb_stat_value/kit.schema.json +4 -1
  51. data/app/pb_kits/playbook/pb_table/_table.tsx +9 -0
  52. data/app/pb_kits/playbook/pb_table/docs/_playground.json +29 -1
  53. data/app/pb_kits/playbook/pb_table/docs/_playground.overrides.json +23 -0
  54. data/app/pb_kits/playbook/pb_table/docs/_sections.yml +2 -1
  55. data/app/pb_kits/playbook/pb_table/docs/_table_action_middle.html.erb +38 -32
  56. data/app/pb_kits/playbook/pb_table/docs/_table_action_middle.jsx +35 -35
  57. data/app/pb_kits/playbook/pb_table/docs/_table_as_wrapper.html.erb +34 -0
  58. data/app/pb_kits/playbook/pb_table/docs/_table_as_wrapper.jsx +46 -0
  59. data/app/pb_kits/playbook/pb_table/docs/_table_as_wrapper.md +2 -0
  60. data/app/pb_kits/playbook/pb_table/docs/_table_colspan.html.erb +32 -0
  61. data/app/pb_kits/playbook/pb_table/docs/_table_colspan.jsx +52 -0
  62. data/app/pb_kits/playbook/pb_table/docs/_table_colspan.md +5 -0
  63. data/app/pb_kits/playbook/pb_table/docs/_table_container.html.erb +32 -32
  64. data/app/pb_kits/playbook/pb_table/docs/_table_container.jsx +32 -32
  65. data/app/pb_kits/playbook/pb_table/docs/_table_contrast_border.html.erb +64 -64
  66. data/app/pb_kits/playbook/pb_table/docs/_table_contrast_border.jsx +64 -65
  67. data/app/pb_kits/playbook/pb_table/docs/_table_data_table.html.erb +32 -32
  68. data/app/pb_kits/playbook/pb_table/docs/_table_data_table.jsx +32 -32
  69. data/app/pb_kits/playbook/pb_table/docs/_table_disable_hover.html.erb +33 -33
  70. data/app/pb_kits/playbook/pb_table/docs/_table_disable_hover.jsx +32 -32
  71. data/app/pb_kits/playbook/pb_table/docs/_table_header.html.erb +8 -8
  72. data/app/pb_kits/playbook/pb_table/docs/_table_icon_buttons.html.erb +35 -35
  73. data/app/pb_kits/playbook/pb_table/docs/_table_icon_buttons.jsx +37 -37
  74. data/app/pb_kits/playbook/pb_table/docs/_table_lg.html.erb +33 -33
  75. data/app/pb_kits/playbook/pb_table/docs/_table_lg.jsx +32 -32
  76. data/app/pb_kits/playbook/pb_table/docs/_table_md.html.erb +32 -32
  77. data/app/pb_kits/playbook/pb_table/docs/_table_md.jsx +32 -32
  78. data/app/pb_kits/playbook/pb_table/docs/_table_multiline.html.erb +54 -33
  79. data/app/pb_kits/playbook/pb_table/docs/_table_multiline.jsx +38 -38
  80. data/app/pb_kits/playbook/pb_table/docs/_table_one_action.html.erb +38 -32
  81. data/app/pb_kits/playbook/pb_table/docs/_table_one_action.jsx +35 -35
  82. data/app/pb_kits/playbook/pb_table/docs/_table_outer_padding.html.erb +38 -32
  83. data/app/pb_kits/playbook/pb_table/docs/_table_outer_padding.jsx +35 -41
  84. data/app/pb_kits/playbook/pb_table/docs/_table_responsive_table.html.erb +72 -74
  85. data/app/pb_kits/playbook/pb_table/docs/_table_responsive_table.jsx +72 -72
  86. data/app/pb_kits/playbook/pb_table/docs/_table_side_highlight.html.erb +99 -99
  87. data/app/pb_kits/playbook/pb_table/docs/_table_side_highlight.jsx +93 -93
  88. data/app/pb_kits/playbook/pb_table/docs/_table_single_line.html.erb +53 -46
  89. data/app/pb_kits/playbook/pb_table/docs/_table_single_line.jsx +38 -38
  90. data/app/pb_kits/playbook/pb_table/docs/_table_sm.html.erb +33 -33
  91. data/app/pb_kits/playbook/pb_table/docs/_table_sm.jsx +32 -32
  92. data/app/pb_kits/playbook/pb_table/docs/_table_sticky.html.erb +89 -82
  93. data/app/pb_kits/playbook/pb_table/docs/_table_sticky.jsx +82 -81
  94. data/app/pb_kits/playbook/pb_table/docs/_table_sticky.md +3 -3
  95. data/app/pb_kits/playbook/pb_table/docs/_table_sticky_columns.html.erb +73 -72
  96. data/app/pb_kits/playbook/pb_table/docs/_table_sticky_columns.jsx +72 -72
  97. data/app/pb_kits/playbook/pb_table/docs/_table_sticky_left_columns.html.erb +72 -75
  98. data/app/pb_kits/playbook/pb_table/docs/_table_sticky_left_columns.jsx +73 -73
  99. data/app/pb_kits/playbook/pb_table/docs/_table_sticky_right_columns.html.erb +72 -72
  100. data/app/pb_kits/playbook/pb_table/docs/_table_sticky_right_columns.jsx +73 -73
  101. data/app/pb_kits/playbook/pb_table/docs/_table_two_actions.html.erb +41 -41
  102. data/app/pb_kits/playbook/pb_table/docs/_table_two_actions.jsx +37 -37
  103. data/app/pb_kits/playbook/pb_table/docs/_table_two_plus_actions.html.erb +44 -32
  104. data/app/pb_kits/playbook/pb_table/docs/_table_two_plus_actions.jsx +57 -54
  105. data/app/pb_kits/playbook/pb_table/docs/_table_vertical_border.html.erb +32 -32
  106. data/app/pb_kits/playbook/pb_table/docs/_table_vertical_border.jsx +32 -32
  107. data/app/pb_kits/playbook/pb_table/docs/_table_with_background_kit.html.erb +64 -62
  108. data/app/pb_kits/playbook/pb_table/docs/_table_with_background_kit.jsx +69 -79
  109. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_rails.md +1 -1
  110. data/app/pb_kits/playbook/pb_table/docs/_table_with_filter_variant_react.md +1 -1
  111. data/app/pb_kits/playbook/pb_table/docs/_table_with_header_style_borderless.html.erb +32 -32
  112. data/app/pb_kits/playbook/pb_table/docs/_table_with_header_style_borderless.jsx +32 -32
  113. data/app/pb_kits/playbook/pb_table/docs/example.yml +5 -1
  114. data/app/pb_kits/playbook/pb_table/docs/index.js +3 -1
  115. data/app/pb_kits/playbook/pb_table/kit.schema.json +6 -0
  116. data/app/pb_kits/playbook/pb_table/subcomponents/_table_cell.tsx +6 -1
  117. data/app/pb_kits/playbook/pb_table/subcomponents/_table_header.tsx +6 -1
  118. data/app/pb_kits/playbook/pb_table/table.test.js +62 -0
  119. data/app/pb_kits/playbook/pb_table/table_cell.html.erb +1 -1
  120. data/app/pb_kits/playbook/pb_table/table_cell.rb +1 -0
  121. data/app/pb_kits/playbook/pb_text_input/kit.schema.json +4 -1
  122. data/app/pb_kits/playbook/pb_timeline/kit.schema.json +4 -1
  123. data/app/pb_kits/playbook/pb_user_badge/user_badge.rb +1 -5
  124. data/dist/chunks/{_table-SGw4kUye.js → _table-CUU7TisO.js} +1 -1
  125. data/dist/chunks/vendor.js +2 -2
  126. data/lib/playbook/changelog_generator.rb +204 -23
  127. data/lib/playbook/engine.rb +0 -2
  128. data/lib/playbook/version.rb +1 -1
  129. 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": "GenericObject",
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": "string[] | any",
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
-
@@ -26,7 +26,10 @@
26
26
  "default": "card_light"
27
27
  },
28
28
  "dynamic": {
29
- "type": "false",
29
+ "type": {
30
+ "react": "false",
31
+ "rails": "boolean"
32
+ },
30
33
  "platforms": [
31
34
  "react",
32
35
  "rails"
@@ -1,3 +1,3 @@
1
1
  <%= pb_content_tag do %>
2
- <%= will_paginate object.model, renderer: Playbook::Pagination::Rails %>
2
+ <%= helpers.will_paginate object.model, renderer: Playbook::Pagination::Rails %>
3
3
  <% end %>
@@ -36,7 +36,10 @@
36
36
  "default": false
37
37
  },
38
38
  "variant": {
39
- "type": "enum",
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": "enum",
54
+ "type": {
55
+ "react": "string",
56
+ "rails": "enum"
57
+ },
52
58
  "platforms": [
53
59
  "react",
54
60
  "rails"
@@ -16,7 +16,10 @@
16
16
  "default": ""
17
17
  },
18
18
  "checked": {
19
- "type": "boolean, // removed default assignment here",
19
+ "type": {
20
+ "react": "boolean, // removed default assignment here",
21
+ "rails": "boolean"
22
+ },
20
23
  "platforms": [
21
24
  "react",
22
25
  "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: () => editor.chain().focus().toggleBulletList().run(),
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: () => editor.chain().focus().toggleOrderedList().run()
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",
@@ -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
@@ -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.
@@ -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
+ }) %>
@@ -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