playbook_ui 16.5.0.pre.alpha.RTEPOC15747 → 16.5.0.pre.alpha.RTEPOC15779
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_date_picker/date_picker.html.erb +1 -0
- data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_dialog_submission.jsx +62 -0
- data/app/pb_kits/playbook/pb_date_picker/docs/_date_picker_dialog_submission.md +1 -0
- data/app/pb_kits/playbook/pb_date_picker/docs/example.yml +1 -0
- data/app/pb_kits/playbook/pb_date_picker/docs/index.js +1 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_form_with_validate.html.erb +5 -5
- data/app/pb_kits/playbook/pb_form/docs/_form_form_with_validation_msg.html.erb +90 -0
- data/app/pb_kits/playbook/pb_form/docs/_form_form_with_validation_msg.md +13 -0
- data/app/pb_kits/playbook/pb_form/docs/example.yml +1 -0
- data/app/pb_kits/playbook/pb_form/pb_form_validation.js +3 -1
- data/app/pb_kits/playbook/pb_rich_text_editor/_tiptap_styles.scss +76 -56
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_default.md +4 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_simple.html.erb +9 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_simple.md +12 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/example.yml +1 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/kit.schema.json +4 -2
- data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.html.erb +50 -14
- data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.rb +3 -0
- data/app/pb_kits/playbook/pb_select/select.rb +7 -0
- data/dist/playbook-rails.js +1 -1
- data/dist/playbook.css +1 -1
- data/lib/playbook/forms/builder/collection_select_field.rb +1 -0
- data/lib/playbook/forms/builder/date_picker_field.rb +1 -0
- data/lib/playbook/forms/builder/select_field.rb +1 -0
- data/lib/playbook/forms/builder/time_zone_select_field.rb +1 -0
- data/lib/playbook/pb_forms_helper.rb +17 -2
- data/lib/playbook/version.rb +1 -1
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a2c36115f459754d15420a726ab66d5aef94d010e98cccf7cb900c7f181a926e
|
|
4
|
+
data.tar.gz: 065a216cb80347d860b96b44a0d7fcc57bc588a1168f771c9cb2ab1960d49e4f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9948221496986eedce4addad128d3991d441e2d06e0f0fbdfd69630f748771276e74ee3ad532550972b8467a3f175a3cb04719b6811aa4847d79721dd987f0ad
|
|
7
|
+
data.tar.gz: 47ec682ffc0fae5d065c1d5636d21a6d288e3b54e7091fc49fed4180ee95aafd67db4282d0bc700f6af98e7de5428407222080f0305ea3599aacd88eb3573ee6
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Button, Dialog, DatePicker } from "playbook-ui";
|
|
3
|
+
|
|
4
|
+
const DatePickerDialogSubmission = () => {
|
|
5
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
6
|
+
const [dateFixed, setDateFixed] = useState("");
|
|
7
|
+
const [pickerInstance, setPickerInstance] = useState(0);
|
|
8
|
+
|
|
9
|
+
const close = (clearDate = false) => {
|
|
10
|
+
if (clearDate) setDateFixed("");
|
|
11
|
+
setIsOpen(false);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const open = () => {
|
|
15
|
+
setPickerInstance((current) => current + 1);
|
|
16
|
+
setIsOpen(true);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const handleSubmit = () => {
|
|
20
|
+
if (!dateFixed.trim()) return;
|
|
21
|
+
close();
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<Button
|
|
27
|
+
onClick={open}
|
|
28
|
+
text="Open Dialog"
|
|
29
|
+
/>
|
|
30
|
+
<Dialog
|
|
31
|
+
onClose={() => close(true)}
|
|
32
|
+
opened={isOpen}
|
|
33
|
+
size="md"
|
|
34
|
+
title="Date Picker: Dialog Submission Example"
|
|
35
|
+
>
|
|
36
|
+
<Dialog.Body>
|
|
37
|
+
<DatePicker
|
|
38
|
+
defaultDate={dateFixed || undefined}
|
|
39
|
+
key={`fixed-${pickerInstance}`}
|
|
40
|
+
label="Date"
|
|
41
|
+
onChange={(dateStr) => setDateFixed(dateStr || "")}
|
|
42
|
+
pickerId={`datePickerFixed-${pickerInstance}`}
|
|
43
|
+
staticPosition={false}
|
|
44
|
+
/>
|
|
45
|
+
</Dialog.Body>
|
|
46
|
+
<Dialog.Footer>
|
|
47
|
+
<Button
|
|
48
|
+
onClick={handleSubmit}
|
|
49
|
+
text="Submit"
|
|
50
|
+
/>
|
|
51
|
+
<Button
|
|
52
|
+
onClick={() => close(true)}
|
|
53
|
+
text="Cancel"
|
|
54
|
+
variant="link"
|
|
55
|
+
/>
|
|
56
|
+
</Dialog.Footer>
|
|
57
|
+
</Dialog>
|
|
58
|
+
</>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default DatePickerDialogSubmission;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Use this pattern when a DatePicker lives inside a Dialog and needs to submit a selected value before closing. A unique `key` and `pickerId` will force the datePicker to remount each time the dialog opens.
|
|
@@ -29,3 +29,4 @@ export { default as DatePickerQuickPickDefaultDate } from './_date_picker_quick_
|
|
|
29
29
|
export { default as DatePickerRangePattern } from './_date_picker_range_pattern'
|
|
30
30
|
export { default as DatePickerAndDropdownRange } from './_date_picker_and_dropdown_range.jsx'
|
|
31
31
|
export { default as DatePickerRequiredIndicator } from "./_date_picker_required_indicator.jsx";
|
|
32
|
+
export { default as DatePickerDialogSubmission } from "./_date_picker_dialog_submission.jsx";
|
|
@@ -97,8 +97,8 @@
|
|
|
97
97
|
}] %>
|
|
98
98
|
|
|
99
99
|
<%= pb_form_with(scope: :example, method: :get, url: "", validate: true) do |form| %>
|
|
100
|
-
<%= form.typeahead :example_typeahead_validation, props: { data: { typeahead_example2: true, user: {} }, label: true, placeholder: "Search for a user", required: true
|
|
101
|
-
<%= form.typeahead :example_typeahead_validation_react, props: { options: example_typeahead_options, pills: true, label: "Example Typeahead (React Rendered)", placeholder: "Search for a user", required: true
|
|
100
|
+
<%= form.typeahead :example_typeahead_validation, props: { data: { typeahead_example2: true, user: {} }, label: true, placeholder: "Search for a user", required: true } %>
|
|
101
|
+
<%= form.typeahead :example_typeahead_validation_react, props: { options: example_typeahead_options, pills: true, label: "Example Typeahead (React Rendered)", placeholder: "Search for a user", required: true } %>
|
|
102
102
|
<%= form.typeahead :example_typeahead_validation_react_2, props: { options: example_typeahead_options, pills: true, label: "Example Typeahead 2 (React Rendered)", placeholder: "Search for a user", required: true } %>
|
|
103
103
|
<%= form.text_field :example_text_field_validation, props: { label: true, required: true } %>
|
|
104
104
|
<%= form.phone_number_field :example_phone_number_field_validation, props: { label: "Example phone field", hidden_inputs: true, required: true } %>
|
|
@@ -110,14 +110,14 @@
|
|
|
110
110
|
<%= form.text_area :example_text_area_validation, props: { label: true, required: true } %>
|
|
111
111
|
<%= form.dropdown_field :example_dropdown_validation, props: { label: true, options: example_dropdown_options, required: true } %>
|
|
112
112
|
<%= form.dropdown_field :example_dropdown_validation_multi, props: { label: true, options: example_dropdown_options, multi_select: true, required: true } %>
|
|
113
|
-
<%= form.select :example_select_validation, [ ["Yes", 1], ["No", 2] ], props: { label: true, blank_selection: "Select One...", required: true
|
|
113
|
+
<%= form.select :example_select_validation, [ ["Yes", 1], ["No", 2] ], props: { label: true, blank_selection: "Select One...", required: true } %>
|
|
114
114
|
<%= form.collection_select :example_collection_select_validation, example_collection, :value, :name, props: { label: true, blank_selection: "Select One...", required: true } %>
|
|
115
115
|
<%= form.check_box :example_checkbox_validation, props: { text: "Example Checkbox Validation", label: true, required: true }, checked_value: "1", unchecked_value: "0" %>
|
|
116
|
-
<%= form.date_picker :example_date_picker_2, props: { label: true, required: true,
|
|
116
|
+
<%= form.date_picker :example_date_picker_2, props: { label: true, required: true, allow_input: true } %>
|
|
117
117
|
<%= form.star_rating_field :example_star_rating_validation, props: { variant: "interactive", label: true, required: true } %>
|
|
118
118
|
<%= form.time_zone_select_field :example_time_zone_select, ActiveSupport::TimeZone.us_zones, { default: "Eastern Time (US & Canada)" }, props: { label: true, blank_selection: "Select a Time Zone...", required: true } %>
|
|
119
119
|
<%= form.multi_level_select :example_multi_level_select, props: { id: "multi-level-select-form", tree_data: treeData, margin_bottom: "sm", required: true, label: "Example Multi Level Select field" } %>
|
|
120
|
-
<%= form.time_picker :example_time_picker_validation, props: { label: true, required: true
|
|
120
|
+
<%= form.time_picker :example_time_picker_validation, props: { label: true, required: true } %>
|
|
121
121
|
|
|
122
122
|
<%= form.actions do |action| %>
|
|
123
123
|
<%= action.submit %>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<%
|
|
2
|
+
example_collection = [
|
|
3
|
+
OpenStruct.new(name: "Alabama", value: 1),
|
|
4
|
+
OpenStruct.new(name: "Alaska", value: 2),
|
|
5
|
+
OpenStruct.new(name: "Arizona", value: 3),
|
|
6
|
+
OpenStruct.new(name: "Arkansas", value: 4),
|
|
7
|
+
OpenStruct.new(name: "California", value: 5),
|
|
8
|
+
OpenStruct.new(name: "Colorado", value: 6),
|
|
9
|
+
OpenStruct.new(name: "Connecticut", value: 7),
|
|
10
|
+
OpenStruct.new(name: "Delaware", value: 8),
|
|
11
|
+
OpenStruct.new(name: "Florida", value: 9),
|
|
12
|
+
OpenStruct.new(name: "Georgia", value: 10),
|
|
13
|
+
]
|
|
14
|
+
%>
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
<%
|
|
18
|
+
example_typeahead_options = [
|
|
19
|
+
{ label: 'Orange', value: '#FFA500' },
|
|
20
|
+
{ label: 'Red', value: '#FF0000' },
|
|
21
|
+
{ label: 'Green', value: '#00FF00' },
|
|
22
|
+
{ label: 'Blue', value: '#0000FF' },
|
|
23
|
+
]
|
|
24
|
+
%>
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
<%= pb_form_with(scope: :example, method: :get, url: "", validate: true) do |form| %>
|
|
28
|
+
<%= form.text_field :example_text_field_validation_msg, props: { label: "Text Field With Validation Message", required: true, validation: { message: "I am a custom validation message for text field." } } %>
|
|
29
|
+
<%= form.typeahead :example_typeahead_validation_msg, props: { data: { typeahead_example_msg: true, user: {} }, label: "Typeahead With Validation Message", placeholder: "Search for a user", required: true, validation: { message: "I am a custom validation message for typeahead." } } %>
|
|
30
|
+
<%= form.typeahead :example_typeahead_validation_react_msg, props: { options: example_typeahead_options, pills: true, label: "Typeahead With Validation Message (React Rendered)", placeholder: "Search for a color", required: true, validation: { message: "I am a custom validation message for React typeahead." } } %>
|
|
31
|
+
<%= form.select :example_select_validation_msg, [ ["Yes", 1], ["No", 2] ], props: { label: true, blank_selection: "Select One...", required: true, validation_message: "I am a custom validation message for select." } %>
|
|
32
|
+
<%= form.collection_select :example_collection_select_validation_msg, example_collection, :value, :name, props: { label: "Collection Select With Validation Message", blank_selection: "Select a State...", required: true, validation_message: "I am a custom validation message for collection select." } %>
|
|
33
|
+
<%= form.date_picker :example_date_picker_validation_msg, props: { label: "Date Picker With Validation Message", required: true, validation_message: "I am a custom validation message for date picker.", allow_input: true } %>
|
|
34
|
+
<%= form.time_picker :example_time_picker_validation_msg, props: { label: "Time Picker With Validation Message", required: true, validation_message: "I am a custom validation message for time picker." } %>
|
|
35
|
+
|
|
36
|
+
<%= form.actions do |action| %>
|
|
37
|
+
<%= action.submit %>
|
|
38
|
+
<%= action.button props: { type: "reset", text: "Cancel", variant: "secondary" } %>
|
|
39
|
+
<% end %>
|
|
40
|
+
<% end %>
|
|
41
|
+
|
|
42
|
+
<!-- form.typeahead user results example template -->
|
|
43
|
+
<template data-typeahead-example-result-option>
|
|
44
|
+
<%= pb_rails("user", props: {
|
|
45
|
+
name: tag(:slot, name: "name"),
|
|
46
|
+
orientation: "horizontal",
|
|
47
|
+
align: "left",
|
|
48
|
+
avatar_url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGP6zwAAAgcBApocMXEAAAAASUVORK5CYII=",
|
|
49
|
+
avatar: true
|
|
50
|
+
}) %>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<!-- form.typeahead JS example implementation -->
|
|
54
|
+
<%= javascript_tag defer: "defer" do %>
|
|
55
|
+
document.addEventListener("pb-typeahead-kit-search", function(event) {
|
|
56
|
+
if (!event.target.dataset || !event.target.dataset.typeaheadExampleMsg) return
|
|
57
|
+
|
|
58
|
+
fetch(`https://api.github.com/search/users?q=${encodeURIComponent(event.detail.searchingFor)}`)
|
|
59
|
+
.then(response => response.json())
|
|
60
|
+
.then((result) => {
|
|
61
|
+
const resultOptionTemplate = document.querySelector("[data-typeahead-example-result-option]")
|
|
62
|
+
|
|
63
|
+
event.detail.setResults((result.items || []).map((user) => {
|
|
64
|
+
const wrapper = resultOptionTemplate.content.cloneNode(true)
|
|
65
|
+
wrapper.children[0].dataset.user = JSON.stringify(user)
|
|
66
|
+
wrapper.querySelector('slot[name="name"]').replaceWith(user.login)
|
|
67
|
+
wrapper.querySelector('img').dataset.src = user.avatar_url
|
|
68
|
+
return wrapper
|
|
69
|
+
}))
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
document.addEventListener("pb-typeahead-kit-result-option-selected", function(event) {
|
|
75
|
+
if (!event.target.dataset.typeaheadExampleMsg) return
|
|
76
|
+
|
|
77
|
+
const selectedUserJSON = event.detail.selected.firstElementChild.dataset.user
|
|
78
|
+
const selectedUserData = JSON.parse(selectedUserJSON)
|
|
79
|
+
|
|
80
|
+
// set the input field's value
|
|
81
|
+
event.target.querySelector('input[name=example_typeahead_validation_msg]').value = selectedUserData.login
|
|
82
|
+
|
|
83
|
+
// log the selected option's dataset
|
|
84
|
+
console.log('The selected user data:')
|
|
85
|
+
console.dir(selectedUserData)
|
|
86
|
+
|
|
87
|
+
// do even more with the data later - TBD
|
|
88
|
+
event.target.dataset.user = selectedUserJSON
|
|
89
|
+
})
|
|
90
|
+
<% end %>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Custom validation messages allow you to override the browser's default validation text with your own messaging. This provides a better user experience by giving specific, actionable feedback.
|
|
2
|
+
|
|
3
|
+
**Text-based inputs** (TextInput, Typeahead) use the `validation` prop with a `message` key:
|
|
4
|
+
```ruby
|
|
5
|
+
validation: { message: "Please enter a valid email address." }
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
**Selection-based inputs** (Select, DatePicker, TimePicker) use the `validation_message` prop:
|
|
9
|
+
```ruby
|
|
10
|
+
validation_message: "Please select an option."
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
When a required field is left empty or fails validation, your custom message will display instead of the generic browser default.
|
|
@@ -3,5 +3,6 @@ examples:
|
|
|
3
3
|
rails:
|
|
4
4
|
- form_form_with: Default
|
|
5
5
|
- form_form_with_validate: Default + Validation
|
|
6
|
+
- form_form_with_validation_msg: Validation + Custom Validation Message
|
|
6
7
|
- form_form_with_loading: Default + Loading
|
|
7
8
|
- form_with_required_indicator: With Optional Required Indicator
|
|
@@ -134,61 +134,15 @@
|
|
|
134
134
|
height: $space_xl;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
//
|
|
137
|
+
// React ToolbarDropdown — match master (fixed width trigger; prod playbook.cloud).
|
|
138
138
|
.editor-dropdown-button {
|
|
139
139
|
background: transparent;
|
|
140
140
|
border: none;
|
|
141
141
|
color: $text_lt_light;
|
|
142
142
|
cursor: pointer;
|
|
143
143
|
font-weight: $light;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
min-height: unset;
|
|
147
|
-
max-width: 100%;
|
|
148
|
-
// Plain length only — SassC cannot compile CSS min(%, px). Narrow modals rely on kit/root min-width: 0 above.
|
|
149
|
-
min-width: $space_xl * 5;
|
|
150
|
-
padding: ($space_xs - 1) $space_xs;
|
|
151
|
-
width: auto;
|
|
152
|
-
|
|
153
|
-
// Undo Playbook .pb_button_kit defaults that throw off icon vs label (line-height 1.5, min-height 40px).
|
|
154
|
-
.pb_button_content {
|
|
155
|
-
align-items: center;
|
|
156
|
-
display: inline-flex;
|
|
157
|
-
line-height: 1;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// React: single Flex row inside the button.
|
|
161
|
-
.pb_button_content > .pb_flex_kit {
|
|
162
|
-
align-items: center;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Rails: block-style trigger row (spans + icons).
|
|
166
|
-
.rte-block-style-trigger-inner {
|
|
167
|
-
align-items: center;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
.rte-block-style-trigger-icon,
|
|
171
|
-
.rte-block-style-chevron {
|
|
172
|
-
display: inline-flex;
|
|
173
|
-
flex-shrink: 0;
|
|
174
|
-
line-height: 0;
|
|
175
|
-
|
|
176
|
-
.pb_icon_kit {
|
|
177
|
-
align-items: center;
|
|
178
|
-
display: flex;
|
|
179
|
-
line-height: 0;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
svg {
|
|
183
|
-
display: block;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
.rte-block-style-trigger-label {
|
|
188
|
-
align-items: center;
|
|
189
|
-
display: inline-flex;
|
|
190
|
-
line-height: 1.2;
|
|
191
|
-
}
|
|
144
|
+
padding: ($space_xs - 1) 0px;
|
|
145
|
+
width: $space_xl * 3;
|
|
192
146
|
|
|
193
147
|
&:focus-visible {
|
|
194
148
|
box-shadow: unset;
|
|
@@ -196,6 +150,12 @@
|
|
|
196
150
|
}
|
|
197
151
|
|
|
198
152
|
// Rails TipTap toolbar: mirror React Toolbar.tsx — <Flex paddingX="sm" paddingY="xxs" justify="between">.
|
|
153
|
+
&.rte-rails-toolbar-layout--simple {
|
|
154
|
+
.rte-toolbar-left {
|
|
155
|
+
overflow-x: visible;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
199
159
|
&.rte-rails-toolbar-layout {
|
|
200
160
|
max-width: 100%;
|
|
201
161
|
min-width: 0;
|
|
@@ -237,23 +197,71 @@
|
|
|
237
197
|
align-items: center;
|
|
238
198
|
display: inline-flex;
|
|
239
199
|
}
|
|
200
|
+
|
|
201
|
+
// Override master’s fixed-width React trigger: Rails block-style label + icons needs flexible width.
|
|
202
|
+
.editor-dropdown-button {
|
|
203
|
+
justify-content: flex-start;
|
|
204
|
+
letter-spacing: normal;
|
|
205
|
+
line-height: 1;
|
|
206
|
+
max-width: 100%;
|
|
207
|
+
min-height: unset;
|
|
208
|
+
padding: ($space_xs - 1) $space_xs;
|
|
209
|
+
text-align: left;
|
|
210
|
+
width: auto;
|
|
211
|
+
|
|
212
|
+
.pb_button_content {
|
|
213
|
+
align-items: center;
|
|
214
|
+
display: inline-flex;
|
|
215
|
+
line-height: 1;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.pb_button_content > .pb_flex_kit {
|
|
219
|
+
align-items: center;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.rte-block-style-trigger-inner {
|
|
223
|
+
align-items: center;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.rte-block-style-trigger-icon,
|
|
227
|
+
.rte-block-style-chevron {
|
|
228
|
+
display: inline-flex;
|
|
229
|
+
flex-shrink: 0;
|
|
230
|
+
line-height: 0;
|
|
231
|
+
|
|
232
|
+
.pb_icon_kit {
|
|
233
|
+
align-items: center;
|
|
234
|
+
display: flex;
|
|
235
|
+
line-height: 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
svg {
|
|
239
|
+
display: block;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.rte-block-style-trigger-label {
|
|
244
|
+
align-items: center;
|
|
245
|
+
display: inline-flex;
|
|
246
|
+
line-height: 1.2;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
&:focus-visible {
|
|
250
|
+
box-shadow: unset;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
240
253
|
}
|
|
241
254
|
}
|
|
242
255
|
|
|
256
|
+
// TipTap content — match master (prod playbook React tab).
|
|
243
257
|
.ProseMirror {
|
|
244
258
|
background: $white;
|
|
245
259
|
border: 1px solid $input_border_default;
|
|
246
260
|
border-radius: $border_rad_heaviest;
|
|
247
|
-
box-sizing: border-box;
|
|
248
261
|
height: 100%;
|
|
249
262
|
line-height: $lh_loose;
|
|
250
|
-
|
|
251
|
-
padding: 1.25rem 1.5rem 1.5rem 1.5rem;
|
|
252
|
-
word-break: break-word;
|
|
263
|
+
padding: 1rem 1.5rem 1.5rem 1.5rem;
|
|
253
264
|
@include transition_default;
|
|
254
|
-
:first-child {
|
|
255
|
-
margin-top: 0;
|
|
256
|
-
}
|
|
257
265
|
|
|
258
266
|
h4,
|
|
259
267
|
h5,
|
|
@@ -305,6 +313,11 @@
|
|
|
305
313
|
ul {
|
|
306
314
|
@include preview_tiptap_ul;
|
|
307
315
|
}
|
|
316
|
+
|
|
317
|
+
// After heading mixins: first block should not pick up extra top margin (wins over `p { margin-top: 1rem }`).
|
|
318
|
+
> :first-child {
|
|
319
|
+
margin-top: 0;
|
|
320
|
+
}
|
|
308
321
|
}
|
|
309
322
|
|
|
310
323
|
// Toolbar + editor stack: toolbar keeps its border; editor has no top stroke (classic layout).
|
|
@@ -319,6 +332,13 @@
|
|
|
319
332
|
}
|
|
320
333
|
}
|
|
321
334
|
|
|
335
|
+
// Rails-only: outer kit sets `data-pb-rte-tiptap` — roomier padding + wrapping for vanilla TipTap in forms/dialogs.
|
|
336
|
+
[data-pb-rte-tiptap="true"] .pb_rich_text_editor_kit .ProseMirror {
|
|
337
|
+
overflow-wrap: anywhere;
|
|
338
|
+
padding: 1.25rem 1.5rem 1.5rem 1.5rem;
|
|
339
|
+
word-break: break-word;
|
|
340
|
+
}
|
|
341
|
+
|
|
322
342
|
.pb_tiptap_toolbar_dropdown_list_item {
|
|
323
343
|
&.is-active,
|
|
324
344
|
&:active {
|
|
@@ -10,3 +10,7 @@ The Rails rich text editor is a TipTap surface with no React. The UI (toolbar, b
|
|
|
10
10
|
|
|
11
11
|
- Same core: both use TipTap v2 on top of ProseMirror; styling lives in Playbook SCSS (`_tiptap_styles.scss`) so the editor chrome lines up between platforms.
|
|
12
12
|
- Different shell: Rails uses ERB + Playbook Rails components + inline module script. React uses `RichTextEditor` / `_tiptap_editor.tsx` and TipTap wired through the bundled Playbook React package—see Advanced Default for that stack and when you need TipTap installed in your JavaScript bundle.
|
|
13
|
+
|
|
14
|
+
### Simple toolbar (`simple: true`)
|
|
15
|
+
|
|
16
|
+
**Bold**, **Italic**, **Undo**, and **Redo** only (no block dropdown / Popover). See the **Rails (TipTap — Simple toolbar)** example.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
### Simple toolbar (`simple: true`)
|
|
2
|
+
|
|
3
|
+
Pass **`simple: true`** for a compact toolbar: **Bold**, **Italic**, **Undo**, and **Redo** (same history controls as the full toolbar—plain buttons, not popovers).
|
|
4
|
+
|
|
5
|
+
- No block-style dropdown (no “Paragraph” / headings / lists in the menu).
|
|
6
|
+
- No **`pb_popover`** on the toolbar—useful in **native `<dialog>`** modals, turbo-loaded panels, or other tight layouts where the full block menu is awkward to position.
|
|
7
|
+
|
|
8
|
+
The underlying TipTap document still accepts the same HTML as the default Rails editor; `simple` only changes which **toolbar controls** are shown.
|
|
9
|
+
|
|
10
|
+
### When to use the default instead
|
|
11
|
+
|
|
12
|
+
Keep the **default** toolbar (omit `simple` or pass `simple: false`) when you need the block-style menu, strikethrough, code block, and link actions in the chrome.
|
|
@@ -68,7 +68,38 @@
|
|
|
68
68
|
</label>
|
|
69
69
|
<% end %>
|
|
70
70
|
<input type="hidden" name="<%= object.input_name %>" id="<%= object.input_id %>" value="" />
|
|
71
|
-
<div class="pb_rich_text_editor_advanced_container toolbar-active">
|
|
71
|
+
<div class="pb_rich_text_editor_advanced_container toolbar-active<%= " pb_rich_text_editor_rte--simple" if object.simple %>">
|
|
72
|
+
<% if object.simple %>
|
|
73
|
+
<%# Compact toolbar: Bold/Italic + Undo/Redo — no block-style Popover (avoids dialog positioning issues). %>
|
|
74
|
+
<div class="pb_background_kit pb_background_color_white toolbar rte-rails-toolbar-layout rte-rails-toolbar-layout--simple" id="<%= object.toolbar_id %>">
|
|
75
|
+
<div class="rte-rails-toolbar-row">
|
|
76
|
+
<div class="toolbar_block rte-toolbar-left">
|
|
77
|
+
<button type="button" class="toolbar_button" data-action="bold" title="Bold" role="button" tabindex="0">
|
|
78
|
+
<%= pb_rails("flex", props: { align: "center", justify: "center", classname: "toolbar_button_icon" }) do %>
|
|
79
|
+
<%= pb_rails("icon", props: { icon: "bold", size: "lg" }) %>
|
|
80
|
+
<% end %>
|
|
81
|
+
</button>
|
|
82
|
+
<button type="button" class="toolbar_button" data-action="italic" title="Italic" role="button" tabindex="0">
|
|
83
|
+
<%= pb_rails("flex", props: { align: "center", justify: "center", classname: "toolbar_button_icon" }) do %>
|
|
84
|
+
<%= pb_rails("icon", props: { icon: "italic", size: "lg" }) %>
|
|
85
|
+
<% end %>
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="toolbar_block rte-toolbar-right">
|
|
89
|
+
<button type="button" class="toolbar_button" data-action="undo" title="Undo" role="button" tabindex="0">
|
|
90
|
+
<%= pb_rails("flex", props: { align: "center", justify: "center", classname: "toolbar_button_icon" }) do %>
|
|
91
|
+
<%= pb_rails("icon", props: { icon: "undo", size: "lg" }) %>
|
|
92
|
+
<% end %>
|
|
93
|
+
</button>
|
|
94
|
+
<button type="button" class="toolbar_button" data-action="redo" title="Redo" role="button" tabindex="0">
|
|
95
|
+
<%= pb_rails("flex", props: { align: "center", justify: "center", classname: "toolbar_button_icon" }) do %>
|
|
96
|
+
<%= pb_rails("icon", props: { icon: "redo", size: "lg" }) %>
|
|
97
|
+
<% end %>
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<% else %>
|
|
72
103
|
<% block_style_options = [
|
|
73
104
|
{ value: "paragraph", text: "Paragraph", icon: "paragraph" },
|
|
74
105
|
{ value: "heading-1", text: "Heading 1", icon: "h1" },
|
|
@@ -174,6 +205,7 @@
|
|
|
174
205
|
</span>
|
|
175
206
|
<% end %>
|
|
176
207
|
</div>
|
|
208
|
+
<% end %>
|
|
177
209
|
<div class="rte-editor-wrap">
|
|
178
210
|
<div id="<%= object.editor_node_id %>"></div>
|
|
179
211
|
</div>
|
|
@@ -193,8 +225,9 @@
|
|
|
193
225
|
const hiddenInput = document.getElementById(inputId);
|
|
194
226
|
const editorNode = document.getElementById("<%= object.editor_node_id %>");
|
|
195
227
|
const toolbar = document.getElementById("<%= object.toolbar_id %>");
|
|
228
|
+
const rteSimple = <%= object.simple ? "true" : "false" %> === "true";
|
|
196
229
|
const blockTooltipId = "<%= object.rte_block_style_tooltip_id %>";
|
|
197
|
-
const iconTemplatesRoot = document.getElementById("<%= object.container_id %>-block-icon-templates");
|
|
230
|
+
const iconTemplatesRoot = rteSimple ? null : document.getElementById("<%= object.container_id %>-block-icon-templates");
|
|
198
231
|
if (!editorNode || !hiddenInput || !toolbar) return;
|
|
199
232
|
|
|
200
233
|
function syncToHiddenInput(editor) {
|
|
@@ -239,6 +272,7 @@
|
|
|
239
272
|
}
|
|
240
273
|
|
|
241
274
|
function syncBlockTrigger() {
|
|
275
|
+
if (rteSimple) return;
|
|
242
276
|
const current = getCurrentBlockValue();
|
|
243
277
|
const triggerRoot = toolbar.querySelector("[data-rte-block-trigger]");
|
|
244
278
|
let tpl = iconTemplatesRoot && [...iconTemplatesRoot.children].find(
|
|
@@ -276,18 +310,20 @@
|
|
|
276
310
|
else if (value === "blockquote") chain.toggleBlockquote().run();
|
|
277
311
|
}
|
|
278
312
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
313
|
+
if (!rteSimple) {
|
|
314
|
+
const blockStyleTooltip = document.getElementById(blockTooltipId);
|
|
315
|
+
if (blockStyleTooltip) {
|
|
316
|
+
blockStyleTooltip.addEventListener("click", (e) => {
|
|
317
|
+
const a = e.target.closest("a[href^='#']");
|
|
318
|
+
if (!a || !blockStyleTooltip.contains(a)) return;
|
|
319
|
+
e.preventDefault();
|
|
320
|
+
const href = a.getAttribute("href") || "";
|
|
321
|
+
const v = href.startsWith("#") ? href.slice(1) : "";
|
|
322
|
+
if (!v) return;
|
|
323
|
+
applyBlockType(v);
|
|
324
|
+
updateActiveStates();
|
|
325
|
+
});
|
|
326
|
+
}
|
|
291
327
|
}
|
|
292
328
|
|
|
293
329
|
function updateActiveStates() {
|
|
@@ -9,6 +9,9 @@ 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
|
+
# When true, TipTap toolbar matches React `simple`: Bold + Italic only (no block-style Popover).
|
|
13
|
+
# Use in modals or narrow layouts where the block dropdown misbehaves.
|
|
14
|
+
prop :simple, type: Playbook::Props::Boolean, default: false
|
|
12
15
|
|
|
13
16
|
# Match React default (globalProps maxWidth "md").
|
|
14
17
|
def max_width
|
|
@@ -43,9 +43,16 @@ module Playbook
|
|
|
43
43
|
multiple: multiple,
|
|
44
44
|
onchange: onchange,
|
|
45
45
|
include_blank: include_blank,
|
|
46
|
+
data: validation_data,
|
|
46
47
|
}.merge(attributes).merge(input_options)
|
|
47
48
|
end
|
|
48
49
|
|
|
50
|
+
def validation_data
|
|
51
|
+
fields = input_options[:data] || {}
|
|
52
|
+
fields[:message] = validation_message unless validation_message.blank?
|
|
53
|
+
fields
|
|
54
|
+
end
|
|
55
|
+
|
|
49
56
|
# Same resolved id as the native +<select>+ (+all_attributes[:id]+) for label +for+.
|
|
50
57
|
def select_input_id
|
|
51
58
|
all_attributes[:id].presence
|