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
This commit is contained in:
Régis Hanol 2024-05-24 18:06:29 +02:00
parent a658465b7d
commit bc089dc52b
3 changed files with 42 additions and 16 deletions

View File

@ -8,6 +8,7 @@ import PostTextSelectionToolbar from "discourse/components/post-text-selection-t
import isElementInViewport from "discourse/lib/is-element-in-viewport"; import isElementInViewport from "discourse/lib/is-element-in-viewport";
import toMarkdown from "discourse/lib/to-markdown"; import toMarkdown from "discourse/lib/to-markdown";
import { import {
getElement,
selectedNode, selectedNode,
selectedRange, selectedRange,
selectedText, selectedText,
@ -32,6 +33,13 @@ function getQuoteTitle(element) {
return titleEl.textContent.trim().replace(/:$/, ""); 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 { export default class PostTextSelection extends Component {
@service appEvents; @service appEvents;
@service capabilities; @service capabilities;
@ -122,14 +130,8 @@ export default class PostTextSelection extends Component {
let postId; let postId;
for (let r = 0; r < selection.rangeCount; r++) { for (let r = 0; r < selection.rangeCount; r++) {
const range = selection.getRangeAt(r); const range = selection.getRangeAt(r);
const selectionStart = const selectionStart = getElement(range.startContainer);
range.startContainer.nodeType === Node.ELEMENT_NODE const ancestor = getElement(range.commonAncestorContainer);
? range.startContainer
: range.startContainer.parentElement;
const ancestor =
range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
? range.commonAncestorContainer
: range.commonAncestorContainer.parentElement;
if (!selectionStart.closest(".cooked")) { if (!selectionStart.closest(".cooked")) {
return await this.hideToolbar(); return await this.hideToolbar();
@ -142,10 +144,7 @@ export default class PostTextSelection extends Component {
} }
} }
const _selectedElement = const _selectedElement = getElement(selectedNode());
selectedNode().nodeType === Node.ELEMENT_NODE
? selectedNode()
: selectedNode().parentElement;
const cooked = const cooked =
_selectedElement.querySelector(".cooked") || _selectedElement.querySelector(".cooked") ||
_selectedElement.closest(".cooked"); _selectedElement.closest(".cooked");
@ -176,7 +175,14 @@ export default class PostTextSelection extends Component {
quoteState.selected(postId, _selectedText, opts); quoteState.selected(postId, _selectedText, opts);
let supportsFastEdit = this.canEditPost; 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 regexp = new RegExp(escapeRegExp(quoteState.buffer), "gi");
const matches = cooked.innerHTML.match(regexp); const matches = cooked.innerHTML.match(regexp);
@ -184,11 +190,9 @@ export default class PostTextSelection extends Component {
quoteState.buffer.length === 0 || quoteState.buffer.length === 0 ||
quoteState.buffer.includes("|") || // tables are too complex quoteState.buffer.includes("|") || // tables are too complex
quoteState.buffer.match(/\n/g) || // linebreaks 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; supportsFastEdit = false;
} else if (matches?.length === 1) {
supportsFastEdit = true;
} }
} }

View File

@ -749,3 +749,7 @@ export function cleanNullQueryParams(params) {
} }
return params; return params;
} }
export function getElement(node) {
return node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
}

View File

@ -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!(:spanish_post) { Fabricate(:post, topic: topic, raw: "Hola Juan, ¿cómo estás?") }
fab!(:chinese_post) { Fabricate(:post, topic: topic, raw: "这是一个测试") } fab!(:chinese_post) { Fabricate(:post, topic: topic, raw: "这是一个测试") }
fab!(:post_with_emoji) { Fabricate(:post, topic: topic, raw: "Good morning :wave:!") } 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) } fab!(:current_user) { Fabricate(:admin) }
before { sign_in(current_user) } before { sign_in(current_user) }
@ -40,6 +47,17 @@ describe "Post selection | Fast edit", type: :system do
end end
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 context "when editing text that has strange characters" do
it "saves when paragraph contains apostrophe" do it "saves when paragraph contains apostrophe" do
topic_page.visit_topic(topic) topic_page.visit_topic(topic)