playbook_ui 17.2.0.pre.rc.2 → 17.2.0.pre.rc.3
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_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 +6 -0
- data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.test.jsx +39 -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 +8 -0
- 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 +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_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 +1 -1
- 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/example.yml +3 -1
- data/app/pb_kits/playbook/pb_table/docs/index.js +2 -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/dist/chunks/{_table-SGw4kUye.js → _table-CUU7TisO.js} +1 -1
- data/dist/chunks/vendor.js +2 -2
- data/lib/playbook/version.rb +1 -1
- metadata +14 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b7287af0b8d2bde74e68c570b2997cae4eccb91cd0e6091b3323cb168fe79b9a
|
|
4
|
+
data.tar.gz: 35a49a7c94cfa00b247e687ee9766f6668ab3e15364a9f975d7b3e1c03c48242
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a1ee4f80a2e9a88affce23654cd842e4c2a8e25de724d74b9a2836073cb9fda8048cc245d2f755ba3d43c79de0b479a6cb39bde761bea22dc22fe9c63ef3a316
|
|
7
|
+
data.tar.gz: 91ad77dd27483c9f3f91568a5ad3c8161f69f8aa72c6a71c196f93914cf34aa55292d0b90b3f71b13446b831592a8ae5dcfc5327e9ddeb4a5d11aa45ebcb656c
|
|
@@ -49,12 +49,19 @@ interface MultiLevelSelectComponent extends React.ForwardRefExoticComponent<
|
|
|
49
49
|
Options: typeof MultiLevelSelectOptions;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
type MultiLevelSelectItem = { [key: string]: any };
|
|
53
|
+
type SelectedDisplayContext = {
|
|
54
|
+
ancestors: MultiLevelSelectItem[];
|
|
55
|
+
path: MultiLevelSelectItem[];
|
|
56
|
+
};
|
|
57
|
+
|
|
52
58
|
type MultiLevelSelectProps = {
|
|
53
59
|
aria?: { [key: string]: string };
|
|
54
60
|
className?: string;
|
|
55
61
|
data?: { [key: string]: string };
|
|
56
62
|
disabled?: boolean;
|
|
57
63
|
error?: string;
|
|
64
|
+
formatSelectedDisplay?: (item: MultiLevelSelectItem, context: SelectedDisplayContext) => string;
|
|
58
65
|
htmlOptions?: { [key: string]: string | number | boolean | (() => void) };
|
|
59
66
|
id?: string;
|
|
60
67
|
inputDisplay?: "pills" | "none";
|
|
@@ -105,6 +112,7 @@ const MultiLevelSelect = forwardRef<HTMLInputElement, MultiLevelSelectProps>(
|
|
|
105
112
|
data = {},
|
|
106
113
|
disabled = false,
|
|
107
114
|
error,
|
|
115
|
+
formatSelectedDisplay,
|
|
108
116
|
htmlOptions = {},
|
|
109
117
|
id,
|
|
110
118
|
inputDisplay = "pills",
|
|
@@ -175,6 +183,26 @@ const MultiLevelSelect = forwardRef<HTMLInputElement, MultiLevelSelectProps>(
|
|
|
175
183
|
item: [],
|
|
176
184
|
});
|
|
177
185
|
|
|
186
|
+
const getSelectedDisplay = (
|
|
187
|
+
item: MultiLevelSelectItem,
|
|
188
|
+
data: MultiLevelSelectItem[],
|
|
189
|
+
) => {
|
|
190
|
+
const ancestors: MultiLevelSelectItem[] = [];
|
|
191
|
+
let parentId = item.parent_id;
|
|
192
|
+
|
|
193
|
+
while (parentId) {
|
|
194
|
+
const parent = filterFormattedDataById(data, parentId)[0];
|
|
195
|
+
if (!parent) break;
|
|
196
|
+
|
|
197
|
+
ancestors.unshift(parent);
|
|
198
|
+
parentId = parent.parent_id;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return formatSelectedDisplay
|
|
202
|
+
? formatSelectedDisplay(item, { ancestors, path: [...ancestors, item] })
|
|
203
|
+
: item.label;
|
|
204
|
+
};
|
|
205
|
+
|
|
178
206
|
const arrowDownElementId = `arrow_down_${id}`;
|
|
179
207
|
const arrowUpElementId = `arrow_up_${id}`;
|
|
180
208
|
// Control id for label htmlFor: use suffix to avoid conflict with outer div's id
|
|
@@ -771,7 +799,9 @@ const MultiLevelSelect = forwardRef<HTMLInputElement, MultiLevelSelectProps>(
|
|
|
771
799
|
: placeholderText
|
|
772
800
|
}
|
|
773
801
|
required={required}
|
|
774
|
-
value={singleSelectedItem.
|
|
802
|
+
value={singleSelectedItem.item.length
|
|
803
|
+
? getSelectedDisplay(singleSelectedItem.item[0], formattedData)
|
|
804
|
+
: singleSelectedItem.value || filterItem}
|
|
775
805
|
/>
|
|
776
806
|
</div>
|
|
777
807
|
|
data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_format_selected_display.jsx
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import MultiLevelSelect from "../_multi_level_select";
|
|
3
|
+
|
|
4
|
+
const treeData = [
|
|
5
|
+
{
|
|
6
|
+
label: "HQ",
|
|
7
|
+
value: "hQ",
|
|
8
|
+
id: "hq1",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
label: "Philadelphia",
|
|
12
|
+
value: "philadelphia",
|
|
13
|
+
id: "phl1",
|
|
14
|
+
expanded: true,
|
|
15
|
+
children: [
|
|
16
|
+
{
|
|
17
|
+
label: "Marketing & Sales PHL",
|
|
18
|
+
value: "marketingAndSalesPhl",
|
|
19
|
+
id: "marketingPHL1",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
label: "Installation Office PHL",
|
|
23
|
+
value: "installationOfficePhl",
|
|
24
|
+
id: "installationPHL1",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
label: "Warehouse PHL",
|
|
28
|
+
value: "warehousePhl",
|
|
29
|
+
id: "warehousePHL1",
|
|
30
|
+
},
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: "New Jersey",
|
|
35
|
+
value: "newJersey",
|
|
36
|
+
id: "nj1",
|
|
37
|
+
children: [
|
|
38
|
+
{
|
|
39
|
+
label: "New Jersey",
|
|
40
|
+
value: "newJersey",
|
|
41
|
+
id: "nj11",
|
|
42
|
+
children: [
|
|
43
|
+
{
|
|
44
|
+
label: "Marketing & Sales NJ",
|
|
45
|
+
value: "marketingAndSalesNj",
|
|
46
|
+
id: "marketingNJ1",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
label: "Installation Office NJ",
|
|
50
|
+
value: "installationOfficeNj",
|
|
51
|
+
id: "installationNJ1",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
label: "Warehouse NJ",
|
|
55
|
+
value: "warehouseNj",
|
|
56
|
+
id: "warehouseNJ1",
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
label: "Princeton",
|
|
62
|
+
value: "princeton",
|
|
63
|
+
id: "princeton1",
|
|
64
|
+
children: [
|
|
65
|
+
{
|
|
66
|
+
label: "Marketing & Sales Princeton",
|
|
67
|
+
value: "marketingAndSalesPrinceton",
|
|
68
|
+
id: "marketingPR1",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
label: "Installation Office Princeton",
|
|
72
|
+
value: "installationOfficePrinceton",
|
|
73
|
+
id: "installationPR1",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
label: "Warehouse Princeton",
|
|
77
|
+
value: "warehousePrinceton",
|
|
78
|
+
id: "warehousePR1",
|
|
79
|
+
},
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const treeData2 = [
|
|
87
|
+
{
|
|
88
|
+
label: "HQ",
|
|
89
|
+
value: "hQ",
|
|
90
|
+
id: "hq2",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
label: "Philadelphia",
|
|
94
|
+
value: "philadelphia",
|
|
95
|
+
id: "phl2",
|
|
96
|
+
expanded: true,
|
|
97
|
+
children: [
|
|
98
|
+
{
|
|
99
|
+
label: "Marketing & Sales PHL",
|
|
100
|
+
value: "marketingAndSalesPhl",
|
|
101
|
+
id: "marketingPHL2",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
label: "Installation Office PHL",
|
|
105
|
+
value: "installationOfficePhl",
|
|
106
|
+
id: "installationPHL2",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
label: "Warehouse PHL",
|
|
110
|
+
value: "warehousePhl",
|
|
111
|
+
id: "warehousePHL2",
|
|
112
|
+
},
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
label: "New Jersey",
|
|
117
|
+
value: "newJersey",
|
|
118
|
+
id: "nj2",
|
|
119
|
+
children: [
|
|
120
|
+
{
|
|
121
|
+
label: "New Jersey",
|
|
122
|
+
value: "newJersey",
|
|
123
|
+
id: "nj22",
|
|
124
|
+
children: [
|
|
125
|
+
{
|
|
126
|
+
label: "Marketing & Sales NJ",
|
|
127
|
+
value: "marketingAndSalesNj",
|
|
128
|
+
id: "marketingNJ2",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
label: "Installation Office NJ",
|
|
132
|
+
value: "installationOfficeNj",
|
|
133
|
+
id: "installationNJ2",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
label: "Warehouse NJ",
|
|
137
|
+
value: "warehouseNj",
|
|
138
|
+
id: "warehouseNJ2",
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
label: "Princeton",
|
|
144
|
+
value: "princeton",
|
|
145
|
+
id: "princeton2",
|
|
146
|
+
children: [
|
|
147
|
+
{
|
|
148
|
+
label: "Marketing & Sales Princeton",
|
|
149
|
+
value: "marketingAndSalesPrinceton",
|
|
150
|
+
id: "marketingPR2",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
label: "Installation Office Princeton",
|
|
154
|
+
value: "installationOfficePrinceton",
|
|
155
|
+
id: "installationPR2",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
label: "Warehouse Princeton",
|
|
159
|
+
value: "warehousePrinceton",
|
|
160
|
+
id: "warehousePR2",
|
|
161
|
+
},
|
|
162
|
+
]
|
|
163
|
+
},
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
const MultiLevelSelectFormatSelectedDisplay = (props) => (
|
|
169
|
+
<>
|
|
170
|
+
<MultiLevelSelect
|
|
171
|
+
formatSelectedDisplay={(_, { path }) =>
|
|
172
|
+
path.map(({ label }) => label).join(" / ")
|
|
173
|
+
}
|
|
174
|
+
treeData={treeData}
|
|
175
|
+
variant="single"
|
|
176
|
+
{...props}
|
|
177
|
+
/>
|
|
178
|
+
<br />
|
|
179
|
+
<MultiLevelSelect
|
|
180
|
+
formatSelectedDisplay={(item, { ancestors }) => {
|
|
181
|
+
const parent = ancestors[ancestors.length - 1];
|
|
182
|
+
return parent ? `${item.label} (${parent.label})` : item.label;
|
|
183
|
+
}}
|
|
184
|
+
treeData={treeData2}
|
|
185
|
+
variant="single"
|
|
186
|
+
{...props}
|
|
187
|
+
/>
|
|
188
|
+
</>
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
export default MultiLevelSelectFormatSelectedDisplay;
|
data/app/pb_kits/playbook/pb_multi_level_select/docs/_multi_level_select_format_selected_display.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Use `formatSelectedDisplay` to customize the string shown in the input after a single-select option is chosen. The formatter receives the selected item and a context object containing `ancestors` (ordered from the root through the immediate parent) and `path` (the ancestors plus the selected item). This changes only the displayed string; selection callbacks and form values remain unchanged.
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"name",
|
|
32
32
|
"placeholder",
|
|
33
33
|
"selectedIds",
|
|
34
|
-
"inputName"
|
|
34
|
+
"inputName",
|
|
35
|
+
"formatSelectedDisplay"
|
|
35
36
|
]
|
|
36
37
|
},
|
|
37
38
|
{
|
|
@@ -48,7 +49,8 @@
|
|
|
48
49
|
"props": [
|
|
49
50
|
"inputDisplay",
|
|
50
51
|
"variant",
|
|
51
|
-
"wrapped"
|
|
52
|
+
"wrapped",
|
|
53
|
+
"pillColor"
|
|
52
54
|
]
|
|
53
55
|
},
|
|
54
56
|
{
|
|
@@ -92,6 +94,20 @@
|
|
|
92
94
|
"onSelect": "(selectedNode) => console.log('Selected Node', selectedNode)"
|
|
93
95
|
}
|
|
94
96
|
},
|
|
97
|
+
{
|
|
98
|
+
"name": "Custom Single Select Display",
|
|
99
|
+
"structureMode": "standard",
|
|
100
|
+
"props": {
|
|
101
|
+
"variant": "single",
|
|
102
|
+
"label": "Pick one team",
|
|
103
|
+
"placeholder": "Choose...",
|
|
104
|
+
"selectedIds": [
|
|
105
|
+
"initiative1"
|
|
106
|
+
],
|
|
107
|
+
"formatSelectedDisplay": "(_, { path }) => path.map(({ label }) => label).join(' / ')",
|
|
108
|
+
"onSelect": "(selectedNode) => console.log('Selected Node', selectedNode)"
|
|
109
|
+
}
|
|
110
|
+
},
|
|
95
111
|
{
|
|
96
112
|
"name": "Selected IDs",
|
|
97
113
|
"structureMode": "standard",
|
|
@@ -246,6 +262,11 @@
|
|
|
246
262
|
}
|
|
247
263
|
],
|
|
248
264
|
"conditionals": {
|
|
265
|
+
"formatSelectedDisplay": {
|
|
266
|
+
"requires": {
|
|
267
|
+
"variant": "single"
|
|
268
|
+
}
|
|
269
|
+
},
|
|
249
270
|
"requiredIndicator": {
|
|
250
271
|
"requires": "label"
|
|
251
272
|
}
|
|
@@ -266,6 +287,11 @@
|
|
|
266
287
|
"message": "Single Select uses radios and should receive at most one selectedIds value because only one node can be selected.",
|
|
267
288
|
"type": "info"
|
|
268
289
|
},
|
|
290
|
+
"custom_single_select_display": {
|
|
291
|
+
"presetName": "Custom Single Select Display",
|
|
292
|
+
"message": "formatSelectedDisplay receives the selected item plus ancestors and path context, and must return the string shown in the single-select input. Edit the function in Content to choose which hierarchy details to display.",
|
|
293
|
+
"type": "info"
|
|
294
|
+
},
|
|
269
295
|
"selected_ids": {
|
|
270
296
|
"presetName": "Selected IDs",
|
|
271
297
|
"message": "selectedIds checks matching treeData node ids on load. In multi mode, multiple ids can be passed.",
|
|
@@ -319,6 +345,9 @@
|
|
|
319
345
|
"editable": false,
|
|
320
346
|
"default": ""
|
|
321
347
|
},
|
|
348
|
+
"editableFunctionProps": [
|
|
349
|
+
"formatSelectedDisplay"
|
|
350
|
+
],
|
|
322
351
|
"customProps": {
|
|
323
352
|
"treeData": {
|
|
324
353
|
"type": "array",
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
"editable": false,
|
|
17
17
|
"default": ""
|
|
18
18
|
},
|
|
19
|
+
"editableFunctionProps": [
|
|
20
|
+
"formatSelectedDisplay"
|
|
21
|
+
],
|
|
19
22
|
"customProps": {
|
|
20
23
|
"treeData": {
|
|
21
24
|
"type": "array",
|
|
@@ -135,7 +138,8 @@
|
|
|
135
138
|
"name",
|
|
136
139
|
"placeholder",
|
|
137
140
|
"selectedIds",
|
|
138
|
-
"inputName"
|
|
141
|
+
"inputName",
|
|
142
|
+
"formatSelectedDisplay"
|
|
139
143
|
]
|
|
140
144
|
},
|
|
141
145
|
{
|
|
@@ -152,7 +156,8 @@
|
|
|
152
156
|
"props": [
|
|
153
157
|
"inputDisplay",
|
|
154
158
|
"variant",
|
|
155
|
-
"wrapped"
|
|
159
|
+
"wrapped",
|
|
160
|
+
"pillColor"
|
|
156
161
|
]
|
|
157
162
|
},
|
|
158
163
|
{
|
|
@@ -196,6 +201,20 @@
|
|
|
196
201
|
"onSelect": "(selectedNode) => console.log('Selected Node', selectedNode)"
|
|
197
202
|
}
|
|
198
203
|
},
|
|
204
|
+
{
|
|
205
|
+
"name": "Custom Single Select Display",
|
|
206
|
+
"structureMode": "standard",
|
|
207
|
+
"props": {
|
|
208
|
+
"variant": "single",
|
|
209
|
+
"label": "Pick one team",
|
|
210
|
+
"placeholder": "Choose...",
|
|
211
|
+
"selectedIds": [
|
|
212
|
+
"initiative1"
|
|
213
|
+
],
|
|
214
|
+
"formatSelectedDisplay": "(_, { path }) => path.map(({ label }) => label).join(' / ')",
|
|
215
|
+
"onSelect": "(selectedNode) => console.log('Selected Node', selectedNode)"
|
|
216
|
+
}
|
|
217
|
+
},
|
|
199
218
|
{
|
|
200
219
|
"name": "Selected IDs",
|
|
201
220
|
"structureMode": "standard",
|
|
@@ -350,6 +369,11 @@
|
|
|
350
369
|
}
|
|
351
370
|
],
|
|
352
371
|
"conditionals": {
|
|
372
|
+
"formatSelectedDisplay": {
|
|
373
|
+
"requires": {
|
|
374
|
+
"variant": "single"
|
|
375
|
+
}
|
|
376
|
+
},
|
|
353
377
|
"requiredIndicator": {
|
|
354
378
|
"requires": "label"
|
|
355
379
|
}
|
|
@@ -370,6 +394,11 @@
|
|
|
370
394
|
"message": "Single Select uses radios and should receive at most one selectedIds value because only one node can be selected.",
|
|
371
395
|
"type": "info"
|
|
372
396
|
},
|
|
397
|
+
"custom_single_select_display": {
|
|
398
|
+
"presetName": "Custom Single Select Display",
|
|
399
|
+
"message": "formatSelectedDisplay receives the selected item plus ancestors and path context, and must return the string shown in the single-select input. Edit the function in Content to choose which hierarchy details to display.",
|
|
400
|
+
"type": "info"
|
|
401
|
+
},
|
|
373
402
|
"selected_ids": {
|
|
374
403
|
"presetName": "Selected IDs",
|
|
375
404
|
"message": "selectedIds checks matching treeData node ids on load. In multi mode, multiple ids can be passed.",
|
|
@@ -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"
|
|
@@ -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
|
-
|
|
@@ -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 }
|