actiontext 8.1.0.beta1 → 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: 56212aa1ae96d6d13c6ed5e92259b100dcfdf2b8547f224458e7c4dcd763ad49
4
- data.tar.gz: 8da839fef7a4ecf0889705929429ab2fc40b89527e5e9a97a7803c8c32806f3b
3
+ metadata.gz: 8f8a237e651fd987b5cd33e599577648aedf8a2ff14decaee0b2a3c2b2c6ab1c
4
+ data.tar.gz: 9a84f4bb3bc90cddc9c651e3571ee872a87add9150b33e28d3eef50e728493f3
5
5
  SHA512:
6
- metadata.gz: ec1b6d10b4b38e06e0cc36e99ea293b12344233f1d5291eeb0655f1f828726b46ca0d4e4c0cf5dfb27f3f4ba88752d32d197c614ba561b29bdda944174ad93ec
7
- data.tar.gz: fdae5de429bea12f1054e08f7c524cd89b1397b03d73d57485f9ec7a1a839bb278b85392b1c6b1961ad23cf08eaa2e1934fd60eea2e473c178cbb896341c0ac9
6
+ metadata.gz: c3fb8c1e86cd5017a454ba8615db889b4775ea0afe9f276fcfb46ed2b82583a85a6a11daf097624104a1600753b9c995c5dd7720ce289499fab9d7e57814ab22
7
+ data.tar.gz: 340ac8fc6e495d800d75ba68c205b9213d34e2cc925b542e4f46eb32fcdebda2fe4d439125219bc6d1cab4b3b01789034e0c163e23868c51c5293c294db06d31
data/CHANGELOG.md CHANGED
@@ -1,3 +1,43 @@
1
+ ## Rails 8.1.0.rc1 (October 15, 2025) ##
2
+
3
+ * De-couple `@rails/actiontext/attachment_upload.js` from `Trix.Attachment`
4
+
5
+ Implement `@rails/actiontext/index.js` with a `direct-upload:progress` event
6
+ listeners and `Promise` resolution.
7
+
8
+ *Sean Doyle*
9
+
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
+ ```
30
+
31
+ *Sean Doyle*
32
+
33
+ * Generalize `:rich_text_area` Capybara selector
34
+
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.
38
+
39
+ *Sean Doyle*
40
+
1
41
  ## Rails 8.1.0.beta1 (September 04, 2025) ##
2
42
 
3
43
  * Forward `fill_in_rich_text_area` options to Capybara
@@ -882,19 +882,21 @@ function autostart() {
882
882
  setTimeout(autostart, 1);
883
883
 
884
884
  class AttachmentUpload {
885
- constructor(attachment, element) {
885
+ constructor(attachment, element, file = attachment.file) {
886
886
  this.attachment = attachment;
887
887
  this.element = element;
888
- this.directUpload = new DirectUpload(attachment.file, this.directUploadUrl, this);
888
+ this.directUpload = new DirectUpload(file, this.directUploadUrl, this);
889
+ this.file = file;
889
890
  }
890
891
  start() {
891
- this.directUpload.create(this.directUploadDidComplete.bind(this));
892
- 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
+ }));
893
896
  }
894
897
  directUploadWillStoreFileWithXHR(xhr) {
895
898
  xhr.upload.addEventListener("progress", (event => {
896
899
  const progress = event.loaded / event.total * 90;
897
- this.attachment.setUploadProgress(progress);
898
900
  if (progress) {
899
901
  this.dispatch("progress", {
900
902
  progress: progress
@@ -913,7 +915,6 @@ class AttachmentUpload {
913
915
  const estimatedResponseTime = this.estimateResponseTime();
914
916
  const responseProgress = Math.min(elapsed / estimatedResponseTime, 1);
915
917
  progress = 90 + responseProgress * 9;
916
- this.attachment.setUploadProgress(progress);
917
918
  this.dispatch("progress", {
918
919
  progress: progress
919
920
  });
@@ -922,7 +923,6 @@ class AttachmentUpload {
922
923
  }
923
924
  };
924
925
  xhr.addEventListener("loadend", (() => {
925
- this.attachment.setUploadProgress(100);
926
926
  this.dispatch("progress", {
927
927
  progress: 100
928
928
  });
@@ -930,7 +930,7 @@ class AttachmentUpload {
930
930
  requestAnimationFrame(updateProgress);
931
931
  }
932
932
  estimateResponseTime() {
933
- const fileSize = this.attachment.file.size;
933
+ const fileSize = this.file.size;
934
934
  const MB = 1024 * 1024;
935
935
  if (fileSize < MB) {
936
936
  return 1e3;
@@ -940,11 +940,11 @@ class AttachmentUpload {
940
940
  return 3e3 + fileSize / MB * 50;
941
941
  }
942
942
  }
943
- directUploadDidComplete(error, attributes) {
943
+ directUploadDidComplete(error, attributes, resolve, reject) {
944
944
  if (error) {
945
- this.dispatchError(error);
945
+ this.dispatchError(error, reject);
946
946
  } else {
947
- this.attachment.setAttributes({
947
+ resolve({
948
948
  sgid: attributes.attachable_sgid,
949
949
  url: this.createBlobUrl(attributes.signed_id, attributes.filename)
950
950
  });
@@ -960,12 +960,12 @@ class AttachmentUpload {
960
960
  detail: detail
961
961
  });
962
962
  }
963
- dispatchError(error) {
963
+ dispatchError(error, reject) {
964
964
  const event = this.dispatch("error", {
965
965
  error: error
966
966
  });
967
967
  if (!event.defaultPrevented) {
968
- alert(error);
968
+ reject(error);
969
969
  }
970
970
  }
971
971
  get directUploadUrl() {
@@ -979,7 +979,11 @@ class AttachmentUpload {
979
979
  addEventListener("trix-attachment-add", (event => {
980
980
  const {attachment: attachment, target: target} = event;
981
981
  if (attachment.file) {
982
- const upload = new AttachmentUpload(attachment, target);
983
- 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)));
984
986
  }
985
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: {}
@@ -855,19 +856,21 @@
855
856
  }
856
857
  setTimeout(autostart, 1);
857
858
  class AttachmentUpload {
858
- constructor(attachment, element) {
859
+ constructor(attachment, element, file = attachment.file) {
859
860
  this.attachment = attachment;
860
861
  this.element = element;
861
- this.directUpload = new DirectUpload(attachment.file, this.directUploadUrl, this);
862
+ this.directUpload = new DirectUpload(file, this.directUploadUrl, this);
863
+ this.file = file;
862
864
  }
863
865
  start() {
864
- this.directUpload.create(this.directUploadDidComplete.bind(this));
865
- 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
+ }));
866
870
  }
867
871
  directUploadWillStoreFileWithXHR(xhr) {
868
872
  xhr.upload.addEventListener("progress", (event => {
869
873
  const progress = event.loaded / event.total * 90;
870
- this.attachment.setUploadProgress(progress);
871
874
  if (progress) {
872
875
  this.dispatch("progress", {
873
876
  progress: progress
@@ -886,7 +889,6 @@
886
889
  const estimatedResponseTime = this.estimateResponseTime();
887
890
  const responseProgress = Math.min(elapsed / estimatedResponseTime, 1);
888
891
  progress = 90 + responseProgress * 9;
889
- this.attachment.setUploadProgress(progress);
890
892
  this.dispatch("progress", {
891
893
  progress: progress
892
894
  });
@@ -895,7 +897,6 @@
895
897
  }
896
898
  };
897
899
  xhr.addEventListener("loadend", (() => {
898
- this.attachment.setUploadProgress(100);
899
900
  this.dispatch("progress", {
900
901
  progress: 100
901
902
  });
@@ -903,7 +904,7 @@
903
904
  requestAnimationFrame(updateProgress);
904
905
  }
905
906
  estimateResponseTime() {
906
- const fileSize = this.attachment.file.size;
907
+ const fileSize = this.file.size;
907
908
  const MB = 1024 * 1024;
908
909
  if (fileSize < MB) {
909
910
  return 1e3;
@@ -913,11 +914,11 @@
913
914
  return 3e3 + fileSize / MB * 50;
914
915
  }
915
916
  }
916
- directUploadDidComplete(error, attributes) {
917
+ directUploadDidComplete(error, attributes, resolve, reject) {
917
918
  if (error) {
918
- this.dispatchError(error);
919
+ this.dispatchError(error, reject);
919
920
  } else {
920
- this.attachment.setAttributes({
921
+ resolve({
921
922
  sgid: attributes.attachable_sgid,
922
923
  url: this.createBlobUrl(attributes.signed_id, attributes.filename)
923
924
  });
@@ -933,12 +934,12 @@
933
934
  detail: detail
934
935
  });
935
936
  }
936
- dispatchError(error) {
937
+ dispatchError(error, reject) {
937
938
  const event = this.dispatch("error", {
938
939
  error: error
939
940
  });
940
941
  if (!event.defaultPrevented) {
941
- alert(error);
942
+ reject(error);
942
943
  }
943
944
  }
944
945
  get directUploadUrl() {
@@ -951,8 +952,14 @@
951
952
  addEventListener("trix-attachment-add", (event => {
952
953
  const {attachment: attachment, target: target} = event;
953
954
  if (attachment.file) {
954
- const upload = new AttachmentUpload(attachment, target);
955
- 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)));
956
959
  }
957
960
  }));
961
+ exports.AttachmentUpload = AttachmentUpload;
962
+ Object.defineProperty(exports, "__esModule", {
963
+ value: true
964
+ });
958
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
- add_default_name_and_id(options)
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,22 +1,24 @@
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
20
  // Scale upload progress to 0-90% range
18
21
  const progress = (event.loaded / event.total) * 90
19
- this.attachment.setUploadProgress(progress)
20
22
  if (progress) {
21
23
  this.dispatch("progress", { progress: progress })
22
24
  }
@@ -39,7 +41,6 @@ export class AttachmentUpload {
39
41
  const responseProgress = Math.min(elapsed / estimatedResponseTime, 1)
40
42
  progress = 90 + (responseProgress * 9) // 90% to 99%
41
43
 
42
- this.attachment.setUploadProgress(progress)
43
44
  this.dispatch("progress", { progress })
44
45
 
45
46
  // Continue until response arrives or we hit 99%
@@ -50,7 +51,6 @@ export class AttachmentUpload {
50
51
 
51
52
  // Stop simulation when response arrives
52
53
  xhr.addEventListener("loadend", () => {
53
- this.attachment.setUploadProgress(100)
54
54
  this.dispatch("progress", { progress: 100 })
55
55
  })
56
56
 
@@ -59,7 +59,7 @@ export class AttachmentUpload {
59
59
 
60
60
  estimateResponseTime() {
61
61
  // Base estimate: 1 second for small files, scaling up for larger files
62
- const fileSize = this.attachment.file.size
62
+ const fileSize = this.file.size
63
63
  const MB = 1024 * 1024
64
64
 
65
65
  if (fileSize < MB) {
@@ -71,11 +71,11 @@ export class AttachmentUpload {
71
71
  }
72
72
  }
73
73
 
74
- directUploadDidComplete(error, attributes) {
74
+ directUploadDidComplete(error, attributes, resolve, reject) {
75
75
  if (error) {
76
- this.dispatchError(error)
76
+ this.dispatchError(error, reject)
77
77
  } else {
78
- this.attachment.setAttributes({
78
+ resolve({
79
79
  sgid: attributes.attachable_sgid,
80
80
  url: this.createBlobUrl(attributes.signed_id, attributes.filename)
81
81
  })
@@ -94,10 +94,10 @@ export class AttachmentUpload {
94
94
  return dispatchEvent(this.element, `direct-upload:${name}`, { detail })
95
95
  }
96
96
 
97
- dispatchError(error) {
97
+ dispatchError(error, reject) {
98
98
  const event = this.dispatch("error", { error })
99
99
  if (!event.defaultPrevented) {
100
- alert(error);
100
+ reject(error)
101
101
  }
102
102
  }
103
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 }
@@ -12,7 +12,7 @@ module ActionText
12
12
  MAJOR = 8
13
13
  MINOR = 1
14
14
  TINY = 0
15
- PRE = "beta1"
15
+ PRE = "rc1"
16
16
 
17
17
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
18
18
  end
@@ -7,70 +7,70 @@ module ActionText
7
7
  extend self
8
8
 
9
9
  def node_to_plain_text(node)
10
- remove_trailing_newlines(plain_text_for_node(node))
10
+ BottomUpReducer.new(node).reduce do |n, child_values|
11
+ plain_text_for_node(n, child_values)
12
+ end.then(&method(:remove_trailing_newlines))
11
13
  end
12
14
 
13
15
  private
14
- def plain_text_for_node(node, index = 0)
16
+ def plain_text_for_node(node, child_values)
15
17
  if respond_to?(plain_text_method_for_node(node), true)
16
- send(plain_text_method_for_node(node), node, index)
18
+ send(plain_text_method_for_node(node), node, child_values)
17
19
  else
18
- plain_text_for_node_children(node)
20
+ plain_text_for_child_values(child_values)
19
21
  end
20
22
  end
21
23
 
22
- def plain_text_for_node_children(node)
23
- texts = []
24
- node.children.each_with_index do |child, index|
25
- next if skippable?(child)
24
+ def plain_text_method_for_node(node)
25
+ :"plain_text_for_#{node.name}_node"
26
+ end
26
27
 
27
- texts << plain_text_for_node(child, index)
28
- end
29
- texts.join
28
+ def plain_text_for_child_values(child_values)
29
+ child_values.join
30
30
  end
31
31
 
32
- def skippable?(node)
33
- node.name == "script" || node.name == "style"
32
+ def plain_text_for_unsupported_node(node, _child_values)
33
+ ""
34
34
  end
35
35
 
36
- def plain_text_method_for_node(node)
37
- :"plain_text_for_#{node.name}_node"
36
+ %i[ script style].each do |element|
37
+ alias_method :"plain_text_for_#{element}_node", :plain_text_for_unsupported_node
38
38
  end
39
39
 
40
- def plain_text_for_block(node, index = 0)
41
- "#{remove_trailing_newlines(plain_text_for_node_children(node))}\n\n"
40
+ def plain_text_for_block(node, child_values)
41
+ "#{remove_trailing_newlines(plain_text_for_child_values(child_values))}\n\n"
42
42
  end
43
43
 
44
44
  %i[ h1 p ].each do |element|
45
45
  alias_method :"plain_text_for_#{element}_node", :plain_text_for_block
46
46
  end
47
47
 
48
- def plain_text_for_list(node, index)
49
- "#{break_if_nested_list(node, plain_text_for_block(node))}"
48
+ def plain_text_for_list(node, child_values)
49
+ "#{break_if_nested_list(node, plain_text_for_block(node, child_values))}"
50
50
  end
51
51
 
52
52
  %i[ ul ol ].each do |element|
53
53
  alias_method :"plain_text_for_#{element}_node", :plain_text_for_list
54
54
  end
55
55
 
56
- def plain_text_for_br_node(node, index)
56
+ def plain_text_for_br_node(node, _child_values)
57
57
  "\n"
58
58
  end
59
59
 
60
- def plain_text_for_text_node(node, index)
60
+ def plain_text_for_text_node(node, _child_values)
61
61
  remove_trailing_newlines(node.text)
62
62
  end
63
63
 
64
- def plain_text_for_div_node(node, index)
65
- "#{remove_trailing_newlines(plain_text_for_node_children(node))}\n"
64
+ def plain_text_for_div_node(node, child_values)
65
+ "#{remove_trailing_newlines(plain_text_for_child_values(child_values))}\n"
66
66
  end
67
67
 
68
- def plain_text_for_figcaption_node(node, index)
69
- "[#{remove_trailing_newlines(plain_text_for_node_children(node))}]"
68
+ def plain_text_for_figcaption_node(node, child_values)
69
+ "[#{remove_trailing_newlines(plain_text_for_child_values(child_values))}]"
70
70
  end
71
71
 
72
- def plain_text_for_blockquote_node(node, index)
73
- text = plain_text_for_block(node)
72
+ def plain_text_for_blockquote_node(node, child_values)
73
+ text = plain_text_for_block(node, child_values)
74
74
  return "“”" if text.blank?
75
75
 
76
76
  text = text.dup
@@ -79,9 +79,9 @@ module ActionText
79
79
  text
80
80
  end
81
81
 
82
- def plain_text_for_li_node(node, index)
83
- bullet = bullet_for_li_node(node, index)
84
- text = remove_trailing_newlines(plain_text_for_node_children(node))
82
+ def plain_text_for_li_node(node, child_values)
83
+ bullet = bullet_for_li_node(node)
84
+ text = remove_trailing_newlines(plain_text_for_child_values(child_values))
85
85
  indentation = indentation_for_li_node(node)
86
86
 
87
87
  "#{indentation}#{bullet} #{text}\n"
@@ -91,8 +91,9 @@ module ActionText
91
91
  text.chomp("")
92
92
  end
93
93
 
94
- def bullet_for_li_node(node, index)
94
+ def bullet_for_li_node(node)
95
95
  if list_node_name_for_li_node(node) == "ol"
96
+ index = node.parent.elements.index(node)
96
97
  "#{index + 1}."
97
98
  else
98
99
  "•"
@@ -121,5 +122,33 @@ module ActionText
121
122
  text
122
123
  end
123
124
  end
125
+
126
+ class BottomUpReducer # :nodoc:
127
+ def initialize(node)
128
+ @node = node
129
+ @values = {}
130
+ end
131
+
132
+ def reduce(&block)
133
+ traverse_bottom_up(@node) do |n|
134
+ child_values = @values.values_at(*n.children)
135
+ @values[n] = block.call(n, child_values)
136
+ end
137
+ @values[@node]
138
+ end
139
+
140
+ private
141
+ def traverse_bottom_up(node, &block)
142
+ call_stack, processing_stack = [ node ], []
143
+
144
+ until call_stack.empty?
145
+ node = call_stack.pop
146
+ processing_stack.push(node)
147
+ call_stack.concat node.children
148
+ end
149
+
150
+ processing_stack.reverse_each(&block)
151
+ end
152
+ end
124
153
  end
125
154
  end
@@ -35,7 +35,13 @@ module ActionText
35
35
  # # <trix-editor input="trix_input_1"></trix-editor>
36
36
  # fill_in_rich_textarea "message[content]", with: "Hello <em>world!</em>"
37
37
  def fill_in_rich_textarea(locator = nil, with:, **)
38
- find(:rich_textarea, locator, **).execute_script("this.editor.loadHTML(arguments[0])", with.to_s)
38
+ find(:rich_textarea, locator, **).execute_script(<<~JS, with.to_s)
39
+ if ("value" in this) {
40
+ this.value = arguments[0]
41
+ } else {
42
+ this.editor.loadHTML(arguments[0])
43
+ }
44
+ JS
39
45
  end
40
46
  alias_method :fill_in_rich_text_area, :fill_in_rich_textarea
41
47
  end
@@ -45,13 +51,18 @@ end
45
51
  Capybara.add_selector rich_textarea do
46
52
  label "rich-text area"
47
53
  xpath do |locator|
54
+ xpath = XPath.descendant[[
55
+ XPath.attribute(:role) == "textbox",
56
+ (XPath.attribute(:contenteditable) == "") | (XPath.attribute(:contenteditable) == "true")
57
+ ].reduce(:&)]
58
+
48
59
  if locator.nil?
49
- XPath.descendant(:"trix-editor")
60
+ xpath
50
61
  else
51
62
  input_located_by_name = XPath.anywhere(:input).where(XPath.attr(:name) == locator).attr(:id)
52
63
  input_located_by_label = XPath.anywhere(:label).where(XPath.string.n.is(locator)).attr(:for)
53
64
 
54
- XPath.descendant(:"trix-editor").where \
65
+ xpath.where \
55
66
  XPath.attr(:id).equals(locator) |
56
67
  XPath.attr(:placeholder).equals(locator) |
57
68
  XPath.attr(:"aria-label").equals(locator) |
@@ -47,18 +47,6 @@ module ActionText
47
47
  "app/views/layouts/action_text/contents/_content.html.erb"
48
48
  end
49
49
 
50
- def enable_image_processing_gem
51
- if (gemfile_path = Pathname(destination_root).join("Gemfile")).exist?
52
- say "Ensure image_processing gem has been enabled so image uploads will work (remember to bundle!)"
53
- image_processing_regex = /gem ["']image_processing["']/
54
- if File.readlines(gemfile_path).grep(image_processing_regex).any?
55
- uncomment_lines gemfile_path, image_processing_regex
56
- else
57
- run "bundle add --skip-install image_processing"
58
- end
59
- end
60
- end
61
-
62
50
  def create_migrations
63
51
  rails_command "railties:install:migrations FROM=active_storage,action_text", inline: true
64
52
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rails/actiontext",
3
- "version": "8.1.0-beta1",
3
+ "version": "8.1.0-rc1",
4
4
  "description": "Edit and display rich text in Rails applications",
5
5
  "module": "app/assets/javascripts/actiontext.esm.js",
6
6
  "main": "app/assets/javascripts/actiontext.js",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actiontext
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.1.0.beta1
4
+ version: 8.1.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Javan Makhmali
@@ -17,56 +17,56 @@ dependencies:
17
17
  requirements:
18
18
  - - '='
19
19
  - !ruby/object:Gem::Version
20
- version: 8.1.0.beta1
20
+ version: 8.1.0.rc1
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
25
  - - '='
26
26
  - !ruby/object:Gem::Version
27
- version: 8.1.0.beta1
27
+ version: 8.1.0.rc1
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: activerecord
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
32
  - - '='
33
33
  - !ruby/object:Gem::Version
34
- version: 8.1.0.beta1
34
+ version: 8.1.0.rc1
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - '='
40
40
  - !ruby/object:Gem::Version
41
- version: 8.1.0.beta1
41
+ version: 8.1.0.rc1
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: activestorage
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
46
  - - '='
47
47
  - !ruby/object:Gem::Version
48
- version: 8.1.0.beta1
48
+ version: 8.1.0.rc1
49
49
  type: :runtime
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
53
  - - '='
54
54
  - !ruby/object:Gem::Version
55
- version: 8.1.0.beta1
55
+ version: 8.1.0.rc1
56
56
  - !ruby/object:Gem::Dependency
57
57
  name: actionpack
58
58
  requirement: !ruby/object:Gem::Requirement
59
59
  requirements:
60
60
  - - '='
61
61
  - !ruby/object:Gem::Version
62
- version: 8.1.0.beta1
62
+ version: 8.1.0.rc1
63
63
  type: :runtime
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
67
  - - '='
68
68
  - !ruby/object:Gem::Version
69
- version: 8.1.0.beta1
69
+ version: 8.1.0.rc1
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: nokogiri
72
72
  requirement: !ruby/object:Gem::Requirement
@@ -174,10 +174,10 @@ licenses:
174
174
  - MIT
175
175
  metadata:
176
176
  bug_tracker_uri: https://github.com/rails/rails/issues
177
- changelog_uri: https://github.com/rails/rails/blob/v8.1.0.beta1/actiontext/CHANGELOG.md
178
- documentation_uri: https://api.rubyonrails.org/v8.1.0.beta1/
177
+ changelog_uri: https://github.com/rails/rails/blob/v8.1.0.rc1/actiontext/CHANGELOG.md
178
+ documentation_uri: https://api.rubyonrails.org/v8.1.0.rc1/
179
179
  mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
180
- source_code_uri: https://github.com/rails/rails/tree/v8.1.0.beta1/actiontext
180
+ source_code_uri: https://github.com/rails/rails/tree/v8.1.0.rc1/actiontext
181
181
  rubygems_mfa_required: 'true'
182
182
  rdoc_options: []
183
183
  require_paths: