forked from zulip/zulip
-
Notifications
You must be signed in to change notification settings - Fork 0
/
copy_and_paste.js
256 lines (228 loc) · 9.02 KB
/
copy_and_paste.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
var copy_and_paste = (function () {
var exports = {};
function find_boundary_tr(initial_tr, iterate_row) {
var j;
var skip_same_td_check = false;
var 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;
}
// 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;
} 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) {
var message_header_content = rows.get_message_recipient_header(message_row)
.text()
.replace(/\s+/g, " ")
.replace(/^\s/, "").replace(/\s$/, "");
return $('<p>').append($('<strong>').text(message_header_content));
}
function construct_copy_div(div, start_id, end_id) {
var start_row = current_msg_list.get_row(start_id);
var start_recipient_row = rows.get_message_recipient_row(start_row);
var start_recipient_row_id = rows.id_for_recipient_row(start_recipient_row);
var should_include_start_recipient_header = false;
var last_recipient_row_id = start_recipient_row_id;
for (var row = start_row; rows.id(row) <= end_id; row = rows.next_visible(row)) {
var 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;
}
var message = current_msg_list.get(rows.id(row));
var 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 copy_handler() {
var selection = window.getSelection();
var i;
var range;
var ranges = [];
var startc;
var endc;
var initial_end_tr;
var start_id;
var end_id;
var start_data;
var end_data;
var skip_same_td_check = false;
var div = $('<div>');
for (i = 0; i < selection.rangeCount; i += 1) {
range = selection.getRangeAt(i);
ranges.push(range);
startc = $(range.startContainer);
start_data = find_boundary_tr($(startc.parents('.selectable_row, .message_header')[0]), function (row) {
return row.next();
});
if (start_data === undefined) {
return;
}
start_id = start_data[0];
endc = $(range.endContainer);
// If the selection ends in the bottom whitespace, we should act as
// though the selection ends on the final message
// Chrome seems to like selecting the compose_close button
// when you go off the end of the last message
if (endc.attr('id') === "bottom_whitespace" || endc.attr('id') === "compose_close") {
initial_end_tr = $(".message_row:last");
skip_same_td_check = true;
} else {
initial_end_tr = $(endc.parents('.selectable_row')[0]);
}
end_data = find_boundary_tr(initial_end_tr, function (row) {
return row.prev();
});
if (end_data === undefined) {
return;
}
end_id = end_data[0];
if (start_data[1] || end_data[1]) {
skip_same_td_check = true;
}
// we should let the browser handle the copy-paste entirely on its own
// (In this case, there is no need for our special copy code)
if (!skip_same_td_check &&
startc.parents('.selectable_row>div')[0] === endc.parents('.selectable_row>div')[0]) {
return;
}
// Construct a div for what we want to copy (div)
construct_copy_div(div, start_id, end_id);
}
// Select div so that the browser will copy it
// instead of copying the original selection
div.css({position: 'absolute', left: '-99999px'})
.attr('id', 'copytempdiv');
$('body').append(div);
selection.selectAllChildren(div[0]);
/*
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, return from
this function, 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.
*/
window.setTimeout(function () {
selection = window.getSelection();
selection.removeAllRanges();
_.each(ranges, function (range) {
selection.addRange(range);
});
$('#copytempdiv').remove();
},0);
}
exports.paste_handler_converter = function (paste_html) {
var converters = {
converters: [
{
filter: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
replacement: function (content) {
return content;
},
},
{
filter: ['em', 'i'],
replacement: function (content) {
return '*' + content + '*';
},
},
{
// Checks for raw links without custom text or title.
filter: function (node) {
return node.nodeName === "A" &&
node.href === node.innerHTML &&
node.href === node.title;
},
replacement: function (content) {
return content;
},
},
{
// Checks for escaped ordered list syntax.
filter: function (node) {
return /(\d+)\\\. /.test(node.innerHTML);
},
replacement: function (content) {
return content.replace(/(\d+)\\\. /g, '$1. ');
},
},
],
};
var markdown_html = toMarkdown(paste_html, converters);
// Now that we've done the main conversion, we want to remove
// any HTML tags that weren't converted to markdown-style
// text, since Bugdown doesn't support those.
var div = document.createElement("div");
div.innerHTML = markdown_html;
// Using textContent for modern browsers, innerText works for Internet Explorer
var markdown_text = div.textContent || div.innerText || "";
markdown_text = markdown_text.trim();
// Removes newlines before the start of a list and between list elements.
markdown_text = markdown_text.replace(/\n+([*+-])/g, '\n$1');
return markdown_text;
};
exports.paste_handler = function (event) {
var clipboardData = event.originalEvent.clipboardData;
if (!clipboardData) {
// On IE11, ClipboardData isn't defined. One can instead
// access it with `window.clipboardData`, but even that
// doesn't support text/html, so this code path couldn't do
// anything special anyway. So we instead just let the
// default paste handler run on IE11.
return;
}
if (clipboardData.getData) {
var paste_html = clipboardData.getData('text/html');
if (paste_html && page_params.development_environment) {
event.preventDefault();
var text = exports.paste_handler_converter(paste_html);
compose_ui.insert_syntax_and_focus(text);
}
}
};
exports.initialize = function () {
$(document).on('copy', copy_handler);
$("#compose-textarea").bind('paste', exports.paste_handler);
};
return exports;
}());
if (typeof module !== 'undefined') {
module.exports = copy_and_paste;
}
window.copy_and_paste = copy_and_paste;