mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 04:52:12 +00:00
copy_messages: Expand selection for math expressions and blocks.
With this we introduce selection expansion when the selection spans across multiple inline expressions or expressions within mutliple math blocks. This makes the selection consistent with what is pasted while also fixing some bugs encountered in cases such as: 1. Focus present in an inline math expression and anchor present in a different inline math expression and vice-versa. 2. Focus present in a math block and anchor present in a different math expression outside that math block and vice-versa. 3. When selecting expressions within a math block, the first expression wasn't retained in the past because of the selection not containing <annotation>. Fixes #33676. Also fixes https://chat.zulip.org/#narrow/channel/9-issues/topic/.F0.9F.93.82.20pasting.20LaTeX/near/2159869 and other related issues. Signed-off-by: apoorvapendse <apoorvavpendse@gmail.com>
This commit is contained in:
committed by
Tim Abbott
parent
2e294e23c1
commit
080346c0e0
@@ -119,54 +119,88 @@ function get_nearest_html_element(node: Node | null): Element | null {
|
||||
return node.parentElement;
|
||||
}
|
||||
|
||||
// selection_element will be either the start_element or end_element
|
||||
function expand_range_based_on_katex_parent(
|
||||
selection_element: Element,
|
||||
is_range_start: boolean,
|
||||
range: Range,
|
||||
): void {
|
||||
// Here, we have three cases:
|
||||
// 1. This element lies within a math block expression i.e. within a `.katex-display`
|
||||
// 2. This element lies within an inline math expression i.e. inside a `.katex` span
|
||||
// with no `.katex-display` parent for that `.katex`
|
||||
// 3. This element does not lie within a math expression, we directly return without expansion.
|
||||
// We cascade through these cases, expanding the range and prioritizing math blocks over expressions
|
||||
// in case we encounter them.
|
||||
|
||||
const is_within_math_block = selection_element.closest(".katex-display") !== null;
|
||||
const is_within_math_expression = selection_element.closest(".katex") !== null;
|
||||
if (!is_within_math_block && !is_within_math_expression) {
|
||||
return;
|
||||
}
|
||||
if (is_within_math_block) {
|
||||
// One might think that this will break in case of empty katex-display(s)
|
||||
// being the start or end node which is/are created when we insert
|
||||
// some extra newlines within a math block.
|
||||
// However, is it not possible to select those empty katex-displays
|
||||
// as per my observation on Chrome and Firefox.
|
||||
if (is_range_start) {
|
||||
range.setStart(selection_element.closest(".katex-display")!, 0);
|
||||
} else {
|
||||
// The offset 1 selects the only child of `.katex-display`
|
||||
// which is `.katex`.
|
||||
range.setEnd(selection_element.closest(".katex-display")!, 1);
|
||||
}
|
||||
} else {
|
||||
if (is_range_start) {
|
||||
range.setStart(selection_element.closest(".katex")!, 0);
|
||||
} else {
|
||||
// The offset 2 selects the two children of `.katex`
|
||||
// namely `.katex-mathml` and `.katex-html`
|
||||
range.setEnd(selection_element.closest(".katex")!, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
This is done to make the copying within math blocks smarter.
|
||||
We mutate the selection for selecting singular katex expressions
|
||||
within math blocks when applicable, which will be
|
||||
converted into the inline $$<expr>$$ syntax.
|
||||
Our paste behavior for KaTeX relies on processing the MathML
|
||||
annotations generated by KaTeX in `<annotation>` tags. This
|
||||
function is responsible for expanding selections of math copied
|
||||
out of Zulip to ensure the annotations are included in what is
|
||||
copied, so that it pastes nicely.
|
||||
|
||||
In case when a single expression or its subset is selected
|
||||
within a math block, we adjust the selection so that it
|
||||
selects the katex span which is parenting that expression.
|
||||
This converts the selection into an inline expression as
|
||||
per the turndown rules below.
|
||||
We expand the selection range only in the following cases:
|
||||
|
||||
We want to avoid this behavior if the selection
|
||||
spreads across multiple katex displays i.e. the
|
||||
focus and anchor are not part of the same katex span.
|
||||
1. Either the startContainer or endContainer or both are within an
|
||||
inline expression where the range covers one or more math
|
||||
expressions.
|
||||
2. Either the startContainer, endContainer, or both are within a
|
||||
math block where the range covers one or more math expressions.
|
||||
|
||||
In principle, we only need to expand the start of the selection
|
||||
range for the cases where multiple expressions are selected
|
||||
because the end of the range always contains the annotation
|
||||
element in case it lies within the math block.
|
||||
|
||||
But, we still expand the end of the range to select the complete
|
||||
expression, since our paste handler has no way to split the
|
||||
annotation, so we'll always be converting entire expressions.
|
||||
*/
|
||||
function improve_katex_selection_range(selection: Selection): void {
|
||||
const anchor_element = get_nearest_html_element(selection.anchorNode);
|
||||
const focus_element = get_nearest_html_element(selection.focusNode);
|
||||
// If the anchor and focus end up in different katex spans, this selection
|
||||
// isn't meant to be an inline expression, so we perform an early return.
|
||||
if (
|
||||
focus_element &&
|
||||
anchor_element &&
|
||||
focus_element?.closest(".katex") !== anchor_element?.closest(".katex")
|
||||
) {
|
||||
function improve_katex_selection_range(range: Range): void {
|
||||
const start_element = get_nearest_html_element(range.startContainer);
|
||||
const end_element = get_nearest_html_element(range.endContainer);
|
||||
if (!end_element || !start_element) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (anchor_element) {
|
||||
const parent = anchor_element.closest(".katex");
|
||||
const is_math_block = parent !== null && parent !== selection.anchorNode;
|
||||
if (is_math_block) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(parent);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
} else if (focus_element) {
|
||||
const parent = focus_element.closest(".katex");
|
||||
const is_math_block = parent !== null && parent !== selection.focusNode;
|
||||
if (is_math_block) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(parent);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
// Only perform expansion if either the start or end element
|
||||
// is itself a `.katex` element or is contained within one.
|
||||
if (end_element.closest(".katex") === null && start_element.closest(".katex") === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
expand_range_based_on_katex_parent(start_element, true, range);
|
||||
expand_range_based_on_katex_parent(end_element, false, range);
|
||||
}
|
||||
|
||||
export function copy_handler(ev: ClipboardEvent): boolean {
|
||||
@@ -187,7 +221,6 @@ export function copy_handler(ev: ClipboardEvent): boolean {
|
||||
|
||||
const selection = window.getSelection();
|
||||
assert(selection !== null);
|
||||
improve_katex_selection_range(selection);
|
||||
|
||||
const analysis = analyze_selection(selection);
|
||||
const start_id = analysis.start_id;
|
||||
@@ -215,6 +248,23 @@ export function copy_handler(ev: ClipboardEvent): boolean {
|
||||
if (!skip_same_td_check && start_id === end_id) {
|
||||
// Check whether the selection both starts and ends in the
|
||||
// same message and let the browser handle the copying.
|
||||
|
||||
// Firefox uses multiple ranges when selecting multiple messages.
|
||||
// See https://drafts.csswg.org/css-ui-4/#valdef-user-select-none
|
||||
// Instead of relying on Selection API's anchorNode and focusNode,
|
||||
// we iterate over all ranges and expand them if needed.
|
||||
//
|
||||
// The reason is that anchorNode and focusNode only reflect the first range,
|
||||
// which becomes an issue in Firefox. When the selection spans multiple ranges,
|
||||
// for example, due to `user-select: none` elements in between the selection,
|
||||
// Firefox creates disjoint ranges but only sets anchor/focus for the first one.
|
||||
//
|
||||
// So to handle multi-range selections correctly (especially in Firefox),
|
||||
// we process all ranges individually.
|
||||
for (let i = 0; i < selection.rangeCount; i += 1) {
|
||||
improve_katex_selection_range(selection.getRangeAt(i));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user