actiontext 8.0.3 → 8.1.0.rc1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b81c8405c83266d51dc6c55079aa626c20dda10a35553bfe079a2408d5b6693
4
- data.tar.gz: d7a7e71c2fe3dfbda11d7460c0576c1d01e05eb5c67707de15aa7dfaa479890f
3
+ metadata.gz: 8f8a237e651fd987b5cd33e599577648aedf8a2ff14decaee0b2a3c2b2c6ab1c
4
+ data.tar.gz: 9a84f4bb3bc90cddc9c651e3571ee872a87add9150b33e28d3eef50e728493f3
5
5
  SHA512:
6
- metadata.gz: 52b350b3484af8bdbb39cee82c410e2342773e1747c2554379ca9b838c333a95ee1c49c863faf5e2648544b92e9f1c162bd80f37a45434069fe5b0e8c894634d
7
- data.tar.gz: b5aeb2c5cdde40c910b608de82a946c7af408351b57582ca441f704a5b8aac58832a71ab4de2cca2107a91745303d2edf811e229197cdd966a8479fb256d1f9d
6
+ metadata.gz: c3fb8c1e86cd5017a454ba8615db889b4775ea0afe9f276fcfb46ed2b82583a85a6a11daf097624104a1600753b9c995c5dd7720ce289499fab9d7e57814ab22
7
+ data.tar.gz: 340ac8fc6e495d800d75ba68c205b9213d34e2cc925b542e4f46eb32fcdebda2fe4d439125219bc6d1cab4b3b01789034e0c163e23868c51c5293c294db06d31
data/CHANGELOG.md CHANGED
@@ -1,92 +1,65 @@
1
- ## Rails 8.0.3 (September 22, 2025) ##
1
+ ## Rails 8.1.0.rc1 (October 15, 2025) ##
2
2
 
3
- * Add rollup-plugin-terser as a dev dependency.
3
+ * De-couple `@rails/actiontext/attachment_upload.js` from `Trix.Attachment`
4
4
 
5
- *Édouard Chin*
6
-
7
-
8
- ## Rails 8.0.2.1 (August 13, 2025) ##
9
-
10
- * No changes.
11
-
12
-
13
- ## Rails 8.0.2 (March 12, 2025) ##
14
-
15
- * No changes.
16
-
17
- ## Rails 8.0.2 (March 12, 2025) ##
18
-
19
- * No changes.
20
-
21
-
22
- ## Rails 8.0.1 (December 13, 2024) ##
23
-
24
- * No changes.
25
-
26
-
27
- ## Rails 8.0.0.1 (December 10, 2024) ##
28
-
29
- * Update vendored trix version to 2.1.10
30
-
31
- *John Hawthorn*
32
-
33
-
34
- ## Rails 8.0.0 (November 07, 2024) ##
35
-
36
- * No changes.
37
-
38
-
39
- ## Rails 8.0.0.rc2 (October 30, 2024) ##
40
-
41
- * No changes.
42
-
43
-
44
- ## Rails 8.0.0.rc1 (October 19, 2024) ##
45
-
46
- * No changes.
5
+ Implement `@rails/actiontext/index.js` with a `direct-upload:progress` event
6
+ listeners and `Promise` resolution.
47
7
 
8
+ *Sean Doyle*
48
9
 
49
- ## Rails 8.0.0.beta1 (September 26, 2024) ##
10
+ * Capture block content for form helper methods
11
+
12
+ ```erb
13
+ <%= rich_textarea_tag :content, nil do %>
14
+ <h1>hello world</h1>
15
+ <% end %>
16
+ <!-- <input type="hidden" name="content" id="trix_input_1" value="&lt;h1&gt;hello world&lt;/h1&gt;"/><trix-editor … -->
17
+
18
+ <%= rich_textarea :message, :content, input: "trix_input_1" do %>
19
+ <h1>hello world</h1>
20
+ <% end %>
21
+ <!-- <input type="hidden" name="message[content]" id="trix_input_1" value="&lt;h1&gt;hello world&lt;/h1&gt;"/><trix-editor … -->
22
+
23
+ <%= form_with model: Message.new do |form| %>
24
+ <%= form.rich_textarea :content do %>
25
+ <h1>hello world</h1>
26
+ <% end %>
27
+ <% end %>
28
+ <!-- <form action="/messages" accept-charset="UTF-8" method="post"><input type="hidden" name="message[content]" id="message_content_trix_input_message" value="&lt;h1&gt;hello world&lt;/h1&gt;"/><trix-editor … -->
29
+ ```
50
30
 
