Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions apps/docs/core/superdoc/methods.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,61 @@ const superdoc = new SuperDoc({

</CodeGroup>

### `scrollToComment`

Scroll to a comment in the document and set it as active.

<ParamField path="commentId" type="string" required>
The comment ID to scroll to
</ParamField>

<ParamField path="options" type="object">
Scroll behavior options
<Expandable>
<ParamField path="behavior" type="ScrollBehavior" default="smooth">
Scroll behavior — `"smooth"`, `"instant"`, or `"auto"`
</ParamField>
<ParamField path="block" type="ScrollLogicalPosition" default="start">
Vertical alignment — `"start"`, `"center"`, `"end"`, or `"nearest"`
</ParamField>
</Expandable>
</ParamField>

**Returns:** `boolean` — `true` if the comment was found, `false` otherwise.

<CodeGroup>

```javascript Usage
// Get a comment ID from the document API
const { items } = superdoc.editor.doc.comments.list();
const commentId = items[0].id;

// Scroll to it
superdoc.scrollToComment(commentId);
```

```javascript Full Example
import { SuperDoc } from 'superdoc';
import 'superdoc/style.css';

const superdoc = new SuperDoc({
selector: '#editor',
document: yourFile,
modules: { comments: {} },
onReady: (superdoc) => {
const { items } = superdoc.editor.doc.comments.list();
if (items.length > 0) {
superdoc.scrollToComment(items[0].id, {
behavior: 'smooth',
block: 'center',
});
}
},
});
```

</CodeGroup>

## User management

### `addSharedUser`
Expand Down
33 changes: 33 additions & 0 deletions apps/docs/modules/comments.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,39 @@ const superdoc = new SuperDoc({

</CodeGroup>

### `scrollToComment`

Scroll the document to a comment and set it as active. Unlike `setCursorById`, this is a top-level SuperDoc method that works without accessing the editor directly.

<CodeGroup>

```javascript Usage
superdoc.scrollToComment("comment-123");
```

```javascript Full Example
import { SuperDoc } from 'superdoc';
import 'superdoc/style.css';

const superdoc = new SuperDoc({
selector: '#editor',
document: yourFile,
modules: { comments: {} },
onReady: (superdoc) => {
const { items } = superdoc.editor.doc.comments.list();
if (items.length > 0) {
superdoc.scrollToComment(items[0].id);
}
},
});
```

</CodeGroup>

<Tip>
See [SuperDoc Methods](/core/superdoc/methods#scrolltocomment) for full parameter documentation.
</Tip>

## Events

### `onCommentsUpdate`
Expand Down
9 changes: 1 addition & 8 deletions packages/superdoc/src/SuperDoc.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1028,14 +1028,7 @@ watch(showCommentsSidebar, (value) => {
* @param {String} commentId The commentId to scroll to
*/
const scrollToComment = (commentId) => {
const commentsConfig = proxy.$superdoc.config?.modules?.comments;
if (!commentsConfig || commentsConfig === false) return;

const element = document.querySelector(`[data-thread-id=${commentId}]`);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
commentsStore.setActiveComment(proxy.$superdoc, commentId);
}
proxy.$superdoc.scrollToComment(commentId);
};

onMounted(() => {
Expand Down
23 changes: 23 additions & 0 deletions packages/superdoc/src/core/SuperDoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,29 @@ export class SuperDoc extends EventEmitter {
}
}

/**
* Scroll the document to a given comment by id.
*
* @param {string} commentId The comment id
* @param {{ behavior?: ScrollBehavior, block?: ScrollLogicalPosition }} [options]
* @returns {boolean} Whether a matching element was found
*/
scrollToComment(commentId, options = {}) {
const commentsConfig = this.config?.modules?.comments;
if (!commentsConfig || commentsConfig === false) return false;
if (!commentId || typeof commentId !== 'string') return false;

const root = this.element || document;
const escaped = globalThis.CSS?.escape ? globalThis.CSS.escape(commentId) : commentId.replace(/"/g, '\\"');
const element = root.querySelector(`[data-comment-ids*="${escaped}"]`);
if (!element) return false;

const { behavior = 'smooth', block = 'start' } = options ?? {};
element.scrollIntoView({ behavior, block });
this.commentsStore?.setActiveComment?.(this, commentId);
return true;
}

/**
* Toggle the custom context menu globally.
* Updates both flow editors and PresentationEditor instances so downstream listeners can short-circuit early.
Expand Down
38 changes: 38 additions & 0 deletions packages/superdoc/src/core/SuperDoc.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,44 @@ describe('SuperDoc core', () => {
expect(instance.user).toEqual(expect.objectContaining({ name: 'Default SuperDoc user', email: null }));
});

it('scrolls to a comment and sets it active', async () => {
const { commentsStore } = createAppHarness();
const instance = new SuperDoc({
selector: '#host',
document: 'https://example.com/doc.docx',
documents: [],
modules: { comments: {}, toolbar: {} },
colors: ['red'],
user: { name: 'Jane', email: 'jane@example.com' },
onException: vi.fn(),
});
await flushMicrotasks();

const target = document.createElement('div');
target.setAttribute('data-comment-ids', 'comment-1');
target.scrollIntoView = vi.fn();
document.querySelector('#host').appendChild(target);

const result = instance.scrollToComment('comment-1');
expect(result).toBe(true);
expect(target.scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth', block: 'start' });
expect(commentsStore.setActiveComment).toHaveBeenCalledWith(instance, 'comment-1');
});

it('returns false when comment element is not found', async () => {
createAppHarness();
const instance = new SuperDoc({
selector: '#host',
document: 'https://example.com/doc.docx',
documents: [],
modules: { comments: {}, toolbar: {} },
onException: vi.fn(),
});
await flushMicrotasks();

expect(instance.scrollToComment('nonexistent-id')).toBe(false);
});

it('warns when both document object and documents list provided', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
createAppHarness();
Expand Down
51 changes: 51 additions & 0 deletions tests/behavior/tests/comments/scroll-to-comment.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { test, expect } from '../../fixtures/superdoc.js';
import { addCommentByText, assertDocumentApiReady } from '../../helpers/document-api.js';

test.use({ config: { toolbar: 'full', comments: 'on' } });

test('scrollToComment scrolls to the comment and activates it', async ({ superdoc }) => {
await assertDocumentApiReady(superdoc.page);

// Create enough content so the comment is off-screen
for (let i = 0; i < 30; i++) {
await superdoc.type(`Line ${i}`);
await superdoc.newLine();
}
await superdoc.type('target text');
await superdoc.waitForStable();

const commentId = await addCommentByText(superdoc.page, {
pattern: 'target text',
text: 'scroll test comment',
});
await superdoc.waitForStable();
await superdoc.assertCommentHighlightExists({ text: 'target text', timeoutMs: 20_000 });

// Scroll to the top so the comment is out of view
await superdoc.page.evaluate(() => {
document.querySelector('.superdoc')?.scrollTo({ top: 0 });
});
await superdoc.waitForStable();

// Call scrollToComment via the public API.
// WebKit can lag on DOM attribute propagation, so poll until it succeeds.
await expect
.poll(async () => superdoc.page.evaluate((id) => (window as any).superdoc.scrollToComment(id), commentId), {
timeout: 10_000,
})
.toBe(true);

// Verify the comment highlight is now visible in the viewport
const highlight = superdoc.page.locator('.superdoc-comment-highlight').filter({ hasText: 'target text' });
await expect(highlight.first()).toBeVisible({ timeout: 5_000 });
});

test('scrollToComment returns false for a nonexistent comment', async ({ superdoc }) => {
await assertDocumentApiReady(superdoc.page);

const result = await superdoc.page.evaluate(() => {
return (window as any).superdoc.scrollToComment('nonexistent-id');
});

expect(result).toBe(false);
});
Loading