playbook_ui 16.5.0.pre.alpha.RTEPOC15708 → 16.5.0.pre.alpha.RTEPOC15742
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/pb_kits/playbook/pb_rich_text_editor/_tiptap_styles.scss +143 -29
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_default.md +12 -1
- data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.html.erb +187 -145
- data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.rb +15 -0
- data/dist/playbook.css +1 -1
- data/lib/playbook/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0ae730fde19e05cbbd23bebf541db944a7e7e92ede9564b91886c776e27d930a
|
|
4
|
+
data.tar.gz: bfc177ae612fe3e2032a4959b3d949f064c478455be2c502fd335ec4147574f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f5e1b1ef50ebee06c617e1dbe5078f95d2770fc216a399398805db6cd6e4fb7795a0d0efe5db34275c361891a49117f50e2dc5659483aecb3a5ca20d03220a81
|
|
7
|
+
data.tar.gz: d2056b8d90992bbb246ee13c7617100e30bf484450b2180d4e1bcc188d2f6b2ecd13d3efcb48f43f91f4d12287d3c13a10bb1654b43def8faa64747a8fe6c37f
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
@import "../tokens/transition";
|
|
9
9
|
@import "previewer_mixin";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
.pb_rich_text_editor_kit {
|
|
12
12
|
&.inline {
|
|
13
13
|
.toolbar {
|
|
14
14
|
opacity: 0;
|
|
@@ -94,18 +94,112 @@
|
|
|
94
94
|
align-items: center;
|
|
95
95
|
gap: $space_xs;
|
|
96
96
|
}
|
|
97
|
+
|
|
98
|
+
// Vertical section separators use ::before/::after with height: 100%. With
|
|
99
|
+
// align-items: center on .toolbar_block the kit’s cross size was 0, so the
|
|
100
|
+
// lines disappeared (master only had gap on .toolbar_block, default stretch).
|
|
101
|
+
.pb_section_separator_kit.pb_section_separator_vertical {
|
|
102
|
+
align-self: center;
|
|
103
|
+
flex-shrink: 0;
|
|
104
|
+
height: $space_xl;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Match React ToolbarDropdown: secondary trigger flattened to text-style control.
|
|
97
108
|
.editor-dropdown-button {
|
|
98
109
|
background: transparent;
|
|
99
110
|
border: none;
|
|
100
111
|
color: $text_lt_light;
|
|
101
112
|
cursor: pointer;
|
|
102
113
|
font-weight: $light;
|
|
103
|
-
|
|
104
|
-
|
|
114
|
+
letter-spacing: normal;
|
|
115
|
+
line-height: 1;
|
|
116
|
+
min-height: unset;
|
|
117
|
+
min-width: $space_xl * 5;
|
|
118
|
+
padding: ($space_xs - 1) $space_xs;
|
|
119
|
+
width: auto;
|
|
120
|
+
|
|
121
|
+
// Undo Playbook .pb_button_kit defaults that throw off icon vs label (line-height 1.5, min-height 40px).
|
|
122
|
+
.pb_button_content {
|
|
123
|
+
align-items: center;
|
|
124
|
+
display: inline-flex;
|
|
125
|
+
line-height: 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// React: single Flex row inside the button.
|
|
129
|
+
.pb_button_content > .pb_flex_kit {
|
|
130
|
+
align-items: center;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Rails: block-style trigger row (spans + icons).
|
|
134
|
+
.rte-block-style-trigger-inner {
|
|
135
|
+
align-items: center;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.rte-block-style-trigger-icon,
|
|
139
|
+
.rte-block-style-chevron {
|
|
140
|
+
display: inline-flex;
|
|
141
|
+
flex-shrink: 0;
|
|
142
|
+
line-height: 0;
|
|
143
|
+
|
|
144
|
+
.pb_icon_kit {
|
|
145
|
+
align-items: center;
|
|
146
|
+
display: flex;
|
|
147
|
+
line-height: 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
svg {
|
|
151
|
+
display: block;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.rte-block-style-trigger-label {
|
|
156
|
+
align-items: center;
|
|
157
|
+
display: inline-flex;
|
|
158
|
+
line-height: 1.2;
|
|
159
|
+
}
|
|
160
|
+
|
|
105
161
|
&:focus-visible {
|
|
106
162
|
box-shadow: unset;
|
|
107
163
|
}
|
|
108
164
|
}
|
|
165
|
+
|
|
166
|
+
// Rails TipTap toolbar: mirror React Toolbar.tsx — <Flex paddingX="sm" paddingY="xxs" justify="between">.
|
|
167
|
+
&.rte-rails-toolbar-layout {
|
|
168
|
+
.rte-rails-toolbar-row {
|
|
169
|
+
align-items: center;
|
|
170
|
+
box-sizing: border-box;
|
|
171
|
+
column-gap: 0;
|
|
172
|
+
display: flex;
|
|
173
|
+
flex-wrap: wrap;
|
|
174
|
+
justify-content: flex-start;
|
|
175
|
+
padding: $space_xxs $space_sm;
|
|
176
|
+
row-gap: $space_xs;
|
|
177
|
+
width: 100%;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.rte-toolbar-left {
|
|
181
|
+
align-items: center;
|
|
182
|
+
display: flex;
|
|
183
|
+
flex: 1 1 auto;
|
|
184
|
+
flex-wrap: wrap;
|
|
185
|
+
gap: $space_xs;
|
|
186
|
+
min-width: 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.rte-toolbar-right {
|
|
190
|
+
align-items: center;
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-shrink: 0;
|
|
193
|
+
gap: $space_xs;
|
|
194
|
+
margin-left: auto;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Align dropdown trigger with icon row (React wraps Popover + SectionSeparator in one flex line).
|
|
198
|
+
.pb_popover_reference_wrapper {
|
|
199
|
+
align-items: center;
|
|
200
|
+
display: inline-flex;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
109
203
|
}
|
|
110
204
|
|
|
111
205
|
.ProseMirror {
|
|
@@ -171,6 +265,17 @@
|
|
|
171
265
|
@include preview_tiptap_ul;
|
|
172
266
|
}
|
|
173
267
|
}
|
|
268
|
+
|
|
269
|
+
// Toolbar + editor stack: toolbar keeps its border; editor has no top stroke (classic layout).
|
|
270
|
+
// Avoid relying on a wrapper-only frame — partial deploys then looked “borderless” because
|
|
271
|
+
// ProseMirror had been forced to border: none.
|
|
272
|
+
.pb_rich_text_editor_advanced_container.toolbar-active {
|
|
273
|
+
.ProseMirror {
|
|
274
|
+
border-top: none;
|
|
275
|
+
border-top-left-radius: initial;
|
|
276
|
+
border-top-right-radius: initial;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
174
279
|
}
|
|
175
280
|
|
|
176
281
|
.pb_tiptap_toolbar_dropdown_list_item {
|
|
@@ -197,42 +302,51 @@
|
|
|
197
302
|
}
|
|
198
303
|
}
|
|
199
304
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
305
|
+
|
|
306
|
+
// Rails RTE: block-style menu uses Nav (popover) instead of React NavItem class hook.
|
|
307
|
+
.pb_rich_text_editor_kit .pb_popover_tooltip .pb_nav_list_item_link.is-active {
|
|
308
|
+
background-color: $bg_light;
|
|
309
|
+
border-radius: unset !important;
|
|
310
|
+
color: $primary;
|
|
311
|
+
|
|
312
|
+
.pb_nav_list_item_text,
|
|
313
|
+
.pb_nav_list_item_icon_left {
|
|
314
|
+
color: $primary !important;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.pb_rich_text_editor_kit .pb_popover_tooltip .pb_nav_list_kit_item:hover .pb_nav_list_item_link:not(.is-active) {
|
|
319
|
+
background-color: $neutral_subtle;
|
|
320
|
+
border-radius: unset !important;
|
|
321
|
+
|
|
322
|
+
.pb_nav_list_item_text,
|
|
323
|
+
.pb_nav_list_item_icon_left {
|
|
324
|
+
background-color: unset;
|
|
325
|
+
color: $text_lt_light !important;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// No toolbar: ring the whole control.
|
|
330
|
+
.pb_rich_text_editor_advanced_container:not(.toolbar-active) {
|
|
331
|
+
transition: box-shadow 0.3s ease-in-out, border-radius 0.3s ease-in-out;
|
|
203
332
|
&:focus-visible,
|
|
204
333
|
&:focus-within {
|
|
205
|
-
outline: unset;
|
|
206
|
-
box-shadow: 0 0 0 1px $input_border_state;
|
|
207
334
|
border-radius: $border_rad_heaviest;
|
|
335
|
+
box-shadow: 0 0 0 1px $input_border_state;
|
|
336
|
+
outline: unset;
|
|
208
337
|
transition: box-shadow 0.3s ease-in-out, border-radius 0.3s ease-in-out;
|
|
209
338
|
}
|
|
210
|
-
|
|
211
|
-
&.toolbar-active {
|
|
212
|
-
border: 1px solid $input_border_default;
|
|
213
|
-
border-radius: $border_rad_heaviest;
|
|
214
|
-
overflow: hidden;
|
|
215
|
-
|
|
216
|
-
&:focus-visible,
|
|
217
|
-
&:focus-within {
|
|
218
|
-
outline: unset;
|
|
219
|
-
box-shadow: none;
|
|
220
|
-
border-color: $input_border_state;
|
|
221
|
-
}
|
|
339
|
+
}
|
|
222
340
|
|
|
341
|
+
// Toolbar + editor: use border color (not an outer box-shadow) so the bottom isn’t doubled.
|
|
342
|
+
.pb_rich_text_editor_advanced_container.toolbar-active {
|
|
343
|
+
&:focus-within {
|
|
223
344
|
.toolbar {
|
|
224
|
-
border:
|
|
225
|
-
border-bottom: 1px solid $input_border_default;
|
|
226
|
-
border-radius: 0;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
&:focus-within .toolbar {
|
|
230
|
-
border-bottom-color: $input_border_state;
|
|
345
|
+
border-color: $input_border_state;
|
|
231
346
|
}
|
|
232
347
|
|
|
233
348
|
.ProseMirror {
|
|
234
|
-
border:
|
|
235
|
-
border-radius: 0;
|
|
349
|
+
border-color: $input_border_state;
|
|
236
350
|
}
|
|
237
351
|
}
|
|
238
352
|
}
|
|
@@ -1 +1,12 @@
|
|
|
1
|
-
TipTap (
|
|
1
|
+
The Rails rich text editor is a TipTap surface with no React. The UI (toolbar, block-style menu, formatting actions) is rendered with Playbook Rails kits (`pb_rails`). The editor document is a vanilla TipTap `Editor` instance; HTML is synced to a hidden `<input>` so standard Rails forms can submit the value.
|
|
2
|
+
|
|
3
|
+
### How TipTap is loaded (Rails)
|
|
4
|
+
|
|
5
|
+
- The kit ships an `importmap` in the ERB template that maps `@tiptap/*` and ProseMirror packages to ES module URLs on a CDN (see `rich_text_editor.html.erb`). A small `type="module"` script `import()`s `@tiptap/core`, `@tiptap/starter-kit`, `@tiptap/extension-link`, and related modules at runtime.
|
|
6
|
+
- You do not need to add TipTap to your app’s npm dependencies or Gemfile for this kit to work out of the box—the browser loads those modules from the CDN when the page runs.
|
|
7
|
+
- Your app must support import maps and ES modules in the browser (modern browsers; ensure CSP allows the CDN if you lock scripts down).
|
|
8
|
+
|
|
9
|
+
### Relation to the React implementation
|
|
10
|
+
|
|
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
|
+
- 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.
|
|
@@ -69,140 +69,111 @@
|
|
|
69
69
|
<% end %>
|
|
70
70
|
<input type="hidden" name="<%= object.input_name %>" id="<%= object.input_id %>" value="" />
|
|
71
71
|
<div class="pb_rich_text_editor_advanced_container toolbar-active">
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
variant: "link",
|
|
169
|
-
html_options: {
|
|
170
|
-
type: "button",
|
|
171
|
-
data: { action: "blockquote" },
|
|
172
|
-
title: "Quote",
|
|
173
|
-
role: "button",
|
|
174
|
-
tabindex: 0,
|
|
175
|
-
class: "toolbar_button"
|
|
176
|
-
}
|
|
177
|
-
}) %>
|
|
178
|
-
<%= pb_rails("button", props: {
|
|
179
|
-
icon: "code",
|
|
180
|
-
size: "sm",
|
|
181
|
-
variant: "link",
|
|
182
|
-
html_options: {
|
|
183
|
-
type: "button",
|
|
184
|
-
data: { action: "codeBlock" },
|
|
185
|
-
title: "Code block",
|
|
186
|
-
role: "button",
|
|
187
|
-
tabindex: 0,
|
|
188
|
-
class: "toolbar_button"
|
|
189
|
-
}
|
|
190
|
-
}) %>
|
|
191
|
-
<%= pb_rails("button", props: {
|
|
192
|
-
icon: "link",
|
|
193
|
-
size: "sm",
|
|
194
|
-
variant: "link",
|
|
195
|
-
html_options: {
|
|
196
|
-
type: "button",
|
|
197
|
-
data: { action: "link" },
|
|
198
|
-
title: "Link",
|
|
199
|
-
role: "button",
|
|
200
|
-
tabindex: 0,
|
|
201
|
-
class: "toolbar_button"
|
|
202
|
-
}
|
|
203
|
-
}) %>
|
|
72
|
+
<% block_style_options = [
|
|
73
|
+
{ value: "paragraph", text: "Paragraph", icon: "paragraph" },
|
|
74
|
+
{ value: "heading-1", text: "Heading 1", icon: "h1" },
|
|
75
|
+
{ value: "heading-2", text: "Heading 2", icon: "h2" },
|
|
76
|
+
{ value: "heading-3", text: "Heading 3", icon: "h3" },
|
|
77
|
+
{ value: "bulletList", text: "Bullet List", icon: "list" },
|
|
78
|
+
{ value: "orderedList", text: "Ordered List", icon: "list-ol" },
|
|
79
|
+
{ value: "blockquote", text: "Block Quote", icon: "block-quote" },
|
|
80
|
+
] %>
|
|
81
|
+
<div class="pb_background_kit pb_background_color_white toolbar rte-rails-toolbar-layout" id="<%= object.toolbar_id %>">
|
|
82
|
+
<div class="rte-rails-toolbar-row">
|
|
83
|
+
<div class="toolbar_block rte-toolbar-left">
|
|
84
|
+
<span class="pb_popover_reference_wrapper">
|
|
85
|
+
<%# Button kit wraps content in <span class="pb_button_content"> — block <div>s inside are invalid and break layout. Single <span> row + JS sync from templates. %>
|
|
86
|
+
<%= pb_rails("button", props: {
|
|
87
|
+
id: object.rte_block_style_trigger_id,
|
|
88
|
+
variant: "secondary",
|
|
89
|
+
classname: "editor-dropdown-button",
|
|
90
|
+
html_options: {
|
|
91
|
+
type: "button",
|
|
92
|
+
"aria-label": "Text style",
|
|
93
|
+
"aria-haspopup": "true",
|
|
94
|
+
},
|
|
95
|
+
}) do %>
|
|
96
|
+
<span class="pb_flex_kit pb_flex_kit_orientation_row pb_flex_kit_justify_content_left pb_flex_kit_align_items_center pb_flex_kit_spacing_none pb_flex_kit_gap_xs gap_xs rte-block-style-trigger-inner" data-rte-block-trigger>
|
|
97
|
+
<span class="rte-block-style-trigger-icon">
|
|
98
|
+
<%= pb_rails("icon", props: { icon: "paragraph", size: "lg" }) %>
|
|
99
|
+
</span>
|
|
100
|
+
<span class="rte-block-style-trigger-label">Paragraph</span>
|
|
101
|
+
<span class="display_inline_flex rte-block-style-chevron">
|
|
102
|
+
<%= pb_rails("icon", props: { icon: "chevron-down", fixed_width: true }) %>
|
|
103
|
+
</span>
|
|
104
|
+
</span>
|
|
105
|
+
<% end %>
|
|
106
|
+
</span>
|
|
107
|
+
<%= pb_rails("popover", props: {
|
|
108
|
+
trigger_element_id: object.rte_block_style_trigger_id,
|
|
109
|
+
tooltip_id: object.rte_block_style_tooltip_id,
|
|
110
|
+
position: "bottom",
|
|
111
|
+
padding: "none",
|
|
112
|
+
close_on_click: "any",
|
|
113
|
+
offset: true,
|
|
114
|
+
}) do %>
|
|
115
|
+
<%= pb_rails("nav", props: { variant: "subtle", padding_top: "xs", padding_bottom: "xs" }) do %>
|
|
116
|
+
<% block_style_options.each do |opt| %>
|
|
117
|
+
<%= pb_rails("nav/item", props: {
|
|
118
|
+
link: "##{opt[:value]}",
|
|
119
|
+
text: opt[:text],
|
|
120
|
+
icon_left: opt[:icon],
|
|
121
|
+
margin: "none",
|
|
122
|
+
padding_top: "xxs",
|
|
123
|
+
padding_bottom: "xxs",
|
|
124
|
+
}) %>
|
|
125
|
+
<% end %>
|
|
126
|
+
<% end %>
|
|
127
|
+
<% end %>
|
|
128
|
+
<%= pb_rails("section_separator", props: { orientation: "vertical" }) %>
|
|
129
|
+
<button type="button" class="toolbar_button" data-action="bold" title="Bold" role="button" tabindex="0">
|
|
130
|
+
<%= pb_rails("flex", props: { align: "center", justify: "center", classname: "toolbar_button_icon" }) do %>
|
|
131
|
+
<%= pb_rails("icon", props: { icon: "bold", size: "lg" }) %>
|
|
132
|
+
<% end %>
|
|
133
|
+
</button>
|
|
134
|
+
<button type="button" class="toolbar_button" data-action="italic" title="Italic" role="button" tabindex="0">
|
|
135
|
+
<%= pb_rails("flex", props: { align: "center", justify: "center", classname: "toolbar_button_icon" }) do %>
|
|
136
|
+
<%= pb_rails("icon", props: { icon: "italic", size: "lg" }) %>
|
|
137
|
+
<% end %>
|
|
138
|
+
</button>
|
|
139
|
+
<button type="button" class="toolbar_button" data-action="strike" title="Strikethrough" role="button" tabindex="0">
|
|
140
|
+
<%= pb_rails("flex", props: { align: "center", justify: "center", classname: "toolbar_button_icon" }) do %>
|
|
141
|
+
<%= pb_rails("icon", props: { icon: "strikethrough", size: "lg" }) %>
|
|
142
|
+
<% end %>
|
|
143
|
+
</button>
|
|
144
|
+
<%= pb_rails("section_separator", props: { orientation: "vertical" }) %>
|
|
145
|
+
<button type="button" class="toolbar_button" data-action="codeBlock" title="Code block" role="button" tabindex="0">
|
|
146
|
+
<%= pb_rails("flex", props: { align: "center", justify: "center", classname: "toolbar_button_icon" }) do %>
|
|
147
|
+
<%= pb_rails("icon", props: { icon: "code", size: "lg" }) %>
|
|
148
|
+
<% end %>
|
|
149
|
+
</button>
|
|
150
|
+
<button type="button" class="toolbar_button" data-action="link" title="Link" role="button" tabindex="0">
|
|
151
|
+
<%= pb_rails("flex", props: { align: "center", justify: "center", classname: "toolbar_button_icon" }) do %>
|
|
152
|
+
<%= pb_rails("icon", props: { icon: "link", size: "lg" }) %>
|
|
153
|
+
<% end %>
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="toolbar_block rte-toolbar-right">
|
|
157
|
+
<button type="button" class="toolbar_button" data-action="undo" title="Undo" role="button" tabindex="0">
|
|
158
|
+
<%= pb_rails("flex", props: { align: "center", justify: "center", classname: "toolbar_button_icon" }) do %>
|
|
159
|
+
<%= pb_rails("icon", props: { icon: "undo", size: "lg" }) %>
|
|
160
|
+
<% end %>
|
|
161
|
+
</button>
|
|
162
|
+
<button type="button" class="toolbar_button" data-action="redo" title="Redo" role="button" tabindex="0">
|
|
163
|
+
<%= pb_rails("flex", props: { align: "center", justify: "center", classname: "toolbar_button_icon" }) do %>
|
|
164
|
+
<%= pb_rails("icon", props: { icon: "redo", size: "lg" }) %>
|
|
165
|
+
<% end %>
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
204
168
|
</div>
|
|
205
169
|
</div>
|
|
170
|
+
<div id="<%= object.container_id %>-block-icon-templates" hidden aria-hidden="true">
|
|
171
|
+
<% block_style_options.each do |opt| %>
|
|
172
|
+
<span data-block-template-for="<%= opt[:value] %>" data-label="<%= opt[:text] %>">
|
|
173
|
+
<%= pb_rails("icon", props: { icon: opt[:icon], size: "lg" }) %>
|
|
174
|
+
</span>
|
|
175
|
+
<% end %>
|
|
176
|
+
</div>
|
|
206
177
|
<div class="rte-editor-wrap">
|
|
207
178
|
<div id="<%= object.editor_node_id %>"></div>
|
|
208
179
|
</div>
|
|
@@ -222,6 +193,8 @@
|
|
|
222
193
|
const hiddenInput = document.getElementById(inputId);
|
|
223
194
|
const editorNode = document.getElementById("<%= object.editor_node_id %>");
|
|
224
195
|
const toolbar = document.getElementById("<%= object.toolbar_id %>");
|
|
196
|
+
const blockTooltipId = "<%= object.rte_block_style_tooltip_id %>";
|
|
197
|
+
const iconTemplatesRoot = document.getElementById("<%= object.container_id %>-block-icon-templates");
|
|
225
198
|
if (!editorNode || !hiddenInput || !toolbar) return;
|
|
226
199
|
|
|
227
200
|
function syncToHiddenInput(editor) {
|
|
@@ -251,28 +224,90 @@
|
|
|
251
224
|
bold: "toggleBold",
|
|
252
225
|
italic: "toggleItalic",
|
|
253
226
|
strike: "toggleStrike",
|
|
254
|
-
bulletList: "toggleBulletList",
|
|
255
|
-
orderedList: "toggleOrderedList",
|
|
256
|
-
blockquote: "toggleBlockquote",
|
|
257
227
|
codeBlock: "toggleCodeBlock",
|
|
258
228
|
};
|
|
259
229
|
|
|
230
|
+
function getCurrentBlockValue() {
|
|
231
|
+
let value = "paragraph";
|
|
232
|
+
if (editor.isActive("heading", { level: 1 })) value = "heading-1";
|
|
233
|
+
else if (editor.isActive("heading", { level: 2 })) value = "heading-2";
|
|
234
|
+
else if (editor.isActive("heading", { level: 3 })) value = "heading-3";
|
|
235
|
+
else if (editor.isActive("bulletList")) value = "bulletList";
|
|
236
|
+
else if (editor.isActive("orderedList")) value = "orderedList";
|
|
237
|
+
else if (editor.isActive("blockquote")) value = "blockquote";
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function syncBlockTrigger() {
|
|
242
|
+
const current = getCurrentBlockValue();
|
|
243
|
+
const triggerRoot = toolbar.querySelector("[data-rte-block-trigger]");
|
|
244
|
+
let tpl = iconTemplatesRoot && [...iconTemplatesRoot.children].find(
|
|
245
|
+
(el) => el.getAttribute("data-block-template-for") === current
|
|
246
|
+
);
|
|
247
|
+
if (!tpl && iconTemplatesRoot) {
|
|
248
|
+
tpl = [...iconTemplatesRoot.children].find(
|
|
249
|
+
(el) => el.getAttribute("data-block-template-for") === "paragraph"
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
if (triggerRoot && tpl) {
|
|
253
|
+
const iconWrap = triggerRoot.querySelector(".rte-block-style-trigger-icon");
|
|
254
|
+
const labelEl = triggerRoot.querySelector(".rte-block-style-trigger-label");
|
|
255
|
+
if (iconWrap) iconWrap.innerHTML = tpl.innerHTML;
|
|
256
|
+
if (labelEl) labelEl.textContent = tpl.getAttribute("data-label") || "";
|
|
257
|
+
}
|
|
258
|
+
const tooltip = document.getElementById(blockTooltipId);
|
|
259
|
+
if (tooltip) {
|
|
260
|
+
tooltip.querySelectorAll("a.pb_nav_list_item_link").forEach((a) => {
|
|
261
|
+
const href = a.getAttribute("href") || "";
|
|
262
|
+
const v = href.startsWith("#") ? href.slice(1) : "";
|
|
263
|
+
a.classList.toggle("is-active", v === current);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function applyBlockType(value) {
|
|
269
|
+
const chain = editor.chain().focus();
|
|
270
|
+
if (value === "paragraph") chain.setParagraph().run();
|
|
271
|
+
else if (value === "heading-1") chain.toggleHeading({ level: 1 }).run();
|
|
272
|
+
else if (value === "heading-2") chain.toggleHeading({ level: 2 }).run();
|
|
273
|
+
else if (value === "heading-3") chain.toggleHeading({ level: 3 }).run();
|
|
274
|
+
else if (value === "bulletList") chain.toggleBulletList().run();
|
|
275
|
+
else if (value === "orderedList") chain.toggleOrderedList().run();
|
|
276
|
+
else if (value === "blockquote") chain.toggleBlockquote().run();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const blockStyleTooltip = document.getElementById(blockTooltipId);
|
|
280
|
+
if (blockStyleTooltip) {
|
|
281
|
+
blockStyleTooltip.addEventListener("click", (e) => {
|
|
282
|
+
const a = e.target.closest("a[href^='#']");
|
|
283
|
+
if (!a || !blockStyleTooltip.contains(a)) return;
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
const href = a.getAttribute("href") || "";
|
|
286
|
+
const v = href.startsWith("#") ? href.slice(1) : "";
|
|
287
|
+
if (!v) return;
|
|
288
|
+
applyBlockType(v);
|
|
289
|
+
updateActiveStates();
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
260
293
|
function updateActiveStates() {
|
|
294
|
+
syncBlockTrigger();
|
|
261
295
|
toolbar.querySelectorAll("button[data-action]").forEach((btn) => {
|
|
262
296
|
const action = btn.dataset.action;
|
|
263
|
-
const level = btn.dataset.level ? parseInt(btn.dataset.level, 10) : null;
|
|
264
297
|
let active = false;
|
|
265
298
|
if (action === "bold") active = editor.isActive("bold");
|
|
266
299
|
else if (action === "italic") active = editor.isActive("italic");
|
|
267
300
|
else if (action === "strike") active = editor.isActive("strike");
|
|
268
|
-
else if (action === "blockquote") active = editor.isActive("blockquote");
|
|
269
|
-
else if (action === "bulletList") active = editor.isActive("bulletList");
|
|
270
|
-
else if (action === "orderedList") active = editor.isActive("orderedList");
|
|
271
301
|
else if (action === "codeBlock") active = editor.isActive("codeBlock");
|
|
272
302
|
else if (action === "link") active = editor.isActive("link");
|
|
273
|
-
else if (action === "heading" && level != null) active = editor.isActive("heading", { level });
|
|
274
303
|
btn.classList.toggle("is-active", active);
|
|
275
304
|
});
|
|
305
|
+
toolbar.querySelectorAll("button[data-action='undo']").forEach((btn) => {
|
|
306
|
+
btn.disabled = !editor.can().undo();
|
|
307
|
+
});
|
|
308
|
+
toolbar.querySelectorAll("button[data-action='redo']").forEach((btn) => {
|
|
309
|
+
btn.disabled = !editor.can().redo();
|
|
310
|
+
});
|
|
276
311
|
}
|
|
277
312
|
|
|
278
313
|
toolbar.addEventListener("click", (e) => {
|
|
@@ -280,13 +315,20 @@
|
|
|
280
315
|
if (!btn) return;
|
|
281
316
|
e.preventDefault();
|
|
282
317
|
const action = btn.dataset.action;
|
|
283
|
-
const level = btn.dataset.level ? parseInt(btn.dataset.level, 10) : null;
|
|
284
318
|
|
|
285
|
-
if (action === "
|
|
286
|
-
editor.chain().focus().
|
|
319
|
+
if (action === "undo") {
|
|
320
|
+
editor.chain().focus().undo().run();
|
|
321
|
+
} else if (action === "redo") {
|
|
322
|
+
editor.chain().focus().redo().run();
|
|
287
323
|
} else if (action === "link") {
|
|
288
|
-
const
|
|
289
|
-
|
|
324
|
+
const previousUrl = editor.getAttributes("link").href || "";
|
|
325
|
+
const url = window.prompt("URL", previousUrl);
|
|
326
|
+
if (url === null) return;
|
|
327
|
+
if (url === "") {
|
|
328
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
329
|
+
} else {
|
|
330
|
+
editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
|
|
331
|
+
}
|
|
290
332
|
} else {
|
|
291
333
|
const chainMethod = actionToChain[action];
|
|
292
334
|
if (chainMethod && typeof editor.chain().focus()[chainMethod] === "function") {
|
|
@@ -10,6 +10,12 @@ module Playbook
|
|
|
10
10
|
prop :label
|
|
11
11
|
prop :required_indicator, type: Playbook::Props::Boolean, default: false
|
|
12
12
|
|
|
13
|
+
# Match React default (globalProps maxWidth "md").
|
|
14
|
+
def max_width
|
|
15
|
+
v = values[:max_width] || values["max_width"]
|
|
16
|
+
v.nil? || v == "" ? "md" : v
|
|
17
|
+
end
|
|
18
|
+
|
|
13
19
|
def classname
|
|
14
20
|
generate_classname("pb_rich_text_editor_kit", "rte-container")
|
|
15
21
|
end
|
|
@@ -40,6 +46,15 @@ module Playbook
|
|
|
40
46
|
def toolbar_id
|
|
41
47
|
"#{container_id}-toolbar"
|
|
42
48
|
end
|
|
49
|
+
|
|
50
|
+
# Stable DOM ids for TipTap toolbar popover (used in ERB + module script; must be kit methods — not ERB locals).
|
|
51
|
+
def rte_block_style_trigger_id
|
|
52
|
+
"#{toolbar_id}-block-trigger"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def rte_block_style_tooltip_id
|
|
56
|
+
"#{toolbar_id}-block-tooltip"
|
|
57
|
+
end
|
|
43
58
|
end
|
|
44
59
|
end
|
|
45
60
|
end
|