playbook_ui 16.5.0.pre.alpha.RTEPOC15708 → 16.5.0.pre.alpha.RTEPOC15745
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 +154 -32
- 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: 0ca3a00f10560ff55c0a92fce48ffd9643497f3b36332260569207d555ed8790
|
|
4
|
+
data.tar.gz: f952e25426aa178f3b61f76d3ab30161430d91f8f831349f2939744428ba3eac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d7cebe4685fbff28fffd03205496b5b6dc85055e368a8af13ab2ceab28532852be660c745420df82f988fe3d3ca6fe50d44743c9b13a6a28e4406be68c900412
|
|
7
|
+
data.tar.gz: b71b66f81f6f93556dd27f7b933e51cf4171a9cd1e4de4555bd1b1d87514bbfe37ebdf7310076a5202e721724cf47135d1f7076f9aca5ff0b55da91a62bd7b45
|
|
@@ -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;
|
|
@@ -88,33 +88,135 @@
|
|
|
88
88
|
border-radius: $border_rad_heaviest $border_rad_heaviest 0 0;
|
|
89
89
|
border: 1px solid $input_border_default;
|
|
90
90
|
overflow-x: auto;
|
|
91
|
+
// Single horizontal row + scroll in narrow modals/sidebars (wrap used to stack controls vertically).
|
|
91
92
|
&_block {
|
|
92
|
-
display: flex;
|
|
93
|
-
flex-wrap: wrap;
|
|
94
93
|
align-items: center;
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-wrap: nowrap;
|
|
95
96
|
gap: $space_xs;
|
|
97
|
+
min-width: 0;
|
|
98
|
+
overflow-x: auto;
|
|
99
|
+
-webkit-overflow-scrolling: touch;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Vertical section separators use ::before/::after with height: 100%. With
|
|
103
|
+
// align-items: center on .toolbar_block the kit’s cross size was 0, so the
|
|
104
|
+
// lines disappeared (master only had gap on .toolbar_block, default stretch).
|
|
105
|
+
.pb_section_separator_kit.pb_section_separator_vertical {
|
|
106
|
+
align-self: center;
|
|
107
|
+
flex-shrink: 0;
|
|
108
|
+
height: $space_xl;
|
|
96
109
|
}
|
|
110
|
+
|
|
111
|
+
// Match React ToolbarDropdown: secondary trigger flattened to text-style control.
|
|
97
112
|
.editor-dropdown-button {
|
|
98
113
|
background: transparent;
|
|
99
114
|
border: none;
|
|
100
115
|
color: $text_lt_light;
|
|
101
116
|
cursor: pointer;
|
|
102
117
|
font-weight: $light;
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
letter-spacing: normal;
|
|
119
|
+
line-height: 1;
|
|
120
|
+
min-height: unset;
|
|
121
|
+
max-width: 100%;
|
|
122
|
+
min-width: $space_xl * 5;
|
|
123
|
+
padding: ($space_xs - 1) $space_xs;
|
|
124
|
+
width: auto;
|
|
125
|
+
|
|
126
|
+
// Undo Playbook .pb_button_kit defaults that throw off icon vs label (line-height 1.5, min-height 40px).
|
|
127
|
+
.pb_button_content {
|
|
128
|
+
align-items: center;
|
|
129
|
+
display: inline-flex;
|
|
130
|
+
line-height: 1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// React: single Flex row inside the button.
|
|
134
|
+
.pb_button_content > .pb_flex_kit {
|
|
135
|
+
align-items: center;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Rails: block-style trigger row (spans + icons).
|
|
139
|
+
.rte-block-style-trigger-inner {
|
|
140
|
+
align-items: center;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.rte-block-style-trigger-icon,
|
|
144
|
+
.rte-block-style-chevron {
|
|
145
|
+
display: inline-flex;
|
|
146
|
+
flex-shrink: 0;
|
|
147
|
+
line-height: 0;
|
|
148
|
+
|
|
149
|
+
.pb_icon_kit {
|
|
150
|
+
align-items: center;
|
|
151
|
+
display: flex;
|
|
152
|
+
line-height: 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
svg {
|
|
156
|
+
display: block;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.rte-block-style-trigger-label {
|
|
161
|
+
align-items: center;
|
|
162
|
+
display: inline-flex;
|
|
163
|
+
line-height: 1.2;
|
|
164
|
+
}
|
|
165
|
+
|
|
105
166
|
&:focus-visible {
|
|
106
167
|
box-shadow: unset;
|
|
107
168
|
}
|
|
108
169
|
}
|
|
170
|
+
|
|
171
|
+
// Rails TipTap toolbar: mirror React Toolbar.tsx — <Flex paddingX="sm" paddingY="xxs" justify="between">.
|
|
172
|
+
&.rte-rails-toolbar-layout {
|
|
173
|
+
.rte-rails-toolbar-row {
|
|
174
|
+
align-items: center;
|
|
175
|
+
box-sizing: border-box;
|
|
176
|
+
column-gap: 0;
|
|
177
|
+
display: flex;
|
|
178
|
+
flex-wrap: nowrap;
|
|
179
|
+
justify-content: flex-start;
|
|
180
|
+
padding: $space_xxs $space_sm;
|
|
181
|
+
row-gap: 0;
|
|
182
|
+
width: 100%;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.rte-toolbar-left {
|
|
186
|
+
align-items: center;
|
|
187
|
+
display: flex;
|
|
188
|
+
flex: 1 1 auto;
|
|
189
|
+
flex-wrap: nowrap;
|
|
190
|
+
gap: $space_xs;
|
|
191
|
+
min-width: 0;
|
|
192
|
+
overflow-x: auto;
|
|
193
|
+
-webkit-overflow-scrolling: touch;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.rte-toolbar-right {
|
|
197
|
+
align-items: center;
|
|
198
|
+
display: flex;
|
|
199
|
+
flex-shrink: 0;
|
|
200
|
+
gap: $space_xs;
|
|
201
|
+
margin-left: auto;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Align dropdown trigger with icon row (React wraps Popover + SectionSeparator in one flex line).
|
|
205
|
+
.pb_popover_reference_wrapper {
|
|
206
|
+
align-items: center;
|
|
207
|
+
display: inline-flex;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
109
210
|
}
|
|
110
211
|
|
|
111
212
|
.ProseMirror {
|
|
112
213
|
background: $white;
|
|
113
214
|
border: 1px solid $input_border_default;
|
|
114
215
|
border-radius: $border_rad_heaviest;
|
|
216
|
+
box-sizing: border-box;
|
|
115
217
|
height: 100%;
|
|
116
|
-
padding: 1rem 1.5rem 1.5rem 1.5rem;
|
|
117
218
|
line-height: $lh_loose;
|
|
219
|
+
padding: 1.25rem 1.5rem 1.5rem 1.5rem;
|
|
118
220
|
@include transition_default;
|
|
119
221
|
:first-child {
|
|
120
222
|
margin-top: 0;
|
|
@@ -171,6 +273,17 @@
|
|
|
171
273
|
@include preview_tiptap_ul;
|
|
172
274
|
}
|
|
173
275
|
}
|
|
276
|
+
|
|
277
|
+
// Toolbar + editor stack: toolbar keeps its border; editor has no top stroke (classic layout).
|
|
278
|
+
// Avoid relying on a wrapper-only frame — partial deploys then looked “borderless” because
|
|
279
|
+
// ProseMirror had been forced to border: none.
|
|
280
|
+
.pb_rich_text_editor_advanced_container.toolbar-active {
|
|
281
|
+
.ProseMirror {
|
|
282
|
+
border-top: none;
|
|
283
|
+
border-top-left-radius: initial;
|
|
284
|
+
border-top-right-radius: initial;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
174
287
|
}
|
|
175
288
|
|
|
176
289
|
.pb_tiptap_toolbar_dropdown_list_item {
|
|
@@ -197,42 +310,51 @@
|
|
|
197
310
|
}
|
|
198
311
|
}
|
|
199
312
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
313
|
+
|
|
314
|
+
// Rails RTE: block-style menu uses Nav (popover) instead of React NavItem class hook.
|
|
315
|
+
.pb_rich_text_editor_kit .pb_popover_tooltip .pb_nav_list_item_link.is-active {
|
|
316
|
+
background-color: $bg_light;
|
|
317
|
+
border-radius: unset !important;
|
|
318
|
+
color: $primary;
|
|
319
|
+
|
|
320
|
+
.pb_nav_list_item_text,
|
|
321
|
+
.pb_nav_list_item_icon_left {
|
|
322
|
+
color: $primary !important;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.pb_rich_text_editor_kit .pb_popover_tooltip .pb_nav_list_kit_item:hover .pb_nav_list_item_link:not(.is-active) {
|
|
327
|
+
background-color: $neutral_subtle;
|
|
328
|
+
border-radius: unset !important;
|
|
329
|
+
|
|
330
|
+
.pb_nav_list_item_text,
|
|
331
|
+
.pb_nav_list_item_icon_left {
|
|
332
|
+
background-color: unset;
|
|
333
|
+
color: $text_lt_light !important;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// No toolbar: ring the whole control.
|
|
338
|
+
.pb_rich_text_editor_advanced_container:not(.toolbar-active) {
|
|
339
|
+
transition: box-shadow 0.3s ease-in-out, border-radius 0.3s ease-in-out;
|
|
203
340
|
&:focus-visible,
|
|
204
341
|
&:focus-within {
|
|
205
|
-
outline: unset;
|
|
206
|
-
box-shadow: 0 0 0 1px $input_border_state;
|
|
207
342
|
border-radius: $border_rad_heaviest;
|
|
343
|
+
box-shadow: 0 0 0 1px $input_border_state;
|
|
344
|
+
outline: unset;
|
|
208
345
|
transition: box-shadow 0.3s ease-in-out, border-radius 0.3s ease-in-out;
|
|
209
346
|
}
|
|
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
|
-
}
|
|
347
|
+
}
|
|
222
348
|
|
|
349
|
+
// Toolbar + editor: use border color (not an outer box-shadow) so the bottom isn’t doubled.
|
|
350
|
+
.pb_rich_text_editor_advanced_container.toolbar-active {
|
|
351
|
+
&:focus-within {
|
|
223
352
|
.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;
|
|
353
|
+
border-color: $input_border_state;
|
|
231
354
|
}
|
|
232
355
|
|
|
233
356
|
.ProseMirror {
|
|
234
|
-
border:
|
|
235
|
-
border-radius: 0;
|
|
357
|
+
border-color: $input_border_state;
|
|
236
358
|
}
|
|
237
359
|
}
|
|
238
360
|
}
|
|
@@ -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") {
|