@radioactive-labs/plutonium 0.4.8 → 0.4.10

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.
@@ -10,6 +10,7 @@ import DomElement from "../support/dom_element"
10
10
  export default class extends Controller {
11
11
  static values = {
12
12
  identifier: String,
13
+ endpoint: String,
13
14
 
14
15
  maxFileSize: { type: Number, default: null },
15
16
  minFileSize: { type: Number, default: null },
@@ -25,6 +26,8 @@ export default class extends Controller {
25
26
  //======= Lifecycle
26
27
 
27
28
  connect() {
29
+ if (this.uppy) return;
30
+
28
31
  // initialize
29
32
  this.uploadedFiles = []
30
33
 
@@ -37,10 +40,48 @@ export default class extends Controller {
37
40
  this.#buildTriggers()
38
41
  // init state
39
42
  this.#onAttachmentsChanged()
43
+
44
+ // Just recreate Uppy after morphing - preserve existing attachments
45
+ this.element.addEventListener("turbo:morph-element", (event) => {
46
+ if (event.target === this.element && !this.morphing) {
47
+ this.morphing = true;
48
+ requestAnimationFrame(() => {
49
+ this.#handleMorph();
50
+ this.morphing = false;
51
+ });
52
+ }
53
+ });
40
54
  }
41
55
 
42
56
  disconnect() {
43
- this.uppy = null
57
+ this.#cleanupUppy();
58
+ }
59
+
60
+ #handleMorph() {
61
+ if (!this.element.isConnected) return;
62
+
63
+ // Clean up the old instance
64
+ this.#cleanupUppy();
65
+
66
+ // Recreate everything - Uppy, triggers, etc.
67
+ this.uploadedFiles = []
68
+ this.element.style["display"] = "none"
69
+ this.configureUppy()
70
+ this.#buildTriggers()
71
+ this.#onAttachmentsChanged()
72
+ }
73
+
74
+ #cleanupUppy() {
75
+ if (this.uppy) {
76
+ this.uppy.destroy();
77
+ this.uppy = null;
78
+ }
79
+
80
+ // Clean up triggers
81
+ if (this.triggerContainer && this.triggerContainer.parentNode) {
82
+ this.triggerContainer.parentNode.removeChild(this.triggerContainer);
83
+ this.triggerContainer = null;
84
+ }
44
85
  }
45
86
 
46
87
  attachmentPreviewOutletConnected(outlet, element) {
@@ -75,7 +116,7 @@ export default class extends Controller {
75
116
  #configureUploader() {
76
117
  this.uppy
77
118
  .use(XHRUpload, {
78
- endpoint: '/upload', // path to the upload endpoint
119
+ endpoint: this.endpointValue, // path to the upload endpoint
79
120
  })
80
121
  }
81
122
 
@@ -4,21 +4,60 @@ import { marked } from 'marked';
4
4
 
5
5
  // Connects to data-controller="easymde"
6
6
  export default class extends Controller {
7
+ static targets = ["textarea"]
8
+
7
9
  connect() {
10
+ if (this.easyMDE) return
11
+
12
+ this.originalValue = this.element.value
8
13
  this.easyMDE = new EasyMDE(this.#buildOptions())
9
- this.element.setAttribute("data-action", "turbo:morph-element->easymde#reconnect")
14
+
15
+ // Store the editor content before morphing
16
+ this.element.addEventListener("turbo:before-morph-element", (event) => {
17
+ if (event.target === this.element && this.easyMDE) {
18
+ this.storedValue = this.easyMDE.value()
19
+ }
20
+ })
21
+
22
+ // Restore after morphing
23
+ this.element.addEventListener("turbo:morph-element", (event) => {
24
+ if (event.target === this.element) {
25
+ requestAnimationFrame(() => this.#handleMorph())
26
+ }
27
+ })
10
28
  }
11
29
 
12
30
  disconnect() {
13
31
  if (this.easyMDE) {
14
- this.easyMDE.toTextArea()
32
+ try {
33
+ // Only call toTextArea if the element is still in the DOM
34
+ if (this.element.isConnected && this.element.parentNode) {
35
+ this.easyMDE.toTextArea()
36
+ }
37
+ } catch (error) {
38
+ console.warn('EasyMDE cleanup error:', error)
39
+ }
15
40
  this.easyMDE = null
16
41
  }
17
42
  }
18
-
19
- reconnect() {
20
- this.disconnect()
21
- this.connect()
43
+
44
+ #handleMorph() {
45
+ if (!this.element.isConnected) return
46
+
47
+ // Don't call toTextArea during morph - just clean up references
48
+ if (this.easyMDE) {
49
+ // Skip toTextArea cleanup - it causes DOM errors during morphing
50
+ this.easyMDE = null
51
+ }
52
+
53
+ // Recreate the editor
54
+ this.easyMDE = new EasyMDE(this.#buildOptions())
55
+
56
+ // Restore the stored value if we have it
57
+ if (this.storedValue !== undefined) {
58
+ this.easyMDE.value(this.storedValue)
59
+ this.storedValue = undefined
60
+ }
22
61
  }
23
62
 
24
63
  #buildOptions() {
@@ -1,35 +1,62 @@
1
- import { Controller } from "@hotwired/stimulus"
1
+ import { Controller } from "@hotwired/stimulus";
2
2
 
3
3
  // Connects to data-controller="flatpickr"
4
4
  export default class extends Controller {
5
5
  connect() {
6
- this.picker = new flatpickr(this.element, this.#buildOptions())
7
- this.element.setAttribute("data-action", "turbo:morph-element->flatpickr#reconnect")
6
+ if (this.picker) return;
7
+
8
+ this.modal = document.querySelector("[data-controller=remote-modal]");
9
+ this.picker = new flatpickr(this.element, this.#buildOptions());
10
+
11
+ // Just recreate Flatpickr after morphing - the DOM will have correct value
12
+ this.element.addEventListener("turbo:morph-element", (event) => {
13
+ if (event.target === this.element && !this.morphing) {
14
+ this.morphing = true;
15
+ requestAnimationFrame(() => {
16
+ this.#handleMorph();
17
+ this.morphing = false;
18
+ });
19
+ }
20
+ });
8
21
  }
9
22
 
10
23
  disconnect() {
11
24
  if (this.picker) {
12
- this.picker.destroy()
13
- this.picker = null
25
+ this.picker.destroy();
26
+ this.picker = null;
14
27
  }
15
28
  }
16
29
 
17
- reconnect() {
18
- this.disconnect()
19
- this.connect()
30
+ #handleMorph() {
31
+ if (!this.element.isConnected) return;
32
+
33
+ // Clean up the old instance
34
+ if (this.picker) {
35
+ this.picker.destroy();
36
+ this.picker = null;
37
+ }
38
+
39
+ // Recreate the picker - it will pick up the current DOM value
40
+ this.modal = document.querySelector("[data-controller=remote-modal]");
41
+ this.picker = new flatpickr(this.element, this.#buildOptions());
20
42
  }
21
43
 
22
44
  #buildOptions() {
23
- let options = { altInput: true }
45
+ let options = { altInput: true };
46
+
24
47
  if (this.element.attributes.type.value == "datetime-local") {
25
- options.enableTime = true
26
- }
27
- else if (this.element.attributes.type.value == "time") {
28
- options.enableTime = true
29
- options.noCalendar = true
48
+ options.enableTime = true;
49
+ } else if (this.element.attributes.type.value == "time") {
50
+ options.enableTime = true;
51
+ options.noCalendar = true;
30
52
  // options.time_24hr = true
31
53
  // options.altFormat = "H:i"
32
54
  }
33
- return options
55
+
56
+ if (this.modal) {
57
+ options.appendTo = this.modal;
58
+ }
59
+
60
+ return options;
34
61
  }
35
62
  }
@@ -12,10 +12,20 @@ export default class extends Controller {
12
12
  }
13
13
 
14
14
  inputTargetConnected() {
15
- if (!this.hasInputTarget) return;
15
+ if (!this.hasInputTarget || this.iti) return;
16
16
 
17
17
  this.iti = window.intlTelInput(this.inputTarget, this.#buildOptions())
18
- this.inputTarget.setAttribute("data-action", "turbo:morph-element->intl-tel-input#reconnect")
18
+
19
+ // Just recreate IntlTelInput after morphing - the DOM will have correct value
20
+ this.element.addEventListener("turbo:morph-element", (event) => {
21
+ if (event.target === this.element && !this.morphing) {
22
+ this.morphing = true;
23
+ requestAnimationFrame(() => {
24
+ this.#handleMorph();
25
+ this.morphing = false;
26
+ });
27
+ }
28
+ });
19
29
  }
20
30
 
21
31
  inputTargetDisconnected() {
@@ -25,9 +35,17 @@ export default class extends Controller {
25
35
  }
26
36
  }
27
37
 
28
- reconnect() {
29
- this.inputTargetDisconnected()
30
- this.inputTargetConnected()
38
+ #handleMorph() {
39
+ if (!this.inputTarget || !this.inputTarget.isConnected) return;
40
+
41
+ // Clean up the old instance
42
+ if (this.iti) {
43
+ this.iti.destroy();
44
+ this.iti = null;
45
+ }
46
+
47
+ // Recreate the intl tel input - it will pick up the current DOM value
48
+ this.iti = window.intlTelInput(this.inputTarget, this.#buildOptions());
31
49
  }
32
50
 
33
51
  #buildOptions() {
@@ -3,6 +3,19 @@ import { Controller } from "@hotwired/stimulus";
3
3
  // Connects to data-controller="slim-select"
4
4
  export default class extends Controller {
5
5
  connect() {
6
+ if (this.slimSelect) return;
7
+
8
+ this.#setupSlimSelect();
9
+
10
+ // Just recreate SlimSelect after morphing - the DOM will have correct selections
11
+ this.element.addEventListener("turbo:morph-element", (event) => {
12
+ if (event.target === this.element) {
13
+ requestAnimationFrame(() => this.#handleMorph());
14
+ }
15
+ });
16
+ }
17
+
18
+ #setupSlimSelect() {
6
19
  const settings = {};
7
20
  const modal = document.querySelector('[data-controller="remote-modal"]');
8
21
 
@@ -48,11 +61,6 @@ export default class extends Controller {
48
61
 
49
62
  // Add mutation observer to track aria-expanded attribute
50
63
  this.setupAriaObserver();
51
-
52
- this.element.setAttribute(
53
- "data-action",
54
- "turbo:morph-element->slim-select#reconnect"
55
- );
56
64
  }
57
65
 
58
66
  handleDropdownPosition() {
@@ -162,6 +170,20 @@ export default class extends Controller {
162
170
  }
163
171
 
164
172
  disconnect() {
173
+ this.#cleanupSlimSelect();
174
+ }
175
+
176
+ #handleMorph() {
177
+ if (!this.element.isConnected) return;
178
+
179
+ // Clean up the old instance without DOM manipulation
180
+ this.#cleanupSlimSelect();
181
+
182
+ // Recreate the select - it will automatically pick up the current DOM selections
183
+ this.#setupSlimSelect();
184
+ }
185
+
186
+ #cleanupSlimSelect() {
165
187
  // Clean up event listeners
166
188
  if (this.element) {
167
189
  if (this.boundHandleDropdownOpen) {
@@ -208,11 +230,4 @@ export default class extends Controller {
208
230
  this.modifiedSelectWrapper = null;
209
231
  }
210
232
  }
211
-
212
- reconnect() {
213
- this.disconnect();
214
- // dispatch this on the next frame.
215
- // there's some funny issue where my elements get removed from the DOM
216
- setTimeout(() => this.connect(), 10);
217
- }
218
233
  }