playbook_ui 16.3.0.pre.alpha.PLAY2779dropdowncustomeventtype14814 → 16.3.0.pre.alpha.PLAY2782RTEPOCs14847
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_dropdown/docs/example.yml +0 -1
- data/app/pb_kits/playbook/pb_dropdown/dropdown.rb +1 -4
- data/app/pb_kits/playbook/pb_dropdown/index.js +0 -161
- data/app/pb_kits/playbook/pb_rich_text_editor/_lexxy_styles.scss +69 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/_quill_styles.scss +86 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/_rich_text_editor.scss +2 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/_rich_text_editor.tsx +3 -3
- data/app/pb_kits/playbook/pb_rich_text_editor/_tiptap_styles.scss +9 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_lexxy_default.html.erb +1 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_lexxy_default.md +1 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_quill_default.html.erb +1 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_quill_default.md +1 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_tiptap_default.html.erb +1 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/_rich_text_editor_tiptap_default.md +1 -0
- data/app/pb_kits/playbook/pb_rich_text_editor/docs/example.yml +3 -0
- data/app/pb_kits/playbook/pb_rich_text_editor_lexxy/rich_text_editor_lexxy.html.erb +32 -0
- data/app/pb_kits/playbook/pb_rich_text_editor_lexxy/rich_text_editor_lexxy.rb +29 -0
- data/app/pb_kits/playbook/pb_rich_text_editor_quill/rich_text_editor_quill.html.erb +47 -0
- data/app/pb_kits/playbook/pb_rich_text_editor_quill/rich_text_editor_quill.rb +37 -0
- data/app/pb_kits/playbook/pb_rich_text_editor_tiptap/README.md +35 -0
- data/app/pb_kits/playbook/pb_rich_text_editor_tiptap/rich_text_editor_tiptap.html.erb +302 -0
- data/app/pb_kits/playbook/pb_rich_text_editor_tiptap/rich_text_editor_tiptap.rb +44 -0
- data/dist/chunks/{_typeahead-DdGKR1rQ.js → _typeahead-DAA1_5Wa.js} +1 -1
- data/dist/chunks/vendor.js +1 -1
- data/dist/playbook-rails-react-bindings.js +1 -1
- data/dist/playbook-rails.js +1 -1
- data/dist/playbook.css +1 -1
- data/lib/playbook/version.rb +1 -1
- metadata +18 -5
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_custom_event_type.html.erb +0 -224
- data/app/pb_kits/playbook/pb_dropdown/docs/_dropdown_custom_event_type.md +0 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8e1992dda937b2b5cbfccb239549fe6ca111676bcb86bae3b296df9b7a5c2d39
|
|
4
|
+
data.tar.gz: 8ce49f7e0b5a67a7060af0cb5e03181df203a704b5b8ac9e9573ade7a538370e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 70cb1043ad6fd7b1222e695336edd15e3ab6565dccd1d6ec09ca03253101855ab40f925b57e51722d78f9ce53834bfb658e6072a52a10d835ae8dc10bd289600
|
|
7
|
+
data.tar.gz: 14fd821a7c6da64695c77b7eec55238eddf41dd478f84c61d082dcaf2d0d0195da0a39295f691d3a5bddb083cf105fa302137912a9fb3577116e9f682bb6feca
|
|
@@ -33,7 +33,6 @@ examples:
|
|
|
33
33
|
- dropdown_quickpick_with_date_pickers_rails: Quick Pick with Date Pickers
|
|
34
34
|
- dropdown_quickpick_with_date_pickers_default_rails: Quick Pick with Date Pickers (Default Value)
|
|
35
35
|
- dropdown_required_indicator: Required Indicator
|
|
36
|
-
- dropdown_custom_event_type: Custom Event Type
|
|
37
36
|
|
|
38
37
|
react:
|
|
39
38
|
- dropdown_default: Default
|
|
@@ -55,8 +55,6 @@ module Playbook
|
|
|
55
55
|
default: false
|
|
56
56
|
prop :required_indicator, type: Playbook::Props::Boolean,
|
|
57
57
|
default: false
|
|
58
|
-
prop :custom_event_type, type: Playbook::Props::String,
|
|
59
|
-
default: ""
|
|
60
58
|
|
|
61
59
|
def data
|
|
62
60
|
Hash(prop(:data)).merge(
|
|
@@ -69,8 +67,7 @@ module Playbook
|
|
|
69
67
|
start_date_id: variant == "quickpick" ? start_date_id : nil,
|
|
70
68
|
end_date_id: variant == "quickpick" ? end_date_id : nil,
|
|
71
69
|
controls_start_id: variant == "quickpick" && controls_start_id.present? ? controls_start_id : nil,
|
|
72
|
-
controls_end_id: variant == "quickpick" && controls_end_id.present? ? controls_end_id : nil
|
|
73
|
-
custom_event_type: custom_event_type.presence
|
|
70
|
+
controls_end_id: variant == "quickpick" && controls_end_id.present? ? controls_end_id : nil
|
|
74
71
|
).compact
|
|
75
72
|
end
|
|
76
73
|
|
|
@@ -95,25 +95,6 @@ export default class PbDropdown extends PbEnhancedElement {
|
|
|
95
95
|
this.clearBtn.addEventListener("click", this.clearBtnHandler);
|
|
96
96
|
}
|
|
97
97
|
this.updateClearButton();
|
|
98
|
-
|
|
99
|
-
// Listen for clear and select events from external source
|
|
100
|
-
this.handleClearEventBound = this.handleClearEvent.bind(this);
|
|
101
|
-
document.addEventListener("pb:dropdown:clear", this.handleClearEventBound);
|
|
102
|
-
this.handleSelectEventBound = this.handleSelectEvent.bind(this);
|
|
103
|
-
document.addEventListener("pb:dropdown:select", this.handleSelectEventBound);
|
|
104
|
-
|
|
105
|
-
// Listen for custom_event_type to clear on custom events
|
|
106
|
-
const customEventTypeString = this.element.dataset.customEventType;
|
|
107
|
-
if (customEventTypeString) {
|
|
108
|
-
this.customClearEventTypes = customEventTypeString
|
|
109
|
-
.split(",")
|
|
110
|
-
.map((e) => e.trim())
|
|
111
|
-
.filter(Boolean);
|
|
112
|
-
this.handleCustomClearBound = this.handleCustomClearEvent.bind(this);
|
|
113
|
-
this.customClearEventTypes.forEach((eventType) => {
|
|
114
|
-
document.addEventListener(eventType, this.handleCustomClearBound);
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
98
|
}
|
|
118
99
|
|
|
119
100
|
disconnect() {
|
|
@@ -165,19 +146,6 @@ export default class PbDropdown extends PbEnhancedElement {
|
|
|
165
146
|
if (this.clearBtn && this.clearBtnHandler) {
|
|
166
147
|
this.clearBtn.removeEventListener('click', this.clearBtnHandler)
|
|
167
148
|
}
|
|
168
|
-
|
|
169
|
-
// Clean up external clear/select listeners
|
|
170
|
-
if (this.handleClearEventBound) {
|
|
171
|
-
document.removeEventListener("pb:dropdown:clear", this.handleClearEventBound)
|
|
172
|
-
}
|
|
173
|
-
if (this.handleSelectEventBound) {
|
|
174
|
-
document.removeEventListener("pb:dropdown:select", this.handleSelectEventBound)
|
|
175
|
-
}
|
|
176
|
-
if (this.customClearEventTypes && this.handleCustomClearBound) {
|
|
177
|
-
this.customClearEventTypes.forEach((eventType) => {
|
|
178
|
-
document.removeEventListener(eventType, this.handleCustomClearBound)
|
|
179
|
-
})
|
|
180
|
-
}
|
|
181
149
|
}
|
|
182
150
|
|
|
183
151
|
updateClearButton() {
|
|
@@ -386,135 +354,6 @@ export default class PbDropdown extends PbEnhancedElement {
|
|
|
386
354
|
}
|
|
387
355
|
}
|
|
388
356
|
|
|
389
|
-
// ----- External events handling section -----
|
|
390
|
-
// Handles pb:dropdown:clear - clear this dropdown when event.detail.dropdownId matches.
|
|
391
|
-
handleClearEvent(event) {
|
|
392
|
-
const targetId = event.detail?.dropdownId;
|
|
393
|
-
if (targetId && this.element.id === targetId) {
|
|
394
|
-
this.clearSelection();
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Handles custom_event_type events (e.g. turbo:submit-end) - clear when detail.dropdownId matches or is omitted.
|
|
399
|
-
handleCustomClearEvent(event) {
|
|
400
|
-
const targetId = event.detail?.dropdownId;
|
|
401
|
-
if (targetId == null || this.element.id === targetId) {
|
|
402
|
-
this.clearSelection();
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Handles pb:dropdown:select - set dropdown selection by option id(s).
|
|
407
|
-
// Single: detail: { dropdownId, optionId }. Multi: detail: { dropdownId, optionIds: ['id1', 'id2'] }.
|
|
408
|
-
handleSelectEvent(event) {
|
|
409
|
-
const targetId = event.detail?.dropdownId;
|
|
410
|
-
if (!targetId || this.element.id !== targetId) return;
|
|
411
|
-
|
|
412
|
-
const optionId = event.detail?.optionId;
|
|
413
|
-
const optionIds = event.detail?.optionIds;
|
|
414
|
-
if (optionId != null) {
|
|
415
|
-
this.setSelectionByOptionId(optionId);
|
|
416
|
-
} else if (Array.isArray(optionIds)) {
|
|
417
|
-
this.setSelectionByOptionIds(optionIds);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Set single-select dropdown to the option with the given id. No-op if id not found.
|
|
422
|
-
setSelectionByOptionId(optionId) {
|
|
423
|
-
if (this.isMultiSelect) return;
|
|
424
|
-
const hiddenInput = this.baseInput;
|
|
425
|
-
const optionEls = Array.from(this.element.querySelectorAll(OPTION_SELECTOR));
|
|
426
|
-
const selectedOption = optionEls.find((opt) => {
|
|
427
|
-
try {
|
|
428
|
-
return JSON.parse(opt.dataset.dropdownOptionLabel).id === optionId;
|
|
429
|
-
} catch {
|
|
430
|
-
return false;
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
if (!selectedOption) return;
|
|
434
|
-
|
|
435
|
-
optionEls.forEach((opt) => opt.classList.remove("pb_dropdown_option_selected"));
|
|
436
|
-
selectedOption.classList.add("pb_dropdown_option_selected");
|
|
437
|
-
if (hiddenInput) hiddenInput.value = optionId;
|
|
438
|
-
const optionData = JSON.parse(selectedOption.dataset.dropdownOptionLabel);
|
|
439
|
-
const customDisplayElement = this.element.querySelector(
|
|
440
|
-
'[data-dropdown-trigger-custom-display]',
|
|
441
|
-
);
|
|
442
|
-
if (customDisplayElement) {
|
|
443
|
-
this.setTriggerElementText("");
|
|
444
|
-
customDisplayElement.style.display = "block";
|
|
445
|
-
customDisplayElement.style.paddingRight = "8px";
|
|
446
|
-
} else {
|
|
447
|
-
this.setTriggerElementText(optionData.label);
|
|
448
|
-
}
|
|
449
|
-
if (this.searchInput) {
|
|
450
|
-
this.searchInput.value = optionData.label;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
if (optionData.formatted_start_date && optionData.formatted_end_date) {
|
|
454
|
-
const startDateId = this.element.dataset.startDateId;
|
|
455
|
-
const endDateId = this.element.dataset.endDateId;
|
|
456
|
-
const controlsStartId = this.element.dataset.controlsStartId;
|
|
457
|
-
const controlsEndId = this.element.dataset.controlsEndId;
|
|
458
|
-
if (startDateId) {
|
|
459
|
-
const startDateInput = document.getElementById(startDateId);
|
|
460
|
-
if (startDateInput) startDateInput.value = optionData.formatted_start_date;
|
|
461
|
-
}
|
|
462
|
-
if (endDateId) {
|
|
463
|
-
const endDateInput = document.getElementById(endDateId);
|
|
464
|
-
if (endDateInput) endDateInput.value = optionData.formatted_end_date;
|
|
465
|
-
}
|
|
466
|
-
const syncDatePickers = () => {
|
|
467
|
-
if (controlsStartId) {
|
|
468
|
-
const startPicker = document.querySelector(`#${CSS.escape(controlsStartId)}`)?._flatpickr;
|
|
469
|
-
if (startPicker) startPicker.setDate(optionData.formatted_start_date, true, "m/d/Y");
|
|
470
|
-
}
|
|
471
|
-
if (controlsEndId) {
|
|
472
|
-
const endPicker = document.querySelector(`#${CSS.escape(controlsEndId)}`)?._flatpickr;
|
|
473
|
-
if (endPicker) endPicker.setDate(optionData.formatted_end_date, true, "m/d/Y");
|
|
474
|
-
}
|
|
475
|
-
};
|
|
476
|
-
syncDatePickers();
|
|
477
|
-
setTimeout(syncDatePickers, 100);
|
|
478
|
-
setTimeout(syncDatePickers, 300);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
this.updateClearButton();
|
|
482
|
-
this.emitSelectionChange();
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Set multi-select dropdown to the options with the given ids. Invalid ids are skipped.
|
|
486
|
-
setSelectionByOptionIds(optionIds) {
|
|
487
|
-
if (!this.isMultiSelect || !optionIds.length) return;
|
|
488
|
-
const optionEls = Array.from(this.element.querySelectorAll(OPTION_SELECTOR));
|
|
489
|
-
this.selectedOptions.clear();
|
|
490
|
-
optionEls.forEach((opt) => {
|
|
491
|
-
opt.classList.remove("pb_dropdown_option_selected");
|
|
492
|
-
opt.style.display = "";
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
optionIds.forEach((id) => {
|
|
496
|
-
const opt = optionEls.find((o) => {
|
|
497
|
-
try {
|
|
498
|
-
return JSON.parse(o.dataset.dropdownOptionLabel).id === id;
|
|
499
|
-
} catch {
|
|
500
|
-
return false;
|
|
501
|
-
}
|
|
502
|
-
});
|
|
503
|
-
if (opt) {
|
|
504
|
-
const raw = opt.dataset.dropdownOptionLabel;
|
|
505
|
-
this.selectedOptions.add(raw);
|
|
506
|
-
opt.style.display = "none";
|
|
507
|
-
}
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
this.updatePills();
|
|
511
|
-
this.updateClearButton();
|
|
512
|
-
this.adjustDropdownHeight();
|
|
513
|
-
this.syncHiddenInputs();
|
|
514
|
-
this.emitSelectionChange();
|
|
515
|
-
}
|
|
516
|
-
// ----- End External events handling section -----
|
|
517
|
-
|
|
518
357
|
isClickOutside(event) {
|
|
519
358
|
const label = event.target.closest(LABEL_SELECTOR);
|
|
520
359
|
if (label && this.element.contains(label)) return false;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Playbook-themed overrides for Lexxy POC (toolbar + icons).
|
|
2
|
+
// Scoped to [data-pb-rte-lexxy] so default Lexxy styling is unchanged elsewhere.
|
|
3
|
+
|
|
4
|
+
@import "../tokens/border_radius";
|
|
5
|
+
@import "../tokens/colors";
|
|
6
|
+
@import "../tokens/spacing";
|
|
7
|
+
@import "../tokens/transition";
|
|
8
|
+
|
|
9
|
+
[data-pb-rte-lexxy] {
|
|
10
|
+
// Remove Lexxy’s outer wrapper border and padding so only toolbar + content form one card
|
|
11
|
+
lexxy-editor,
|
|
12
|
+
lexxy-editor.lexxy-content {
|
|
13
|
+
border: none;
|
|
14
|
+
border-radius: 0;
|
|
15
|
+
box-shadow: none;
|
|
16
|
+
padding: 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
lexxy-toolbar {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-wrap: wrap;
|
|
22
|
+
align-items: center;
|
|
23
|
+
gap: $space_xs;
|
|
24
|
+
background: $white;
|
|
25
|
+
border: 1px solid $input_border_default;
|
|
26
|
+
border-bottom: none;
|
|
27
|
+
border-radius: $border_rad_heaviest $border_rad_heaviest 0 0;
|
|
28
|
+
padding: $space_xs;
|
|
29
|
+
@include transition_default;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.lexxy-editor__content {
|
|
33
|
+
border: 1px solid $input_border_default;
|
|
34
|
+
border-radius: 0 0 $border_rad_heaviest $border_rad_heaviest;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.lexxy-editor__toolbar-button {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: center;
|
|
41
|
+
background: transparent;
|
|
42
|
+
border: none;
|
|
43
|
+
border-radius: $border_rad_heaviest;
|
|
44
|
+
color: $text_lt_light;
|
|
45
|
+
width: $space_xl + 2;
|
|
46
|
+
height: $space_xl + 2;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
@include transition_default;
|
|
49
|
+
|
|
50
|
+
&:hover:not([disabled]) {
|
|
51
|
+
background-color: $neutral_subtle;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
&[aria-pressed="true"] {
|
|
55
|
+
color: $primary;
|
|
56
|
+
background-color: $bg_light;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
&:focus-visible {
|
|
60
|
+
outline: none;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.lexxy-editor__toolbar-button svg {
|
|
65
|
+
width: 1.25rem;
|
|
66
|
+
height: 1.25rem;
|
|
67
|
+
fill: currentColor;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Playbook-themed overrides for Quill POC (toolbar + icons).
|
|
2
|
+
// Scoped to [data-pb-rte-quill] so default Quill styling is unchanged elsewhere.
|
|
3
|
+
|
|
4
|
+
@import "../tokens/border_radius";
|
|
5
|
+
@import "../tokens/colors";
|
|
6
|
+
@import "../tokens/spacing";
|
|
7
|
+
@import "../tokens/transition";
|
|
8
|
+
|
|
9
|
+
[data-pb-rte-quill] {
|
|
10
|
+
.ql-toolbar.ql-snow {
|
|
11
|
+
background: $white;
|
|
12
|
+
border: 1px solid $input_border_default;
|
|
13
|
+
border-bottom: 1px solid $input_border_default;
|
|
14
|
+
border-radius: $border_rad_heaviest $border_rad_heaviest 0 0;
|
|
15
|
+
padding: $space_xs;
|
|
16
|
+
@include transition_default;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.ql-container.ql-snow {
|
|
20
|
+
border: 1px solid $input_border_default;
|
|
21
|
+
border-radius: 0 0 $border_rad_heaviest $border_rad_heaviest;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Single border: remove Quill Snow’s extra border/outline from the inner editor to avoid double-thick bottom and blue focus
|
|
25
|
+
.ql-container .ql-editor {
|
|
26
|
+
border: none;
|
|
27
|
+
outline: none;
|
|
28
|
+
box-shadow: none;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.ql-toolbar .ql-formats {
|
|
32
|
+
margin-right: $space_sm;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.ql-toolbar.ql-snow button {
|
|
36
|
+
background: transparent;
|
|
37
|
+
border: none;
|
|
38
|
+
border-radius: $border_rad_heaviest;
|
|
39
|
+
color: $text_lt_light;
|
|
40
|
+
width: $space_xl + 2;
|
|
41
|
+
height: $space_xl + 2;
|
|
42
|
+
@include transition_default;
|
|
43
|
+
|
|
44
|
+
&:hover {
|
|
45
|
+
background-color: $neutral_subtle;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
&.ql-active {
|
|
49
|
+
color: $primary;
|
|
50
|
+
background-color: $bg_light;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&:focus-visible {
|
|
54
|
+
outline: none;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Quill uses .ql-stroke and .ql-fill for SVG strokes/fills; use currentColor so icons match toolbar color
|
|
59
|
+
.ql-toolbar .ql-stroke {
|
|
60
|
+
stroke: currentColor;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.ql-toolbar .ql-fill {
|
|
64
|
+
fill: currentColor;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.ql-toolbar button svg {
|
|
68
|
+
width: 1.25rem;
|
|
69
|
+
height: 1.25rem;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Picker (e.g. heading dropdown) align with Playbook
|
|
73
|
+
.ql-toolbar .ql-picker {
|
|
74
|
+
color: $text_lt_light;
|
|
75
|
+
|
|
76
|
+
&.ql-expanded {
|
|
77
|
+
.ql-picker-label {
|
|
78
|
+
color: $primary;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.ql-toolbar .ql-picker-label:hover {
|
|
84
|
+
color: $primary;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -107,14 +107,14 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
|
|
|
107
107
|
const hiddenInput = document.getElementById(oldId) as HTMLElement | null
|
|
108
108
|
if (!hiddenInput) return
|
|
109
109
|
|
|
110
|
-
const hiddenInputId = (inputOptions
|
|
110
|
+
const hiddenInputId = (inputOptions?.id as string) || oldId
|
|
111
111
|
|
|
112
112
|
if (hiddenInputId !== oldId) {
|
|
113
113
|
hiddenInput.id = hiddenInputId
|
|
114
114
|
editorInstance.element?.setAttribute("input", hiddenInputId)
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
if (inputOptions
|
|
117
|
+
if (inputOptions?.name) {
|
|
118
118
|
hiddenInput.setAttribute("name", inputOptions.name as string)
|
|
119
119
|
}
|
|
120
120
|
|
|
@@ -240,7 +240,7 @@ const RichTextEditor = (props: RichTextEditorProps): React.ReactElement => {
|
|
|
240
240
|
// Determine if toolbar should be shown
|
|
241
241
|
const shouldShowToolbar = focus && advancedEditor ? showToolbarOnFocus : advancedEditorToolbar
|
|
242
242
|
|
|
243
|
-
const labelFor = advancedEditor ? fieldId : (id ? id : (inputOptions
|
|
243
|
+
const labelFor = advancedEditor ? fieldId : (id ? id : (inputOptions?.id ? `${inputOptions.id}_trix` : undefined))
|
|
244
244
|
|
|
245
245
|
return (
|
|
246
246
|
<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_lexxy", props: { input_options: { id: 'lexxy_editor_id', name: "hidden_input_name" }, placeholder: "Write something…" }) %>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Lexxy (37signals / Lexical) kit — form-associated custom element; submits with the form like a textarea. Attachments disabled in this demo. Use `pb_rails("rich_text_editor_lexxy", props: { input_options: { id: "...", name: "..." }, placeholder: "..." })`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= pb_rails("rich_text_editor_quill", 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
|
+
Quill kit — framework-agnostic editor with Snow theme. Content is synced to a hidden input for Rails form submission. Use `pb_rails("rich_text_editor_quill", props: { input_options: { id: "...", name: "..." }, value: "..." })`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= pb_rails("rich_text_editor_tiptap", 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) kit — 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_tiptap", props: { input_options: { id: "...", name: "..." }, value: "..." })`.
|
|
@@ -11,6 +11,9 @@ examples:
|
|
|
11
11
|
- rich_text_editor_inline: Inline
|
|
12
12
|
- rich_text_editor_required_indicator: Required Indicator
|
|
13
13
|
- rich_text_editor_preview: Preview
|
|
14
|
+
- rich_text_editor_tiptap_default: "TipTap (Vanilla JS)"
|
|
15
|
+
- rich_text_editor_quill_default: "Quill"
|
|
16
|
+
- rich_text_editor_lexxy_default: "Lexxy"
|
|
14
17
|
|
|
15
18
|
react:
|
|
16
19
|
- rich_text_editor_default: Default
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<link rel="stylesheet" href="https://unpkg.com/@37signals/lexxy@0.7.6-beta/dist/stylesheets/lexxy-content.css" />
|
|
2
|
+
<link rel="stylesheet" href="https://unpkg.com/@37signals/lexxy@0.7.6-beta/dist/stylesheets/lexxy-editor.css" />
|
|
3
|
+
<%= pb_content_tag(:div, id: object.container_id, class: object.classname, data: { pb_rte_lexxy: true }) do %>
|
|
4
|
+
<div class="pb_rich_text_editor_kit">
|
|
5
|
+
<% if object.label.present? %>
|
|
6
|
+
<label for="<%= object.input_id %>">
|
|
7
|
+
<% if object.required_indicator %>
|
|
8
|
+
<%= pb_rails("caption", props: { color: "lighter", text: object.label, dark: object.dark }) %><span style="color: #DA0014;"> *</span>
|
|
9
|
+
<% else %>
|
|
10
|
+
<%= pb_rails("caption", props: { color: "lighter", text: object.label, dark: object.dark }) %>
|
|
11
|
+
<% end %>
|
|
12
|
+
</label>
|
|
13
|
+
<% end %>
|
|
14
|
+
<lexxy-editor
|
|
15
|
+
id="<%= object.input_id %>"
|
|
16
|
+
name="<%= object.input_name %>"
|
|
17
|
+
placeholder="<%= h(object.placeholder.presence || 'Write something…') %>"
|
|
18
|
+
class="lexxy-content"
|
|
19
|
+
></lexxy-editor>
|
|
20
|
+
</div>
|
|
21
|
+
<% end %>
|
|
22
|
+
|
|
23
|
+
<script type="module">
|
|
24
|
+
(async function() {
|
|
25
|
+
const container = document.getElementById("<%= object.container_id %>");
|
|
26
|
+
if (!container) return;
|
|
27
|
+
const Lexxy = await import("https://esm.sh/@37signals/lexxy@0.7.6-beta");
|
|
28
|
+
if (Lexxy.configure) {
|
|
29
|
+
Lexxy.configure({ default: { attachments: false } });
|
|
30
|
+
}
|
|
31
|
+
})();
|
|
32
|
+
</script>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Playbook
|
|
4
|
+
module PbRichTextEditorLexxy
|
|
5
|
+
class RichTextEditorLexxy < Playbook::KitBase
|
|
6
|
+
prop :value
|
|
7
|
+
prop :placeholder
|
|
8
|
+
prop :input_options, type: Playbook::Props::HashProp, default: {}
|
|
9
|
+
prop :label
|
|
10
|
+
prop :required_indicator, type: Playbook::Props::Boolean, default: false
|
|
11
|
+
|
|
12
|
+
def classname
|
|
13
|
+
generate_classname("pb_rich_text_editor_kit", "rte-container")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def input_id
|
|
17
|
+
input_options[:id].presence || (id.present? ? "#{id}-input" : "rich_text_editor_lexxy-input")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def input_name
|
|
21
|
+
input_options[:name].presence || "content"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def container_id
|
|
25
|
+
id.present? ? "rte-lexxy-#{id}" : "rte-lexxy-#{input_id.gsub(/[^a-z0-9_-]/i, '')}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css" rel="stylesheet" />
|
|
2
|
+
<%= pb_content_tag(:div, id: object.container_id, class: object.classname, data: { pb_rte_quill: true, input_id: object.input_id, initial_html: object.initial_html }) do %>
|
|
3
|
+
<div class="pb_rich_text_editor_kit">
|
|
4
|
+
<% if object.label.present? %>
|
|
5
|
+
<label for="<%= object.input_id %>">
|
|
6
|
+
<% if object.required_indicator %>
|
|
7
|
+
<%= pb_rails("caption", props: { color: "lighter", text: object.label, dark: object.dark }) %><span style="color: #DA0014;"> *</span>
|
|
8
|
+
<% else %>
|
|
9
|
+
<%= pb_rails("caption", props: { color: "lighter", text: object.label, dark: object.dark }) %>
|
|
10
|
+
<% end %>
|
|
11
|
+
</label>
|
|
12
|
+
<% end %>
|
|
13
|
+
<input type="hidden" name="<%= object.input_name %>" id="<%= object.input_id %>" value="" />
|
|
14
|
+
<div id="<%= object.editor_node_id %>">
|
|
15
|
+
<%= raw object.initial_html %>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
<% end %>
|
|
19
|
+
|
|
20
|
+
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>
|
|
21
|
+
<script>
|
|
22
|
+
(function() {
|
|
23
|
+
const container = document.getElementById("<%= object.container_id %>");
|
|
24
|
+
if (!container) return;
|
|
25
|
+
const inputId = container.dataset.inputId;
|
|
26
|
+
const hiddenInput = document.getElementById(inputId);
|
|
27
|
+
const editorNode = document.getElementById("<%= object.editor_node_id %>");
|
|
28
|
+
if (!editorNode || !hiddenInput) return;
|
|
29
|
+
|
|
30
|
+
function syncToHiddenInput() {
|
|
31
|
+
if (typeof Quill !== "undefined" && window._pbQuillInstances && window._pbQuillInstances["<%= object.container_id %>"]) {
|
|
32
|
+
const quill = window._pbQuillInstances["<%= object.container_id %>"];
|
|
33
|
+
hiddenInput.value = quill.root.innerHTML;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof Quill === "undefined") return;
|
|
38
|
+
const quill = new Quill("#<%= object.editor_node_id %>", {
|
|
39
|
+
theme: "snow",
|
|
40
|
+
placeholder: "<%= j(object.placeholder.presence || 'Compose...') %>",
|
|
41
|
+
});
|
|
42
|
+
window._pbQuillInstances = window._pbQuillInstances || {};
|
|
43
|
+
window._pbQuillInstances["<%= object.container_id %>"] = quill;
|
|
44
|
+
quill.on("text-change", syncToHiddenInput);
|
|
45
|
+
syncToHiddenInput();
|
|
46
|
+
})();
|
|
47
|
+
</script>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Playbook
|
|
4
|
+
module PbRichTextEditorQuill
|
|
5
|
+
class RichTextEditorQuill < Playbook::KitBase
|
|
6
|
+
prop :value
|
|
7
|
+
prop :placeholder
|
|
8
|
+
prop :input_options, type: Playbook::Props::HashProp, default: {}
|
|
9
|
+
prop :label
|
|
10
|
+
prop :required_indicator, type: Playbook::Props::Boolean, default: false
|
|
11
|
+
|
|
12
|
+
def classname
|
|
13
|
+
generate_classname("pb_rich_text_editor_kit", "rte-container")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def input_id
|
|
17
|
+
input_options[:id].presence || (id.present? ? "#{id}-input" : "rich_text_editor_quill-input")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def input_name
|
|
21
|
+
input_options[:name].presence || "content"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initial_html
|
|
25
|
+
value.present? ? value.to_s : "<p><br></p>"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def container_id
|
|
29
|
+
id.present? ? "rte-quill-#{id}" : "rte-quill-#{input_id.gsub(/[^a-z0-9_-]/i, '')}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def editor_node_id
|
|
33
|
+
"#{container_id}-editor"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Rich Text Editor (TipTap) — Vanilla JS Kit
|
|
2
|
+
|
|
3
|
+
This kit loads TipTap from CDN via an **import map** so the editor runs without a build step.
|
|
4
|
+
|
|
5
|
+
## Using in your app
|
|
6
|
+
|
|
7
|
+
Use a Playbook gem that includes this kit, include Playbook CSS, and (if you use CSP) allow `cdn.jsdelivr.net` and inline scripts. For full details see the docs guide **Rich Text Editor POCs**.
|
|
8
|
+
|
|
9
|
+
## Drawbacks of the current approach
|
|
10
|
+
|
|
11
|
+
- **Deep dependency tree**: TipTap and ProseMirror pull in many packages (`orderedmap`, `w3c-keyname`, `rope-sequence`, `@tiptap/extension-text-style`, etc.). Each must be listed in the import map or the browser fails with "Failed to resolve module specifier".
|
|
12
|
+
- **Whack-a-mole**: Adding one missing dependency can surface another. The list may grow when TipTap or ProseMirror versions change.
|
|
13
|
+
- **Version pinning**: All mapped packages are pinned to specific versions. Upgrading TipTap may require updating the import map and re-testing.
|
|
14
|
+
|
|
15
|
+
## Recommended alternative for production
|
|
16
|
+
|
|
17
|
+
**Use a single bundled script** instead of the import map:
|
|
18
|
+
|
|
19
|
+
1. In your app (e.g. playbook-website), add a small build step that bundles TipTap once:
|
|
20
|
+
```js
|
|
21
|
+
// tiptap-bundle.js (built with esbuild, rollup, etc.)
|
|
22
|
+
import { Editor } from '@tiptap/core'
|
|
23
|
+
import StarterKit from '@tiptap/starter-kit'
|
|
24
|
+
import Link from '@tiptap/extension-link'
|
|
25
|
+
export { Editor, StarterKit, Link }
|
|
26
|
+
```
|
|
27
|
+
2. Serve the output (e.g. `tiptap-bundle.js`) from your assets or CDN.
|
|
28
|
+
3. In this kit’s template, replace the import map + bare-specifier `import()` with a single script that loads that bundle and then creates the editor.
|
|
29
|
+
|
|
30
|
+
That way there are no import-map entries to maintain, and you get one consistent dependency set.
|
|
31
|
+
|
|
32
|
+
## When this kit is still useful
|
|
33
|
+
|
|
34
|
+
- **Docs/demos**: Showing the kit on the Playbook docs site with the import map is fine; we add dependencies as needed.
|
|
35
|
+
- **Quick Rails-only setups**: If you can’t add a front-end build, the import map is the only no-build option; just be aware more deps may need to be added over time.
|