51
- * Dispatch direct-upload events on attachment uploads
31
+ *Sean Doyle*
52
32
 
53
- When using Action Text's rich textarea, it's possible to attach files to the
54
- editor. Previously, that action didn't dispatch any events, which made it hard
55
- to react to the file uploads. For instance, if an upload failed, there was no
56
- way to notify the user about it, or remove the attachment from the editor.
33
+ * Generalize `:rich_text_area` Capybara selector
57
34
 
58
- This commits adds new events - `direct-upload:start`, `direct-upload:progress`,
59
- and `direct-upload:end` - similar to how Active Storage's direct uploads work.
35
+ Prepare for more Action Text-capable WYSIWYG editors by making
36
+ `:rich_text_area` rely on the presence of `[role="textbox"]` and
37
+ `[contenteditable]` HTML attributes rather than a `<trix-editor>` element.
60
38
 
61
- *Matheus Richard*, *Brad Rees*
39
+ *Sean Doyle*
62
40
 
63
- * Add `store_if_blank` option to `has_rich_text`
41
+ ## Rails 8.1.0.beta1 (September 04, 2025) ##
64
42
 
65
- Pass `store_if_blank: false` to not create `ActionText::RichText` records when saving with a blank attribute, such as from an optional form parameter.
43
+ * Forward `fill_in_rich_text_area` options to Capybara
66
44
 
67
45
  ```ruby
68
- class Message
69
- has_rich_text :content, store_if_blank: false
70
- end
71
-
72
- Message.create(content: "hi") # creates an ActionText::RichText
73
- Message.create(content: "") # does not create an ActionText::RichText
46
+ fill_in_rich_textarea "Rich text editor", id: "trix_editor_1", with: "Hello world!"
74
47
  ```
75
48
 
76
- *Alex Ghiculescu*
77
-
78
- * Strip `content` attribute if the key is present but the value is empty
49
+ *Sean Doyle*
79
50
 
80
- *Jeremy Green*
51
+ * Attachment upload progress accounts for server processing time.
81
52
 
82
- * Rename `rich_text_area` methods into `rich_textarea`
53
+ *Jeremy Daer*
83
54
 
84
- Old names are still available as aliases.
55
+ * The Trix dependency is now satisfied by a gem, `action_text-trix`, rather than vendored
56
+ files. This allows applications to bump Trix versions independently of Rails
57
+ releases. Effectively this also upgrades Trix to `>= 2.1.15`.
85
58
 
86
- *Sean Doyle*
59
+ *Mike Dalessio*
87
60
 
88
- * Only sanitize `content` attribute when present in attachments.
61
+ * Change `ActionText::RichText#embeds` assignment from `before_save` to `before_validation`
89
62
 
90
- *Petrik de Heus*
63
+ *Sean Doyle*
91
64
 
92
- Please check [7-2-stable](https://github.com/rails/rails/blob/7-2-stable/actiontext/CHANGELOG.md) for previous changes.
65
+ Please check [8-0-stable](https://github.com/rails/rails/blob/8-0-stable/actiontext/CHANGELOG.md) for previous changes.
@@ -672,7 +672,7 @@ class DirectUploadController {
672
672
  }));
673
673
  }
