From bc089dc52bdab64c3a806e7edc7e0ffbb75a1def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr> Date: Fri, 24 May 2024 18:06:29 +0200 Subject: [PATCH] FIX: bypass fast edit when selected text isn't editable When selected some text inside a post, we offer the ability to "fast edit" the selected text without opening the composer. However, there are certain cases where this isn't working quite a expected, due to the fact that we have some text in the "cooked" version of the post that isn't literally in the "raw" version of the post. This ensures that whenever someone selects the within - a quote - a onebox - an encrypted message - a "cooked" date we directly show the composer instead of showing the fast edit modal and then leaving the user with an invisible error. Internal ref. t/128400 --- .../app/components/post-text-selection.gjs | 36 ++++++++++--------- .../discourse/app/lib/utilities.js | 4 +++ spec/system/post_selection_fast_edit_spec.rb | 18 ++++++++++ 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/discourse/app/components/post-text-selection.gjs b/app/assets/javascripts/discourse/app/components/post-text-selection.gjs index 1c054715272..f92e55c7f0e 100644 --- a/app/assets/javascripts/discourse/app/components/post-text-selection.gjs +++ b/app/assets/javascripts/discourse/app/components/post-text-selection.gjs @@ -8,6 +8,7 @@ import PostTextSelectionToolbar from "discourse/components/post-text-selection-t import isElementInViewport from "discourse/lib/is-element-in-viewport"; import toMarkdown from "discourse/lib/to-markdown"; import { + getElement, selectedNode, selectedRange, selectedText, @@ -32,6 +33,13 @@ function getQuoteTitle(element) { return titleEl.textContent.trim().replace(/:$/, ""); } +const CSS_TO_DISABLE_FAST_EDIT = [ + "aside.quote", + "aside.onebox", + ".cooked-date", + "body.encrypted-topic-page", +].join(","); + export default class PostTextSelection extends Component { @service appEvents; @service capabilities; @@ -122,14 +130,8 @@ export default class PostTextSelection extends Component { let postId; for (let r = 0; r < selection.rangeCount; r++) { const range = selection.getRangeAt(r); - const selectionStart = - range.startContainer.nodeType === Node.ELEMENT_NODE - ? range.startContainer - : range.startContainer.parentElement; - const ancestor = - range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE - ? range.commonAncestorContainer - : range.commonAncestorContainer.parentElement; + const selectionStart = getElement(range.startContainer); + const ancestor = getElement(range.commonAncestorContainer); if (!selectionStart.closest(".cooked")) { return await this.hideToolbar(); @@ -142,10 +144,7 @@ export default class PostTextSelection extends Component { } } - const _selectedElement = - selectedNode().nodeType === Node.ELEMENT_NODE - ? selectedNode() - : selectedNode().parentElement; + const _selectedElement = getElement(selectedNode()); const cooked = _selectedElement.querySelector(".cooked") || _selectedElement.closest(".cooked"); @@ -176,7 +175,14 @@ export default class PostTextSelection extends Component { quoteState.selected(postId, _selectedText, opts); let supportsFastEdit = this.canEditPost; - if (this.canEditPost) { + + const start = getElement(selection.getRangeAt(0).startContainer); + + if (!start || start.closest(CSS_TO_DISABLE_FAST_EDIT)) { + supportsFastEdit = false; + } + + if (supportsFastEdit) { const regexp = new RegExp(escapeRegExp(quoteState.buffer), "gi"); const matches = cooked.innerHTML.match(regexp); @@ -184,11 +190,9 @@ export default class PostTextSelection extends Component { quoteState.buffer.length === 0 || quoteState.buffer.includes("|") || // tables are too complex quoteState.buffer.match(/\n/g) || // linebreaks are too complex - matches?.length > 1 // duplicates are too complex + matches?.length !== 1 // duplicates are too complex ) { supportsFastEdit = false; - } else if (matches?.length === 1) { - supportsFastEdit = true; } } diff --git a/app/assets/javascripts/discourse/app/lib/utilities.js b/app/assets/javascripts/discourse/app/lib/utilities.js index 3e72bb344b8..c8657df0cf2 100644 --- a/app/assets/javascripts/discourse/app/lib/utilities.js +++ b/app/assets/javascripts/discourse/app/lib/utilities.js @@ -749,3 +749,7 @@ export function cleanNullQueryParams(params) { } return params; } + +export function getElement(node) { + return node.nodeType === Node.TEXT_NODE ? node.parentElement : node; +} diff --git a/spec/system/post_selection_fast_edit_spec.rb b/spec/system/post_selection_fast_edit_spec.rb index 5a89126ca6d..fe03dd4d8fe 100644 --- a/spec/system/post_selection_fast_edit_spec.rb +++ b/spec/system/post_selection_fast_edit_spec.rb @@ -9,6 +9,13 @@ describe "Post selection | Fast edit", type: :system do fab!(:spanish_post) { Fabricate(:post, topic: topic, raw: "Hola Juan, ¿cómo estás?") } fab!(:chinese_post) { Fabricate(:post, topic: topic, raw: "这是一个测试") } fab!(:post_with_emoji) { Fabricate(:post, topic: topic, raw: "Good morning :wave:!") } + fab!(:post_with_quote) do + Fabricate( + :post, + topic: topic, + raw: "[quote]\n#{post_2.raw}\n[/quote]\n\nBelle journée, n'est-ce pas ?", + ) + end fab!(:current_user) { Fabricate(:admin) } before { sign_in(current_user) } @@ -40,6 +47,17 @@ describe "Post selection | Fast edit", type: :system do end end + context "when text selected is inside a quote" do + it "opens the composer directly" do + topic_page.visit_topic(topic) + + select_text_range("#{topic_page.post_by_number_selector(6)} .cooked p", 5, 10) + topic_page.click_fast_edit_button + + expect(topic_page).to have_expanded_composer + end + end + context "when editing text that has strange characters" do it "saves when paragraph contains apostrophe" do topic_page.visit_topic(topic)