@radioactive-labs/plutonium 0.34.1 → 0.35.1
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.
- package/README.md +100 -19
- package/package.json +2 -2
- package/src/css/components.css +471 -0
- package/src/css/intl_tel_input.css +2 -2
- package/src/css/plutonium.css +2 -0
- package/src/css/tokens.css +149 -0
- package/src/dist/css/plutonium.css +1 -11
- package/src/dist/js/plutonium.js +1685 -1146
- package/src/dist/js/plutonium.js.map +4 -4
- package/src/dist/js/plutonium.min.js +70 -70
- package/src/dist/js/plutonium.min.js.map +4 -4
- package/src/js/controllers/bulk_actions_controller.js +109 -0
- package/src/js/controllers/filter_panel_controller.js +35 -0
- package/src/js/controllers/register_controllers.js +5 -1
- package/src/js/controllers/resource_drop_down_controller.js +25 -1
- package/src/js/controllers/slim_select_controller.js +6 -2
- package/src/js/turbo/turbo_actions.js +1 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Connects to data-controller="bulk-actions"
|
|
4
|
+
// Manages bulk action selection in resource tables
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["checkbox", "checkboxAll", "toolbar", "selectedCount", "actionButton", "selectionCell"]
|
|
7
|
+
static values = {
|
|
8
|
+
hasActions: { type: Boolean, default: false }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
connect() {
|
|
12
|
+
// Show selection column only if bulk actions exist
|
|
13
|
+
if (this.hasActionsValue) {
|
|
14
|
+
this.enableSelection()
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
enableSelection() {
|
|
19
|
+
// Show all selection cells (header + body cells)
|
|
20
|
+
this.selectionCellTargets.forEach(el => el.classList.remove("hidden"))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
toggle() {
|
|
24
|
+
this.updateUI()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
toggleAll(event) {
|
|
28
|
+
const checked = event.target.checked
|
|
29
|
+
this.checkboxTargets.forEach(cb => cb.checked = checked)
|
|
30
|
+
this.updateUI()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
updateUI() {
|
|
34
|
+
const checked = this.checked
|
|
35
|
+
const total = this.checkboxTargets.length
|
|
36
|
+
|
|
37
|
+
// Update "select all" checkbox state (checked, unchecked, or indeterminate)
|
|
38
|
+
if (this.hasCheckboxAllTarget) {
|
|
39
|
+
this.checkboxAllTarget.checked = checked.length === total && total > 0
|
|
40
|
+
this.checkboxAllTarget.indeterminate = checked.length > 0 && checked.length < total
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Show toolbar only when items are selected
|
|
44
|
+
if (this.hasToolbarTarget) {
|
|
45
|
+
this.toolbarTarget.classList.toggle("hidden", checked.length === 0)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Update selected count display
|
|
49
|
+
if (this.hasSelectedCountTarget) {
|
|
50
|
+
this.selectedCountTarget.textContent = checked.length
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Update action button URLs and visibility based on allowed actions
|
|
54
|
+
this.updateActionButtons()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
updateActionButtons() {
|
|
58
|
+
const checked = this.checked
|
|
59
|
+
const ids = checked.map(cb => cb.value)
|
|
60
|
+
const idsParam = ids.map(id => `ids[]=${encodeURIComponent(id)}`).join("&")
|
|
61
|
+
|
|
62
|
+
// Compute intersection of allowed actions across all selected records
|
|
63
|
+
const allowedActions = this.computeAllowedActions(checked)
|
|
64
|
+
|
|
65
|
+
this.actionButtonTargets.forEach(button => {
|
|
66
|
+
const baseUrl = button.dataset.bulkActionUrl
|
|
67
|
+
const actionName = button.dataset.bulkActionName
|
|
68
|
+
|
|
69
|
+
// Update URL with selected IDs
|
|
70
|
+
if (baseUrl) {
|
|
71
|
+
button.href = idsParam ? `${baseUrl}?${idsParam}` : baseUrl
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Show/hide button based on whether action is allowed for all selected records
|
|
75
|
+
button.style.display = allowedActions.has(actionName) ? '' : 'none'
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Compute the intersection of allowed actions across all selected checkboxes
|
|
80
|
+
computeAllowedActions(checked) {
|
|
81
|
+
if (checked.length === 0) {
|
|
82
|
+
return new Set()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Start with actions allowed for the first selected record
|
|
86
|
+
let intersection = new Set(this.getAllowedActionsForCheckbox(checked[0]))
|
|
87
|
+
|
|
88
|
+
// Intersect with actions allowed for each subsequent record
|
|
89
|
+
for (let i = 1; i < checked.length; i++) {
|
|
90
|
+
const actions = this.getAllowedActionsForCheckbox(checked[i])
|
|
91
|
+
intersection = new Set([...intersection].filter(a => actions.includes(a)))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return intersection
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getAllowedActionsForCheckbox(checkbox) {
|
|
98
|
+
const allowedActions = checkbox.dataset.allowedActions
|
|
99
|
+
return allowedActions ? allowedActions.split(",").filter(a => a) : []
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get checked() {
|
|
103
|
+
return this.checkboxTargets.filter(cb => cb.checked)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
get unchecked() {
|
|
107
|
+
return this.checkboxTargets.filter(cb => !cb.checked)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Connects to data-controller="filter-panel"
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
clear() {
|
|
6
|
+
this.element.querySelectorAll('input, select, textarea').forEach(input => {
|
|
7
|
+
if (input.type === 'checkbox' || input.type === 'radio') {
|
|
8
|
+
input.checked = false
|
|
9
|
+
} else if (input.tagName === 'SELECT') {
|
|
10
|
+
input.selectedIndex = 0
|
|
11
|
+
} else if (input.type === 'hidden') {
|
|
12
|
+
// Clear hidden inputs that are filter values (e.g., flatpickr)
|
|
13
|
+
if (input.dataset.controller === 'flatpickr') {
|
|
14
|
+
input.value = ''
|
|
15
|
+
}
|
|
16
|
+
} else {
|
|
17
|
+
input.value = ''
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// Clear flatpickr instances via Stimulus controller
|
|
22
|
+
this.element.querySelectorAll('[data-controller="flatpickr"]').forEach(input => {
|
|
23
|
+
const controller = this.application.getControllerForElementAndIdentifier(input, 'flatpickr')
|
|
24
|
+
if (controller?.picker) {
|
|
25
|
+
controller.picker.clear()
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Submit the parent form
|
|
30
|
+
const form = this.element.closest('form')
|
|
31
|
+
if (form) {
|
|
32
|
+
form.requestSubmit()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -19,7 +19,9 @@ import AttachmentPreviewContainerController from "./attachment_preview_container
|
|
|
19
19
|
import SidebarController from "./sidebar_controller.js"
|
|
20
20
|
import PasswordVisibilityController from "./password_visibility_controller.js"
|
|
21
21
|
import RemoteModalController from "./remote_modal_controller.js"
|
|
22
|
-
import KeyValueStoreController from "./
|
|
22
|
+
import KeyValueStoreController from "./key_value_store_controller.js"
|
|
23
|
+
import BulkActionsController from "./bulk_actions_controller.js"
|
|
24
|
+
import FilterPanelController from "./filter_panel_controller.js"
|
|
23
25
|
|
|
24
26
|
export default function (application) {
|
|
25
27
|
// Register controllers here
|
|
@@ -44,4 +46,6 @@ export default function (application) {
|
|
|
44
46
|
application.register("attachment-preview-container", AttachmentPreviewContainerController)
|
|
45
47
|
application.register("remote-modal", RemoteModalController)
|
|
46
48
|
application.register("key-value-store", KeyValueStoreController)
|
|
49
|
+
application.register("bulk-actions", BulkActionsController)
|
|
50
|
+
application.register("filter-panel", FilterPanelController)
|
|
47
51
|
}
|
|
@@ -4,6 +4,9 @@ import { createPopper } from '@popperjs/core'
|
|
|
4
4
|
// Connects to data-controller="resource-drop-down"
|
|
5
5
|
export default class extends Controller {
|
|
6
6
|
static targets = ["trigger", "menu"]
|
|
7
|
+
static values = {
|
|
8
|
+
placement: { type: String, default: 'bottom' }
|
|
9
|
+
}
|
|
7
10
|
|
|
8
11
|
connect() {
|
|
9
12
|
this.visible = false
|
|
@@ -11,7 +14,7 @@ export default class extends Controller {
|
|
|
11
14
|
|
|
12
15
|
// Default options matching Flowbite's defaults
|
|
13
16
|
this.options = {
|
|
14
|
-
placement:
|
|
17
|
+
placement: this.placementValue,
|
|
15
18
|
triggerType: 'click',
|
|
16
19
|
offsetSkidding: 0,
|
|
17
20
|
offsetDistance: 10,
|
|
@@ -25,7 +28,9 @@ export default class extends Controller {
|
|
|
25
28
|
init() {
|
|
26
29
|
if (this.triggerTarget && this.menuTarget && !this.initialized) {
|
|
27
30
|
// Initialize popper instance
|
|
31
|
+
// Use 'fixed' strategy to escape overflow containers (e.g., table rows)
|
|
28
32
|
this.popperInstance = createPopper(this.triggerTarget, this.menuTarget, {
|
|
33
|
+
strategy: 'fixed',
|
|
29
34
|
placement: this.options.placement,
|
|
30
35
|
modifiers: [
|
|
31
36
|
{
|
|
@@ -34,6 +39,21 @@ export default class extends Controller {
|
|
|
34
39
|
offset: [this.options.offsetSkidding, this.options.offsetDistance],
|
|
35
40
|
},
|
|
36
41
|
},
|
|
42
|
+
{
|
|
43
|
+
name: 'flip',
|
|
44
|
+
options: {
|
|
45
|
+
fallbackPlacements: ['left-end', 'right-start', 'right-end', 'bottom-start', 'bottom-end', 'top-start', 'top-end'],
|
|
46
|
+
boundary: 'clippingParents',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'preventOverflow',
|
|
51
|
+
options: {
|
|
52
|
+
boundary: 'clippingParents',
|
|
53
|
+
altAxis: true,
|
|
54
|
+
padding: 8,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
37
57
|
],
|
|
38
58
|
})
|
|
39
59
|
|
|
@@ -116,11 +136,15 @@ export default class extends Controller {
|
|
|
116
136
|
})
|
|
117
137
|
}
|
|
118
138
|
|
|
139
|
+
// Ignore clicks on flatpickr calendars and other floating UI elements
|
|
140
|
+
const isFloatingUI = clickedEl.closest('.flatpickr-calendar, .ss-main, .ss-content')
|
|
141
|
+
|
|
119
142
|
if (
|
|
120
143
|
clickedEl !== this.menuTarget &&
|
|
121
144
|
!this.menuTarget.contains(clickedEl) &&
|
|
122
145
|
!this.triggerTarget.contains(clickedEl) &&
|
|
123
146
|
!isIgnored &&
|
|
147
|
+
!isFloatingUI &&
|
|
124
148
|
this.visible
|
|
125
149
|
) {
|
|
126
150
|
this.hide()
|
|
@@ -9,8 +9,12 @@ export default class extends Controller {
|
|
|
9
9
|
|
|
10
10
|
// Just recreate SlimSelect after morphing - the DOM will have correct selections
|
|
11
11
|
this.element.addEventListener("turbo:morph-element", (event) => {
|
|
12
|
-
if (event.target === this.element) {
|
|
13
|
-
|
|
12
|
+
if (event.target === this.element && !this.morphing) {
|
|
13
|
+
this.morphing = true;
|
|
14
|
+
requestAnimationFrame(() => {
|
|
15
|
+
this.#handleMorph();
|
|
16
|
+
this.morphing = false;
|
|
17
|
+
});
|
|
14
18
|
}
|
|
15
19
|
});
|
|
16
20
|
}
|