674
674
  uploadRequestDidProgress(event) {
675
- const progress = event.loaded / event.total * 100;
675
+ const progress = event.loaded / event.total * 90;
676
676
  if (progress) {
677
677
  this.dispatch("progress", {
678
678
  progress: progress
@@ -707,6 +707,42 @@ class DirectUploadController {
707
707
  xhr: xhr
708
708
  });
709
709
  xhr.upload.addEventListener("progress", (event => this.uploadRequestDidProgress(event)));
710
+ xhr.upload.addEventListener("loadend", (() => {
711
+ this.simulateResponseProgress(xhr);
712
+ }));
713
+ }
714
+ simulateResponseProgress(xhr) {
715
+ let progress = 90;
716
+ const startTime = Date.now();
717
+ const updateProgress = () => {
718
+ const elapsed = Date.now() - startTime;
719
+ const estimatedResponseTime = this.estimateResponseTime();
720
+ const responseProgress = Math.min(elapsed / estimatedResponseTime, 1);
721
+ progress = 90 + responseProgress * 9;
722
+ this.dispatch("progress", {
723
+ progress: progress
724
+ });
725
+ if (xhr.readyState !== XMLHttpRequest.DONE && progress < 99) {
726
+ requestAnimationFrame(updateProgress);
727
+ }
728
+ };
729
+ xhr.addEventListener("loadend", (() => {
730
+ this.dispatch("progress", {
731
+ progress: 100
732
+ });
733
+ }));
734
+ requestAnimationFrame(updateProgress);
735
+ }
736
+ estimateResponseTime() {
737
+ const fileSize = this.file.size;
738
+ const MB = 1024 * 1024;
739
+ if (fileSize < MB) {
740
+ return 1e3;
741
+ } else if (fileSize < 10 * MB) {
742
+ return 2e3;
743
+ } else {
744
+ return 3e3 + fileSize / MB * 50;
745
+ }
710
746
  }
711
747
  }
712
748
 
@@ -846,31 +882,69 @@ function autostart() {
846
882
  setTimeout(autostart, 1);
847
883
 
848
884
  class AttachmentUpload {
849
- constructor(attachment, element) {
885
+ constructor(attachment, element, file = attachment.file) {
850
886
  this.attachment = attachment;
851
887
  this.element = element;
852
- this.directUpload = new DirectUpload(attachment.file, this.directUploadUrl, this);
888
+ this.directUpload = new DirectUpload(file, this.directUploadUrl, this);
889
+ this.file = file;
853
890
  }
854
891
  start() {
855
- this.directUpload.create(this.directUploadDidComplete.bind(this));
856
- this.dispatch("start");
892
+ return new Promise(((resolve, reject) => {
893
+ this.directUpload.create(((error, attributes) => this.directUploadDidComplete(error, attributes, resolve, reject)));
894
+ this.dispatch("start");
895
+ }));
857
896
  }
858
897
  directUploadWillStoreFileWithXHR(xhr) {
859
898
  xhr.upload.addEventListener("progress", (event => {
860
- const progress = event.loaded / event.total * 100;
861
- this.attachment.setUploadProgress(progress);
899
+ const progress = event.loaded / event.total * 90;
862
900
  if (progress) {
863
901
  this.dispatch("progress", {
864
902
  progress: progress
865
903
  });
866
904
  }
867
905
  }));
906
+ xhr.upload.addEventListener("loadend", (() => {
907
+ this.simulateResponseProgress(xhr);
908
+ }));
868
909
  }
869
- directUploadDidComplete(error, attributes) {
910
+ simulateResponseProgress(xhr) {
911
+ let progress = 90;
912
+ const startTime = Date.now();
913
+ const updateProgress = () => {
914
+ const elapsed = Date.now() - startTime;
915
+ const estimatedResponseTime = this.estimateResponseTime();
916
+ const responseProgress = Math.min(elapsed / estimatedResponseTime, 1);
917
+ progress = 90 + responseProgress * 9;
918
+ this.dispatch("progress", {
919
+ progress: progress
920
+ });
921
+ if (xhr.readyState !== XMLHttpRequest.DONE && progress < 99) {
922
+ requestAnimationFrame(updateProgress);
923
+ }
924
+ };
925
+ xhr.addEventListener("loadend", (() => {
926
+ this.dispatch("progress", {
927
+ progress: 100
928
+ });
929
+ }));
930
+ requestAnimationFrame(updateProgress);
931
+ }
932
+ estimateResponseTime() {
933
+ const fileSize = this.file.size;
934
+ const MB = 1024 * 1024;
935
+ if (fileSize < MB) {
936
+ return 1e3;
937
+ } else if (fileSize < 10 * MB) {
938
+ return 2e3;
939
+ } else {
940
+ return 3e3 + fileSize / MB * 50;
941
+ }
942
+ }
943
+ directUploadDidComplete(error, attributes, resolve, reject) {
870
944
  if (error) {
871
- this.dispatchError(error);
945
+ this.dispatchError(error, reject);
872
946
  } else {
873
- this.attachment.setAttributes({
947
+ resolve({
874
948
  sgid: attributes.attachable_sgid,
875
949
  url: this.createBlobUrl(attributes.signed_id, attributes.filename)
876
950
  });
@@ -886,12 +960,12 @@ class AttachmentUpload {
886
960
  detail: detail
887
961
  });
888
962
  }
889
- dispatchError(error) {
963
+ dispatchError(error, reject) {
890
964
  const event = this.dispatch("error", {
891
965
  error: error
892
966
  });
893
967
  if (!event.defaultPrevented) {
894
- alert(error);
968
+ reject(error);
895
969
  }
896
970
  }
897
971
  get directUploadUrl() {
@@ -905,7 +979,11 @@ class AttachmentUpload {
905
979
  addEventListener("trix-attachment-add", (event => {
906
980
  const {attachment: attachment, target: target} = event;
907
981
  if (attachment.file) {
908
- const upload = new AttachmentUpload(attachment, target);
909
- upload.start();
982
+ const upload = new AttachmentUpload(attachment, target, attachment.file);
983
+ const onProgress = event => attachment.setUploadProgress(event.detail.progress);
984
+ target.addEventListener("direct-upload:progress", onProgress);
985
+ upload.start().then((attributes => attachment.setAttributes(attributes))).catch((error => alert(error))).finally((() => target.removeEventListener("direct-upload:progress", onProgress)));
910
986
  }
911
987
  }));
988
+
989
+ export { AttachmentUpload };
@@ -1,6 +1,7 @@
1
- (function(factory) {
2
- typeof define === "function" && define.amd ? define(factory) : factory();
3
- })((function() {
1
+ (function(global, factory) {
2
+ typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
3
+ factory(global.ActionText = {}));
4
+ })(this, (function(exports) {
4
5
  "use strict";
5
6
  var sparkMd5 = {
6
7
  exports: {}
@@ -661,7 +662,7 @@
661
662
  }));
662
663
  }
663
664
  uploadRequestDidProgress(event) {
664
- const progress = event.loaded / event.total * 100;
665
+ const progress = event.loaded / event.total * 90;
665
666
  if (progress) {
666
667
  this.dispatch("progress", {
667
668
  progress: progress
@@ -696,6 +697,42 @@
696
697
  xhr: xhr
697
698
  });
698
699
  xhr.upload.addEventListener("progress", (event => this.uploadRequestDidProgress(event)));
700
+ xhr.upload.addEventListener("loadend", (() => {
701
+ this.simulateResponseProgress(xhr);
702
+ }));
703
+ }
704
+ simulateResponseProgress(xhr) {
705
+ let progress = 90;
706
+ const startTime = Date.now();
707
+ const updateProgress = () => {
708
+ const elapsed = Date.now() - startTime;
709
+ const estimatedResponseTime = this.estimateResponseTime();
710
+ const responseProgress = Math.min(elapsed / estimatedResponseTime, 1);
711
+ progress = 90 + responseProgress * 9;
712
+ this.dispatch("progress", {
713
+ progress: progress
714
+ });
715
+ if (xhr.readyState !== XMLHttpRequest.DONE && progress < 99) {
716
+ requestAnimationFrame(updateProgress);
717
+ }
718
+ };
719
+ xhr.addEventListener("loadend", (() => {
720
+ this.dispatch("progress", {
721
+ progress: 100
722
+ });
723
+ }));
724
+ requestAnimationFrame(updateProgress);
725
+ }
726
+ estimateResponseTime() {
727
+ const fileSize = this.file.size;
728
+ const MB = 1024 * 1024;
729
+ if (fileSize < MB) {
730
+ return 1e3;
731
+ } else if (fileSize < 10 * MB) {
732
+ return 2e3;
733
+ } else {
734
+ return 3e3 + fileSize / MB * 50;
735
+ }
699
736
  }
700
737
  }
701
738
  const inputSelector = "input[type=file][data-direct-upload-url]:not([disabled])";
@@ -819,31 +856,69 @@
819
856
  }
820
857
  setTimeout(autostart, 1);
821
858
  class AttachmentUpload {
822
- constructor(attachment, element) {
859
+ constructor(attachment, element, file = attachment.file) {
823
860
  this.attachment = attachment;
824
861
  this.element = element;
825
- this.directUpload = new DirectUpload(attachment.file, this.directUploadUrl, this);
862
+ this.directUpload = new DirectUpload(file, this.directUploadUrl, this);
863
+ this.file = file;
826
864
  }
827
865
  start() {
828
- this.directUpload.create(this.directUploadDidComplete.bind(this));
829
- this.dispatch("start");
866
+ return new Promise(((resolve, reject) => {
867
+ this.directUpload.create(((error, attributes) => this.directUploadDidComplete(error, attributes, resolve, reject)));
868
+ this.dispatch("start");
869
+ }));
830
870
  }
831
871
  directUploadWillStoreFileWithXHR(xhr) {
832
872
  xhr.upload.addEventListener("progress", (event => {
833
- const progress = event.loaded / event.total * 100;
834
- this.attachment.setUploadProgress(progress);
873
+ const progress = event.loaded / event.total * 90;
835
874
  if (progress) {
836
875
  this.dispatch("progress", {
837
876
  progress: progress
838
877
  });
839
878
  }
840
879
  }));
880
+ xhr.upload.addEventListener("loadend", (() => {
881
+ this.simulateResponseProgress(xhr);
882
+ }));
883
+ }
884
+ simulateResponseProgress(xhr) {
885
+ let progress = 90;
886
+ const startTime = Date.now();
887
+ const updateProgress = () => {
888
+ const elapsed = Date.now() - startTime;
889
+ const estimatedResponseTime = this.estimateResponseTime();
890
+ const responseProgress = Math.min(elapsed / estimatedResponseTime, 1);
891
+ progress = 90 + responseProgress * 9;
892
+ this.dispatch("progress", {
893
+ progress: progress
894
+ });
895
+ if (xhr.readyState !== XMLHttpRequest.DONE && progress < 99) {
896
+ requestAnimationFrame(updateProgress);
897
+ }
898
+ };
899
+ xhr.addEventListener("loadend", (() => {
900
+ this.dispatch("progress", {
901
+ progress: 100
902
+ });
903
+ }));
904
+ requestAnimationFrame(updateProgress);
905
+ }
906
+ estimateResponseTime() {
907
+ const fileSize = this.file.size;
908
+ const MB = 1024 * 1024;
909
+ if (fileSize < MB) {
910
+ return 1e3;
911
+ } else if (fileSize < 10 * MB) {
912
+ return 2e3;
913
+ } else {
914
+ return 3e3 + fileSize / MB * 50;
915
+ }
841
916
  }
842
- directUploadDidComplete(error, attributes) {
917
+ directUploadDidComplete(error, attributes, resolve, reject) {
843
918
  if (error) {
844
- this.dispatchError(error);
919
+ this.dispatchError(error, reject);
845
920
  } else {
846
- this.attachment.setAttributes({
921
+ resolve({
847
922
  sgid: attributes.attachable_sgid,
848
923
  url: this.createBlobUrl(attributes.signed_id, attributes.filename)
849
924
  });
@@ -859,12 +934,12 @@
859
934
  detail: detail
860
935
  });
861
936
  }
862
- dispatchError(error) {
937
+ dispatchError(error, reject) {
863
938
  const event = this.dispatch("error", {
864
939
  error: error
865
940
  });
866
941
  if (!event.defaultPrevented) {
867
- alert(error);
942
+ reject(error);
868
943
  }
869
944
  }
870
945
  get directUploadUrl() {
@@ -877,8 +952,14 @@
877
952
  addEventListener("trix-attachment-add", (event => {
878
953
  const {attachment: attachment, target: target} = event;
879
954
  if (attachment.file) {
880
- const upload = new AttachmentUpload(attachment, target);
881
- upload.start();
955
+ const upload = new AttachmentUpload(attachment, target, attachment.file);
956
+ const onProgress = event => attachment.setUploadProgress(event.detail.progress);
957
+ target.addEventListener("direct-upload:progress", onProgress);
958
+ upload.start().then((attributes => attachment.setAttributes(attributes))).catch((error => alert(error))).finally((() => target.removeEventListener("direct-upload:progress", onProgress)));
882
959
  }
883
960
  }));
961
+ exports.AttachmentUpload = AttachmentUpload;
962
+ Object.defineProperty(exports, "__esModule", {
963
+ value: true
964
+ });
884
965
  }));
@@ -27,7 +27,14 @@ module ActionText
27
27
  # rich_textarea_tag "content", message.content
28
28
  # # <input type="hidden" name="content" id="trix_input_post_1">
29
29
  # # <trix-editor id="content" input="trix_input_post_1" class="trix-content" ...></trix-editor>
30
- def rich_textarea_tag(name, value = nil, options = {})
30
+ #
31
+ # rich_textarea_tag "content", nil do
32
+ # "<h1>Default content</h1>"
33
+ # end
34
+ # # <input type="hidden" name="content" id="trix_input_post_1" value="&lt;h1&gt;Default content&lt;/h1&gt;">
35
+ # # <trix-editor id="content" input="trix_input_post_1" class="trix-content" ...></trix-editor>
36
+ def rich_textarea_tag(name, value = nil, options = {}, &block)
37
+ value = capture(&block) if value.nil? && block_given?
31
38
  options = options.symbolize_keys
32
39
  form = options.delete(:form)
33
40
 
@@ -53,11 +60,11 @@ module ActionView::Helpers
53
60
 
54
61
  delegate :dom_id, to: ActionView::RecordIdentifier
55
62
 
56
- def render
63
+ def render(&block)
57
64
  options = @options.stringify_keys
58
65
  add_default_name_and_field(options)
59
66
  options["input"] ||= dom_id(object, [options["id"], :trix_input].compact.join("_")) if object
60
- html_tag = @template_object.rich_textarea_tag(options.delete("name"), options.fetch("value") { value }, options.except("value"))
67
+ html_tag = @template_object.rich_textarea_tag(options.delete("name"), options.fetch("value") { value }, options.except("value"), &block)
61
68
  error_wrapping(html_tag)
62
69
  end
63
70
  end
@@ -82,10 +89,16 @@ module ActionView::Helpers
82
89
  # # <trix-editor id="content" input="message_content_trix_input_message_1" class="trix-content" ...></trix-editor>
83
90
  #
84
91
  # rich_textarea :message, :content, value: "<h1>Default message</h1>"
85
- # # <input type="hidden" name="message[content]" id="message_content_trix_input_message_1" value="<h1>Default message</h1>">
92
+ # # <input type="hidden" name="message[content]" id="message_content_trix_input_message_1" value="&lt;h1&gt;Default message&lt;/h1&gt;">
93
+ # # <trix-editor id="content" input="message_content_trix_input_message_1" class="trix-content" ...></trix-editor>
94
+ #
95
+ # rich_textarea :message, :content do
96
+ # "<h1>Default message</h1>"
97
+ # end
98
+ # # <input type="hidden" name="message[content]" id="message_content_trix_input_message_1" value="&lt;h1&gt;Default message&lt;/h1&gt;">
86
99
  # # <trix-editor id="content" input="message_content_trix_input_message_1" class="trix-content" ...></trix-editor>
87
- def rich_textarea(object_name, method, options = {})
88
- Tags::ActionText.new(object_name, method, self, options).render
100
+ def rich_textarea(object_name, method, options = {}, &block)
101
+ Tags::ActionText.new(object_name, method, self, options).render(&block)
89
102
  end
90
103
  alias_method :rich_text_area, :rich_textarea
91
104
  end
@@ -98,8 +111,8 @@ module ActionView::Helpers
98
111
  # <% end %>
99
112
  #
100
113
  # Please refer to the documentation of the base helper for details.
101
- def rich_textarea(method, options = {})
102
- @template.rich_textarea(@object_name, method, objectify_options(options))
114
+ def rich_textarea(method, options = {}, &block)
115
+ @template.rich_textarea(@object_name, method, objectify_options(options), &block)
103
116
  end
104
117
  alias_method :rich_text_area, :rich_textarea
105
118
  end
@@ -1,32 +1,81 @@
1
1
  import { DirectUpload, dispatchEvent } from "@rails/activestorage"
2
2
 
3
3
  export class AttachmentUpload {
4
- constructor(attachment, element) {
4
+ constructor(attachment, element, file = attachment.file) {
5
5
  this.attachment = attachment
6
6
  this.element = element
7
- this.directUpload = new DirectUpload(attachment.file, this.directUploadUrl, this)
7
+ this.directUpload = new DirectUpload(file, this.directUploadUrl, this)
8
+ this.file = file
8
9
  }
9
10
 
10
11
  start() {
11
- this.directUpload.create(this.directUploadDidComplete.bind(this))
12
- this.dispatch("start")
12
+ return new Promise((resolve, reject) => {
13
+ this.directUpload.create((error, attributes) => this.directUploadDidComplete(error, attributes, resolve, reject))
14
+ this.dispatch("start")
15
+ })
13
16
  }
14
17
 
15
18
  directUploadWillStoreFileWithXHR(xhr) {
16
19
  xhr.upload.addEventListener("progress", event => {
17
- const progress = event.loaded / event.total * 100
18
- this.attachment.setUploadProgress(progress)
20
+ // Scale upload progress to 0-90% range
21
+ const progress = (event.loaded / event.total) * 90
19
22
  if (progress) {
20
23
  this.dispatch("progress", { progress: progress })
21
24
  }
22
25
  })
26
+
27
+ // Start simulating progress after upload completes
28
+ xhr.upload.addEventListener("loadend", () => {
29
+ this.simulateResponseProgress(xhr)
30
+ })
31
+ }
32
+
33
+ simulateResponseProgress(xhr) {
34
+ let progress = 90
35
+ const startTime = Date.now()
36
+
37
+ const updateProgress = () => {
38
+ // Simulate progress from 90% to 99% over estimated time
39
+ const elapsed = Date.now() - startTime
40
+ const estimatedResponseTime = this.estimateResponseTime()
41
+ const responseProgress = Math.min(elapsed / estimatedResponseTime, 1)
42
+ progress = 90 + (responseProgress * 9) // 90% to 99%
43
+
44
+ this.dispatch("progress", { progress })
45
+
46
+ // Continue until response arrives or we hit 99%
47
+ if (xhr.readyState !== XMLHttpRequest.DONE && progress < 99) {
48
+ requestAnimationFrame(updateProgress)
49
+ }
50
+ }
51
+
52
+ // Stop simulation when response arrives
53
+ xhr.addEventListener("loadend", () => {
54
+ this.dispatch("progress", { progress: 100 })
55
+ })
56
+
57
+ requestAnimationFrame(updateProgress)
58
+ }
59
+
60
+ estimateResponseTime() {
61
+ // Base estimate: 1 second for small files, scaling up for larger files
62
+ const fileSize = this.file.size
63
+ const MB = 1024 * 1024
64
+
65
+ if (fileSize < MB) {
66
+ return 1000 // 1 second for files under 1MB
67
+ } else if (fileSize < 10 * MB) {
68
+ return 2000 // 2 seconds for files 1-10MB
69
+ } else {
70
+ return 3000 + (fileSize / MB * 50) // 3+ seconds for larger files
71
+ }
23
72
  }
24
73
 
25
- directUploadDidComplete(error, attributes) {
74
+ directUploadDidComplete(error, attributes, resolve, reject) {
26
75
  if (error) {
27
- this.dispatchError(error)
76
+ this.dispatchError(error, reject)
28
77
  } else {
29
- this.attachment.setAttributes({
78
+ resolve({
30
79
  sgid: attributes.attachable_sgid,
31
80
  url: this.createBlobUrl(attributes.signed_id, attributes.filename)
32
81
  })
@@ -45,10 +94,10 @@ export class AttachmentUpload {
45
94
  return dispatchEvent(this.element, `direct-upload:${name}`, { detail })
46
95
  }
47
96
 
48
- dispatchError(error) {
97
+ dispatchError(error, reject) {
49
98
  const event = this.dispatch("error", { error })
50
99
  if (!event.defaultPrevented) {
51
- alert(error);
100
+ reject(error)
52
101
  }
53
102
  }
54
103
 
@@ -4,7 +4,16 @@ addEventListener("trix-attachment-add", event => {
4
4
  const { attachment, target } = event
5
5
 
6
6
  if (attachment.file) {
7
- const upload = new AttachmentUpload(attachment, target)
7
+ const upload = new AttachmentUpload(attachment, target, attachment.file)
8
+ const onProgress = event => attachment.setUploadProgress(event.detail.progress)
9
+
10
+ target.addEventListener("direct-upload:progress", onProgress)
11
+
8
12
  upload.start()
13
+ .then(attributes => attachment.setAttributes(attributes))
14
+ .catch(error => alert(error))
15
+ .finally(() => target.removeEventListener("direct-upload:progress", onProgress))
9
16
  }
10
17
  })
18
+
19
+ export { AttachmentUpload }