plutonium 0.25.2 → 0.26.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.
- checksums.yaml +4 -4
- data/app/assets/plutonium.css +2 -2
- data/app/assets/plutonium.js +152 -12
- data/app/assets/plutonium.js.map +3 -3
- data/app/assets/plutonium.min.js +32 -32
- data/app/assets/plutonium.min.js.map +3 -3
- data/docs/guide/deep-dive/resources.md +1 -1
- data/docs/modules/definition.md +55 -20
- data/docs/modules/table.md +1 -1
- data/docs/public/plutonium.mdc +27 -14
- data/lib/plutonium/core/controller.rb +2 -2
- data/lib/plutonium/core/controllers/entity_scoping.rb +5 -0
- data/lib/plutonium/engine.rb +3 -2
- data/lib/plutonium/resource/register.rb +1 -1
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +8 -2
- data/lib/plutonium/ui/page/interactive_action.rb +23 -0
- data/lib/plutonium/ui/table/resource.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/src/css/slim_select.css +37 -0
- data/src/js/controllers/remote_modal_controller.js +24 -17
- data/src/js/controllers/slim_select_controller.js +201 -8
- data/src/js/core.js +2 -1
- data/src/js/plutonium.js +0 -2
- data/yarn.lock +3840 -0
- metadata +15 -14
@@ -1,25 +1,218 @@
|
|
1
|
-
import { Controller } from "@hotwired/stimulus"
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
2
|
|
3
3
|
// Connects to data-controller="slim-select"
|
4
4
|
export default class extends Controller {
|
5
5
|
connect() {
|
6
|
+
const settings = {};
|
7
|
+
const modal = document.querySelector('[data-controller="remote-modal"]');
|
8
|
+
|
9
|
+
if (modal) {
|
10
|
+
// Create a dedicated container div right after the select element
|
11
|
+
this.dropdownContainer = document.createElement("div");
|
12
|
+
this.dropdownContainer.className = "ss-dropdown-container";
|
13
|
+
|
14
|
+
// Make the select wrapper position relative to contain the absolute dropdown
|
15
|
+
const selectWrapper = this.element.parentNode;
|
16
|
+
const originalPosition = getComputedStyle(selectWrapper).position;
|
17
|
+
if (originalPosition === "static") {
|
18
|
+
selectWrapper.style.position = "relative";
|
19
|
+
this.modifiedSelectWrapper = selectWrapper;
|
20
|
+
}
|
21
|
+
|
22
|
+
// Insert the container right after the select element
|
23
|
+
this.element.parentNode.insertBefore(
|
24
|
+
this.dropdownContainer,
|
25
|
+
this.element.nextSibling
|
26
|
+
);
|
27
|
+
|
28
|
+
settings.contentLocation = this.dropdownContainer;
|
29
|
+
settings.contentPosition = "absolute";
|
30
|
+
settings.openPosition = "auto";
|
31
|
+
}
|
32
|
+
|
6
33
|
this.slimSelect = new SlimSelect({
|
7
|
-
select: this.element
|
8
|
-
|
9
|
-
|
34
|
+
select: this.element,
|
35
|
+
settings: settings,
|
36
|
+
});
|
37
|
+
|
38
|
+
// Add event listeners for better positioning
|
39
|
+
this.handleDropdownPosition();
|
40
|
+
|
41
|
+
// Bind event handlers for proper cleanup
|
42
|
+
this.boundHandleDropdownOpen = this.handleDropdownOpen.bind(this);
|
43
|
+
this.boundHandleDropdownClose = this.handleDropdownClose.bind(this);
|
44
|
+
|
45
|
+
// Add event listeners to properly handle dropdown visibility
|
46
|
+
this.element.addEventListener("ss:open", this.boundHandleDropdownOpen);
|
47
|
+
this.element.addEventListener("ss:close", this.boundHandleDropdownClose);
|
48
|
+
|
49
|
+
// Add mutation observer to track aria-expanded attribute
|
50
|
+
this.setupAriaObserver();
|
51
|
+
|
52
|
+
this.element.setAttribute(
|
53
|
+
"data-action",
|
54
|
+
"turbo:morph-element->slim-select#reconnect"
|
55
|
+
);
|
56
|
+
}
|
57
|
+
|
58
|
+
handleDropdownPosition() {
|
59
|
+
if (this.dropdownContainer) {
|
60
|
+
// Reposition dropdown when window resizes or scrolls
|
61
|
+
const repositionDropdown = () => {
|
62
|
+
const selectRect = this.element.getBoundingClientRect();
|
63
|
+
|
64
|
+
// Calculate if there's enough space below
|
65
|
+
const spaceBelow = window.innerHeight - selectRect.bottom;
|
66
|
+
const spaceAbove = selectRect.top;
|
67
|
+
|
68
|
+
if (spaceBelow < 200 && spaceAbove > spaceBelow) {
|
69
|
+
// Position above if not enough space below
|
70
|
+
this.dropdownContainer.style.top = "auto";
|
71
|
+
this.dropdownContainer.style.bottom = "100%";
|
72
|
+
this.dropdownContainer.style.borderRadius = "0.375rem 0.375rem 0 0";
|
73
|
+
} else {
|
74
|
+
// Position below (default)
|
75
|
+
this.dropdownContainer.style.bottom = "auto";
|
76
|
+
this.dropdownContainer.style.borderRadius = "0 0 0.375rem 0.375rem";
|
77
|
+
}
|
78
|
+
};
|
79
|
+
|
80
|
+
// Initial positioning
|
81
|
+
setTimeout(repositionDropdown, 0);
|
82
|
+
|
83
|
+
// Reposition on events
|
84
|
+
window.addEventListener("resize", repositionDropdown);
|
85
|
+
window.addEventListener("scroll", repositionDropdown);
|
86
|
+
|
87
|
+
// Store references for cleanup
|
88
|
+
this.repositionDropdown = repositionDropdown;
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
handleDropdownOpen() {
|
93
|
+
if (this.dropdownContainer) {
|
94
|
+
// When dropdown opens, ensure our container is properly sized
|
95
|
+
this.dropdownContainer.style.height = "auto";
|
96
|
+
this.dropdownContainer.style.overflow = "visible";
|
97
|
+
|
98
|
+
// Add open class for better CSS targeting
|
99
|
+
this.dropdownContainer.classList.add("ss-active");
|
100
|
+
|
101
|
+
// Ensure this dropdown appears above others
|
102
|
+
const allContainers = document.querySelectorAll(".ss-dropdown-container");
|
103
|
+
allContainers.forEach((container) => {
|
104
|
+
if (container !== this.dropdownContainer) {
|
105
|
+
container.style.zIndex = "9999";
|
106
|
+
}
|
107
|
+
});
|
108
|
+
this.dropdownContainer.style.zIndex = "10000";
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
handleDropdownClose() {
|
113
|
+
if (this.dropdownContainer) {
|
114
|
+
// Remove active class
|
115
|
+
this.dropdownContainer.classList.remove("ss-active");
|
116
|
+
}
|
117
|
+
}
|
118
|
+
|
119
|
+
setupAriaObserver() {
|
120
|
+
// Track aria-expanded attribute on the select element or its wrapper
|
121
|
+
if (this.element) {
|
122
|
+
this.ariaObserver = new MutationObserver((mutations) => {
|
123
|
+
mutations.forEach((mutation) => {
|
124
|
+
if (mutation.attributeName === "aria-expanded") {
|
125
|
+
const expanded =
|
126
|
+
mutation.target.getAttribute("aria-expanded") === "true";
|
127
|
+
if (expanded) {
|
128
|
+
this.handleDropdownOpen();
|
129
|
+
} else {
|
130
|
+
this.handleDropdownClose();
|
131
|
+
}
|
132
|
+
}
|
133
|
+
});
|
134
|
+
});
|
135
|
+
|
136
|
+
// Look for the actual element that gets the aria-expanded attribute
|
137
|
+
const possibleTargets = [
|
138
|
+
this.element,
|
139
|
+
this.element.parentNode.querySelector(".ss-main"),
|
140
|
+
this.element.parentNode.querySelector("[aria-expanded]"),
|
141
|
+
];
|
142
|
+
|
143
|
+
const target = possibleTargets.find(
|
144
|
+
(el) => el && el.hasAttribute && el.hasAttribute("aria-expanded")
|
145
|
+
);
|
146
|
+
|
147
|
+
if (target) {
|
148
|
+
this.ariaObserver.observe(target, {
|
149
|
+
attributes: true,
|
150
|
+
attributeFilter: ["aria-expanded"],
|
151
|
+
});
|
152
|
+
|
153
|
+
// Check initial state
|
154
|
+
const expanded = target.getAttribute("aria-expanded") === "true";
|
155
|
+
if (expanded) {
|
156
|
+
this.handleDropdownOpen();
|
157
|
+
} else {
|
158
|
+
this.handleDropdownClose();
|
159
|
+
}
|
160
|
+
}
|
161
|
+
}
|
10
162
|
}
|
11
163
|
|
12
164
|
disconnect() {
|
165
|
+
// Clean up event listeners
|
166
|
+
if (this.element) {
|
167
|
+
if (this.boundHandleDropdownOpen) {
|
168
|
+
this.element.removeEventListener(
|
169
|
+
"ss:open",
|
170
|
+
this.boundHandleDropdownOpen
|
171
|
+
);
|
172
|
+
}
|
173
|
+
if (this.boundHandleDropdownClose) {
|
174
|
+
this.element.removeEventListener(
|
175
|
+
"ss:close",
|
176
|
+
this.boundHandleDropdownClose
|
177
|
+
);
|
178
|
+
}
|
179
|
+
}
|
180
|
+
|
181
|
+
// Disconnect observer
|
182
|
+
if (this.ariaObserver) {
|
183
|
+
this.ariaObserver.disconnect();
|
184
|
+
this.ariaObserver = null;
|
185
|
+
}
|
186
|
+
|
13
187
|
if (this.slimSelect) {
|
14
|
-
this.slimSelect.destroy()
|
15
|
-
this.slimSelect = null
|
188
|
+
this.slimSelect.destroy();
|
189
|
+
this.slimSelect = null;
|
190
|
+
}
|
191
|
+
|
192
|
+
// Clean up event listeners
|
193
|
+
if (this.repositionDropdown) {
|
194
|
+
window.removeEventListener("resize", this.repositionDropdown);
|
195
|
+
window.removeEventListener("scroll", this.repositionDropdown);
|
196
|
+
this.repositionDropdown = null;
|
197
|
+
}
|
198
|
+
|
199
|
+
// Clean up the dropdown container if it exists
|
200
|
+
if (this.dropdownContainer && this.dropdownContainer.parentNode) {
|
201
|
+
this.dropdownContainer.parentNode.removeChild(this.dropdownContainer);
|
202
|
+
this.dropdownContainer = null;
|
203
|
+
}
|
204
|
+
|
205
|
+
// Restore original positioning if we modified it
|
206
|
+
if (this.modifiedSelectWrapper) {
|
207
|
+
this.modifiedSelectWrapper.style.position = "";
|
208
|
+
this.modifiedSelectWrapper = null;
|
16
209
|
}
|
17
210
|
}
|
18
211
|
|
19
212
|
reconnect() {
|
20
|
-
this.disconnect()
|
213
|
+
this.disconnect();
|
21
214
|
// dispatch this on the next frame.
|
22
215
|
// there's some funny issue where my elements get removed from the DOM
|
23
|
-
setTimeout(() => this.connect(), 10)
|
216
|
+
setTimeout(() => this.connect(), 10);
|
24
217
|
}
|
25
218
|
}
|
data/src/js/core.js
CHANGED