ep_comments_page 11.0.22 → 11.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/locales/en.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"ep_comments_page.comments_template.accept_change.value" : "Accept Change",
|
|
13
13
|
"ep_comments_page.comments_template.revert_change.value" : "Revert Change",
|
|
14
14
|
"ep_comments_page.comments_template.suggested_change_from" : "Suggested change from \"{{changeFrom}}\" to \"{{changeTo}}\"",
|
|
15
|
+
"ep_comments_page.comments_template.suggested_change_from_label" : "Suggested change from",
|
|
15
16
|
"ep_comments_page.comments_template.suggest_change_from" : "Suggest change from \"{{changeFrom}}\" to",
|
|
16
17
|
"ep_comments_page.comments_template.to" : "To",
|
|
17
18
|
"ep_comments_page.comments_template.include_suggestion" : "Include suggested change",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"description": "Adds comments on sidebar and link it to the text. For no-skin use ep_page_view.",
|
|
3
3
|
"name": "ep_comments_page",
|
|
4
|
-
"version": "11.0.
|
|
4
|
+
"version": "11.0.24",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Nicolas Lescop",
|
|
7
7
|
"email": "limplementeur@gmail.com"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"eslint": "^8.57.1",
|
|
31
|
-
"eslint-config-etherpad": "^4.0.
|
|
31
|
+
"eslint-config-etherpad": "^4.0.5",
|
|
32
32
|
"socket.io-client": "^4.8.3",
|
|
33
33
|
"superagent": "^10.3.0",
|
|
34
34
|
"typescript": "^6.0.2"
|
package/static/js/index.js
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
const _ = require('underscore');
|
|
10
|
-
const browser = require('ep_etherpad-lite/static/js/browser');
|
|
11
10
|
const commentBoxes = require('ep_comments_page/static/js/commentBoxes');
|
|
12
11
|
const commentIcons = require('ep_comments_page/static/js/commentIcons');
|
|
13
12
|
const commentL10n = require('ep_comments_page/static/js/commentL10n');
|
|
@@ -113,6 +112,11 @@ EpComments.prototype.init = async function () {
|
|
|
113
112
|
});
|
|
114
113
|
$(window).resize(_.debounce(() => { this.setYofComments(); }, 100));
|
|
115
114
|
|
|
115
|
+
// Refresh the "N minutes ago" relative-time strings (#154). The original
|
|
116
|
+
// date is stored in the `datetime` attribute so we can recompute
|
|
117
|
+
// `fromNow()` without needing to track the cached string anywhere.
|
|
118
|
+
setInterval(() => this.refreshRelativeDates(), 60 * 1000);
|
|
119
|
+
|
|
116
120
|
// On click comment icon toolbar
|
|
117
121
|
$('.addComment').on('click', (e) => {
|
|
118
122
|
e.preventDefault(); // stops focus from being lost
|
|
@@ -351,29 +355,21 @@ EpComments.prototype.init = async function () {
|
|
|
351
355
|
// Check to see if we should show already..
|
|
352
356
|
$('#options-comments').trigger('change');
|
|
353
357
|
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
// The same does not occur when the user selects more than the span, for example:
|
|
362
|
-
// text<comment class='comment'><span>to be copied</span></comment>
|
|
363
|
-
if (browser.chrome || browser.firefox) {
|
|
364
|
-
this.padInner.contents().on('copy', (e) => {
|
|
365
|
-
events.addTextOnClipboard(
|
|
366
|
-
e, this.ace, this.padInner, false, this.comments, this.commentReplies);
|
|
367
|
-
});
|
|
358
|
+
// Override copy, cut, paste events so that the comment class is preserved.
|
|
359
|
+
// Chrome doesn't copy classes when only the inner span of a comment is
|
|
360
|
+
// selected, so we re-apply them on paste.
|
|
361
|
+
this.padInner.contents().on('copy', (e) => {
|
|
362
|
+
events.addTextOnClipboard(
|
|
363
|
+
e, this.ace, this.padInner, false, this.comments, this.commentReplies);
|
|
364
|
+
});
|
|
368
365
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
366
|
+
this.padInner.contents().on('cut', (e) => {
|
|
367
|
+
events.addTextOnClipboard(e, this.ace, this.padInner, true);
|
|
368
|
+
});
|
|
372
369
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
370
|
+
this.padInner.contents().on('paste', (e) => {
|
|
371
|
+
events.saveCommentsAndReplies(e);
|
|
372
|
+
});
|
|
377
373
|
};
|
|
378
374
|
|
|
379
375
|
EpComments.prototype.findCommentText = function ($commentBox) {
|
|
@@ -485,6 +481,12 @@ EpComments.prototype.collectComments = function (callback) {
|
|
|
485
481
|
});
|
|
486
482
|
|
|
487
483
|
this.padInner.contents().on('click', '.comment', function (e) {
|
|
484
|
+
// When comments are displayed as gutter icons, the user opens the
|
|
485
|
+
// comment by clicking the icon — the inline `.comment` span should
|
|
486
|
+
// behave like ordinary prose, so links inside a comment remain
|
|
487
|
+
// clickable. Previously the click handler popped up a modal
|
|
488
|
+
// intercepting the link click (#90).
|
|
489
|
+
if (clientVars.displayCommentAsIcon) return;
|
|
488
490
|
const commentId = self.commentIdOf(e);
|
|
489
491
|
commentBoxes.highlightComment(commentId, e, $(this));
|
|
490
492
|
});
|
|
@@ -694,6 +696,19 @@ EpComments.prototype.allCommentsOnCorrectYPosition = function () {
|
|
|
694
696
|
return allCommentsAreCorrect;
|
|
695
697
|
};
|
|
696
698
|
|
|
699
|
+
// Recompute "N minutes ago" style text on every `.comment-created-at`
|
|
700
|
+
// element, using the ISO timestamp stored in the element's `datetime`
|
|
701
|
+
// attribute as the source of truth (#154).
|
|
702
|
+
EpComments.prototype.refreshRelativeDates = function () {
|
|
703
|
+
if (this.container == null) return;
|
|
704
|
+
this.container.find('.comment-created-at[datetime]').each(function () {
|
|
705
|
+
const iso = $(this).attr('datetime');
|
|
706
|
+
if (!iso) return;
|
|
707
|
+
const next = moment(iso).fromNow();
|
|
708
|
+
if ($(this).text() !== next) $(this).text(next);
|
|
709
|
+
});
|
|
710
|
+
};
|
|
711
|
+
|
|
697
712
|
EpComments.prototype.localizeExistingComments = function () {
|
|
698
713
|
const self = this;
|
|
699
714
|
const padComments = this.padInner.contents().find('.comment');
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('assert').strict;
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const pluginRoot = path.resolve(__dirname, '..', '..', '..', '..');
|
|
8
|
+
const enPath = path.join(pluginRoot, 'locales', 'en.json');
|
|
9
|
+
const commentsTemplatePath = path.join(pluginRoot, 'templates', 'comments.html');
|
|
10
|
+
|
|
11
|
+
const readJSON = (p) => JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
12
|
+
|
|
13
|
+
describe(__filename, function () {
|
|
14
|
+
let en;
|
|
15
|
+
let template;
|
|
16
|
+
|
|
17
|
+
before(function () {
|
|
18
|
+
en = readJSON(enPath);
|
|
19
|
+
template = fs.readFileSync(commentsTemplatePath, 'utf8');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('every data-l10n-id referenced in comments.html exists in en.json', function () {
|
|
23
|
+
const ids = new Set();
|
|
24
|
+
const re = /data-l10n-id="([^"]+)"/g;
|
|
25
|
+
let m;
|
|
26
|
+
while ((m = re.exec(template)) !== null) ids.add(m[1]);
|
|
27
|
+
assert(ids.size > 0, 'expected at least one data-l10n-id in comments.html');
|
|
28
|
+
for (const id of ids) {
|
|
29
|
+
// Some data-l10n-id values are computed via jquery template syntax like
|
|
30
|
+
// `{{if reply}}...{{/if}}`. Enumerate the possible keys in that case.
|
|
31
|
+
if (id.includes('{{if reply}}')) {
|
|
32
|
+
const a = id.replace('{{if reply}}reply{{else}}comment{{/if}}', 'reply');
|
|
33
|
+
const b = id.replace('{{if reply}}reply{{else}}comment{{/if}}', 'comment');
|
|
34
|
+
assert(Object.prototype.hasOwnProperty.call(en, a), `missing ${a}`);
|
|
35
|
+
assert(Object.prototype.hasOwnProperty.call(en, b), `missing ${b}`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
assert(Object.prototype.hasOwnProperty.call(en, id),
|
|
39
|
+
`missing translation for data-l10n-id "${id}"`);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('suggestion label does not use a key with unresolved placeholders (regression for #273)',
|
|
44
|
+
function () {
|
|
45
|
+
// The display-suggestion template previously used
|
|
46
|
+
// `suggested_change_from` whose English value contains {{changeFrom}} /
|
|
47
|
+
// {{changeTo}}. No data-l10n-args is provided on the span, so the
|
|
48
|
+
// placeholders were rendered as literal text. The replacement label
|
|
49
|
+
// key must not require any arguments.
|
|
50
|
+
const labelKey = 'ep_comments_page.comments_template.suggested_change_from_label';
|
|
51
|
+
assert(Object.prototype.hasOwnProperty.call(en, labelKey),
|
|
52
|
+
`expected ${labelKey} in en.json`);
|
|
53
|
+
assert(!/\{\{/.test(en[labelKey]),
|
|
54
|
+
`${labelKey} must not contain {{placeholders}}; got: ${en[labelKey]}`);
|
|
55
|
+
|
|
56
|
+
// The template references the new label key.
|
|
57
|
+
assert(template.includes(`data-l10n-id="${labelKey}"`),
|
|
58
|
+
`comments.html should reference ${labelKey} for the suggestion display label`);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('assert').strict;
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const pluginRoot = path.resolve(__dirname, '..', '..', '..', '..');
|
|
8
|
+
const indexPath = path.join(pluginRoot, 'static', 'js', 'index.js');
|
|
9
|
+
|
|
10
|
+
describe(__filename, function () {
|
|
11
|
+
let src;
|
|
12
|
+
before(function () {
|
|
13
|
+
src = fs.readFileSync(indexPath, 'utf8');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('registers a setInterval that calls refreshRelativeDates (#154)', function () {
|
|
17
|
+
// Any cadence works, but the interval must actually be scheduled so the
|
|
18
|
+
// relative-time strings update without a page reload. A one-line regex
|
|
19
|
+
// can't reliably parse nested parens (`setInterval(() => ...)`), so
|
|
20
|
+
// just require the two identifiers to live in the same statement.
|
|
21
|
+
const stmt = src.match(/setInterval[\s\S]*?refreshRelativeDates[\s\S]*?\);/);
|
|
22
|
+
assert(stmt, 'init should schedule setInterval(() => this.refreshRelativeDates(), ...)');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('refreshRelativeDates recomputes text from the datetime attribute (#154)',
|
|
26
|
+
function () {
|
|
27
|
+
const match = src.match(
|
|
28
|
+
/EpComments\.prototype\.refreshRelativeDates\s*=\s*function[\s\S]*?\n\};/);
|
|
29
|
+
assert(match, 'refreshRelativeDates should be defined on EpComments.prototype');
|
|
30
|
+
const body = match[0];
|
|
31
|
+
assert(/comment-created-at\[datetime\]/.test(body),
|
|
32
|
+
'refreshRelativeDates must target elements with a datetime attribute so the ISO ' +
|
|
33
|
+
'timestamp can be used as the source of truth');
|
|
34
|
+
assert(/moment\([^)]+\)\.fromNow\(\)/.test(body),
|
|
35
|
+
'refreshRelativeDates should recompute the relative time via moment(iso).fromNow()');
|
|
36
|
+
});
|
|
37
|
+
});
|
package/templates/comments.html
CHANGED
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
{{if changeTo}}
|
|
98
98
|
<form class="comment-changeTo-form suggestion-display">
|
|
99
99
|
<div>
|
|
100
|
-
<span class="from-label" data-l10n-id="ep_comments_page.comments_template.
|
|
100
|
+
<span class="from-label" data-l10n-id="ep_comments_page.comments_template.suggested_change_from_label">Suggested change from</span>
|
|
101
101
|
<span class="hidden from-value">${changeFrom}</span>
|
|
102
102
|
<span class="hidden to-value">${changeTo}</span>
|
|
103
103
|
</div>
|