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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 007e13930c03383e3d8d12e920f115ba93f90a42a7209e1892e5573a035ac924
4
- data.tar.gz: db7d6dcd4bf18b7a56d864b5a9e8dd7e95110ba894ff68ece84bf67518a6d69d
3
+ metadata.gz: 0ca3a00f10560ff55c0a92fce48ffd9643497f3b36332260569207d555ed8790
4
+ data.tar.gz: f952e25426aa178f3b61f76d3ab30161430d91f8f831349f2939744428ba3eac
5
5
  SHA512:
6
- metadata.gz: c3d8760e03b24889ae15e84e210eb86d600423ae7935ac2a29a23e010bfba4c9510d31a13d55c4fd527f5d64204c16cd0183336f561a9d65840eb3e8ed7714be
7
- data.tar.gz: 04b2d00ab21c6bc07c1e2c19d580948b4898cd010138dcf1260fdebfc3081848b5ab6b2da5d7649825390839fff108f07537159d72e131ca08f78760a9d30036
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
- [class^="pb_rich_text_editor_kit"] {
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
- padding: ($space_xs - 1) 0px;
104
- width: $space_xl * 3;
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
- .pb_rich_text_editor_advanced_container {
201
- transition: box-shadow 0.3s ease-in-out, border-radius 0.3s ease-in-out,
202
- border-color 0.3s ease-in-out;
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
- // Single outer frame (toolbar + editor): avoids inner ProseMirror border + focus ring looking like two bottoms.
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: none;
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: none;
235
- border-radius: 0;
357
+ border-color: $input_border_state;
236
358
  }
237
359
  }
238
360
  }
@@ -1 +1,12 @@
1
- TipTap (vanilla JS) the Playbook **Rails** rich text editor. No React; same editor core as the React TipTap variant. Content is synced to a hidden input for Rails form submission. Use `pb_rails("rich_text_editor", props: { input_options: { id: "...", name: "..." }, value: "..." })`.
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
- <div class="pb_background_kit pb_background_color_white toolbar" id="<%= object.toolbar_id %>">
73
- <div class="toolbar_block">
74
- <%= pb_rails("button", props: {
75
- icon: "bold",
76
- size: "sm",
77
- variant: "link",
78
- html_options: {
79
- type: "button",
80
- data: { action: "bold" },
81
- title: "Bold",
82
- role: "button",
83
- tabindex: 0,
84
- class: "toolbar_button"
85
- }
86
- }) %>
87
- <%= pb_rails("button", props: {
88
- icon: "italic",
89
- size: "sm",
90
- variant: "link",
91
- html_options: {
92
- type: "button",
93
- data: { action: "italic" },
94
- title: "Italic",
95
- role: "button",
96
- tabindex: 0,
97
- class: "toolbar_button"
98
- }
99
- }) %>
100
- <%= pb_rails("button", props: {
101
- icon: "strikethrough",
102
- size: "sm",
103
- variant: "link",
104
- html_options: {
105
- type: "button",
106
- data: { action: "strike" },
107
- title: "Strikethrough",
108
- role: "button",
109
- tabindex: 0,
110
- class: "toolbar_button"
111
- }
112
- }) %>
113
- <%= pb_rails("button", props: {
114
- icon: "h1",
115
- size: "sm",
116
- variant: "link",
117
- html_options: {
118
- type: "button",
119
- data: { action: "heading", level: "1" },
120
- title: "Heading 1",
121
- role: "button",
122
- tabindex: 0,
123
- class: "toolbar_button"
124
- }
125
- }) %>
126
- <%= pb_rails("button", props: {
127
- icon: "h2",
128
- size: "sm",
129
- variant: "link",
130
- html_options: {
131
- type: "button",
132
- data: { action: "heading", level: "2" },
133
- title: "Heading 2",
134
- role: "button",
135
- tabindex: 0,
136
- class: "toolbar_button"
137
- }
138
- }) %>
139
- <%= pb_rails("button", props: {
140
- icon: "list",
141
- size: "sm",
142
- variant: "link",
143
- html_options: {
144
- type: "button",
145
- data: { action: "bulletList" },
146
- title: "Bullet list",
147
- role: "button",
148
- tabindex: 0,
149
- class: "toolbar_button"
150
- }
151
- }) %>
152
- <%= pb_rails("button", props: {
153
- icon: "list-ol",
154
- size: "sm",
155
- variant: "link",
156
- html_options: {
157
- type: "button",
158
- data: { action: "orderedList" },
159
- title: "Ordered list",
160
- role: "button",
161
- tabindex: 0,
162
- class: "toolbar_button"
163
- }
164
- }) %>
165
- <%= pb_rails("button", props: {
166
- icon: "quote-left",
167
- size: "sm",
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 === "heading" && level) {
286
- editor.chain().focus().toggleHeading({ level }).run();
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 url = window.prompt("URL:");
289
- if (url) editor.chain().focus().setLink({ href: url }).run();
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") {