playbook_ui 16.5.0.pre.alpha.PLAY2893datepickerlabelclicktoggle15576 → 16.5.0.pre.alpha.RTEPOC15682

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.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_advanced_table/table_header.html.erb +1 -1
  3. data/app/pb_kits/playbook/pb_advanced_table/table_header.rb +33 -0
  4. data/app/pb_kits/playbook/pb_rich_text_editor/_rich_text_editor.tsx +1 -1
  5. data/app/pb_kits/playbook/pb_rich_text_editor/_tiptap_styles.scss +9 -0
  6. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_default.html.erb +1 -0
  7. data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_rails_default.md +1 -0
  8. data/app/pb_kits/playbook/pb_rich_text_editor/docs/example.yml +1 -0
  9. data/app/pb_kits/playbook/pb_rich_text_editor/kit.schema.json +14 -7
  10. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.html.erb +303 -0
  11. data/app/pb_kits/playbook/pb_rich_text_editor/rich_text_editor.rb +45 -0
  12. data/app/pb_kits/playbook/pb_select/select.rb +2 -2
  13. data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +4 -4
  14. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_createable.html.erb +29 -0
  15. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_createable.md +1 -0
  16. data/app/pb_kits/playbook/pb_typeahead/docs/example.yml +1 -0
  17. data/app/pb_kits/playbook/pb_typeahead/kit.schema.json +4 -2
  18. data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +4 -1
  19. data/dist/chunks/{_typeahead-B_Avup-T.js → _typeahead-BYUXg9ZT.js} +1 -1
  20. data/dist/chunks/vendor.js +2 -2
  21. data/dist/menu.yml +0 -1
  22. data/dist/playbook-rails-react-bindings.js +1 -1
  23. data/dist/playbook-rails.js +1 -1
  24. data/dist/playbook.css +1 -1
  25. data/lib/playbook/version.rb +1 -1
  26. metadata +9 -4
  27. data/app/pb_kits/playbook/utilities/globalPropNames.mjs +0 -58
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d77932d5ca1ce89caa56b927c3872fab1b7c023388a07072aabdb384268514b6
4
- data.tar.gz: 72e5ce55c6161081686b6eb4e4fde9569aa4c01d171de959ecaefd292378652f
3
+ metadata.gz: '092a215d37d2e1e61ec273d8e0fb5dd90fdf2e8756f34856743051da963a342f'
4
+ data.tar.gz: 9eb1b908eb0575b20f79633081b593538ae371bbc25f962d3d96b59c799f98b0
5
5
  SHA512:
6
- metadata.gz: f168b973e873f2ea3961d24cf24fba9a40d4a5de4391ee7fa9251b13de97e8b7c480d0fe276dcbd0192ae7eaae9efea4f1d490fb3d4364e5777f7c6a42046a25
7
- data.tar.gz: c990aad5891f894aae33d998c597238819426f531e241742f07d6ecc62b356ffef0a6bc1dfb4e9f73d24f0b4aa4c2830e8b67c18aac69c706b2a59323158ec53
6
+ metadata.gz: 752e655ecec4026fe1b19fcfee31dcb3bf595153c64b3e66b297ed0c8bc385cd22de29e09dbd109fd70a6336124903f0f3812e8477cb52657a94a78b3bf3de3b
7
+ data.tar.gz: e118779c805e4969b93c9e7eb5dd8fc3f58434447f818481e6873a3e3d2aa1bccdf5ce5c35d2328ade18691b12b0a1658e17bceb0388da8e38453ef3f23405ca
@@ -7,7 +7,7 @@
7
7
  <% header_row.each_with_index do |cell, cell_index| %>
8
8
  <% header_component = object.header_component_info(cell, cell_index, row_index) %>
9
9
  <%= pb_rails(header_component[:name], props: header_component[:props]) do %>
10
- <%= pb_rails("flex", props: { align: "center", justify: cell_index.zero? ? "start" : row_index === header_rows.size - 1 ? "end" : "center", text_align: (cell[:header_alignment] || "end") }) do %>
10
+ <%= pb_rails("flex", props: { align: "center", justify: object.header_flex_justify(cell, cell_index, row_index), text_align: object.header_flex_text_align(cell) }) do %>
11
11
  <% if cell_index.zero? && row_index === header_rows.size - 1 %>
