diff --git a/frontend/core/__tests__/ArticleForm.test.jsx b/frontend/core/__tests__/ArticleForm.test.jsx index d773260..25ca4a3 100644 --- a/frontend/core/__tests__/ArticleForm.test.jsx +++ b/frontend/core/__tests__/ArticleForm.test.jsx @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -var mockEditor = { +const mockEditor = { document: [ { id: 'p1', type: 'paragraph' }, { id: 'img1', type: 'image' }, @@ -10,15 +10,15 @@ var mockEditor = { setTextCursorPosition: vi.fn(), }; -var mousedownHandler = function (e) { - var target = e.target; +const mousedownHandler = (e) => { + let target = e.target; if (target.nodeType === 3) target = target.parentNode; if (target?.closest?.('.bn-formatting-toolbar')) return; if (target?.closest?.('.bn-block-content[data-content-type="image"]')) return; if (target?.closest?.('.bn-block-content[data-content-type="video"]')) return; try { - var doc = mockEditor.document; - for (var i = 0; i < doc.length; i++) { + const doc = mockEditor.document; + for (let i = 0; i < doc.length; i++) { if (doc[i].type !== 'image' && doc[i].type !== 'video') { mockEditor.setTextCursorPosition(doc[i].id, 'start'); break; @@ -28,54 +28,54 @@ var mousedownHandler = function (e) { }; function createBlock(contentType) { - var el = document.createElement('div'); + const el = document.createElement('div'); el.className = 'bn-block-content'; el.setAttribute('data-content-type', contentType); return el; } -describe('ArticleForm mousedown handler', function () { - beforeEach(function () { +describe('ArticleForm mousedown handler', () => { + beforeEach(() => { document.body.innerHTML = ''; mockEditor.setTextCursorPosition.mockClear(); document.addEventListener('mousedown', mousedownHandler, true); }); - afterEach(function () { + afterEach(() => { document.removeEventListener('mousedown', mousedownHandler, true); document.body.innerHTML = ''; }); - it('moves cursor to first text block when clicking outside image/video', function () { + it('moves cursor to first text block when clicking outside image/video', () => { document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); expect(mockEditor.setTextCursorPosition).toHaveBeenCalledOnce(); expect(mockEditor.setTextCursorPosition).toHaveBeenCalledWith('p1', 'start'); }); - it('returns early when clicking on image block', function () { - var block = createBlock('image'); + it('returns early when clicking on image block', () => { + const block = createBlock('image'); document.body.appendChild(block); block.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); expect(mockEditor.setTextCursorPosition).not.toHaveBeenCalled(); }); - it('returns early when clicking on video block', function () { - var block = createBlock('video'); + it('returns early when clicking on video block', () => { + const block = createBlock('video'); document.body.appendChild(block); block.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); expect(mockEditor.setTextCursorPosition).not.toHaveBeenCalled(); }); - it('returns early when clicking on formatting toolbar', function () { - var toolbar = document.createElement('div'); + it('returns early when clicking on formatting toolbar', () => { + const toolbar = document.createElement('div'); toolbar.className = 'bn-formatting-toolbar'; document.body.appendChild(toolbar); toolbar.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); expect(mockEditor.setTextCursorPosition).not.toHaveBeenCalled(); }); - it('moves cursor to paragraph after image blocks when image clicked then outside', function () { - var block = createBlock('video'); + it('moves cursor to paragraph after image blocks when image clicked then outside', () => { + const block = createBlock('video'); document.body.appendChild(block); block.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); expect(mockEditor.setTextCursorPosition).not.toHaveBeenCalled(); diff --git a/frontend/core/__tests__/ArticleViewer.test.jsx b/frontend/core/__tests__/ArticleViewer.test.jsx index 3848750..57a4fdd 100644 --- a/frontend/core/__tests__/ArticleViewer.test.jsx +++ b/frontend/core/__tests__/ArticleViewer.test.jsx @@ -1,17 +1,17 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; function handler(e) { - var block = document.querySelector( + const block = document.querySelector( '.bn-block-content[data-content-type="image"].ProseMirror-selectednode', ); if (!block) return; - var imgEl = block.querySelector('img'); + const imgEl = block.querySelector('img'); if (!imgEl) return; e.preventDefault(); e.stopPropagation(); - var alt = imgEl.alt || ''; - var src = imgEl.getAttribute('src') || ''; - var text; + const alt = imgEl.alt || ''; + const src = imgEl.getAttribute('src') || ''; + let text; if (src && !src.startsWith('/uploads/')) { text = src; } else { @@ -21,27 +21,27 @@ function handler(e) { new ClipboardItem({ 'text/plain': new Blob([text], { type: 'text/plain' }), }), - ]).catch(function () {}); + ]).catch(() => {}); } function setImageDOM(alt, src, hasSelection) { - var outer = document.createElement('div'); + const outer = document.createElement('div'); outer.className = hasSelection ? 'bn-block-content ProseMirror-selectednode' : 'bn-block-content'; outer.setAttribute('data-content-type', 'image'); - var img = document.createElement('img'); + const img = document.createElement('img'); if (alt) img.alt = alt; img.setAttribute('src', src); outer.appendChild(img); document.body.appendChild(outer); - return { outer: outer, img: img }; + return { outer, img }; } -describe('ArticleViewer copy handler', function () { - var mockWrite; +describe('ArticleViewer copy handler', () => { + let mockWrite; - beforeEach(function () { + beforeEach(() => { mockWrite = vi.fn().mockResolvedValue(undefined); vi.stubGlobal('navigator', { clipboard: { write: mockWrite }, @@ -53,51 +53,51 @@ describe('ArticleViewer copy handler', function () { document.addEventListener('copy', handler, true); }); - afterEach(function () { + afterEach(() => { document.removeEventListener('copy', handler, true); vi.unstubAllGlobals(); vi.clearAllMocks(); document.body.innerHTML = ''; }); - it('writes alt text for selected upload image', async function () { + it('writes alt text for selected upload image', async () => { setImageDOM('my photo', '/uploads/foo.jpg', true); document.dispatchEvent(new ClipboardEvent('copy', { cancelable: true, bubbles: true })); expect(mockWrite).toHaveBeenCalledOnce(); - var text = await mockWrite.mock.calls[0][0][0].items['text/plain'].text(); + const text = await mockWrite.mock.calls[0][0][0].items['text/plain'].text(); expect(text).toBe('my photo'); }); - it('writes filename when upload image has no alt', async function () { + it('writes filename when upload image has no alt', async () => { setImageDOM('', '/uploads/foo.jpg', true); document.dispatchEvent(new ClipboardEvent('copy', { cancelable: true, bubbles: true })); expect(mockWrite).toHaveBeenCalledOnce(); - var text = await mockWrite.mock.calls[0][0][0].items['text/plain'].text(); + const text = await mockWrite.mock.calls[0][0][0].items['text/plain'].text(); expect(text).toBe('foo.jpg'); }); - it('writes URL for external image', async function () { + it('writes URL for external image', async () => { setImageDOM('alt text', 'https://example.com/img.jpg', true); document.dispatchEvent(new ClipboardEvent('copy', { cancelable: true, bubbles: true })); expect(mockWrite).toHaveBeenCalledOnce(); - var text = await mockWrite.mock.calls[0][0][0].items['text/plain'].text(); + const text = await mockWrite.mock.calls[0][0][0].items['text/plain'].text(); expect(text).toBe('https://example.com/img.jpg'); }); - it('does nothing when image lacks .ProseMirror-selectednode', function () { + it('does nothing when image lacks .ProseMirror-selectednode', () => { setImageDOM('alt', '/uploads/foo.jpg', false); document.dispatchEvent(new ClipboardEvent('copy', { cancelable: true, bubbles: true })); expect(mockWrite).not.toHaveBeenCalled(); }); - it('does nothing when no image DOM at all', function () { + it('does nothing when no image DOM at all', () => { document.dispatchEvent(new ClipboardEvent('copy', { cancelable: true, bubbles: true })); expect(mockWrite).not.toHaveBeenCalled(); }); - it('stops event propagation for selected image', function () { - var child = document.createElement('div'); - child.addEventListener('copy', function (e) { + it('stops event propagation for selected image', () => { + const child = document.createElement('div'); + child.addEventListener('copy', (e) => { expect(e.defaultPrevented).toBe(true); expect(e.cancelBubble).toBe(true); }); @@ -107,7 +107,7 @@ describe('ArticleViewer copy handler', function () { }); }); -var mockEditor = { +const mockEditor = { document: [ { id: 'p1', type: 'paragraph' }, { id: 'img1', type: 'image' }, @@ -117,13 +117,13 @@ var mockEditor = { setTextCursorPosition: vi.fn(), }; -var viewerHandler = function (e) { - var target = e.target; +const viewerHandler = (e) => { + let target = e.target; if (target.nodeType === 3) target = target.parentNode; if (target?.closest?.('.bn-block-content[data-content-type="image"]')) return; try { - var doc = mockEditor.document; - for (var i = 0; i < doc.length; i++) { + const doc = mockEditor.document; + for (let i = 0; i < doc.length; i++) { if (doc[i].type !== 'image' && doc[i].type !== 'video') { mockEditor.setTextCursorPosition(doc[i].id, 'start'); break; @@ -133,32 +133,32 @@ var viewerHandler = function (e) { }; function createBlock(contentType) { - var el = document.createElement('div'); + const el = document.createElement('div'); el.className = 'bn-block-content'; el.setAttribute('data-content-type', contentType); return el; } -describe('ArticleViewer mousedown handler', function () { - beforeEach(function () { +describe('ArticleViewer mousedown handler', () => { + beforeEach(() => { document.body.innerHTML = ''; mockEditor.setTextCursorPosition.mockClear(); document.addEventListener('mousedown', viewerHandler, true); }); - afterEach(function () { + afterEach(() => { document.removeEventListener('mousedown', viewerHandler, true); document.body.innerHTML = ''; }); - it('moves cursor to first text block when clicking outside image', function () { + it('moves cursor to first text block when clicking outside image', () => { document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); expect(mockEditor.setTextCursorPosition).toHaveBeenCalledOnce(); expect(mockEditor.setTextCursorPosition).toHaveBeenCalledWith('p1', 'start'); }); - it('returns early when clicking on image block', function () { - var block = createBlock('image'); + it('returns early when clicking on image block', () => { + const block = createBlock('image'); document.body.appendChild(block); block.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); expect(mockEditor.setTextCursorPosition).not.toHaveBeenCalled(); diff --git a/frontend/core/__tests__/CopyBlockButton.test.jsx b/frontend/core/__tests__/CopyBlockButton.test.jsx index ed9ac1a..c587bea 100644 --- a/frontend/core/__tests__/CopyBlockButton.test.jsx +++ b/frontend/core/__tests__/CopyBlockButton.test.jsx @@ -11,9 +11,9 @@ vi.mock('@blocknote/react', () => ({ import { useBlockNoteEditor, useComponentsContext } from '@blocknote/react'; function render(ui) { - var container = document.createElement('div'); - var root = createRoot(container); - act(function () { root.render(ui); }); + const container = document.createElement('div'); + const root = createRoot(container); + act(() => { root.render(ui); }); return container; } @@ -29,11 +29,11 @@ function mockButton(props) { }); } -describe('CopyBlockButton', function () { - var mockWrite; - var lastItems; +describe('CopyBlockButton', () => { + let mockWrite; + let lastItems; - beforeEach(function () { + beforeEach(() => { vi.useFakeTimers(); mockWrite = vi.fn().mockResolvedValue(undefined); lastItems = null; @@ -45,7 +45,7 @@ describe('CopyBlockButton', function () { })); }); - afterEach(function () { + afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals(); vi.clearAllMocks(); @@ -53,7 +53,7 @@ describe('CopyBlockButton', function () { }); function setup(blockData) { - var editor = { + const editor = { getSelection: vi.fn().mockReturnValue({ blocks: [blockData] }), getTextCursorPosition: vi.fn(), }; @@ -64,145 +64,145 @@ describe('CopyBlockButton', function () { return editor; } - describe('null guards', function () { - it('returns null when editor is null', function () { + describe('null guards', () => { + it('returns null when editor is null', () => { useBlockNoteEditor.mockReturnValue(null); useComponentsContext.mockReturnValue({ FormattingToolbar: { Button: mockButton } }); - var container = render(React.createElement(CopyBlockButton)); + const container = render(React.createElement(CopyBlockButton)); expect(container.innerHTML).toBe(''); }); - it('returns null when Components is null', function () { + it('returns null when Components is null', () => { setup(makeBlock()); useComponentsContext.mockReturnValue(null); - var container = render(React.createElement(CopyBlockButton)); + const container = render(React.createElement(CopyBlockButton)); expect(container.innerHTML).toBe(''); }); - it('returns null when block is null', function () { + it('returns null when block is null', () => { setup(null); - var container = render(React.createElement(CopyBlockButton)); + const container = render(React.createElement(CopyBlockButton)); expect(container.innerHTML).toBe(''); }); - it('handles selection error gracefully', function () { - var editor = { getSelection: vi.fn().mockImplementation(function () { throw new Error('fail'); }) }; + it('handles selection error gracefully', () => { + const editor = { getSelection: vi.fn().mockImplementation(() => { throw new Error('fail'); }) }; useBlockNoteEditor.mockReturnValue(editor); useComponentsContext.mockReturnValue({ FormattingToolbar: { Button: mockButton } }); - var container = render(React.createElement(CopyBlockButton)); + const container = render(React.createElement(CopyBlockButton)); expect(container.innerHTML).toBe(''); }); }); - describe('tooltip', function () { - it('shows "Copy" for image block', function () { + describe('tooltip', () => { + it('shows "Copy" for image block', () => { setup(makeBlock({ type: 'image' })); - var container = render(React.createElement(CopyBlockButton)); - var btn = container.querySelector('[data-testid="copy-btn"]'); + const container = render(React.createElement(CopyBlockButton)); + const btn = container.querySelector('[data-testid="copy-btn"]'); expect(btn.dataset.tooltip).toBe('Copy'); }); - it('shows "Copy" for video block', function () { + it('shows "Copy" for video block', () => { setup(makeBlock({ type: 'video' })); - var container = render(React.createElement(CopyBlockButton)); - var btn = container.querySelector('[data-testid="copy-btn"]'); + const container = render(React.createElement(CopyBlockButton)); + const btn = container.querySelector('[data-testid="copy-btn"]'); expect(btn.dataset.tooltip).toBe('Copy'); }); - it('shows "Copy Block" for other block types', function () { + it('shows "Copy Block" for other block types', () => { setup(makeBlock({ type: 'codeBlock' })); - var container = render(React.createElement(CopyBlockButton)); - var btn = container.querySelector('[data-testid="copy-btn"]'); + const container = render(React.createElement(CopyBlockButton)); + const btn = container.querySelector('[data-testid="copy-btn"]'); expect(btn.dataset.tooltip).toBe('Copy Block'); }); }); - describe('plainText per block type', function () { - it('image upload → writes alt text', async function () { + describe('plainText per block type', () => { + it('image upload → writes alt text', async () => { setup(makeBlock({ type: 'image', props: { url: '/uploads/foo.jpg', alt: 'my photo' } })); - var container = render(React.createElement(CopyBlockButton)); - act(function () { container.querySelector('[data-testid="copy-btn"]').click(); }); - var text = await lastItems['text/plain'].text(); + const container = render(React.createElement(CopyBlockButton)); + act(() => { container.querySelector('[data-testid="copy-btn"]').click(); }); + const text = await lastItems['text/plain'].text(); expect(text).toBe('my photo'); }); - it('image upload without alt → writes name', async function () { + it('image upload without alt → writes name', async () => { setup(makeBlock({ type: 'image', props: { url: '/uploads/foo.jpg', name: 'Foo' } })); - var container = render(React.createElement(CopyBlockButton)); - act(function () { container.querySelector('[data-testid="copy-btn"]').click(); }); - var text = await lastItems['text/plain'].text(); + const container = render(React.createElement(CopyBlockButton)); + act(() => { container.querySelector('[data-testid="copy-btn"]').click(); }); + const text = await lastItems['text/plain'].text(); expect(text).toBe('Foo'); }); - it('image external URL → writes URL (ignores alt)', async function () { + it('image external URL → writes URL (ignores alt)', async () => { setup(makeBlock({ type: 'image', props: { url: 'https://example.com/img.jpg', alt: 'alt text' } })); - var container = render(React.createElement(CopyBlockButton)); - act(function () { container.querySelector('[data-testid="copy-btn"]').click(); }); - var text = await lastItems['text/plain'].text(); + const container = render(React.createElement(CopyBlockButton)); + act(() => { container.querySelector('[data-testid="copy-btn"]').click(); }); + const text = await lastItems['text/plain'].text(); expect(text).toBe('https://example.com/img.jpg'); }); - it('video → writes URL', async function () { + it('video → writes URL', async () => { setup(makeBlock({ type: 'video', props: { url: 'https://youtube.com/watch?v=abc' } })); - var container = render(React.createElement(CopyBlockButton)); - act(function () { container.querySelector('[data-testid="copy-btn"]').click(); }); - var text = await lastItems['text/plain'].text(); + const container = render(React.createElement(CopyBlockButton)); + act(() => { container.querySelector('[data-testid="copy-btn"]').click(); }); + const text = await lastItems['text/plain'].text(); expect(text).toBe('https://youtube.com/watch?v=abc'); }); - it('other block (code) → writes URL', async function () { + it('other block (code) → writes URL', async () => { setup(makeBlock({ type: 'codeBlock', props: { url: '' } })); - var container = render(React.createElement(CopyBlockButton)); - act(function () { container.querySelector('[data-testid="copy-btn"]').click(); }); - var text = await lastItems['text/plain'].text(); + const container = render(React.createElement(CopyBlockButton)); + act(() => { container.querySelector('[data-testid="copy-btn"]').click(); }); + const text = await lastItems['text/plain'].text(); expect(text).toBe(''); }); }); - describe('ClipboardItem', function () { - it('contains text/plain and text/html entries', function () { + describe('ClipboardItem', () => { + it('contains text/plain and text/html entries', () => { setup(makeBlock({ type: 'image', props: { url: '/uploads/foo.jpg', alt: 'img' } })); - var container = render(React.createElement(CopyBlockButton)); - act(function () { container.querySelector('[data-testid="copy-btn"]').click(); }); + const container = render(React.createElement(CopyBlockButton)); + act(() => { container.querySelector('[data-testid="copy-btn"]').click(); }); expect(lastItems).toHaveProperty('text/plain'); expect(lastItems).toHaveProperty('text/html'); expect(Object.keys(lastItems).length).toBe(2); }); - it('has correct text/html marker', async function () { + it('has correct text/html marker', async () => { setup(makeBlock({ type: 'paragraph', props: { url: '' }, content: 'hello' })); - var container = render(React.createElement(CopyBlockButton)); - act(function () { container.querySelector('[data-testid="copy-btn"]').click(); }); - var html = await lastItems['text/html'].text(); + const container = render(React.createElement(CopyBlockButton)); + act(() => { container.querySelector('[data-testid="copy-btn"]').click(); }); + const html = await lastItems['text/html'].text(); expect(html).toContain('blocknote-block'); expect(html).toContain('paragraph'); }); }); - describe('toast', function () { - it('shows toast on click', function () { + describe('toast', () => { + it('shows toast on click', () => { setup(makeBlock({ type: 'paragraph' })); - var container = render(React.createElement(CopyBlockButton)); - act(function () { container.querySelector('[data-testid="copy-btn"]').click(); }); - var toast = document.querySelector('.toast'); + const container = render(React.createElement(CopyBlockButton)); + act(() => { container.querySelector('[data-testid="copy-btn"]').click(); }); + const toast = document.querySelector('.toast'); expect(toast).not.toBeNull(); expect(toast.textContent).toBe('Copied to clipboard'); }); - it('removes old toast before creating new one', function () { + it('removes old toast before creating new one', () => { setup(makeBlock({ type: 'paragraph' })); - var container = render(React.createElement(CopyBlockButton)); - var btn = container.querySelector('[data-testid="copy-btn"]'); - act(function () { btn.click(); }); - act(function () { btn.click(); }); - var toasts = document.querySelectorAll('.toast'); + const container = render(React.createElement(CopyBlockButton)); + const btn = container.querySelector('[data-testid="copy-btn"]'); + act(() => { btn.click(); }); + act(() => { btn.click(); }); + const toasts = document.querySelectorAll('.toast'); expect(toasts.length).toBe(1); }); - it('removes toast after 2800ms', function () { + it('removes toast after 2800ms', () => { setup(makeBlock({ type: 'paragraph' })); - var container = render(React.createElement(CopyBlockButton)); - act(function () { container.querySelector('[data-testid="copy-btn"]').click(); }); + const container = render(React.createElement(CopyBlockButton)); + act(() => { container.querySelector('[data-testid="copy-btn"]').click(); }); expect(document.querySelector('.toast')).not.toBeNull(); vi.advanceTimersByTime(2800); expect(document.querySelector('.toast')).toBeNull(); diff --git a/frontend/core/__tests__/custom-code-block-spec.test.js b/frontend/core/__tests__/custom-code-block-spec.test.js index dde2daf..7c3ba0b 100644 --- a/frontend/core/__tests__/custom-code-block-spec.test.js +++ b/frontend/core/__tests__/custom-code-block-spec.test.js @@ -646,7 +646,7 @@ describe('createCustomCodeBlockSpec', () => { spec.implementation.render(block, editor); const deleteBtn = outerDiv.querySelector('.code-delete-btn'); - deleteBtn.click(); + deleteBtn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); expect(editor.removeBlocks).toHaveBeenCalledTimes(1); expect(editor.removeBlocks).toHaveBeenCalledWith([block]); diff --git a/frontend/core/__tests__/custom-formatting-toolbar.test.js b/frontend/core/__tests__/custom-formatting-toolbar.test.js index 5608080..530bf3e 100644 --- a/frontend/core/__tests__/custom-formatting-toolbar.test.js +++ b/frontend/core/__tests__/custom-formatting-toolbar.test.js @@ -2,49 +2,49 @@ import { describe, it, expect } from 'vitest'; import { filterVideoToolbarItems, filterImageToolbarItems } from '../components/CustomFormattingToolbar'; -var MockReplace = function MockReplace() {}; -var MockDownload = function MockDownload() {}; -var MockCaption = function MockCaption() {}; -var MockRename = function MockRename() {}; -var MockPreview = function MockPreview() {}; -var MockNormal = function MockNormal() {}; +const MockReplace = () => {}; +const MockDownload = () => {}; +const MockCaption = () => {}; +const MockRename = () => {}; +const MockPreview = () => {}; +const MockNormal = () => {}; -var HIDDEN = [MockReplace, MockDownload, MockCaption, MockRename, MockPreview]; +const HIDDEN = [MockReplace, MockDownload, MockCaption, MockRename, MockPreview]; -describe('filterVideoToolbarItems', function () { - it('filters out FileReplaceButton', function () { +describe('filterVideoToolbarItems', () => { + it('filters out FileReplaceButton', () => { expect(filterVideoToolbarItems([{ type: MockReplace }], HIDDEN)).toEqual([]); }); - it('filters out FileDownloadButton', function () { + it('filters out FileDownloadButton', () => { expect(filterVideoToolbarItems([{ type: MockDownload }], HIDDEN)).toEqual([]); }); - it('filters out FileCaptionButton', function () { + it('filters out FileCaptionButton', () => { expect(filterVideoToolbarItems([{ type: MockCaption }], HIDDEN)).toEqual([]); }); - it('filters out FileRenameButton', function () { + it('filters out FileRenameButton', () => { expect(filterVideoToolbarItems([{ type: MockRename }], HIDDEN)).toEqual([]); }); - it('filters out FilePreviewButton', function () { + it('filters out FilePreviewButton', () => { expect(filterVideoToolbarItems([{ type: MockPreview }], HIDDEN)).toEqual([]); }); - it('filters out items with textAlignment prop', function () { - var items = [{ type: MockNormal, props: { textAlignment: 'right' } }]; + it('filters out items with textAlignment prop', () => { + const items = [{ type: MockNormal, props: { textAlignment: 'right' } }]; expect(filterVideoToolbarItems(items, HIDDEN)).toEqual([]); }); - it('keeps normal buttons', function () { - var items = [{ type: MockNormal, props: {} }, { type: MockNormal }]; + it('keeps normal buttons', () => { + const items = [{ type: MockNormal, props: {} }, { type: MockNormal }]; expect(filterVideoToolbarItems(items, HIDDEN)).toHaveLength(2); }); - it('filters multiple items together', function () { - var items = [ + it('filters multiple items together', () => { + const items = [ { type: MockNormal }, { type: MockReplace }, { type: MockDownload }, @@ -57,13 +57,13 @@ describe('filterVideoToolbarItems', function () { expect(filterVideoToolbarItems(items, HIDDEN)).toHaveLength(2); }); - it('returns empty array for empty input', function () { + it('returns empty array for empty input', () => { expect(filterVideoToolbarItems([], HIDDEN)).toEqual([]); }); - it('uses default hidden types when none provided', function () { - var MockUnknown = function MockUnknown() {}; - var items = [{ type: MockUnknown }, { type: MockNormal }]; + it('uses default hidden types when none provided', () => { + const MockUnknown = () => {}; + const items = [{ type: MockUnknown }, { type: MockNormal }]; // default HIDDEN_VIDEO_BUTTONS = real File*Button from @blocknote/react // These won't match our mocks, so both items pass // This just verifies the fallback doesn't crash @@ -71,12 +71,12 @@ describe('filterVideoToolbarItems', function () { }); }); -describe('filterImageToolbarItems', function () { - it('filters out FileDownloadButton', function () { +describe('filterImageToolbarItems', () => { + it('filters out FileDownloadButton', () => { expect(filterImageToolbarItems([{ type: MockDownload }], [MockDownload])).toEqual([]); }); - it('keeps normal buttons', function () { + it('keeps normal buttons', () => { expect(filterImageToolbarItems([{ type: MockNormal }])).toHaveLength(1); }); }); diff --git a/frontend/core/__tests__/useCodeBlockGapClick.test.js b/frontend/core/__tests__/useCodeBlockGapClick.test.js index 2c11b0e..4f4d22b 100644 --- a/frontend/core/__tests__/useCodeBlockGapClick.test.js +++ b/frontend/core/__tests__/useCodeBlockGapClick.test.js @@ -8,20 +8,20 @@ function TestHarness({ editorRef }) { return null; } -var roots = []; +let roots = []; function render(ui) { - var container = document.createElement('div'); + const container = document.createElement('div'); container.id = 'test-root'; - var root = createRoot(container); + const root = createRoot(container); roots.push(root); - act(function () { root.render(ui); }); + act(() => { root.render(ui); }); document.body.appendChild(container); return container; } -afterEach(function () { - roots.forEach(function (r) { r.unmount(); }); +afterEach(() => { + roots.forEach((r) => { r.unmount(); }); roots = []; vi.useRealTimers(); document.body.innerHTML = ''; @@ -29,7 +29,7 @@ afterEach(function () { }); function addBlockToDOM(props = {}) { - var { + const { blockId = 'block', top = 0, bottom = 100, @@ -41,7 +41,7 @@ function addBlockToDOM(props = {}) { useMediaWrapper = false, } = props; - var inner = document.createElement('div'); + const inner = document.createElement('div'); if (useMediaWrapper) { inner.className = 'bn-visual-media-wrapper'; } else { @@ -49,22 +49,22 @@ function addBlockToDOM(props = {}) { if (contentType) inner.setAttribute('data-content-type', contentType); } - var blockOuter = document.createElement('div'); + const blockOuter = document.createElement('div'); blockOuter.className = 'bn-block-outer'; blockOuter.setAttribute('data-id', blockId); vi.spyOn(blockOuter, 'getBoundingClientRect').mockReturnValue({ - top: top, bottom: bottom, left: left, right: right, - width: width, height: height, + top, bottom, left, right, + width, height, x: left, y: top, - toJSON: function () {}, + toJSON() {}, }); blockOuter.appendChild(inner); - var container = document.getElementById('test-root') || document.body; + const container = document.getElementById('test-root') || document.body; container.appendChild(blockOuter); - return { inner: inner, blockOuter: blockOuter }; + return { inner, blockOuter }; } describe('useCodeBlockGapClick', () => { @@ -266,17 +266,17 @@ describe('useCodeBlockGapClick', () => { it('inserts paragraph when clicking below video block', () => { vi.useFakeTimers(); - var insertBlocks = vi.fn(function () { return [{ id: 'new-vid-p' }]; }); - var focus = vi.fn(); - var documentBlocks = [{ id: 'vid-1', type: 'video' }]; - var editorRef = { - current: { insertBlocks: insertBlocks, focus: focus, document: documentBlocks }, + const insertBlocks = vi.fn(() => [{ id: 'new-vid-p' }]); + const focus = vi.fn(); + const documentBlocks = [{ id: 'vid-1', type: 'video' }]; + const editorRef = { + current: { insertBlocks, focus, document: documentBlocks }, }; - var container = render(React.createElement(TestHarness, { editorRef: editorRef })); + const container = render(React.createElement(TestHarness, { editorRef })); addBlockToDOM({ blockId: 'vid-1', bottom: 100, useMediaWrapper: true }); - var event = new MouseEvent('mousedown', { + const event = new MouseEvent('mousedown', { clientY: 108, bubbles: true, cancelable: true, @@ -297,17 +297,17 @@ describe('useCodeBlockGapClick', () => { it('inserts paragraph when clicking above video block', () => { vi.useFakeTimers(); - var insertBlocks = vi.fn(function () { return [{ id: 'new-vid-p2' }]; }); - var focus = vi.fn(); - var documentBlocks = [{ id: 'vid-2', type: 'video' }]; - var editorRef = { - current: { insertBlocks: insertBlocks, focus: focus, document: documentBlocks }, + const insertBlocks = vi.fn(() => [{ id: 'new-vid-p2' }]); + const focus = vi.fn(); + const documentBlocks = [{ id: 'vid-2', type: 'video' }]; + const editorRef = { + current: { insertBlocks, focus, document: documentBlocks }, }; - var container = render(React.createElement(TestHarness, { editorRef: editorRef })); + const container = render(React.createElement(TestHarness, { editorRef })); addBlockToDOM({ blockId: 'vid-2', top: 100, bottom: 200, useMediaWrapper: true }); - var event = new MouseEvent('mousedown', { + const event = new MouseEvent('mousedown', { clientY: 92, bubbles: true, cancelable: true, @@ -328,20 +328,20 @@ describe('useCodeBlockGapClick', () => { it('skips insertion above video block when adjacent paragraph is empty', () => { vi.useFakeTimers(); - var insertBlocks = vi.fn(); - var focus = vi.fn(); - var documentBlocks = [ + const insertBlocks = vi.fn(); + const focus = vi.fn(); + const documentBlocks = [ { id: 'p-before-vid', type: 'paragraph' }, { id: 'vid-3', type: 'video' }, ]; - var editorRef = { - current: { document: documentBlocks, insertBlocks: insertBlocks, focus: focus }, + const editorRef = { + current: { document: documentBlocks, insertBlocks, focus }, }; - var container = render(React.createElement(TestHarness, { editorRef: editorRef })); + const container = render(React.createElement(TestHarness, { editorRef })); addBlockToDOM({ blockId: 'vid-3', top: 100, bottom: 200, useMediaWrapper: true }); - var event = new MouseEvent('mousedown', { + const event = new MouseEvent('mousedown', { clientY: 92, bubbles: true, cancelable: true, @@ -355,13 +355,13 @@ describe('useCodeBlockGapClick', () => { }); it('does not insert when clicking far below video block', () => { - var insertBlocks = vi.fn(); - var editorRef = { current: { insertBlocks: insertBlocks, focus: vi.fn(), setTextCursorPosition: vi.fn(), document: [] } }; + const insertBlocks = vi.fn(); + const editorRef = { current: { insertBlocks, focus: vi.fn(), setTextCursorPosition: vi.fn(), document: [] } }; - var container = render(React.createElement(TestHarness, { editorRef: editorRef })); + const container = render(React.createElement(TestHarness, { editorRef })); addBlockToDOM({ blockId: 'vid-4', bottom: 100, useMediaWrapper: true }); - var event = new MouseEvent('mousedown', { + const event = new MouseEvent('mousedown', { clientY: 200, bubbles: true, cancelable: true, @@ -373,17 +373,17 @@ describe('useCodeBlockGapClick', () => { it('inserts paragraph when clicking below image block', () => { vi.useFakeTimers(); - var insertBlocks = vi.fn(function () { return [{ id: 'new-img-p' }]; }); - var focus = vi.fn(); - var documentBlocks = [{ id: 'img-1', type: 'image' }]; - var editorRef = { - current: { insertBlocks: insertBlocks, focus: focus, document: documentBlocks }, + const insertBlocks = vi.fn(() => [{ id: 'new-img-p' }]); + const focus = vi.fn(); + const documentBlocks = [{ id: 'img-1', type: 'image' }]; + const editorRef = { + current: { insertBlocks, focus, document: documentBlocks }, }; - var container = render(React.createElement(TestHarness, { editorRef: editorRef })); + const container = render(React.createElement(TestHarness, { editorRef })); addBlockToDOM({ blockId: 'img-1', bottom: 100, contentType: 'image' }); - var event = new MouseEvent('mousedown', { + const event = new MouseEvent('mousedown', { clientY: 108, bubbles: true, cancelable: true, @@ -419,29 +419,29 @@ describe('useCodeBlockGapClick', () => { render(React.createElement(TestHarness, { editorRef })); - var p = document.createElement('p'); + const p = document.createElement('p'); p.innerHTML = '
'; document.body.appendChild(p); - var sel = window.getSelection(); - var range = document.createRange(); + const sel = window.getSelection(); + const range = document.createRange(); range.setStart(p, 0); range.setEnd(p, 1); sel.removeAllRanges(); sel.addRange(range); expect(range.collapsed).toBe(false); - var event = new KeyboardEvent('keydown', { + const event = new KeyboardEvent('keydown', { key: 'Backspace', cancelable: true, bubbles: true, }); - var preventSpy = vi.spyOn(event, 'preventDefault'); - var stopSpy = vi.spyOn(event, 'stopPropagation'); + const preventSpy = vi.spyOn(event, 'preventDefault'); + const stopSpy = vi.spyOn(event, 'stopPropagation'); document.dispatchEvent(event); expect(preventSpy).toHaveBeenCalled(); expect(stopSpy).toHaveBeenCalled(); - var selAfter = window.getSelection(); + const selAfter = window.getSelection(); expect(selAfter.rangeCount).toBe(1); expect(selAfter.getRangeAt(0).collapsed).toBe(true); }); @@ -461,20 +461,20 @@ describe('useCodeBlockGapClick', () => { render(React.createElement(TestHarness, { editorRef })); - var p = document.createElement('p'); + const p = document.createElement('p'); document.body.appendChild(p); - var sel = window.getSelection(); - var range = document.createRange(); + const sel = window.getSelection(); + const range = document.createRange(); range.setStart(p, 0); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); - var event = new KeyboardEvent('keydown', { + const event = new KeyboardEvent('keydown', { key: 'Backspace', cancelable: true, bubbles: true, }); - var preventSpy = vi.spyOn(event, 'preventDefault'); + const preventSpy = vi.spyOn(event, 'preventDefault'); document.dispatchEvent(event); expect(preventSpy).not.toHaveBeenCalled(); diff --git a/frontend/core/__tests__/video-dict.test.js b/frontend/core/__tests__/video-dict.test.js index 1771b7f..0863ec8 100644 --- a/frontend/core/__tests__/video-dict.test.js +++ b/frontend/core/__tests__/video-dict.test.js @@ -39,46 +39,46 @@ function makeMockEditor() { } -describe('applyVideoDictOverrides', function () { - it('renames slash menu title to YouTube', function () { - var editor = makeMockEditor(); +describe('applyVideoDictOverrides', () => { + it('renames slash menu title to YouTube', () => { + const editor = makeMockEditor(); applyVideoDictOverrides(editor); expect(editor.dictionary.slash_menu.video.title).toBe('YouTube'); }); - it('sets slash menu subtext', function () { - var editor = makeMockEditor(); + it('sets slash menu subtext', () => { + const editor = makeMockEditor(); applyVideoDictOverrides(editor); expect(editor.dictionary.slash_menu.video.subtext).toBe('Paste a YouTube video URL'); }); - it('adds youtube and yt aliases', function () { - var editor = makeMockEditor(); + it('adds youtube and yt aliases', () => { + const editor = makeMockEditor(); applyVideoDictOverrides(editor); expect(editor.dictionary.slash_menu.video.aliases).toContain('youtube'); expect(editor.dictionary.slash_menu.video.aliases).toContain('yt'); }); - it('renames file panel embed tab title', function () { - var editor = makeMockEditor(); + it('renames file panel embed tab title', () => { + const editor = makeMockEditor(); applyVideoDictOverrides(editor); expect(editor.dictionary.file_panel.embed.title).toBe('YouTube URL'); }); - it('updates embed button text for video', function () { - var editor = makeMockEditor(); + it('updates embed button text for video', () => { + const editor = makeMockEditor(); applyVideoDictOverrides(editor); expect(editor.dictionary.file_panel.embed.embed_button.video).toBe('Embed YouTube video'); }); - it('updates embed placeholder', function () { - var editor = makeMockEditor(); + it('updates embed placeholder', () => { + const editor = makeMockEditor(); applyVideoDictOverrides(editor); expect(editor.dictionary.file_panel.embed.url_placeholder).toBe('Paste YouTube video link'); }); - it('updates add button text for video', function () { - var editor = makeMockEditor(); + it('updates add button text for video', () => { + const editor = makeMockEditor(); applyVideoDictOverrides(editor); expect(editor.dictionary.file_blocks.add_button_text.video).toBe('Add YouTube video URL'); }); diff --git a/frontend/core/__tests__/video-override-spec.test.js b/frontend/core/__tests__/video-override-spec.test.js index 8cb972c..a7d2d45 100644 --- a/frontend/core/__tests__/video-override-spec.test.js +++ b/frontend/core/__tests__/video-override-spec.test.js @@ -10,15 +10,15 @@ function makeEditor(editable) { return { isEditable: editable, removeBlocks: vi.fn() }; } -describe('createYouTubeVideoSpec', function () { - var origRender, origToExternalHTML, videoSpec; +describe('createYouTubeVideoSpec', () => { + let origRender, origToExternalHTML, videoSpec; - beforeEach(function () { - document.querySelectorAll('.toast').forEach(function (t) { t.remove(); }); - origRender = vi.fn(function () { + beforeEach(() => { + document.querySelectorAll('.toast').forEach((t) => { t.remove(); }); + origRender = vi.fn(() => { return { dom: document.createElement('div') }; }); - origToExternalHTML = vi.fn(function () { + origToExternalHTML = vi.fn(() => { return { dom: document.createElement('a') }; }); videoSpec = { @@ -27,23 +27,23 @@ describe('createYouTubeVideoSpec', function () { vi.useFakeTimers(); }); - afterEach(function () { - document.querySelectorAll('.toast').forEach(function (t) { t.remove(); }); + afterEach(() => { + document.querySelectorAll('.toast').forEach((t) => { t.remove(); }); vi.useRealTimers(); }); - it('returns spec with same shape', function () { - var spec = createYouTubeVideoSpec(videoSpec); + it('returns spec with same shape', () => { + const spec = createYouTubeVideoSpec(videoSpec); expect(spec).toHaveProperty('implementation'); expect(spec.implementation).toHaveProperty('render'); expect(spec.implementation).toHaveProperty('toExternalHTML'); }); - describe('render', function () { - it('returns bn-block-content with iframe for YouTube URL', function () { - var spec = createYouTubeVideoSpec(videoSpec); - var editor = makeEditor(true); - var result = spec.implementation.render(makeBlock('https://www.youtube.com/watch?v=eme8BnMFthI'), editor); + describe('render', () => { + it('returns bn-block-content with iframe for YouTube URL', () => { + const spec = createYouTubeVideoSpec(videoSpec); + const editor = makeEditor(true); + const result = spec.implementation.render(makeBlock('https://www.youtube.com/watch?v=eme8BnMFthI'), editor); expect(result.dom.className).toContain('bn-block-content'); expect(result.dom.querySelector('iframe')).toBeTruthy(); @@ -51,87 +51,87 @@ describe('createYouTubeVideoSpec', function () { expect(origRender).not.toHaveBeenCalled(); }); - it('returns iframe without pointerEvents in viewer', function () { - var spec = createYouTubeVideoSpec(videoSpec); - var editor = makeEditor(false); - var result = spec.implementation.render(makeBlock('https://youtu.be/abcdefghijk'), editor); + it('returns iframe without pointerEvents in viewer', () => { + const spec = createYouTubeVideoSpec(videoSpec); + const editor = makeEditor(false); + const result = spec.implementation.render(makeBlock('https://youtu.be/abcdefghijk'), editor); - var iframe = result.dom.querySelector('iframe'); + const iframe = result.dom.querySelector('iframe'); expect(iframe.style.pointerEvents).toBe(''); }); - it('shows toast and calls removeBlocks for non-YouTube URL in editor', function () { - var spec = createYouTubeVideoSpec(videoSpec); - var editor = makeEditor(true); + it('shows toast and calls removeBlocks for non-YouTube URL in editor', () => { + const spec = createYouTubeVideoSpec(videoSpec); + const editor = makeEditor(true); spec.implementation.render(makeBlock('https://vimeo.com/123'), editor); vi.advanceTimersByTime(0); expect(origRender).not.toHaveBeenCalled(); - var toast = document.querySelector('.toast'); + const toast = document.querySelector('.toast'); expect(toast).toBeTruthy(); expect(toast.textContent).toContain('Only YouTube'); expect(editor.removeBlocks).toHaveBeenCalledWith(['b1']); }); - it('returns display:none div for non-YouTube URL in viewer', function () { - var spec = createYouTubeVideoSpec(videoSpec); - var editor = makeEditor(false); - var result = spec.implementation.render(makeBlock('https://vimeo.com/123'), editor); + it('returns display:none div for non-YouTube URL in viewer', () => { + const spec = createYouTubeVideoSpec(videoSpec); + const editor = makeEditor(false); + const result = spec.implementation.render(makeBlock('https://vimeo.com/123'), editor); expect(result.dom.style.display).toBe('none'); expect(origRender).not.toHaveBeenCalled(); expect(document.querySelector('.toast')).toBeNull(); }); - it('calls origRender when url is empty', function () { - var spec = createYouTubeVideoSpec(videoSpec); - var editor = makeEditor(true); - var result = spec.implementation.render(makeBlock(''), editor); + it('calls origRender when url is empty', () => { + const spec = createYouTubeVideoSpec(videoSpec); + const editor = makeEditor(true); + const result = spec.implementation.render(makeBlock(''), editor); expect(origRender).toHaveBeenCalledTimes(1); }); - it('calls origRender when url is undefined', function () { - var spec = createYouTubeVideoSpec(videoSpec); - var editor = makeEditor(true); - var block = { id: 'b1', props: {} }; - var result = spec.implementation.render(block, editor); + it('calls origRender when url is undefined', () => { + const spec = createYouTubeVideoSpec(videoSpec); + const editor = makeEditor(true); + const block = { id: 'b1', props: {} }; + const result = spec.implementation.render(block, editor); expect(origRender).toHaveBeenCalledTimes(1); }); - it('calls origRender when url is null', function () { - var spec = createYouTubeVideoSpec(videoSpec); - var editor = makeEditor(true); - var result = spec.implementation.render(makeBlock(null), editor); + it('calls origRender when url is null', () => { + const spec = createYouTubeVideoSpec(videoSpec); + const editor = makeEditor(true); + const result = spec.implementation.render(makeBlock(null), editor); expect(origRender).toHaveBeenCalledTimes(1); }); }); - describe('toExternalHTML', function () { - it('returns iframe for YouTube URL', function () { - var spec = createYouTubeVideoSpec(videoSpec); - var result = spec.implementation.toExternalHTML(makeBlock('https://www.youtube.com/watch?v=eme8BnMFthI')); + describe('toExternalHTML', () => { + it('returns iframe for YouTube URL', () => { + const spec = createYouTubeVideoSpec(videoSpec); + const result = spec.implementation.toExternalHTML(makeBlock('https://www.youtube.com/watch?v=eme8BnMFthI')); expect(result.dom.tagName).toBe('IFRAME'); expect(result.dom.src).toContain('youtube.com/embed/'); expect(origToExternalHTML).not.toHaveBeenCalled(); }); - it('calls origToExternalHTML for non-YouTube URL', function () { - var spec = createYouTubeVideoSpec(videoSpec); - var result = spec.implementation.toExternalHTML(makeBlock('https://vimeo.com/123')); + it('calls origToExternalHTML for non-YouTube URL', () => { + const spec = createYouTubeVideoSpec(videoSpec); + const result = spec.implementation.toExternalHTML(makeBlock('https://vimeo.com/123')); expect(origToExternalHTML).toHaveBeenCalledTimes(1); }); - it('calls origToExternalHTML when url is empty', function () { - var spec = createYouTubeVideoSpec(videoSpec); - var result = spec.implementation.toExternalHTML(makeBlock('')); + it('calls origToExternalHTML when url is empty', () => { + const spec = createYouTubeVideoSpec(videoSpec); + const result = spec.implementation.toExternalHTML(makeBlock('')); expect(origToExternalHTML).toHaveBeenCalledTimes(1); }); diff --git a/frontend/core/components/ArticleForm.jsx b/frontend/core/components/ArticleForm.jsx index 604c2e1..66c2e79 100644 --- a/frontend/core/components/ArticleForm.jsx +++ b/frontend/core/components/ArticleForm.jsx @@ -110,12 +110,12 @@ function BlockNoteEditor({ initialContent, onReady }) { }), }); - useEffect(function () { + useEffect(() => { if (editor) { applyVideoDictOverrides(editor); document.querySelectorAll( '[data-content-type="video"] .bn-add-file-button-text', - ).forEach(function(el) { + ).forEach((el) => { if (el.textContent === 'Add video') { el.textContent = 'Add YouTube video URL'; } @@ -126,7 +126,7 @@ function BlockNoteEditor({ initialContent, onReady }) { const handleSelectionChange = useCallback(() => { let blockType; try { - var pos = editor.getTextCursorPosition(); + const pos = editor.getTextCursorPosition(); blockType = pos.block?.type; if (blockType === 'image') { editor.portalElement?.classList.add('image-selected'); @@ -137,24 +137,24 @@ function BlockNoteEditor({ initialContent, onReady }) { editor.portalElement?.classList.remove('image-selected'); } requestAnimationFrame(() => { - document.querySelectorAll('.ProseMirror-selectednode').forEach(function(el) { + document.querySelectorAll('.ProseMirror-selectednode').forEach((el) => { if (el.classList.contains('bn-visual-media-wrapper') && el.getBoundingClientRect().height < 1) { - var dir = arrowDirRef.current; + const dir = arrowDirRef.current; arrowDirRef.current = null; if (dir && editor) { - var block; - var container = el.closest('.bn-block') || el.closest('[data-id]'); - var dataId = container?.getAttribute('data-id'); - if (dataId) block = editor.document?.find(function(b) { return b.id === dataId; }); + let block; + const container = el.closest('.bn-block') || el.closest('[data-id]'); + const dataId = container?.getAttribute('data-id'); + if (dataId) block = editor.document?.find((b) => b.id === dataId); if (block) { - var idx = editor.document?.indexOf(block); - var target = (idx != null && idx >= 0) + const idx = editor.document?.indexOf(block); + const target = (idx != null && idx >= 0) ? (dir === 'ArrowDown' ? editor.document?.[idx + 1] : editor.document?.[idx - 1]) : null; if (target) { - var targetEl = document.querySelector('[data-id="' + target.id + '"]'); + const targetEl = document.querySelector('[data-id="' + target.id + '"]'); if (targetEl) { - var rect = targetEl.getBoundingClientRect(); + const rect = targetEl.getBoundingClientRect(); window.scrollTo({ top: window.scrollY + rect.top - 100, behavior: 'instant' }); } editor.setTextCursorPosition(target.id, 'start'); @@ -167,13 +167,13 @@ function BlockNoteEditor({ initialContent, onReady }) { } }); if (arrowDirRef.current && editor) { - var dir = arrowDirRef.current; + const dir = arrowDirRef.current; arrowDirRef.current = null; - var pos = editor.getTextCursorPosition(); + const pos = editor.getTextCursorPosition(); if (pos?.block) { - var targetEl = document.querySelector('[data-id="' + pos.block.id + '"]'); + const targetEl = document.querySelector('[data-id="' + pos.block.id + '"]'); if (targetEl) { - var rect = targetEl.getBoundingClientRect(); + const rect = targetEl.getBoundingClientRect(); window.scrollTo({ top: window.scrollY + rect.top - 100, behavior: 'instant' }); } } @@ -208,7 +208,7 @@ function BlockNoteEditor({ initialContent, onReady }) { useEffect(() => { if (!editor) return; - const handler = function (e) { + const handler = (e) => { const html = e.clipboardData.getData('text/html'); if (html && html.includes('blocknote-block')) { e.preventDefault(); @@ -223,13 +223,13 @@ function BlockNoteEditor({ initialContent, onReady }) { catch { return; } editor.updateBlock(block, data); if (data.type === 'image') { - document.querySelectorAll('.ProseMirror-selectednode').forEach(function (el) { + document.querySelectorAll('.ProseMirror-selectednode').forEach((el) => { el.classList.remove('ProseMirror-selectednode'); }); editor.domElement?.blur(); } if (data.type === 'video') { - document.querySelectorAll('.ProseMirror-selectednode').forEach(function (el) { + document.querySelectorAll('.ProseMirror-selectednode').forEach((el) => { el.classList.remove('ProseMirror-selectednode'); }); try { @@ -245,8 +245,8 @@ function BlockNoteEditor({ initialContent, onReady }) { } catch {} } } - requestAnimationFrame(function () { - document.querySelectorAll('.ProseMirror-selectednode').forEach(function (el) { + requestAnimationFrame(() => { + document.querySelectorAll('.ProseMirror-selectednode').forEach((el) => { el.classList.remove('ProseMirror-selectednode'); }); }); @@ -257,7 +257,7 @@ function BlockNoteEditor({ initialContent, onReady }) { useEffect(() => { if (!editor) return; - const handler = function (e) { + const handler = (e) => { let block; try { block = editor.getSelection()?.blocks?.[0] ?? editor.getTextCursorPosition().block; } catch { return; } @@ -280,18 +280,18 @@ function BlockNoteEditor({ initialContent, onReady }) { 'text/plain': new Blob([text], { type: 'text/plain' }), 'text/html': new Blob([html], { type: 'text/html' }), }; - navigator.clipboard.write([new ClipboardItem(items)]).catch(function () {}); + navigator.clipboard.write([new ClipboardItem(items)]).catch(() => {}); }; document.addEventListener('copy', handler, true); return () => document.removeEventListener('copy', handler, true); }, [editor]); - useEffect(function () { + useEffect(() => { if (!editor) return; - const handler = function (e) { + const handler = (e) => { let target = e.target; if (target.nodeType === 3) target = target.parentNode; - if (target?.closest?.('.bn-formatting-toolbar, .bn-panel')) return; + if (target?.closest?.('#bn-grid-suggestion-menu, .bn-formatting-toolbar, .bn-panel, em-emoji-picker')) return; if (target?.closest?.('.bn-block-content[data-content-type="image"]')) return; if (target?.closest?.('.bn-block-content[data-content-type="video"]')) return; try { @@ -305,7 +305,7 @@ function BlockNoteEditor({ initialContent, onReady }) { } catch {} }; document.addEventListener('mousedown', handler, true); - return function () { document.removeEventListener('mousedown', handler, true); }; + return () => { document.removeEventListener('mousedown', handler, true); }; }, [editor]); return ( diff --git a/frontend/core/components/ArticleViewer.jsx b/frontend/core/components/ArticleViewer.jsx index 38e5093..357a4bc 100644 --- a/frontend/core/components/ArticleViewer.jsx +++ b/frontend/core/components/ArticleViewer.jsx @@ -55,8 +55,8 @@ function BlockNoteViewer({ initialContent }) { }), }); - useEffect(function () { - const handler = function (e) { + useEffect(() => { + const handler = (e) => { const block = document.querySelector( '.bn-block-content[data-content-type="image"].ProseMirror-selectednode', ); @@ -77,15 +77,15 @@ function BlockNoteViewer({ initialContent }) { new ClipboardItem({ 'text/plain': new Blob([text], { type: 'text/plain' }), }), - ]).catch(function () {}); + ]).catch(() => {}); }; document.addEventListener('copy', handler, true); - return function () { document.removeEventListener('copy', handler, true); }; + return () => { document.removeEventListener('copy', handler, true); }; }, []); - useEffect(function () { + useEffect(() => { if (!editor) return; - const handler = function (e) { + const handler = (e) => { let target = e.target; if (target.nodeType === 3) target = target.parentNode; if (target?.closest?.('.bn-block-content[data-content-type="image"]')) return; @@ -100,7 +100,7 @@ function BlockNoteViewer({ initialContent }) { } catch {} }; document.addEventListener('mousedown', handler, true); - return function () { document.removeEventListener('mousedown', handler, true); }; + return () => { document.removeEventListener('mousedown', handler, true); }; }, [editor]); return ; diff --git a/frontend/core/components/CopyBlockButton.jsx b/frontend/core/components/CopyBlockButton.jsx index feef585..4df4b03 100644 --- a/frontend/core/components/CopyBlockButton.jsx +++ b/frontend/core/components/CopyBlockButton.jsx @@ -2,11 +2,11 @@ import React from 'react'; import { useBlockNoteEditor, useComponentsContext } from '@blocknote/react'; export default function CopyBlockButton() { - var editor = useBlockNoteEditor(); - var Components = useComponentsContext(); + const editor = useBlockNoteEditor(); + const Components = useComponentsContext(); if (!editor || !Components) return null; - var block; + let block; try { block = editor.getSelection()?.blocks?.[0] ?? editor.getTextCursorPosition().block; } catch { return null; } @@ -16,10 +16,10 @@ export default function CopyBlockButton() { return ( '; - var plainText = ''; + onClick={() => { + const data = { type: block.type, props: block.props, content: block.content }; + const html = ''; + let plainText = ''; if (block.type === 'image') { if (block.props?.url && !block.props.url.startsWith('/uploads/')) { plainText = block.props.url; @@ -31,18 +31,18 @@ export default function CopyBlockButton() { } else { plainText = block.props?.url || ''; } - var items = { + const items = { 'text/plain': new Blob([plainText], { type: 'text/plain' }), 'text/html': new Blob([html], { type: 'text/html' }), }; - navigator.clipboard.write([new ClipboardItem(items)]).catch(function () {}); - var old = document.querySelector('.toast'); + navigator.clipboard.write([new ClipboardItem(items)]).catch(() => {}); + const old = document.querySelector('.toast'); if (old) old.remove(); - var toast = document.createElement('div'); + const toast = document.createElement('div'); toast.className = 'toast'; toast.textContent = 'Copied to clipboard'; document.body.appendChild(toast); - setTimeout(function () { if (toast.parentElement) toast.remove(); }, 2800); + setTimeout(() => { if (toast.parentElement) toast.remove(); }, 2800); }} icon={ diff --git a/frontend/core/components/CustomFormattingToolbar.jsx b/frontend/core/components/CustomFormattingToolbar.jsx index 36aab06..4338f69 100644 --- a/frontend/core/components/CustomFormattingToolbar.jsx +++ b/frontend/core/components/CustomFormattingToolbar.jsx @@ -13,7 +13,7 @@ import JustifyButton from './JustifyButton'; import CopyBlockButton from './CopyBlockButton'; -var HIDDEN_VIDEO_BUTTONS = [ +const HIDDEN_VIDEO_BUTTONS = [ FileReplaceButton, FileDownloadButton, FileCaptionButton, @@ -21,13 +21,13 @@ var HIDDEN_VIDEO_BUTTONS = [ FilePreviewButton, ]; -var HIDDEN_IMAGE_BUTTONS = [ +const HIDDEN_IMAGE_BUTTONS = [ FileDownloadButton, ]; export function filterVideoToolbarItems(items, hiddenTypes) { - var hide = hiddenTypes || HIDDEN_VIDEO_BUTTONS; - return items.filter(function (item) { + const hide = hiddenTypes || HIDDEN_VIDEO_BUTTONS; + return items.filter((item) => { if (hide.includes(item.type)) return false; if (item.props?.textAlignment) return false; return true; @@ -35,20 +35,20 @@ export function filterVideoToolbarItems(items, hiddenTypes) { } export function filterImageToolbarItems(items, hiddenTypes) { - var hide = hiddenTypes || HIDDEN_IMAGE_BUTTONS; - return items.filter(function (item) { + const hide = hiddenTypes || HIDDEN_IMAGE_BUTTONS; + return items.filter((item) => { if (hide.includes(item.type)) return false; return true; }); } function useIsVideo() { - var editor = useBlockNoteEditor(); + const editor = useBlockNoteEditor(); if (!editor) { return false; } try { - var block = editor.getSelection()?.blocks?.[0] + const block = editor.getSelection()?.blocks?.[0] ?? editor.getTextCursorPosition().block; return block.type === 'video'; } catch { @@ -57,12 +57,12 @@ function useIsVideo() { } function useIsImage() { - var editor = useBlockNoteEditor(); + const editor = useBlockNoteEditor(); if (!editor) { return false; } try { - var block = editor.getSelection()?.blocks?.[0] + const block = editor.getSelection()?.blocks?.[0] ?? editor.getTextCursorPosition().block; return block.type === 'image'; } catch { @@ -71,17 +71,17 @@ function useIsImage() { } export default function CustomFormattingToolbar() { - var Components = useComponentsContext(); + const Components = useComponentsContext(); if (!Components) { return null; } - var isVideo = useIsVideo(); - var isImage = useIsImage(); - var items = getFormattingToolbarItems(); + const isVideo = useIsVideo(); + const isImage = useIsImage(); + const items = getFormattingToolbarItems(); if (isVideo) { - var filtered = filterVideoToolbarItems(items); + const filtered = filterVideoToolbarItems(items); return ( @@ -91,7 +91,7 @@ export default function CustomFormattingToolbar() { } if (isImage) { - var filtered = filterImageToolbarItems(items); + const filtered = filterImageToolbarItems(items); return ( @@ -100,8 +100,8 @@ export default function CustomFormattingToolbar() { ); } - var rightIdx = items.findIndex( - function (item) { return item.props?.textAlignment === 'right'; }, + const rightIdx = items.findIndex( + (item) => { return item.props?.textAlignment === 'right'; }, ); return ( diff --git a/frontend/core/hooks/useCodeBlockGapClick.js b/frontend/core/hooks/useCodeBlockGapClick.js index faa809e..f79c1c9 100644 --- a/frontend/core/hooks/useCodeBlockGapClick.js +++ b/frontend/core/hooks/useCodeBlockGapClick.js @@ -44,20 +44,23 @@ export default function useCodeBlockGapClick(editorRef) { const editor = editorRef.current; if (!editor || typeof editor.insertBlocks !== 'function') return; + const emojiTarget = event.target; + if (emojiTarget?.closest?.('#bn-grid-suggestion-menu, .bn-formatting-toolbar, .bn-panel, em-emoji-picker')) return; + const y = event.clientY; - var BLOCK_SELECTOR = [ + const BLOCK_SELECTOR = [ '.bn-block-content[data-content-type="codeBlock"]', '.bn-block-content[data-content-type="image"]', '.bn-block-content[data-content-type="video"]', '.bn-visual-media-wrapper', ].join(','); - var VISUAL_TYPES = ['codeBlock', 'image', 'video']; + const VISUAL_TYPES = ['codeBlock', 'image', 'video']; const blocks = document.querySelectorAll(BLOCK_SELECTOR); - var seen = new Set(); + const seen = new Set(); for (const block of blocks) { const outer = block.closest('.bn-block-outer'); diff --git a/frontend/core/utils/custom-code-block-spec.js b/frontend/core/utils/custom-code-block-spec.js index 1050b04..d4d85aa 100644 --- a/frontend/core/utils/custom-code-block-spec.js +++ b/frontend/core/utils/custom-code-block-spec.js @@ -248,7 +248,7 @@ export function createCustomCodeBlockSpec(options) { const deleteBtn = document.createElement('button'); deleteBtn.className = 'code-delete-btn'; deleteBtn.innerHTML = ''; - deleteBtn.addEventListener('click', (e) => { + deleteBtn.addEventListener('mousedown', (e) => { e.stopPropagation(); editor.removeBlocks([block]); }); diff --git a/frontend/core/utils/video-override-spec.js b/frontend/core/utils/video-override-spec.js index beb7f92..2cec02a 100644 --- a/frontend/core/utils/video-override-spec.js +++ b/frontend/core/utils/video-override-spec.js @@ -1,22 +1,22 @@ -var YOUTUBE_RE = /(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/; +const YOUTUBE_RE = /(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/; function isYouTubeUrl(url) { return YOUTUBE_RE.test(url); } function getYouTubeEmbedUrl(url) { - var m = url.match(YOUTUBE_RE); + const m = url.match(YOUTUBE_RE); return m ? 'https://www.youtube.com/embed/' + m[1] : null; } function buildYouTubeIFrame(url, isEditable) { - var embedUrl = getYouTubeEmbedUrl(url); + const embedUrl = getYouTubeEmbedUrl(url); - var wrapper = document.createElement('div'); + const wrapper = document.createElement('div'); wrapper.className = 'bn-visual-media-wrapper'; wrapper.style.cssText = 'position:relative;width:100%;aspect-ratio:16/9'; - var iframe = document.createElement('iframe'); + const iframe = document.createElement('iframe'); iframe.src = embedUrl; iframe.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;border:0;border-radius:4px'; iframe.allow = 'fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'; @@ -24,11 +24,11 @@ function buildYouTubeIFrame(url, isEditable) { if (isEditable) { iframe.style.pointerEvents = 'none'; - wrapper.addEventListener('dblclick', function () { + wrapper.addEventListener('dblclick', () => { iframe.style.pointerEvents = ''; if (iframe.contentWindow) iframe.contentWindow.focus(); - var onAnyClick = function (e) { + const onAnyClick = (e) => { if (!wrapper.contains(e.target)) { iframe.style.pointerEvents = 'none'; document.removeEventListener('click', onAnyClick, true); @@ -43,35 +43,35 @@ function buildYouTubeIFrame(url, isEditable) { } function showToast(msg) { - var old = document.querySelector('.toast'); + const old = document.querySelector('.toast'); if (old) old.remove(); - var el = document.createElement('div'); + const el = document.createElement('div'); el.className = 'toast'; el.textContent = msg; document.body.appendChild(el); - setTimeout(function () { if (el.parentNode) el.remove(); }, 2800); + setTimeout(() => { if (el.parentNode) el.remove(); }, 2800); } export function createYouTubeVideoSpec(videoSpec) { - var origRender = videoSpec.implementation.render; - var origToExternalHTML = videoSpec.implementation.toExternalHTML; + const origRender = videoSpec.implementation.render; + const origToExternalHTML = videoSpec.implementation.toExternalHTML; return { ...videoSpec, implementation: { ...videoSpec.implementation, render: function (block, editor) { - var url = block.props.url; + const url = block.props.url; if (url && isYouTubeUrl(url)) { - var contentDiv = document.createElement('div'); + const contentDiv = document.createElement('div'); contentDiv.className = 'bn-block-content'; contentDiv.dataset.contentType = 'video'; contentDiv.dataset.fileBlock = ''; contentDiv.dataset.url = url; contentDiv.draggable = 'true'; - var iframeContainer = buildYouTubeIFrame(url, editor.isEditable); + const iframeContainer = buildYouTubeIFrame(url, editor.isEditable); contentDiv.appendChild(iframeContainer); return { dom: contentDiv }; @@ -79,34 +79,34 @@ export function createYouTubeVideoSpec(videoSpec) { if (url && editor.isEditable) { showToast('Only YouTube links are supported'); - setTimeout(function () { editor.removeBlocks([block.id]); }, 0); - var empty = document.createElement('div'); + setTimeout(() => { editor.removeBlocks([block.id]); }, 0); + const empty = document.createElement('div'); empty.style.cssText = 'display:none'; return { dom: empty }; } if (url) { - var empty = document.createElement('div'); + const empty = document.createElement('div'); empty.style.cssText = 'display:none'; return { dom: empty }; } - var result = origRender.call(this, block, editor); + const result = origRender.call(this, block, editor); if (!editor.isEditable && result && result.dom) { - var textEl = result.dom.querySelector('.bn-add-file-button-text'); + const textEl = result.dom.querySelector('.bn-add-file-button-text'); if (textEl) textEl.textContent = 'Add YouTube video URL'; } return result; }, toExternalHTML: function (block) { - var url = block.props && block.props.url; + const url = block.props && block.props.url; if (!url || !isYouTubeUrl(url)) { return origToExternalHTML.call(this, block); } - var embedUrl = getYouTubeEmbedUrl(url); - var iframe = document.createElement('iframe'); + const embedUrl = getYouTubeEmbedUrl(url); + const iframe = document.createElement('iframe'); iframe.src = embedUrl; iframe.width = '560'; iframe.height = '315';