diff --git a/app/serializers/api/product_serializer.rb b/app/serializers/api/product_serializer.rb
index cf37d7464a..11f228c1e6 100644
--- a/app/serializers/api/product_serializer.rb
+++ b/app/serializers/api/product_serializer.rb
@@ -21,7 +21,7 @@ class Api::ProductSerializer < ActiveModel::Serializer
# return a sanitized html description
def description_html
- sanitizer.sanitize_content(object.description)&.html_safe
+ trix_sanitizer.sanitize_content(object.description)
end
def properties_with_values
@@ -37,4 +37,8 @@ class Api::ProductSerializer < ActiveModel::Serializer
def sanitizer
@sanitizer ||= ContentSanitizer.new
end
+
+ def trix_sanitizer
+ @trix_sanitizer ||= TrixSanitizer.new
+ end
end
diff --git a/app/services/trix_sanitizer.rb b/app/services/trix_sanitizer.rb
new file mode 100644
index 0000000000..5b4d83b4e6
--- /dev/null
+++ b/app/services/trix_sanitizer.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class TrixSanitizer
+ include ActionView::Helpers::SanitizeHelper
+
+ def sanitize_content(content)
+ return if content.blank?
+
+ sanitize(content.to_s, scrubber: TrixScrubber.new)
+ end
+end
diff --git a/app/services/trix_scrubber.rb b/app/services/trix_scrubber.rb
index b8328ffc1c..e8ab55afe2 100644
--- a/app/services/trix_scrubber.rb
+++ b/app/services/trix_scrubber.rb
@@ -2,7 +2,7 @@
class TrixScrubber < Rails::Html::PermitScrubber
ALLOWED_TAGS = ["p", "b", "strong", "em", "i", "a", "u", "br", "del", "h1", "blockquote", "pre",
- "ul", "ol", "li"].freeze
+ "ul", "ol", "li", "div", "hr"].freeze
ALLOWED_ATTRIBUTES = ["href", "target", "src", "alt"].freeze
def initialize
diff --git a/app/views/spree/admin/products/_form.html.haml b/app/views/spree/admin/products/_form.html.haml
index 2935fa0dc5..1bd362b7e7 100644
--- a/app/views/spree/admin/products/_form.html.haml
+++ b/app/views/spree/admin/products/_form.html.haml
@@ -7,9 +7,8 @@
= f.field_container :description do
= f.label :description, t(:description)
- %text-angular{'id' => 'product_description', 'name' => 'product[description]', 'class' => 'text-angular', 'textangular-unsafe-sanitizer' => true, "textangular-links-target-blank" => true, 'ta-toolbar' => "[['bold','italics','underline','clear'],['insertLink']]"}
- = sanitize(@product.description, scrubber: ContentScrubber.new)
- = f.error_message_on :description
+ = f.hidden_field :description, id: "product_description", value: @product.description
+ %trix-editor{ input: "product_description", "data-controller": "trixeditor" }
.right.four.columns.omega
.variant_units_form{ 'ng-app' => 'admin.products', 'ng-controller' => 'editUnitsCtrl' }
diff --git a/app/views/spree/admin/products/new.html.haml b/app/views/spree/admin/products/new.html.haml
index a885d77300..71710c1258 100644
--- a/app/views/spree/admin/products/new.html.haml
+++ b/app/views/spree/admin/products/new.html.haml
@@ -92,8 +92,8 @@
= f.field_container :description do
= f.label :product_description, t(".product_description")
%br/
- %text-angular{'id' => 'product_description', 'name' => 'product[description]', 'class' => 'text-angular', "textangular-links-target-blank" => true, 'ta-toolbar' => "[['bold','italics','underline','clear'],['insertLink']]", "ng-model": "product.description"}
- = sanitize(@product.description)
+ = f.hidden_field :description, id: "product_description", value: @product.description
+ %trix-editor{ input: "product_description", "data-controller": "trixeditor" }
= f.error_message_on :description
.four.columns.omega{ style: "text-align: center" }
%fieldset.no-border-bottom{ id: "image" }
diff --git a/app/webpacker/controllers/trixeditor_controller.js b/app/webpacker/controllers/trixeditor_controller.js
index 7975d213ed..506086fb2b 100644
--- a/app/webpacker/controllers/trixeditor_controller.js
+++ b/app/webpacker/controllers/trixeditor_controller.js
@@ -3,10 +3,43 @@ import { Controller } from "stimulus";
export default class extends Controller {
connect() {
window.addEventListener("trix-change", this.#trixChange);
+ this.#trixInitialize();
+ window.addEventListener("trix-initialize", this.#trixInitialize);
}
#trixChange = (event) => {
// trigger a change event on the form that contains the Trix editor
event.target.form.dispatchEvent(new Event("change", { bubbles: true }));
};
+
+ #trixActionInvoke = (event) => {
+ if (event.actionName === "hr") {
+ this.element.editor.insertAttachment(
+ new Trix.Attachment({ content: "
", contentType: "text/html" })
+ );
+ }
+ };
+
+ #trixInitialize = () => {
+ // Add HR button to the Trix toolbar if it's not already there and the editor is present
+ if (
+ this.element.editor &&
+ !this.element.toolbarElement.querySelector(".trix-button--icon-hr")
+ ) {
+ this.#addHRButton();
+ }
+ };
+
+ #addHRButton = () => {
+ const button_html = `
+ `;
+ const buttonGroup = this.element.toolbarElement.querySelector(
+ ".trix-button-group--block-tools"
+ );
+ buttonGroup.insertAdjacentHTML("beforeend", button_html);
+ buttonGroup.querySelector(".trix-button--icon-hr").addEventListener("click", (event) => {
+ event.actionName = "hr";
+ this.#trixActionInvoke(event);
+ });
+ };
}
diff --git a/app/webpacker/css/admin/all.scss b/app/webpacker/css/admin/all.scss
index eba04c98ad..35e4270d8f 100644
--- a/app/webpacker/css/admin/all.scss
+++ b/app/webpacker/css/admin/all.scss
@@ -30,6 +30,8 @@
@import "shared/layout";
@import "shared/scroll_bar";
+@import "../shared/trix";
+
@import "plugins/flatpickr-customization";
@import "plugins/powertip";
@import "plugins/jstree";
diff --git a/app/webpacker/css/admin/trix.scss b/app/webpacker/css/admin/trix.scss
index 7ee927a2f3..78c149dc32 100644
--- a/app/webpacker/css/admin/trix.scss
+++ b/app/webpacker/css/admin/trix.scss
@@ -2,25 +2,29 @@ trix-toolbar [data-trix-button-group="file-tools"] {
display: none;
}
+trix-toolbar .trix-button--icon-hr::before {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M3 12h18v2H3z'/%3E%3C/svg%3E");
+}
+
// Match the rendering into the shopfront (see ../darkswarm/)
trix-editor {
color: #222;
- ol,
- ul {
- margin-left: 1.5em;
- }
-
a {
- color: #f27052;
+ color: #f27052; // Equivalent to text-angular a
}
- // Copy/pasted from _type.scss
- blockquote {
- line-height: 1.6;
- color: #6f6f6f;
- margin: 0 0 1.25rem;
- padding: 0.5625rem 1.25rem 0 1.1875rem;
- border-left: 1px solid #dddddd;
+ @include trix-styles;
+}
+
+// Override app/webpacker/css/admin/shared/forms.scss#L81
+.field trix-editor ul {
+ border-top: none;
+ list-style: disc;
+ padding-top: 0;
+
+ li {
+ padding-right: 0;
+ display: list-item;
}
}
diff --git a/app/webpacker/css/admin_v3/all.scss b/app/webpacker/css/admin_v3/all.scss
index e83df91661..b6a12acf66 100644
--- a/app/webpacker/css/admin_v3/all.scss
+++ b/app/webpacker/css/admin_v3/all.scss
@@ -35,6 +35,8 @@
@import "shared/layout"; // admin_v3
@import "../admin/shared/scroll_bar";
+@import "../shared/trix";
+
@import "../admin/plugins/flatpickr-customization";
@import "../admin/plugins/powertip";
@import "../admin/plugins/jstree";
diff --git a/app/webpacker/css/darkswarm/_shop-modals.scss b/app/webpacker/css/darkswarm/_shop-modals.scss
index bb6b6480dc..1beed67851 100644
--- a/app/webpacker/css/darkswarm/_shop-modals.scss
+++ b/app/webpacker/css/darkswarm/_shop-modals.scss
@@ -11,6 +11,12 @@
.product-description {
margin: 1rem 0.25rem 0.25rem 0;
+
+ .text-small div {
+ margin-bottom: 1.5rem; // Equivalent to p (trix doesn't use p as separator by default, so emulate div as p to be backward compatible)
+ }
+
+ @include trix-styles;
}
.property-selectors li {
diff --git a/app/webpacker/css/darkswarm/_shop-product-rows.scss b/app/webpacker/css/darkswarm/_shop-product-rows.scss
index 401db4f8a0..f731ec4ce6 100644
--- a/app/webpacker/css/darkswarm/_shop-product-rows.scss
+++ b/app/webpacker/css/darkswarm/_shop-product-rows.scss
@@ -158,6 +158,12 @@
// line-clamp is not supported in Safari
line-height: 1rem;
height: 1.75rem;
+
+ > div {
+ margin-bottom: 1.5rem; // Equivalent to p (trix doesn't use p as separator by default, so emulate div as p to be backward compatible)
+ }
+
+ @include trix-styles;
}
.product-properties {
diff --git a/app/webpacker/css/darkswarm/all.scss b/app/webpacker/css/darkswarm/all.scss
index a642ac38fd..a78b2c30e2 100644
--- a/app/webpacker/css/darkswarm/all.scss
+++ b/app/webpacker/css/darkswarm/all.scss
@@ -10,6 +10,7 @@
@import 'branding';
@import 'typography';
@import 'mixins';
+@import '../shared/trix';
@import 'base/colors';
@import 'animations';
diff --git a/app/webpacker/css/darkswarm/shop_tabs.scss b/app/webpacker/css/darkswarm/shop_tabs.scss
index 490231366e..e87b3ae0e1 100644
--- a/app/webpacker/css/darkswarm/shop_tabs.scss
+++ b/app/webpacker/css/darkswarm/shop_tabs.scss
@@ -121,17 +121,7 @@
}
.custom-tab {
- ol, ul {
- margin-left: 1.5em;
- }
-
- ol {
- list-style-type: decimal;
- }
-
- ul {
- list-style-type: disc;
- }
+ @include trix-styles;
}
}
diff --git a/app/webpacker/css/shared/trix.scss b/app/webpacker/css/shared/trix.scss
new file mode 100644
index 0000000000..7953954df7
--- /dev/null
+++ b/app/webpacker/css/shared/trix.scss
@@ -0,0 +1,35 @@
+// A mixin used to include some trix styles in the shopfront that could have been reseted
+@mixin trix-styles {
+ ol,
+ ul {
+ margin-left: 1.5em;
+ }
+
+ ol {
+ list-style-type: decimal;
+ }
+
+ ul {
+ list-style-type: disc;
+ }
+
+ div,
+ pre,
+ h1 {
+ margin-bottom: 1.5rem; // Equivalent to text-angular p (trix doesn't use p as default one, since we could not include figures inside p)
+ }
+
+ h1 {
+ color: #222222;
+ font-size: 1.1rem;
+ }
+
+ // Copy/pasted from _type.scss
+ blockquote {
+ line-height: 1.6;
+ color: #6f6f6f;
+ margin: 0 0 1.25rem;
+ padding: 0.5625rem 1.25rem 0 1.1875rem;
+ border-left: 1px solid #dddddd;
+ }
+}
diff --git a/spec/system/admin/products_spec.rb b/spec/system/admin/products_spec.rb
index a6c2183b6e..d01ff3b5c3 100644
--- a/spec/system/admin/products_spec.rb
+++ b/spec/system/admin/products_spec.rb
@@ -45,7 +45,7 @@ describe '
fill_in 'product_on_hand', with: 5
check 'product_on_demand'
select 'Test Tax Category', from: 'product_tax_category_id'
- page.find("div[id^='taTextElement']").native.send_keys('A description...')
+ fill_in_trix_editor 'product_description', with: 'A description...'
click_button 'Create'
@@ -58,7 +58,8 @@ describe '
expect(page).to have_field 'product_on_hand', with: 5
expect(page).to have_field 'product_on_demand', checked: true
expect(page).to have_field 'product_tax_category_id', with: tax_category.id
- expect(page.find("div[id^='taTextElement']")).to have_content 'A description...'
+ expect(page.find("#product_description",
+ visible: false).value).to eq('A description...
')
expect(page.find("#product_variant_unit_field")).to have_content 'Weight (kg)'
expect(page).to have_content "Name can't be blank"
@@ -80,7 +81,7 @@ describe '
fill_in 'product_on_hand', with: 5
check 'product_on_demand'
select 'Test Tax Category', from: 'product_tax_category_id'
- page.find("div[id^='taTextElement']").native.send_keys('A description...')
+ fill_in_trix_editor 'product_description', with: 'A description...'
click_button 'Create'
@@ -93,7 +94,8 @@ describe '
expect(page).to have_field 'product_on_hand', with: 5
expect(page).to have_field 'product_on_demand', checked: true
expect(page).to have_field 'product_tax_category_id', with: tax_category.id
- expect(page.find("div[id^='taTextElement']")).to have_content 'A description...'
+ expect(page.find("#product_description",
+ visible: false).value).to eq('A description...
')
expect(page.find("#product_variant_unit_field")).to have_content 'Weight (kg)'
expect(page).to have_content "Unit value must be greater than 0"
@@ -128,7 +130,7 @@ describe '
fill_in 'product_price', with: '19.99'
fill_in 'product_on_hand', with: 5
select 'Test Tax Category', from: 'product_tax_category_id'
- page.find("div[id^='taTextElement']").native.send_keys('A description...')
+ fill_in_trix_editor 'product_description', with: 'A description...'
click_button 'Create'
@@ -146,7 +148,7 @@ describe '
expect(product.on_hand).to eq(5)
expect(product.variants.first.tax_category_id).to eq(tax_category.id)
expect(product.variants.first.shipping_category).to eq(shipping_category)
- expect(product.description).to eq("A description...
")
+ expect(product.description).to eq("A description...
")
expect(product.group_buy).to be_falsey
expect(product.variants.first.unit_presentation).to eq("5kg")
end
@@ -166,8 +168,8 @@ describe '
fill_in 'product_on_hand', with: 0
check 'product_on_demand'
select 'Test Tax Category', from: 'product_tax_category_id'
- page.find("div[id^='taTextElement']").native
- .send_keys('In demand, and on_demand! The hottest cakes in town.')
+ fill_in_trix_editor 'product_description',
+ with: 'In demand, and on_demand! The hottest cakes in town.'
click_button 'Create'
@@ -193,9 +195,8 @@ describe '
fill_in 'product_on_hand', with: 0
check 'product_on_demand'
select 'Test Tax Category', from: 'product_tax_category_id'
- find("div[id^='taTextElement']").native
- .send_keys('In demand, and on_demand! The hottest cakes in town.')
-
+ fill_in_trix_editor 'product_description',
+ with: 'In demand, and on_demand! The hottest cakes in town.'
click_button 'Create'
expect(current_path).to eq spree.admin_products_path