Skip to content

Commit e52c651

Browse files
JohnMcLearclaude
andauthored
feat: floating add-comment button anchored to the selection (#95) (#438)
* feat: floating add-comment button anchored to the selection (#95) Add a small floating 'add comment' button that appears next to the selected text and opens the new-comment form, so users don't have to go to the toolbar. It's positioned inline (in #editorcontainerbox space, reusing the new-comment popup's getXYOffsetOfRep) and clamped on-screen with the same idea as the #192 mobile fix, so it behaves the same on desktop and narrow / mobile viewports — it's anchored to the text rather than the sidebar (which is hidden below 900px). It tracks the selection via aceEditEvent, hides when nothing is selected, on read-only pads, and while the new-comment form is already open. mousedown.preventDefault preserves the selection so the form still sees it. Pure plugin change — works on the latest release (2.7.3) and develop without a core update. Adds a Playwright test covering appear-on-select, on-screen clamp, open-the-form, and hide-after. Closes #95 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat: make floating comment button admin-disableable (#95) Gate the floating add-comment button behind ep_comments_page.floatingCommentButton (default true) so admins can turn it off without code changes. Documented in the README. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f069bfe commit e52c651

5 files changed

Lines changed: 161 additions & 1 deletion

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ by default; disable it with:
8080
```
8181
"ep_comments_page": {
8282
"showAuthorColor": false
83+
### Floating comment button
84+
When text is selected, a small floating button appears next to it for quickly
85+
adding a comment. This is on by default; disable it with:
86+
```
87+
"ep_comments_page": {
88+
"floatingCommentButton": false
8389
},
8490
```
8591

index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,9 @@ exports.clientVars = async (hook, context) => {
278278
settings.ep_comments_page ? settings.ep_comments_page.displayCommentAsIcon : false;
279279
const highlightSelectedText =
280280
settings.ep_comments_page ? settings.ep_comments_page.highlightSelectedText : false;
281+
// #95: the floating add-comment button is on unless an admin disables it.
282+
const floatingCommentButton = !(settings.ep_comments_page &&
283+
settings.ep_comments_page.floatingCommentButton === false);
281284
// #6: author-colour accent is on unless an admin disables it.
282285
const showAuthorColor = !(settings.ep_comments_page &&
283286
settings.ep_comments_page.showAuthorColor === false);
@@ -288,7 +291,8 @@ exports.clientVars = async (hook, context) => {
288291
// helper can read padWideSupported/initialPadEnabled/etc.
289292
const helperVars = await commentsToggle.clientVars(hook, context);
290293
return Object.assign(
291-
{displayCommentAsIcon, highlightSelectedText, showAuthorColor, allowReadonlyComments},
294+
{displayCommentAsIcon, highlightSelectedText, floatingCommentButton, showAuthorColor,
295+
allowReadonlyComments},
292296
helperVars);
293297
};
294298

static/css/comment.css

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,37 @@ input.error, textarea.error {
311311
display: none !important;
312312
}
313313

314+
/* #95: floating "add comment" button that follows the selection. Positioned
315+
inline (in #editorcontainerbox space) by the plugin and clamped on-screen, so
316+
it works the same on desktop and narrow/mobile viewports. It is shown
317+
translucent so it doesn't obscure the selection, and becomes fully opaque on
318+
hover/focus (#95). */
319+
.floating-add-comment {
320+
position: absolute;
321+
display: none;
322+
z-index: 100;
323+
width: 26px;
324+
height: 26px;
325+
line-height: 26px;
326+
text-align: center;
327+
border-radius: 50%;
328+
background: #fff;
329+
box-shadow: 0 1px 4px rgba(0, 0, 0, .3);
330+
cursor: pointer;
331+
color: #444;
332+
user-select: none;
333+
opacity: .55;
334+
transition: opacity .15s ease;
335+
}
336+
.floating-add-comment.visible {
337+
display: block;
338+
}
339+
.floating-add-comment:hover,
340+
.floating-add-comment:focus {
341+
background: #f3f3f3;
342+
opacity: 1;
343+
}
344+
314345
/* #12/#5: all-comments overview panel (opened from the "Show all comments"
315346
checkbox in the Settings pane). */
316347
#comments-overview {

static/js/index.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,78 @@ const getXYOffsetOfRep = (rep) => {
10431043
}
10441044
};
10451045

1046+
// #95: floating "add comment" button anchored to the current selection.
1047+
// Lazily build the element once, in the same container the new-comment popup
1048+
// uses (#editorcontainerbox), so it shares the popup's coordinate space.
1049+
EpComments.prototype.ensureFloatingAddCommentButton = function () {
1050+
if (this.$floatingAddComment && this.$floatingAddComment.length) {
1051+
return this.$floatingAddComment;
1052+
}
1053+
const self = this;
1054+
const $btn = $('<div>')
1055+
.addClass('floating-add-comment')
1056+
.attr('role', 'button')
1057+
.attr('tabindex', '0')
1058+
// Localize via data-l10n-id (sets the title/accessible name through
1059+
// html10n) rather than a hardcoded aria-label, so it translates and
1060+
// re-localizes on language change (#95).
1061+
.attr('data-l10n-id', 'ep_comments_page.add_comment.title')
1062+
.attr('title',
1063+
(html10n.translations && html10n.translations['ep_comments_page.add_comment.title']) ||
1064+
'Add comment')
1065+
.append($('<span>').addClass('buttonicon buttonicon-comment-medical'));
1066+
// mousedown.preventDefault keeps the editor selection intact (same trick the
1067+
// toolbar button uses), so displayNewCommentForm still sees the selection.
1068+
$btn.on('mousedown', (e) => e.preventDefault());
1069+
$btn.on('click', (e) => {
1070+
e.preventDefault();
1071+
self.hideFloatingAddCommentButton();
1072+
self.displayNewCommentForm();
1073+
});
1074+
// ARIA button semantics: a non-native button must be activatable by keyboard
1075+
// (Enter / Space), not just click (#95).
1076+
$btn.on('keydown', (e) => {
1077+
if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
1078+
e.preventDefault();
1079+
$btn.trigger('click');
1080+
}
1081+
});
1082+
$btn.appendTo($('#editorcontainerbox'));
1083+
this.$floatingAddComment = $btn;
1084+
return $btn;
1085+
};
1086+
1087+
EpComments.prototype.hideFloatingAddCommentButton = function () {
1088+
if (this.$floatingAddComment) this.$floatingAddComment.removeClass('visible');
1089+
};
1090+
1091+
EpComments.prototype.updateFloatingAddCommentButton = function (rep) {
1092+
if (clientVars.floatingCommentButton === false) return;
1093+
if (!rep || !rep.selStart || !rep.selEnd) return;
1094+
const hasSelection =
1095+
rep.selStart[0] !== rep.selEnd[0] || rep.selStart[1] !== rep.selEnd[1];
1096+
if (!hasSelection) return this.hideFloatingAddCommentButton();
1097+
// On read-only pads commenting isn't possible, so don't offer the button.
1098+
if (clientVars.readonly) return this.hideFloatingAddCommentButton();
1099+
// Don't compete with the new-comment form once it's open over the selection.
1100+
if ($('#newComment').hasClass('popup-show')) return this.hideFloatingAddCommentButton();
1101+
1102+
const position = getXYOffsetOfRep(rep); // [left, top] in #editorcontainerbox space
1103+
if (!position) return this.hideFloatingAddCommentButton();
1104+
1105+
const $btn = this.ensureFloatingAddCommentButton();
1106+
const container = $('#editorcontainerbox');
1107+
const containerWidth = container.width() || 0;
1108+
const btnWidth = $btn.outerWidth(true) || 28;
1109+
// Reuse the on-screen clamp idea from #192 so the button never spills off the
1110+
// edge on a narrow / mobile viewport.
1111+
const margin = 6;
1112+
let left = position[0] + 4;
1113+
left = Math.min(Math.max(left, margin), Math.max(margin, containerWidth - btnWidth - margin));
1114+
const top = Math.max(margin, position[1] + 2);
1115+
$btn.css({left: `${left}px`, top: `${top}px`}).addClass('visible');
1116+
}
1117+
10461118
// #12: a togglable panel listing every comment in the pad, with click-to-jump.
10471119
// Distinct from the Y-aligned sidebar — this is a flat, scrollable index.
10481120
EpComments.prototype.ensureCommentsOverview = function () {
@@ -1165,6 +1237,7 @@ EpComments.prototype.navigateComment = function ($el, dir) {
11651237
};
11661238

11671239
EpComments.prototype.displayNewCommentForm = function () {
1240+
this.hideFloatingAddCommentButton();
11681241
const rep = {};
11691242
const ace = this.ace;
11701243

@@ -1575,6 +1648,13 @@ const hooks = {
15751648

15761649
if (['setup', 'setBaseText', 'importText'].includes(eventType)) return;
15771650

1651+
// Show/position the floating "add comment" button next to the current
1652+
// selection (#95). aceEditEvent fires on selection changes, so this keeps
1653+
// the button anchored to the text and hidden when nothing is selected.
1654+
if (pad.plugins.ep_comments_page) {
1655+
pad.plugins.ep_comments_page.updateFloatingAddCommentButton(context.rep);
1656+
}
1657+
15781658
if (context.callstack.docTextChanged && pad.plugins.ep_comments_page) {
15791659
pad.plugins.ep_comments_page.setYofComments();
15801660
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {expect, test} from '@playwright/test';
2+
import {getPadBody} from 'ep_etherpad-lite/tests/frontend-new/helper/padHelper';
3+
import {aNewCommentsPad} from '../helper/comments';
4+
5+
// #95: a floating "add comment" button appears next to the selection and opens
6+
// the new-comment form, so users don't have to reach for the toolbar. It is
7+
// anchored to the text (not the sidebar) so it also works on narrow viewports.
8+
test.describe('ep_comments_page - Floating add-comment button (#95)', () => {
9+
test('appears on selection, opens the form, and stays on-screen', async ({page}) => {
10+
test.setTimeout(60_000);
11+
await aNewCommentsPad(page);
12+
const inner = await getPadBody(page);
13+
const visibleFab = page.locator('.floating-add-comment.visible');
14+
15+
// No selection yet → button not visible.
16+
await inner.click();
17+
await page.keyboard.press('Control+A');
18+
await page.keyboard.press('Delete');
19+
await page.keyboard.type('please comment on this text');
20+
await expect.poll(async () => visibleFab.count()).toBe(0);
21+
22+
// Select the text → button becomes visible and within the viewport.
23+
await page.keyboard.press('Control+A');
24+
await expect.poll(async () => visibleFab.count()).toBeGreaterThan(0);
25+
26+
const box = await visibleFab.boundingBox();
27+
const vp = page.viewportSize();
28+
expect(box).not.toBeNull();
29+
if (box && vp) {
30+
expect(box.x).toBeGreaterThanOrEqual(-2);
31+
expect(box.x + box.width).toBeLessThanOrEqual(vp.width + 2);
32+
}
33+
34+
// Clicking it opens the new-comment form and hides the button.
35+
await visibleFab.click();
36+
await expect(page.locator('#newComment.popup-show')).toBeVisible();
37+
await expect.poll(async () => visibleFab.count()).toBe(0);
38+
});
39+
});

0 commit comments

Comments
 (0)