12
12
  <% if object.selectable_rows && object.enable_toggle_expansion != "none" %>
13
13
  <%= pb_rails("flex/flex_item", props: { padding_right: "xs" }) do %>
@@ -192,6 +192,19 @@ module Playbook
192
192
  { name: component_name, props: component_props }
193
193
  end
194
194
 
195
+ # Flex justify for header cells: column_styling header_alignment when present (otherwisedefault by column/row index)
196
+ def header_flex_justify(cell, cell_index, row_index)
197
+ ha = cell[:header_alignment]
198
+ return header_alignment_to_justify(ha) if ha.present?
199
+
200
+ default_header_flex_justify(cell_index, row_index)
201
+ end
202
+
203
+ # Flex text_align from column_styling header_alignment (default is end)
204
+ def header_flex_text_align(cell)
205
+ (cell[:header_alignment].presence || "end").to_s
206
+ end
207
+
195
208
  private
196
209
 
197
210
  # Find the original column definition for a cell
@@ -351,6 +364,26 @@ module Playbook
351
364
  row[:children] || row["children"]
352
365
  end
353
366
  end
367
+
368
+ # 2 header alignment helper methods
369
+ def header_alignment_to_justify(header_alignment)
370
+ case header_alignment.to_s
371
+ when "left" then "start"
372
+ when "center" then "center"
373
+ when "right" then "end"
374
+ else "end"
375
+ end
376
+ end
377
+
378
+ def default_header_flex_justify(cell_index, row_index)
379
+ if cell_index.zero?
380
+ "start"
381
+ elsif row_index == header_rows.size - 1
382
+ "end"
383
+ else
384
+ "center"
385
+ end
386
+ end
354
387
  end
355
388
  end
356
389
  end
@@ -142,7 +142,7 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
142
142
  // Determine if toolbar should be shown
143
143
  const shouldShowToolbar = focus && advancedEditor ? showToolbarOnFocus : advancedEditorToolbar
144
144
 
145
- const labelFor = advancedEditor ? fieldId : (id ? id : (inputOptions.id ? `${inputOptions.id}_trix` : undefined))
145
+ const labelFor = advancedEditor ? fieldId : (id ? id : (inputOptions?.id ? `${inputOptions.id}_trix` : undefined))
146
146
 
147
147
  return (
148
148
  <div
@@ -73,6 +73,12 @@
73
73
  }
74
74
  }
75
75
 
