mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-27 01:43:22 +00:00
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:
@@ -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
|
||||
|
||||
11
app/services/trix_sanitizer.rb
Normal file
11
app/services/trix_sanitizer.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
@import "shared/layout";
|
||||
@import "shared/scroll_bar";
|
||||
|
||||
@import "../shared/trix";
|
||||
|
||||
@import "plugins/flatpickr-customization";
|
||||
@import "plugins/powertip";
|
||||
@import "plugins/jstree";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
@import 'branding';
|
||||
@import 'typography';
|
||||
@import 'mixins';
|
||||
@import '../shared/trix';
|
||||
|
||||
@import 'base/colors';
|
||||
@import 'animations';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
35
app/webpacker/css/shared/trix.scss
Normal file
35
app/webpacker/css/shared/trix.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user