Feature #41294 » 0004-Separate-the-quote-reply-implementation-into-quote_reply.js.patch
app/assets/javascripts/application.js | ||
---|---|---|
1262 | 1262 |
tribute.attach(element); |
1263 | 1263 |
} |
1264 | 1264 | |
1265 |
function quoteReply(path, selectorForContentElement) { |
|
1266 |
const contentElement = $(selectorForContentElement).get(0); |
|
1267 |
const quote = QuoteExtractor.extract(contentElement); |
|
1268 | ||
1269 |
$.ajax({ |
|
1270 |
url: path, |
|
1271 |
type: 'post', |
|
1272 |
data: { quote: quote } |
|
1273 |
}); |
|
1274 |
} |
|
1275 | ||
1276 |
class QuoteExtractor { |
|
1277 |
static extract(targetElement) { |
|
1278 |
return new QuoteExtractor(targetElement).extract(); |
|
1279 |
} |
|
1280 | ||
1281 |
constructor(targetElement) { |
|
1282 |
this.targetElement = targetElement; |
|
1283 |
this.selection = window.getSelection(); |
|
1284 |
} |
|
1285 | ||
1286 |
extract() { |
|
1287 |
const range = this.selectedRange; |
|
1288 | ||
1289 |
if (!range) { |
|
1290 |
return null; |
|
1291 |
} |
|
1292 | ||
1293 |
if (!this.targetElement.contains(range.startContainer)) { |
|
1294 |
range.setStartBefore(this.targetElement); |
|
1295 |
} |
|
1296 |
if (!this.targetElement.contains(range.endContainer)) { |
|
1297 |
range.setEndAfter(this.targetElement); |
|
1298 |
} |
|
1299 | ||
1300 |
return this.formatRange(range); |
|
1301 |
} |
|
1302 | ||
1303 |
formatRange(range) { |
|
1304 |
return range.toString().trim(); |
|
1305 |
} |
|
1306 | ||
1307 |
get selectedRange() { |
|
1308 |
if (!this.isSelected) { |
|
1309 |
return null; |
|
1310 |
} |
|
1311 | ||
1312 |
// Retrive the first range that intersects with the target element. |
|
1313 |
// NOTE: Firefox allows to select multiple ranges in the document. |
|
1314 |
for (let i = 0; i < this.selection.rangeCount; i++) { |
|
1315 |
let range = this.selection.getRangeAt(i); |
|
1316 |
if (range.intersectsNode(this.targetElement)) { |
|
1317 |
return range; |
|
1318 |
} |
|
1319 |
} |
|
1320 |
return null; |
|
1321 |
} |
|
1322 | ||
1323 |
get isSelected() { |
|
1324 |
return this.selection.containsNode(this.targetElement, true); |
|
1325 |
} |
|
1326 |
} |
|
1327 | ||
1328 | 1265 |
$(document).ready(setupAjaxIndicator); |
1329 | 1266 |
$(document).ready(hideOnLoad); |
1330 | 1267 |
$(document).ready(addFormObserversForDoubleSubmit); |
app/assets/javascripts/quote_reply.js | ||
---|---|---|
1 |
function quoteReply(path, selectorForContentElement) { |
|
2 |
const contentElement = $(selectorForContentElement).get(0); |
|
3 |
const quote = QuoteExtractor.extract(contentElement); |
|
4 | ||
5 |
$.ajax({ |
|
6 |
url: path, |
|
7 |
type: 'post', |
|
8 |
data: { quote: quote } |
|
9 |
}); |
|
10 |
} |
|
11 | ||
12 |
class QuoteExtractor { |
|
13 |
static extract(targetElement) { |
|
14 |
return new QuoteExtractor(targetElement).extract(); |
|
15 |
} |
|
16 | ||
17 |
constructor(targetElement) { |
|
18 |
this.targetElement = targetElement; |
|
19 |
this.selection = window.getSelection(); |
|
20 |
} |
|
21 | ||
22 |
extract() { |
|
23 |
const range = this.selectedRange; |
|
24 | ||
25 |
if (!range) { |
|
26 |
return null; |
|
27 |
} |
|
28 | ||
29 |
if (!this.targetElement.contains(range.startContainer)) { |
|
30 |
range.setStartBefore(this.targetElement); |
|
31 |
} |
|
32 |
if (!this.targetElement.contains(range.endContainer)) { |
|
33 |
range.setEndAfter(this.targetElement); |
|
34 |
} |
|
35 | ||
36 |
return this.formatRange(range); |
|
37 |
} |
|
38 | ||
39 |
formatRange(range) { |
|
40 |
return range.toString().trim(); |
|
41 |
} |
|
42 | ||
43 |
get selectedRange() { |
|
44 |
if (!this.isSelected) { |
|
45 |
return null; |
|
46 |
} |
|
47 | ||
48 |
// Retrive the first range that intersects with the target element. |
|
49 |
// NOTE: Firefox allows to select multiple ranges in the document. |
|
50 |
for (let i = 0; i < this.selection.rangeCount; i++) { |
|
51 |
let range = this.selection.getRangeAt(i); |
|
52 |
if (range.intersectsNode(this.targetElement)) { |
|
53 |
return range; |
|
54 |
} |
|
55 |
} |
|
56 |
return null; |
|
57 |
} |
|
58 | ||
59 |
get isSelected() { |
|
60 |
return this.selection.containsNode(this.targetElement, true); |
|
61 |
} |
|
62 |
} |
app/views/issues/show.html.erb | ||
---|---|---|
1 |
<% content_for :header_tags do %> |
|
2 |
<%= javascript_include_tag 'quote_reply' %> |
|
3 |
<% end %> |
|
4 | ||
1 | 5 |
<%= render :partial => 'action_menu' %> |
2 | 6 | |
3 | 7 |
<h2 class="inline-block"><%= issue_heading(@issue) %></h2><%= issue_status_type_badge(@issue.status) %> |
app/views/messages/show.html.erb | ||
---|---|---|
1 |
<% content_for :header_tags do %> |
|
2 |
<%= javascript_include_tag 'quote_reply' %> |
|
3 |
<% end %> |
|
4 | ||
1 | 5 |
<%= board_breadcrumb(@message) %> |
2 | 6 | |
3 | 7 |
<div class="contextual"> |