Merge pull request #11140 from jibees/11129-add-trix-editor-to-product-description-editor

Admin, add trix editor to product description editor (both new and edit)
This commit is contained in:
Konrad
2023-09-04 20:22:14 +02:00
committed by GitHub
15 changed files with 136 additions and 42 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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' }

View File

@@ -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" }

View File

@@ -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: "<hr />", 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 = `
<button type="button" class="trix-button trix-button--icon trix-button--icon-hr" data-trix-action="hr" title="Horizontal Rule" tabindex="-1">HR</button>`;
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);
});
};
}

View File

@@ -30,6 +30,8 @@
@import "shared/layout";
@import "shared/scroll_bar";
@import "../shared/trix";
@import "plugins/flatpickr-customization";
@import "plugins/powertip";
@import "plugins/jstree";

View File

@@ -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;
}
}

View File

@@ -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";

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -10,6 +10,7 @@
@import 'branding';
@import 'typography';
@import 'mixins';
@import '../shared/trix';
@import 'base/colors';
@import 'animations';

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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('<div>A description...</div>')
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('<div>A description...</div>')
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("<p>A description...</p>")
expect(product.description).to eq("<div>A description...</div>")
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