import TurndownService from "turndown/lib/turndown.cjs"; import * as rows from "./rows"; function find_boundary_tr(initial_tr, iterate_row) { let j; let skip_same_td_check = false; let tr = initial_tr; // If the selection boundary is somewhere that does not have a // parent tr, we should let the browser handle the copy-paste // entirely on its own if (tr.length === 0) { return undefined; } // If the selection boundary is on a table row that does not have an // associated message id (because the user clicked between messages), // then scan downwards until we hit a table row with a message id. // To ensure we can't enter an infinite loop, bail out (and let the // browser handle the copy-paste on its own) if we don't hit what we // are looking for within 10 rows. for (j = 0; !tr.is(".message_row") && j < 10; j += 1) { tr = iterate_row(tr); } if (j === 10) { return undefined; } else if (j !== 0) { // If we updated tr, then we are not dealing with a selection // that is entirely within one td, and we can skip the same td // check (In fact, we need to because it won't work correctly // in this case) skip_same_td_check = true; } return [rows.id(tr), skip_same_td_check]; } function construct_recipient_header(message_row) { const message_header_content = rows .get_message_recipient_header(message_row) .text() .replace(/\s+/g, " ") .replace(/^\s/, "") .replace(/\s$/, ""); return $("
").append($("").text(message_header_content));
}
/*
The techniques we use in this code date back to
2013 and may be obsolete today (and may not have
been even the best workaround back then).
https://github.com/zulip/zulip/commit/fc0b7c00f16316a554349f0ad58c6517ebdd7ac4
The idea is that we build a temp div, let jQuery process the
selection, then restore the selection on a zero-second timer back
to the original selection.
Do not be afraid to change this code if you understand
how modern browsers deal with copy/paste. Just test
your changes carefully.
*/
function construct_copy_div(div, start_id, end_id) {
const copy_rows = rows.visible_range(start_id, end_id);
const start_row = copy_rows[0];
const start_recipient_row = rows.get_message_recipient_row(start_row);
const start_recipient_row_id = rows.id_for_recipient_row(start_recipient_row);
let should_include_start_recipient_header = false;
let last_recipient_row_id = start_recipient_row_id;
for (const row of copy_rows) {
const recipient_row_id = rows.id_for_recipient_row(rows.get_message_recipient_row(row));
// if we found a message from another recipient,
// it means that we have messages from several recipients,
// so we have to add new recipient's bar to final copied message
// and wouldn't forget to add start_recipient's bar at the beginning of final message
if (recipient_row_id !== last_recipient_row_id) {
div.append(construct_recipient_header(row));
last_recipient_row_id = recipient_row_id;
should_include_start_recipient_header = true;
}
const message = current_msg_list.get(rows.id(row));
const message_firstp = $(message.content).slice(0, 1);
message_firstp.prepend(message.sender_full_name + ": ");
div.append(message_firstp);
div.append($(message.content).slice(1));
}
if (should_include_start_recipient_header) {
div.prepend(construct_recipient_header(start_row));
}
}
function select_div(div, selection) {
div.css({
position: "absolute",
left: "-99999px",
// Color and background is made according to "day mode"
// 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 night mode.
// 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(div[0]);
}
function remove_div(div, ranges, selection) {
window.setTimeout(() => {
selection = window.getSelection();
selection.removeAllRanges();
for (const range of ranges) {
selection.addRange(range);
}
$("#copytempdiv").remove();
}, 0);
}
export function copy_handler() {
// 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
// entirely by the browser, our HTML layout, and our use of the
// no-select/auto-select CSS classes). We put considerable effort
// into producing a nice result that pastes well into other tools.
// Our user-facing specification is the following:
//
// * If the selection is contained within a single message, we
// want to just copy the portion that was selected, which we
// implement by letting the browser handle the Ctrl+C event.
//
// * Otherwise, we want to copy the bodies of all messages that
// were partially covered by the selection.
const selection = window.getSelection();
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;
const div = $("