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.22",
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.4",
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"
@@ -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
- // TODO - Implement to others browser like, Microsoft Edge, Opera, IE
355
- // Override copy, cut, paste events on Google chrome and Mozilla Firefox.
356
- // When an user copies a comment and selects only the span, or part of it, Google chrome
357
- // does not copy the classes only the styles, for example:
358
- // <comment class='comment'><span>text to be copied</span></comment>
359
- // As the comment classes are not only used for styling we have to add these classes when it
360
- // pastes the content
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
- this.padInner.contents().on('cut', (e) => {
370
- events.addTextOnClipboard(e, this.ace, this.padInner, true);
371
- });
366
+ this.padInner.contents().on('cut', (e) => {
367
+ events.addTextOnClipboard(e, this.ace, this.padInner, true);
368
+ });
372
369
 
373
- this.padInner.contents().on('paste', (e) => {
374
- events.saveCommentsAndReplies(e);
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
+ });
@@ -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.suggested_change_from">Suggested Change From</span>
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>