diff --git a/eslint.config.js b/eslint.config.js index 5c51f90..ca7ed42 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -62,6 +62,8 @@ module.exports = [ HTMLInputElement: 'readonly', HTMLTextAreaElement: 'readonly', getComputedStyle: 'readonly', + CSS: 'readonly', + Element: 'readonly', // Electron electronAPI: 'readonly', // Libraries diff --git a/src/renderer.js b/src/renderer.js index ee98788..cc198b0 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -6,6 +6,7 @@ const { ipcRenderer, webUtils } = require('electron'); const { AutosaveController } = require('./renderer/autosave-client'); const writingStats = require('./utils/writing-stats'); +const { mountFootnotePreview } = require('./renderer/footnote-preview'); // Renderer-side autosave controller. The TabManager calls into this when a // tab becomes dirty so the current buffer is periodically persisted under @@ -1181,6 +1182,14 @@ class TabManager { preview.innerHTML = '
Error rendering preview. Please check your markdown syntax.
'; } + + // Wire the footnote hover preview once per preview pane. mountFootnotePreview + // is idempotent per element via the dataset marker; subsequent re-renders + // don't accumulate listeners. + if (!preview.dataset.footnotePreviewMounted) { + mountFootnotePreview(preview); + preview.dataset.footnotePreviewMounted = 'true'; + } } updatePreviewVisibility() { document.querySelectorAll('.tab-content').forEach((content) => { diff --git a/src/renderer/footnote-preview.js b/src/renderer/footnote-preview.js new file mode 100644 index 0000000..64d28d6 --- /dev/null +++ b/src/renderer/footnote-preview.js @@ -0,0 +1,124 @@ +/** + * Footnote hover preview. + * + * marked-footnote renders references as + * and bodies ashello1
+plain text
'; + document.body.appendChild(root); + + mountFootnotePreview(root, { delayMs: 0 }); + + root.querySelector('p').dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); + jest.advanceTimersByTime(0); + + // Popover exists (it was created on first show path normally) but + // should not be visible. + const popover = document.querySelector('.footnote-preview'); + if (popover) expect(popover.style.display).toBe('none'); + }); + + test('does not throw when the footnote body is missing (dangling ref)', () => { + const root = document.createElement('div'); + // Reference exists but no matchingx99
'; + document.body.appendChild(root); + + mountFootnotePreview(root, { delayMs: 0 }); + + const ref = root.querySelector('a[data-footnote-ref]'); + expect(() => ref.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }))).not.toThrow(); + jest.advanceTimersByTime(0); + + const popover = document.querySelector('.footnote-preview'); + if (popover) expect(popover.style.display).toBe('none'); + }); + + test('strips the backref ↩ from the displayed text', () => { + const root = document.createElement('div'); + root.innerHTML = makePreviewHtml('ref', 'see section 3 for context'); + document.body.appendChild(root); + + mountFootnotePreview(root, { delayMs: 0 }); + root + .querySelector('a[data-footnote-ref]') + .dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); + jest.advanceTimersByTime(0); + + const popover = document.querySelector('.footnote-preview'); + expect(popover.textContent).toBe('see section 3 for context'); + expect(popover.textContent).not.toMatch(/↩/); + }); + + test('cancels a pending show when a new ref is hovered', () => { + const root = document.createElement('div'); + root.innerHTML = ` + +