76
+ // Active state for toolbar (Rails kit uses pb_button_kit pb_button_link; override link variant when active)
77
+ .toolbar button.pb_button_kit.is-active {
78
+ color: $primary;
79
+ background-color: $bg_light;
80
+ }
81
+
76
82
  .pb_rich_text_editor_tiptap_toolbar_sticky {
77
83
  position: sticky;
78
84
  top: 0;
@@ -83,6 +89,9 @@
83
89
  border: 1px solid $input_border_default;
84
90
  overflow-x: auto;
85
91
  &_block {
92
+ display: flex;
93
+ flex-wrap: wrap;
94
+ align-items: center;
86
95
  gap: $space_xs;
87
96
  }
88
97
  .editor-dropdown-button {
@@ -0,0 +1 @@
1
+ <%= pb_rails("rich_text_editor", props: { input_options: { id: 'hidden_input_id', name: "hidden_input_name" }, value: "Add your text here. You can format your text, add links, quotes, and bullets." }) %>
@@ -0,0 +1 @@
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,6 +1,7 @@
1
1
  examples:
2
2
 
3
3
  rails:
4
+ - rich_text_editor_rails_default: "Rails (TipTap)"
4
5
 
5
6
  react:
6
7
  - rich_text_editor_advanced_default: Advanced Default
@@ -3,7 +3,8 @@
3
3
  "name": "RichTextEditor",
4
4
  "description": "RichTextEditor component",
5
5
  "platforms": [
6
- "react"
6
+ "react",
7
+ "rails"
7
8
  ],
8
9
  "props": {
9
10
  "advancedEditor": {
@@ -33,7 +34,8 @@
33
34
  "inputOptions": {
34
35
  "type": "{ [key: string]: string | number | boolean | (() => void) }",
35
36
  "platforms": [
36
- "react"
37
+ "react",
38
+ "rails"
37
39
  ]
38
40
  },
39
41
  "inline": {
@@ -45,7 +47,8 @@
45
47
  "label": {
46
48
  "type": "string",
47
49
  "platforms": [
48
- "react"
50
+ "react",
51
+ "rails"
49
52
  ]
50
53
  },
51
54
  "extensions": {
@@ -69,7 +72,8 @@
69
72
  "placeholder": {
70
73
  "type": "string",
71
74
  "platforms": [
72
- "react"
75
+ "react",
76
+ "rails"
73
77
  ]
74
78
  },
75
79
  "inputHeight": {
@@ -97,8 +101,10 @@
97
101
  "requiredIndicator": {
98
102
  "type": "boolean",
99
103
  "platforms": [
100
- "react"
101
- ]
104
+ "react",
105
+ "rails"
106
+ ],
107
+ "default": false
102
108
  },
103
109
  "simple": {
104
110
  "type": "boolean",
@@ -121,7 +127,8 @@
121
127
  "value": {
122
128
  "type": "string",
123
129
  "platforms": [
124
- "react"
130
+ "react",
131
+ "rails"
125
132
  ]
126
133
  },
127
134
  "TrixEditor": {
@@ -0,0 +1,303 @@
1
+ <%# Import map: single CDN copy of TipTap/ProseMirror (avoids duplicate prosemirror-model). %>
2
+ <%# Sidecar ERB only — not render inline/file — so KitBase#pb_content_tag runs on the component. %>
3
+ <script type="importmap">
4
+ {
5
+ "imports": {
6
+ "@tiptap/core": "https://cdn.jsdelivr.net/npm/@tiptap/core@2.8.0/dist/index.js",
7
+ "@tiptap/starter-kit": "https://cdn.jsdelivr.net/npm/@tiptap/starter-kit@2.8.0/dist/index.js",
8
+ "@tiptap/extension-link": "https://cdn.jsdelivr.net/npm/@tiptap/extension-link@2.8.0/dist/index.js",
9
+ "@tiptap/extension-blockquote": "https://cdn.jsdelivr.net/npm/@tiptap/extension-blockquote@2.8.0/dist/index.js",
10
+ "@tiptap/extension-bold": "https://cdn.jsdelivr.net/npm/@tiptap/extension-bold@2.8.0/dist/index.js",
11
+ "@tiptap/extension-bullet-list": "https://cdn.jsdelivr.net/npm/@tiptap/extension-bullet-list@2.8.0/dist/index.js",
12
+ "@tiptap/extension-code": "https://cdn.jsdelivr.net/npm/@tiptap/extension-code@2.8.0/dist/index.js",
13
+ "@tiptap/extension-code-block": "https://cdn.jsdelivr.net/npm/@tiptap/extension-code-block@2.8.0/dist/index.js",
14
+ "@tiptap/extension-document": "https://cdn.jsdelivr.net/npm/@tiptap/extension-document@2.8.0/dist/index.js",
15
+ "@tiptap/extension-dropcursor": "https://cdn.jsdelivr.net/npm/@tiptap/extension-dropcursor@2.8.0/dist/index.js",
16
+ "@tiptap/extension-gapcursor": "https://cdn.jsdelivr.net/npm/@tiptap/extension-gapcursor@2.8.0/dist/index.js",
17
+ "@tiptap/extension-hard-break": "https://cdn.jsdelivr.net/npm/@tiptap/extension-hard-break@2.8.0/dist/index.js",
18
+ "@tiptap/extension-heading": "https://cdn.jsdelivr.net/npm/@tiptap/extension-heading@2.8.0/dist/index.js",
19
+ "@tiptap/extension-history": "https://cdn.jsdelivr.net/npm/@tiptap/extension-history@2.8.0/dist/index.js",
20
+ "@tiptap/extension-horizontal-rule": "https://cdn.jsdelivr.net/npm/@tiptap/extension-horizontal-rule@2.8.0/dist/index.js",
21
+ "@tiptap/extension-italic": "https://cdn.jsdelivr.net/npm/@tiptap/extension-italic@2.8.0/dist/index.js",
22
+ "@tiptap/extension-list-item": "https://cdn.jsdelivr.net/npm/@tiptap/extension-list-item@2.8.0/dist/index.js",
23
+ "@tiptap/extension-ordered-list": "https://cdn.jsdelivr.net/npm/@tiptap/extension-ordered-list@2.8.0/dist/index.js",
24
+ "@tiptap/extension-paragraph": "https://cdn.jsdelivr.net/npm/@tiptap/extension-paragraph@2.8.0/dist/index.js",
25
+ "@tiptap/extension-strike": "https://cdn.jsdelivr.net/npm/@tiptap/extension-strike@2.8.0/dist/index.js",
26
+ "@tiptap/extension-text": "https://cdn.jsdelivr.net/npm/@tiptap/extension-text@2.8.0/dist/index.js",
27
+ "@tiptap/extension-text-style": "https://cdn.jsdelivr.net/npm/@tiptap/extension-text-style@2.8.0/dist/index.js",
28
+ "@tiptap/pm/state": "https://cdn.jsdelivr.net/npm/@tiptap/pm@2.8.0/state/dist/index.js",
29
+ "@tiptap/pm/view": "https://cdn.jsdelivr.net/npm/@tiptap/pm@2.8.0/view/dist/index.js",
30
+ "@tiptap/pm/keymap": "https://cdn.jsdelivr.net/npm/@tiptap/pm@2.8.0/keymap/dist/index.js",
31
+ "@tiptap/pm/model": "https://cdn.jsdelivr.net/npm/@tiptap/pm@2.8.0/model/dist/index.js",
32
+ "@tiptap/pm/transform": "https://cdn.jsdelivr.net/npm/@tiptap/pm@2.8.0/transform/dist/index.js",
33
+ "@tiptap/pm/commands": "https://cdn.jsdelivr.net/npm/@tiptap/pm@2.8.0/commands/dist/index.js",
34
+ "@tiptap/pm/schema-list": "https://cdn.jsdelivr.net/npm/@tiptap/pm@2.8.0/schema-list/dist/index.js",
35
+ "@tiptap/pm/history": "https://cdn.jsdelivr.net/npm/@tiptap/pm@2.8.0/history/dist/index.js",
36
+ "@tiptap/pm/gapcursor": "https://cdn.jsdelivr.net/npm/@tiptap/pm@2.8.0/gapcursor/dist/index.js",
37
+ "@tiptap/pm/dropcursor": "https://cdn.jsdelivr.net/npm/@tiptap/pm@2.8.0/dropcursor/dist/index.js",
38
+ "@tiptap/pm/inputrules": "https://cdn.jsdelivr.net/npm/@tiptap/pm@2.8.0/inputrules/dist/index.js",
39
+ "@tiptap/pm/schema-basic": "https://cdn.jsdelivr.net/npm/@tiptap/pm@2.8.0/schema-basic/dist/index.js",
40
+ "prosemirror-model": "https://cdn.jsdelivr.net/npm/prosemirror-model@1.22.3/dist/index.js",
41
+ "prosemirror-state": "https://cdn.jsdelivr.net/npm/prosemirror-state@1.4.3/dist/index.js",
42
+ "prosemirror-view": "https://cdn.jsdelivr.net/npm/prosemirror-view@1.33.10/dist/index.js",
43
+ "prosemirror-transform": "https://cdn.jsdelivr.net/npm/prosemirror-transform@1.10.0/dist/index.js",
44
+ "prosemirror-commands": "https://cdn.jsdelivr.net/npm/prosemirror-commands@1.6.0/dist/index.js",
45
+ "prosemirror-keymap": "https://cdn.jsdelivr.net/npm/prosemirror-keymap@1.2.2/dist/index.js",
46
+ "prosemirror-history": "https://cdn.jsdelivr.net/npm/prosemirror-history@1.4.1/dist/index.js",
47
+ "prosemirror-gapcursor": "https://cdn.jsdelivr.net/npm/prosemirror-gapcursor@1.3.2/dist/index.js",
48
+ "prosemirror-dropcursor": "https://cdn.jsdelivr.net/npm/prosemirror-dropcursor@1.8.1/dist/index.js",
49
+ "prosemirror-inputrules": "https://cdn.jsdelivr.net/npm/prosemirror-inputrules@1.4.0/dist/index.js",
50
+ "prosemirror-schema-basic": "https://cdn.jsdelivr.net/npm/prosemirror-schema-basic@1.2.3/dist/index.js",
51
+ "prosemirror-schema-list": "https://cdn.jsdelivr.net/npm/prosemirror-schema-list@1.4.1/dist/index.js",
52
+ "linkifyjs": "https://cdn.jsdelivr.net/npm/linkifyjs@4.1.0/dist/linkify.es.js",
53
+ "orderedmap": "https://cdn.jsdelivr.net/npm/orderedmap@2.1.1/dist/index.js",
54
+ "w3c-keyname": "https://cdn.jsdelivr.net/npm/w3c-keyname@2.2.8/index.js",
55
+ "rope-sequence": "https://cdn.jsdelivr.net/npm/rope-sequence@1.3.4/dist/index.js"
56
+ }
57
+ }
58
+ </script>
59
+ <%= pb_content_tag(:div, id: object.container_id, class: object.classname, data: { pb_rte_tiptap: true, input_id: object.input_id, initial_html: object.initial_html }) do %>
60
+ <div class="pb_rich_text_editor_kit">
61
+ <% if object.label.present? %>
62
+ <label for="<%= object.input_id %>">
63
+ <% if object.required_indicator %>
64
+ <%= pb_rails("caption", props: { color: "lighter", text: object.label, dark: object.dark }) %><span style="color: #DA0014;"> *</span>
65
+ <% else %>
66
+ <%= pb_rails("caption", props: { color: "lighter", text: object.label, dark: object.dark }) %>
67
+ <% end %>
68
+ </label>
69
+ <% end %>
70
+ <input type="hidden" name="<%= object.input_name %>" id="<%= object.input_id %>" value="" />
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
+ }) %>
204
+ </div>
205
+ </div>
206
+ <div class="rte-editor-wrap">
207
+ <div id="<%= object.editor_node_id %>"></div>
208
+ </div>
209
+ </div>
210
+ </div>
211
+ <% end %>
212
+
213
+ <script type="module">
214
+ (async function() {
215
+ const container = document.getElementById("<%= object.container_id %>");
216
+ if (!container) return;
217
+ const inputId = container.dataset.inputId;
218
+ let initialHtml = container.dataset.initialHtml || "<p></p>";
219
+ if (initialHtml && !initialHtml.trim().startsWith("<")) {
220
+ initialHtml = "<p>" + initialHtml + "</p>";
221
+ }
222
+ const hiddenInput = document.getElementById(inputId);
223
+ const editorNode = document.getElementById("<%= object.editor_node_id %>");
224
+ const toolbar = document.getElementById("<%= object.toolbar_id %>");
225
+ if (!editorNode || !hiddenInput || !toolbar) return;
226
+
227
+ function syncToHiddenInput(editor) {
228
+ if (editor && hiddenInput) {
229
+ hiddenInput.value = editor.getHTML();
230
+ }
231
+ }
232
+
233
+ const { Editor } = await import("@tiptap/core");
234
+ const { default: StarterKit } = await import("@tiptap/starter-kit");
235
+ const { default: Link } = await import("@tiptap/extension-link");
236
+
237
+ const editor = new Editor({
238
+ element: editorNode,
239
+ extensions: [
240
+ StarterKit.configure({ heading: { levels: [1, 2, 3] } }),
241
+ Link.configure({ openOnClick: false, HTMLAttributes: { target: "_blank", rel: "noopener" } }),
242
+ ],
243
+ content: initialHtml,
244
+ editable: true,
245
+ onUpdate: ({ editor: ed }) => syncToHiddenInput(ed),
246
+ });
247
+
248
+ syncToHiddenInput(editor);
249
+
250
+ const actionToChain = {
251
+ bold: "toggleBold",
252
+ italic: "toggleItalic",
253
+ strike: "toggleStrike",
254
+ bulletList: "toggleBulletList",
255
+ orderedList: "toggleOrderedList",
256
+ blockquote: "toggleBlockquote",
257
+ codeBlock: "toggleCodeBlock",
258
+ };
259
+
260
+ function updateActiveStates() {
261
+ toolbar.querySelectorAll("button[data-action]").forEach((btn) => {
262
+ const action = btn.dataset.action;
263
+ const level = btn.dataset.level ? parseInt(btn.dataset.level, 10) : null;
264
+ let active = false;
265
+ if (action === "bold") active = editor.isActive("bold");
266
+ else if (action === "italic") active = editor.isActive("italic");
267
+ 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
+ else if (action === "codeBlock") active = editor.isActive("codeBlock");
272
+ else if (action === "link") active = editor.isActive("link");
273
+ else if (action === "heading" && level != null) active = editor.isActive("heading", { level });
274
+ btn.classList.toggle("is-active", active);
275
+ });
276
+ }
277
+
278
+ toolbar.addEventListener("click", (e) => {
279
+ const btn = e.target.closest("button[data-action]");
280
+ if (!btn) return;
281
+ e.preventDefault();
282
+ const action = btn.dataset.action;
283
+ const level = btn.dataset.level ? parseInt(btn.dataset.level, 10) : null;
284
+
285
+ if (action === "heading" && level) {
286
+ editor.chain().focus().toggleHeading({ level }).run();
287
+ } else if (action === "link") {
288
+ const url = window.prompt("URL:");
289
+ if (url) editor.chain().focus().setLink({ href: url }).run();
290
+ } else {
291
+ const chainMethod = actionToChain[action];
292
+ if (chainMethod && typeof editor.chain().focus()[chainMethod] === "function") {
293
+ editor.chain().focus()[chainMethod]().run();
294
+ }
295
+ }
296
+ updateActiveStates();
297
+ });
298
+
299
+ editor.on("selectionUpdate", updateActiveStates);
300
+ editor.on("transaction", updateActiveStates);
301
+ updateActiveStates();
302
+ })();
303
+ </script>
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Playbook
4
+ module PbRichTextEditor
5
+ # Rails rich text editor: TipTap (vanilla JS), no React. Content syncs to a hidden input for form submission.
6
+ class RichTextEditor < Playbook::KitBase
7
+ prop :value
8
+ prop :placeholder
9
+ prop :input_options, type: Playbook::Props::HashProp, default: {}
10
+ prop :label
11
+ prop :required_indicator, type: Playbook::Props::Boolean, default: false
12
+
13
+ def classname
14
+ generate_classname("pb_rich_text_editor_kit", "rte-container")
15
+ end
16
+
17
+ def input_id
18
+ input_options[:id].presence || (id.present? ? "#{id}-input" : "rich_text_editor-input")
19
+ end
20
+
21
+ def input_name
22
+ input_options[:name].presence || "content"
23
+ end
24
+
25
+ def initial_html
26
+ raw = value.present? ? value.to_s.strip : ""
27
+ return "<p></p>" if raw.blank?
28
+
29
+ raw.start_with?("<") ? raw : "<p>#{raw}</p>"
30
+ end
31
+
32
+ def container_id
33
+ id.present? ? "rte-tiptap-#{id}" : "rte-tiptap-#{input_id.gsub(/[^a-z0-9_-]/i, '')}"
34
+ end
35
+
36
+ def editor_node_id
37
+ "#{container_id}-editor"
38
+ end
39
+
40
+ def toolbar_id
41
+ "#{container_id}-toolbar"
42
+ end
43
+ end
44
+ end
45
+ end
@@ -103,8 +103,8 @@ module Playbook
103
103
 
104
104
  def data_attributes
105
105
  data = attributes[:data] || {}
106
- data.merge!("data-pb-select" => true)
107
- data.merge!("data-validation-message" => validation_message) if validation_message.present?
106
+ data["data-pb-select"] = true
107
+ data["data-validation-message"] = validation_message if validation_message.present?
108
108
  data
109
109
  end
110
110
 
@@ -516,18 +516,18 @@ const resolvedLoadOptions =
516
516
  }
517
517
 
518
518
  // Reset form submitted state when a selection is made (this is all for react rendered rails kit)
519
- if (action === "select-option") {
519
+ if (action === "select-option" || action === "create-option") {
520
520
  setFormSubmitted(false)
521
521
  // Mark that user has made a selection to disable default value focus behavior
522
522
  setHasUserSelected(true)
523
523
  }
524
524
 
525
- // If a value is selected and we're preserving input on blur, clear the input
526
- if (action === "select-option" && preserveSearchInput) {
525
+ // If a value is selected/created and we're preserving input on blur, clear the input
526
+ if ((action === "select-option" || action === "create-option") && preserveSearchInput) {
527
527
  setInputValue("")
528
528
  }
529
529
 
530
- if (action === "select-option") {
530
+ if (action === "select-option" || action === "create-option") {
531
531
  if (selectProps.onMultiValueClick && option)
532
532
  selectProps.onMultiValueClick(option)
533
533
  const multiValueClearEvent = new CustomEvent(
@@ -0,0 +1,29 @@
1
+ <%
2
+ options = [
3
+ { label: 'Orange', value: '#FFA500' },
4
+ { label: 'Red', value: '#FF0000' },
5
+ { label: 'Green', value: '#00FF00' },
6
+ { label: 'Blue', value: '#0000FF' },
7
+ ]
8
+ %>
9
+
10
+ <%= pb_rails("typeahead", props: {
11
+ id: "typeahead-creatable",
12
+ placeholder: "All Colors",
13
+ options: options,
14
+ label: "Colors",
15
+ name: :foo,
16
+ createable: true,
17
+ pills: true,
18
+ })
19
+ %>
20
+
21
+ <%= javascript_tag defer: "defer" do %>
22
+ document.addEventListener("pb-typeahead-kit-typeahead-creatable-result-option-select", function(event) {
23
+ console.log('Single Option selected')
24
+ console.dir(event.detail)
25
+ })
26
+ document.addEventListener("pb-typeahead-kit-typeahead-creatable-result-clear", function() {
27
+ console.log('All options cleared')
28
+ })
29
+ <% end %>
@@ -0,0 +1 @@
1
+ The `createable` prop allows users to create new options by typing a value that doesn't exist in the options list.
@@ -11,6 +11,7 @@ examples:
11
11
  - typeahead_with_pills_async_users: With Pills (Async Data w/ Users)
12
12
  - typeahead_inline: Inline
13
13
  - typeahead_multi_kit: Multi Kit Options
14
+ - typeahead_createable: Createable
14
15
  - typeahead_error_state: Error State
15
16
  - typeahead_margin_bottom: Margin Bottom
16
17
  - typeahead_with_pills_color: With Pills (Custom Color)
@@ -24,8 +24,10 @@
24
24
  "createable": {
25
25
  "type": "boolean",
26
26
  "platforms": [
27
- "react"
28
- ]
27
+ "react",
28
+ "rails"
29
+ ],
30
+ "default": false
29
31
  },
30
32
  "disabled": {
31
33
  "type": "boolean",
@@ -62,6 +62,8 @@ module Playbook
62
62
  default: false
63
63
  prop :required_indicator, type: Playbook::Props::Boolean,
64
64
  default: false
65
+ prop :createable, type: Playbook::Props::Boolean,
66
+ default: false
65
67
  def classname
66
68
  default_margin_bottom = margin_bottom.present? ? "" : " mb_sm"
67
69
  generate_classname("pb_typeahead_kit") + default_margin_bottom
@@ -83,7 +85,7 @@ module Playbook
83
85
  end
84
86
 
85
87
  def is_react?
86
- pills || !is_multi || wrapped || input_display == "none"
88
+ pills || !is_multi || wrapped || input_display == "none" || createable
87
89
  end
88
90
 
89
91
  def typeahead_react_options
@@ -115,6 +117,7 @@ module Playbook
115
117
  clearOnContextChange: clear_on_context_change,
116
118
  disabled: disabled,
117
119
  preserveSearchInput: preserve_search_input,
120
+ createable: createable,
118
121
  }
119
122
 
120
123
  base_options[:getOptionLabel] = get_option_label if get_option_label.present?