mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 16:14:02 +00:00
copy: Use a global copy event listener.
This attempts to use a global copy event listener instead of triggering the copy handler on Ctrl+C, as this is more robust way to use browser APIs, including not intefering if end users choose to remap copying keys on their keyboard. This has various beneficial side effects, including copying from other Markdown elements like the preview or drafts UI using the same code as copying from the message feed. Fixes: #33949.
This commit is contained in:
committed by
Tim Abbott
parent
353f57e518
commit
11028d5244
@@ -24,24 +24,22 @@ async function copy_messages(
|
||||
window.getSelection()!.removeAllRanges();
|
||||
window.getSelection()!.addRange(selectedRange);
|
||||
|
||||
// Remove existing copy/paste divs, which may linger from the previous
|
||||
// example. (The code clears these out with a zero-second timeout, which
|
||||
// is probably sufficient for human users, but which causes problems here.)
|
||||
document.querySelector("#copytempdiv")?.remove();
|
||||
|
||||
// emulate copy event
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
key: "c",
|
||||
code: "KeyC",
|
||||
ctrlKey: true,
|
||||
keyCode: 67,
|
||||
which: 67,
|
||||
}),
|
||||
);
|
||||
const clipboard_data = new DataTransfer();
|
||||
const copy_event = new ClipboardEvent("copy", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clipboardData: clipboard_data,
|
||||
});
|
||||
document.dispatchEvent(copy_event);
|
||||
|
||||
// find temp div with copied text
|
||||
return [...document.querySelectorAll("#copytempdiv > p")].map((p) => p.textContent!);
|
||||
const copied_html = clipboard_data.getData("text/html");
|
||||
|
||||
// Convert the copied HTML into separate message strings
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(copied_html, "text/html");
|
||||
|
||||
return [...doc.body.children].map((el) => el.textContent!.trim());
|
||||
},
|
||||
start_message,
|
||||
end_message,
|
||||
|
@@ -6,7 +6,6 @@ import assert from "minimalistic-assert";
|
||||
|
||||
import * as message_lists from "./message_lists.ts";
|
||||
import * as rows from "./rows.ts";
|
||||
import * as util from "./util.ts";
|
||||
|
||||
function find_boundary_tr(
|
||||
$initial_tr: JQuery,
|
||||
@@ -108,35 +107,6 @@ function construct_copy_div($div: JQuery, start_id: number, end_id: number): voi
|
||||
}
|
||||
}
|
||||
|
||||
function insert_and_select_div($div: JQuery, selection: Selection): void {
|
||||
$div.css({
|
||||
position: "absolute",
|
||||
left: "-99999px",
|
||||
// Color and background is made according to "light theme"
|
||||
// exclusively here because when copying the content
|
||||
// into, say, Gmail compose box, the styles come along.
|
||||
// This is done to avoid copying the content with dark
|
||||
// background when using the app in dark theme.
|
||||
// We can avoid other custom styles since they are wrapped
|
||||
// inside another parent such as `.message_content`.
|
||||
color: "#333",
|
||||
background: "#FFF",
|
||||
}).attr("id", "copytempdiv");
|
||||
$("body").append($div);
|
||||
selection.selectAllChildren(util.the($div));
|
||||
}
|
||||
|
||||
function restore_original_selection(ranges: Range[]): void {
|
||||
// Should be called inside a setTimeout(..., 0).
|
||||
const selection = window.getSelection();
|
||||
assert(selection !== null);
|
||||
selection.removeAllRanges();
|
||||
|
||||
for (const range of ranges) {
|
||||
selection.addRange(range);
|
||||
}
|
||||
}
|
||||
|
||||
// We want to grab the closest katex span up the tree
|
||||
// in cases where we can resolve the selected katex expression
|
||||
// from a math block into an inline expression.
|
||||
@@ -199,7 +169,7 @@ function improve_katex_selection_range(selection: Selection): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function copy_handler(): boolean {
|
||||
export function copy_handler(ev: ClipboardEvent): boolean {
|
||||
// This is the main handler for copying message content via
|
||||
// `Ctrl+C` in Zulip (note that this is totally independent of the
|
||||
// "select region" copy behavior on Linux; that is handled
|
||||
@@ -220,7 +190,6 @@ export function copy_handler(): boolean {
|
||||
improve_katex_selection_range(selection);
|
||||
|
||||
const analysis = analyze_selection(selection);
|
||||
const ranges = analysis.ranges;
|
||||
const start_id = analysis.start_id;
|
||||
const end_id = analysis.end_id;
|
||||
const skip_same_td_check = analysis.skip_same_td_check;
|
||||
@@ -245,7 +214,7 @@ export function copy_handler(): boolean {
|
||||
|
||||
if (!skip_same_td_check && start_id === end_id) {
|
||||
// Check whether the selection both starts and ends in the
|
||||
// same message. If so, Let the browser handle this.
|
||||
// same message and let the browser handle the copying.
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -261,16 +230,10 @@ export function copy_handler(): boolean {
|
||||
const $div = $("<div>");
|
||||
construct_copy_div($div, start_id, end_id);
|
||||
|
||||
// Select div so that the browser will copy it
|
||||
// instead of copying the original selection
|
||||
insert_and_select_div($div, selection);
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||
document.execCommand("copy");
|
||||
setTimeout(() => {
|
||||
restore_original_selection(ranges);
|
||||
|
||||
$div.remove();
|
||||
}, 0);
|
||||
const html_content = $div.html().trim();
|
||||
const plain_text = $div.text().trim();
|
||||
ev.clipboardData?.setData("text/html", html_content);
|
||||
ev.clipboardData?.setData("text/plain", plain_text);
|
||||
|
||||
// Tell the keyboard code that we did the copy ourselves, and thus
|
||||
// the browser should not handle the copy.
|
||||
@@ -394,3 +357,11 @@ function get_end_tr_from_endc($endc: JQuery<Node>): JQuery {
|
||||
|
||||
return $endc.parents(".selectable_row").first();
|
||||
}
|
||||
|
||||
export function initialize(): void {
|
||||
document.addEventListener("copy", (ev) => {
|
||||
if (copy_handler(ev)) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -15,7 +15,6 @@ import * as compose_send_menu_popover from "./compose_send_menu_popover.js";
|
||||
import * as compose_state from "./compose_state.ts";
|
||||
import * as compose_textarea from "./compose_textarea.ts";
|
||||
import * as condense from "./condense.ts";
|
||||
import * as copy_messages from "./copy_messages.ts";
|
||||
import * as deprecated_feature_notice from "./deprecated_feature_notice.ts";
|
||||
import * as drafts_overlay_ui from "./drafts_overlay_ui.ts";
|
||||
import * as emoji from "./emoji.ts";
|
||||
@@ -1143,8 +1142,6 @@ export function process_hotkey(e, hotkey) {
|
||||
message_scroll_state.set_keyboard_triggered_current_scroll(true);
|
||||
navigate.page_down();
|
||||
return true;
|
||||
case "copy_with_c":
|
||||
return copy_messages.copy_handler();
|
||||
}
|
||||
|
||||
if (
|
||||
|
@@ -37,6 +37,7 @@ import * as compose_tooltips from "./compose_tooltips.ts";
|
||||
import * as compose_validate from "./compose_validate.ts";
|
||||
import * as composebox_typeahead from "./composebox_typeahead.ts";
|
||||
import * as condense from "./condense.ts";
|
||||
import * as copy_messages from "./copy_messages.ts";
|
||||
import * as desktop_integration from "./desktop_integration.ts";
|
||||
import * as desktop_notifications from "./desktop_notifications.ts";
|
||||
import * as drafts from "./drafts.ts";
|
||||
@@ -599,6 +600,7 @@ export function initialize_everything(state_data) {
|
||||
playground_data: realm.realm_playgrounds,
|
||||
pygments_comparator_func: typeahead_helper.compare_language,
|
||||
});
|
||||
copy_messages.initialize();
|
||||
compose_setup.initialize();
|
||||
// Typeahead must be initialized after compose_setup.initialize()
|
||||
composebox_typeahead.initialize({
|
||||
|
Reference in New Issue
Block a user