diff --git a/src/backlink-visualizer.ts b/src/backlink-visualizer.ts index 7b0f6ac7..a0237085 100644 --- a/src/backlink-visualizer.ts +++ b/src/backlink-visualizer.ts @@ -105,9 +105,8 @@ export class BacklinkDomManager extends PDFPlusComponent { for (const el of cacheToDoms.get(cache)) { this.hookBacklinkOpeners(el, cache); this.hookBacklinkViewEventHandlers(el, cache); - this.registerDomEvent(el, 'contextmenu', (evt) => { - onBacklinkVisualizerContextMenu(evt, this.visualizer, cache); - }); + this.hookContextMenuHandler(el, cache); + this.hookClassAdderOnMouseOver(el, cache); if (color?.type === 'name') { el.dataset.highlightColor = color.name.toLowerCase(); @@ -178,6 +177,34 @@ export class BacklinkDomManager extends PDFPlusComponent { } }); } + + hookContextMenuHandler(el: HTMLElement, cache: PDFBacklinkCache) { + this.registerDomEvent(el, 'contextmenu', (evt) => { + onBacklinkVisualizerContextMenu(evt, this.visualizer, cache); + }); + } + + hookClassAdderOnMouseOver(el: HTMLElement, cache: PDFBacklinkCache) { + const pageNumber = cache.page; + + if (typeof pageNumber === 'number') { + const className = 'is-hovered'; + + el.addEventListener('mouseover', () => { + for (const otherEl of this.getCacheToDomsMap(pageNumber).get(cache)) { + otherEl.addClass(className); + } + + const onMouseOut = () => { + for (const otherEl of this.getCacheToDomsMap(pageNumber).get(cache)) { + otherEl.removeClass(className); + } + el.removeEventListener('mouseout', onMouseOut); + }; + el.addEventListener('mouseout', onMouseOut); + }); + } + } } diff --git a/src/color-palette.ts b/src/color-palette.ts index 4d76fd18..3c574882 100644 --- a/src/color-palette.ts +++ b/src/color-palette.ts @@ -141,7 +141,7 @@ export class ColorPalette extends Component { }); } - addDropdown(paletteEl: HTMLElement, itemNames: string[], checkedIndexKey: KeysOfType, tooltip: string, onItemClick?: () => void) { + addDropdown(paletteEl: HTMLElement, itemNames: string[], checkedIndexKey: KeysOfType, tooltip: string, onItemClick?: () => void, beforeShowMenu?: (menu: Menu) => void) { return paletteEl.createDiv('clickable-icon', (buttonEl) => { setIcon(buttonEl, 'lucide-chevron-down'); setTooltip(buttonEl, tooltip); @@ -169,6 +169,8 @@ export class ColorPalette extends Component { }); } + beforeShowMenu?.(menu); + const { x, bottom, width } = buttonEl.getBoundingClientRect(); menu.setParentElement(buttonEl).showAtPosition({ x, @@ -215,6 +217,15 @@ export class ColorPalette extends Component { if (this.plugin.settings.syncColorPaletteAction && this.plugin.settings.syncDefaultColorPaletteAction) { this.plugin.settings.defaultColorPaletteActionIndex = this.actionIndex; } + }, + (menu) => { + menu.addItem((item) => { + item.setTitle('Customize...') + .onClick(() => { + this.plugin.openSettingTab() + .scrollTo('copyCommands'); + }); + }); } ); buttonEl.addClass('pdf-plus-action-menu'); @@ -231,6 +242,15 @@ export class ColorPalette extends Component { if (this.plugin.settings.syncDisplayTextFormat && this.plugin.settings.syncDefaultDisplayTextFormat) { this.plugin.settings.defaultDisplayTextFormatIndex = this.displayTextFormatIndex; } + }, + (menu) => { + menu.addItem((item) => { + item.setTitle('Customize...') + .onClick(() => { + this.plugin.openSettingTab() + .scrollTo('displayTextFormats'); + }); + }); } ); buttonEl.addClass('pdf-plus-display-text-format-menu'); diff --git a/src/context-menu.ts b/src/context-menu.ts index 733fc9bb..67629761 100644 --- a/src/context-menu.ts +++ b/src/context-menu.ts @@ -318,7 +318,7 @@ export const onOutlineItemContextMenu = (plugin: PDFPlus, child: PDFViewerChild, if (settings.openAfterExtractPages) { const leaf = lib.workspace.getLeaf(settings.howToOpenExtractedPDF); await leaf.openFile(file); - app.workspace.revealLeaf(leaf); + lib.workspace.revealLeaf(leaf); } }); }); @@ -380,7 +380,7 @@ export class PDFPlusContextMenu extends Menu { static async fromMouseEvent(plugin: PDFPlus, child: PDFViewerChild, evt: MouseEvent) { const menu = new PDFPlusContextMenu(plugin, child); - menu.addSections(['action', 'selection', 'selection-canvas', 'write-file', 'annotation', 'annotation-canvas', 'modify-annotation', 'link']); + menu.addSections(['action', 'selection', 'selection-canvas', 'write-file', 'annotation', 'annotation-canvas', 'modify-annotation', 'link', 'search']); await menu.addItems(evt); return menu; } @@ -628,6 +628,17 @@ export class PDFPlusContextMenu extends Menu { } } + if (plugin.settings.showCopyLinkToSearchInContextMenu) { + this.addItem((item) => { + item.setSection('search') + .setTitle('Copy link to search') + .setIcon('lucide-copy') + .onClick(() => { + lib.copyLink.copyLinkToSearch(false, child, pageNumber, selectedText.trim()); + }); + }); + } + app.workspace.trigger('pdf-menu', this, { pageNumber, selection: selectedText, diff --git a/src/lib/commands.ts b/src/lib/commands.ts index 5233e530..1af4c749 100644 --- a/src/lib/commands.ts +++ b/src/lib/commands.ts @@ -580,7 +580,7 @@ export class PDFPlusCommands extends PDFPlusLibSubmodule { if (this.settings.openAfterExtractPages) { const leaf = this.lib.workspace.getLeaf(this.settings.howToOpenExtractedPDF); await leaf.openFile(file); - this.app.workspace.revealLeaf(leaf); + this.lib.workspace.revealLeaf(leaf); } }); }); @@ -623,7 +623,7 @@ export class PDFPlusCommands extends PDFPlusLibSubmodule { if (this.settings.openAfterExtractPages) { const leaf = this.lib.workspace.getLeaf(this.settings.howToOpenExtractedPDF); await leaf.openFile(file); - this.app.workspace.revealLeaf(leaf); + this.lib.workspace.revealLeaf(leaf); } }); }); @@ -783,7 +783,7 @@ export class PDFPlusCommands extends PDFPlusLibSubmodule { const openFile = async () => { const { leaf, isExistingLeaf } = await this.lib.copyLink.prepareMarkdownLeafForPaste(file); if (leaf) { - this.app.workspace.revealLeaf(leaf); + this.lib.workspace.revealLeaf(leaf); this.app.workspace.setActiveLeaf(leaf); const view = leaf.view; if (view instanceof MarkdownView) { diff --git a/src/lib/copy-link.ts b/src/lib/copy-link.ts index 754f108b..c20c942f 100644 --- a/src/lib/copy-link.ts +++ b/src/lib/copy-link.ts @@ -18,13 +18,13 @@ export type AutoFocusTarget = export class copyLinkLib extends PDFPlusLibSubmodule { statusDurationMs = 2000; - getPageAndTextRangeFromSelection(selection?: Selection | null): { page: number, selection?: { beginIndex: number, beginOffset: number, endIndex: number, endOffset: number } } | null{ + getPageAndTextRangeFromSelection(selection?: Selection | null): { page: number, selection?: { beginIndex: number, beginOffset: number, endIndex: number, endOffset: number } } | null { selection = selection ?? activeWindow.getSelection(); if (!selection) return null; const pageEl = this.lib.getPageElFromSelection(selection); if (!pageEl || pageEl.dataset.pageNumber === undefined) return null; - + const pageNumber = +pageEl.dataset.pageNumber; const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null; @@ -38,7 +38,7 @@ export class copyLinkLib extends PDFPlusLibSubmodule { return { page: pageNumber }; } - // The same of getTextSelectionRangeStr in app.js, but returns an object instead of a string. + // The same as getTextSelectionRangeStr in Obsidian's app.js, but returns an object instead of a string. getTextSelectionRange(pageEl: HTMLElement, range: Range) { if (range && !range.collapsed) { const startTextLayerNode = getTextLayerNode(pageEl, range.startContainer); @@ -238,26 +238,31 @@ export class copyLinkLib extends PDFPlusLibSubmodule { } if (!checking) { - const evaluated = this.getTextToCopy(child, template, undefined, file, page, subpath, text, colorName?.toLowerCase() ?? ''); - navigator.clipboard.writeText(evaluated); - this.onCopyFinish(evaluated); + (async () => { + const evaluated = this.getTextToCopy(child, template, undefined, file, page, subpath, text, colorName?.toLowerCase() ?? ''); + // Without await, the focus can move to a different document before `writeText` is completed + // if auto-focus is on and the PDF is opened in a secondary window, which causes the copy to fail. + // https://github.com/RyotaUshio/obsidian-pdf-plus/issues/93 + await navigator.clipboard.writeText(evaluated); + this.onCopyFinish(evaluated); - const palette = this.lib.getColorPaletteFromChild(child); - palette?.setStatus('Link copied', this.statusDurationMs); - this.afterCopy(evaluated, autoPaste, palette ?? undefined); + const palette = this.lib.getColorPaletteFromChild(child); + palette?.setStatus('Link copied', this.statusDurationMs); + this.afterCopy(evaluated, autoPaste, palette ?? undefined); - // TODO: Needs refactor - const result = parsePDFSubpath(subpath); - if (result && 'beginIndex' in result) { - const item = child.getPage(page).textLayer?.textContentItems[result.beginIndex]; - if (item) { - const left = item.transform[4]; - const top = item.transform[5] + item.height; - if (typeof left === 'number' && typeof top === 'number') { - this.plugin.lastCopiedDestInfo = { file, destArray: [page - 1, 'XYZ', left, top, null] }; + // TODO: Needs refactor + const result = parsePDFSubpath(subpath); + if (result && 'beginIndex' in result) { + const item = child.getPage(page).textLayer?.textContentItems[result.beginIndex]; + if (item) { + const left = item.transform[4]; + const top = item.transform[5] + item.height; + if (typeof left === 'number' && typeof top === 'number') { + this.plugin.lastCopiedDestInfo = { file, destArray: [page - 1, 'XYZ', left, top, null] }; + } } } - } + })() } return true; @@ -282,7 +287,7 @@ export class copyLinkLib extends PDFPlusLibSubmodule { subpath += `&rect=${rect[0]},${rect[1]},${rect[2]},${rect[3]}`; } const evaluated = this.getTextToCopy(child, template, undefined, file, page, subpath, text ?? '', color); - navigator.clipboard.writeText(evaluated); + await navigator.clipboard.writeText(evaluated); this.onCopyFinish(evaluated); const palette = this.lib.getColorPaletteFromChild(child); @@ -305,13 +310,15 @@ export class copyLinkLib extends PDFPlusLibSubmodule { copyLinkToAnnotationWithGivenTextAndFile(text: string, file: TFile, child: PDFViewerChild, checking: boolean, template: string, page: number, id: string, colorName: string, autoPaste?: boolean) { if (!checking) { - const evaluated = this.getTextToCopy(child, template, undefined, file, page, `#page=${page}&annotation=${id}`, text, colorName) - navigator.clipboard.writeText(evaluated); - this.onCopyFinish(evaluated); + (async () => { + const evaluated = this.getTextToCopy(child, template, undefined, file, page, `#page=${page}&annotation=${id}`, text, colorName) + await navigator.clipboard.writeText(evaluated); + this.onCopyFinish(evaluated); - const palette = this.lib.getColorPaletteFromChild(child); - palette?.setStatus('Link copied', this.statusDurationMs); - this.afterCopy(evaluated, autoPaste, palette ?? undefined); + const palette = this.lib.getColorPaletteFromChild(child); + palette?.setStatus('Link copied', this.statusDurationMs); + this.afterCopy(evaluated, autoPaste, palette ?? undefined); + })(); } return true; @@ -386,7 +393,7 @@ export class copyLinkLib extends PDFPlusLibSubmodule { const extension = this.settings.rectImageExtension; if (!this.settings.rectEmbedStaticImage) { - navigator.clipboard.writeText(text); + await navigator.clipboard.writeText(text); this.onCopyFinish(text); } else if (this.settings.rectImageFormat === 'file') { @@ -395,7 +402,7 @@ export class copyLinkLib extends PDFPlusLibSubmodule { const imageEmbedLink = useWikilinks ? `![[${imagePath}]]` : `![](${encodeLinktext(imagePath)})`; text = imageEmbedLink + '\n\n' + embedLink.slice(1); - navigator.clipboard.writeText(text); + await navigator.clipboard.writeText(text); const createImageFile = async () => { const buffer = await this.lib.pdfPageToImageArrayBuffer(page, { type: `image/${extension}`, cropRect: rect }); @@ -412,7 +419,7 @@ export class copyLinkLib extends PDFPlusLibSubmodule { const imageEmbedLink = `![](${dataUrl})`; text = imageEmbedLink + '\n\n' + embedLink.slice(1); - navigator.clipboard.writeText(text); + await navigator.clipboard.writeText(text); this.onCopyFinish(text); } @@ -427,6 +434,27 @@ export class copyLinkLib extends PDFPlusLibSubmodule { return true; } + copyLinkToSearch(checking: boolean, child: PDFViewerChild, pageNumber: number, query: string, autoPaste?: boolean, sourcePath?: string): boolean { + if (!child.file) return false; + const file = child.file; + + const palette = this.lib.getColorPaletteFromChild(child); + + if (!checking) { + const display = this.lib.copyLink.getDisplayText(child, undefined, file, pageNumber, query); + const link = this.lib.generateMarkdownLink(file, '', `#search=${query}`, display).slice(1); + + (async () => { + await navigator.clipboard.writeText(link); + this.onCopyFinish(link); + palette?.setStatus('Link copied', this.statusDurationMs); + await this.afterCopy(link, autoPaste, palette ?? undefined); + })(); + } + + return true; + } + makeCanvasTextNodeFromSelection(checking: boolean, canvas: Canvas, template: string, colorName?: string): boolean { const variables = this.getTemplateVariables(colorName ? { color: colorName.toLowerCase() } : {}); @@ -538,7 +566,7 @@ export class copyLinkLib extends PDFPlusLibSubmodule { if (file) { // auto-focus target found const { leaf, isExistingLeaf } = await this.prepareMarkdownLeafForPaste(file); if (leaf) { - this.app.workspace.revealLeaf(leaf); + this.lib.workspace.revealLeaf(leaf); this.app.workspace.setActiveLeaf(leaf); const view = leaf.view; if (view instanceof MarkdownView) { @@ -646,7 +674,8 @@ export class copyLinkLib extends PDFPlusLibSubmodule { if (leaf && isExistingLeaf && leaf.view instanceof MarkdownView) { // If the file is already opened in some tab, use the editor interface to respect the current cursor position // https://github.com/RyotaUshio/obsidian-pdf-plus/issues/71 - const editor = leaf.view.editor; + const view = leaf.view; + const editor = view.editor; if (this.settings.respectCursorPositionWhenAutoPaste) { editor.replaceSelection(text); @@ -658,8 +687,14 @@ export class copyLinkLib extends PDFPlusLibSubmodule { editor.setValue(data); } + // MarkdownView's file saving is debounced, so we need to + // explicitly save the new data right after pasting so that + // the backlink highlight will be visibile as soon as possible. + view.save(); + if (this.settings.focusEditorAfterAutoPaste) { editor.focus(); + this.lib.workspace.revealLeaf(leaf); } } else { // Otherwise we just use the vault interface @@ -680,7 +715,7 @@ export class copyLinkLib extends PDFPlusLibSubmodule { this.app.workspace.offref(eventRef); if (info instanceof MarkdownView) { - this.app.workspace.revealLeaf(info.leaf); + this.lib.workspace.revealLeaf(info.leaf); } if (!editor.hasFocus()) editor.focus(); diff --git a/src/lib/highlights/viewer.ts b/src/lib/highlights/viewer.ts index 43827cd6..dfc38dbd 100644 --- a/src/lib/highlights/viewer.ts +++ b/src/lib/highlights/viewer.ts @@ -37,8 +37,12 @@ export class ViewerHighlightLib extends PDFPlusLibSubmodule { return rectEl; } - highlightSubpath(child: PDFViewerChild, subpath: string, duration: number) { - child.applySubpath(subpath); + /** + * + * @param child + * @param duration The duration in seconds to highlight the subpath. If it's 0, the highlight will not be removed until the user clicks on the page. + */ + highlightSubpath(child: PDFViewerChild, duration: number) { if (child.subpathHighlight?.type === 'text') { const component = new Component(); component.load(); diff --git a/src/lib/index.ts b/src/lib/index.ts index 4264dbf4..4b967d9c 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -13,7 +13,7 @@ import { PDFComposer } from './composer'; import { PDFOutlines } from './outlines'; import { NameTree, NumberTree } from './name-or-number-trees'; import { PDFNamedDestinations } from './destinations'; -import { AnnotationElement, CanvasFileNode, CanvasNode, CanvasView, DestArray, EventBus, ObsidianViewer, PDFOutlineViewer, PDFPageView, PDFSidebar, PDFThumbnailView, PDFView, PDFViewExtraState, PDFViewerChild, PDFjsDestArray, PDFViewer, PDFEmbed, PDFViewState, Rect, TextContentItem } from 'typings'; +import { AnnotationElement, CanvasFileNode, CanvasNode, CanvasView, DestArray, EventBus, ObsidianViewer, PDFOutlineViewer, PDFPageView, PDFSidebar, PDFThumbnailView, PDFView, PDFViewExtraState, PDFViewerChild, PDFjsDestArray, PDFViewer, PDFEmbed, PDFViewState, Rect, TextContentItem, PDFFindBar, PDFSearchSettings } from 'typings'; import { PDFCroppedEmbed } from 'pdf-cropped-embed'; import { PDFBacklinkIndex } from './pdf-backlink-index'; @@ -654,6 +654,26 @@ export class PDFPlusLib { return this.getPDFViewer(activeOnly)?.pdfDocument; } + search(findBar: PDFFindBar, query: string, settings?: Partial) { + findBar.showSearch(); + findBar.searchComponent.setValue(query); + + Object.assign(findBar.searchSettings, settings); + findBar.dispatchEvent(''); + + // Update the search settings UI accordingly + const toggleEls = findBar.settingsEl.querySelectorAll('div.checkbox-container'); + const highlightAllToggleEl = toggleEls[0]; + const matchDiacriticsToggleEl = toggleEls[1]; + const entireWordToggleEl = toggleEls[2]; + const caseSensitiveToggleIconEl = findBar.searchComponent.containerEl.querySelector('.input-right-decorator.clickable-icon'); + + if (highlightAllToggleEl) highlightAllToggleEl.toggleClass('is-enabled', findBar.searchSettings.highlightAll); + if (matchDiacriticsToggleEl) matchDiacriticsToggleEl.toggleClass('is-enabled', findBar.searchSettings.matchDiacritics); + if (entireWordToggleEl) entireWordToggleEl.toggleClass('is-enabled', findBar.searchSettings.entireWord); + if (caseSensitiveToggleIconEl) caseSensitiveToggleIconEl.toggleClass('is-active', findBar.searchSettings.caseSensitive); + } + async loadPDFDocument(file: TFile): Promise { const buffer = await this.app.vault.readBinary(file); return await this.loadPDFDocumentFromArrayBuffer(buffer); diff --git a/src/lib/workspace-lib.ts b/src/lib/workspace-lib.ts index c2af4555..e570880e 100644 --- a/src/lib/workspace-lib.ts +++ b/src/lib/workspace-lib.ts @@ -1,4 +1,4 @@ -import { EditableFileView, HoverParent, MarkdownView, OpenViewState, PaneType, TFile, WorkspaceItem, WorkspaceLeaf, WorkspaceSplit, WorkspaceTabs, parseLinktext } from 'obsidian'; +import { EditableFileView, HoverParent, MarkdownView, OpenViewState, PaneType, Platform, TFile, WorkspaceItem, WorkspaceLeaf, WorkspaceSidedock, WorkspaceSplit, WorkspaceTabs, WorkspaceWindow, parseLinktext } from 'obsidian'; import { PDFPlusLibSubmodule } from './submodule'; import { BacklinkView, CanvasView, PDFView, PDFViewerChild, PDFViewerComponent } from 'typings'; @@ -135,7 +135,7 @@ export class WorkspaceLib extends PDFPlusLibSubmodule { } await markdownLeaf.openLinkText(linktext, sourcePath, openViewState); - this.app.workspace.revealLeaf(markdownLeaf); + this.revealLeaf(markdownLeaf); return; } @@ -260,14 +260,46 @@ export class WorkspaceLib extends PDFPlusLibSubmodule { return leaf; } + /** + * Almost the same as Workspace.prototype.revealLeaf, but this version + * properly reveals a leaf even when it is contained in a secondary window. + */ + revealLeaf(leaf: WorkspaceLeaf) { + if (!Platform.isDesktopApp) { + // on mobile, we don't need to care about new windows so just use the original method + this.app.workspace.revealLeaf(leaf); + return; + } + + const root = leaf.getRoot(); + if (root instanceof WorkspaceSidedock && root.collapsed) { + root.toggle(); + } + + const parent = leaf.parent; + if (parent instanceof WorkspaceTabs) { + parent.selectTab(leaf); + } + + // This is the only difference from the original `revealLeaf` method. + // Obsidian's `revealLeaf` uses `root.getContainer().focus()` instead, which causes a bug that the main window is focused when the leaf is in a secondary window. + leaf.getContainer().focus(); + } + openPDFLinkTextInLeaf(leaf: WorkspaceLeaf, linktext: string, sourcePath: string, openViewState?: OpenViewState): Promise { + const { subpath } = parseLinktext(linktext); + if (!this.plugin.patchStatus.pdfInternals) { + this.plugin.subpathWhenPatched = subpath; + } + return leaf.openLinkText(linktext, sourcePath, openViewState).then(() => { - this.app.workspace.revealLeaf(leaf); + this.revealLeaf(leaf); + const view = leaf.view as PDFView; + view.viewer.then((child) => { const duration = this.plugin.settings.highlightDuration; - const { subpath } = parseLinktext(linktext); - this.lib.highlight.viewer.highlightSubpath(child, subpath, duration); + this.lib.highlight.viewer.highlightSubpath(child, duration); }); }); } @@ -315,7 +347,7 @@ export class WorkspaceLib extends PDFPlusLibSubmodule { this.app.workspace.offref(eventRef); } }); - } + } } } diff --git a/src/main.ts b/src/main.ts index d0ddc154..920a923d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -39,6 +39,18 @@ export default class PDFPlus extends Plugin { pdfOutlineViewer: false, backlink: false }; + /** + * When no PDF view or PDF embed is opened at the moment the plugin is loaded, the PDF internals will + * patched when the user opens a PDF link for the first time. + * After patching, the `onPDFInternalsPatchSuccess` function (defined in src/patchers/pdf-internals.ts) will be called, + * in which `PDFViewerComponent.loadFile(file, subpath)` will be re-executed in order to refresh the PDF view and reflect the patch. + * However, `PDFViewerComponent` does not have the information of the subpath to be opened at the moment, so we need to store it here + * so that we can pass it to `loadFile` when the patch is successful. + * + * Without this, when the user opens a link to PDF selection or annotation, it will not be highlighted (Obsidian-native highlight, not PDF++ highlight) + * properly if it is the first time the user opens a PDF link. + */ + subpathWhenPatched?: string; classes: { PDFView?: Constructor; PDFViewerComponent?: Constructor; @@ -161,8 +173,9 @@ export default class PDFPlus extends Plugin { } private registerRibbonIcons() { - this.selectToCopyMode = this.addChild(new SelectToCopyMode(this)); + this.selectToCopyMode = new SelectToCopyMode(this); this.selectToCopyMode.unload(); // disabled by default + this.register(() => this.selectToCopyMode.unload()); if (this.settings.autoFocusToggleRibbonIcon) { this.autoFocusToggleIconEl = this.addRibbonIcon('lucide-zap', `${this.manifest.name}: Toggle auto-focus`, () => { @@ -434,4 +447,9 @@ export default class PDFPlus extends Plugin { const noModKey = this.app.internalPlugins.plugins['page-preview'].instance.overrides['pdf-plus'] === false; return !noModKey; } + + openSettingTab(): PDFPlusSettingTab { + this.app.setting.open(); + return this.app.setting.openTabById(this.manifest.id); + } } diff --git a/src/patchers/pdf-internals.ts b/src/patchers/pdf-internals.ts index c8974d79..d34ae8ec 100644 --- a/src/patchers/pdf-internals.ts +++ b/src/patchers/pdf-internals.ts @@ -7,8 +7,8 @@ import { PDFAnnotationDeleteModal, PDFAnnotationEditModal } from 'modals/annotat import { onContextMenu, onOutlineContextMenu, onThumbnailContextMenu } from 'context-menu'; import { registerAnnotationPopupDrag, registerOutlineDrag, registerThumbnailDrag } from 'drag'; import { patchPDFOutlineViewer } from './pdf-outline-viewer'; -import { hookInternalLinkMouseEventHandlers, isNonEmbedLike, toSingleLine } from 'utils'; -import { AnnotationElement, PDFOutlineViewer, PDFViewerComponent, PDFViewerChild } from 'typings'; +import { camelCaseToKebabCase, hookInternalLinkMouseEventHandlers, isNonEmbedLike, toSingleLine } from 'utils'; +import { AnnotationElement, PDFOutlineViewer, PDFViewerComponent, PDFViewerChild, PDFSearchSettings } from 'typings'; import { PDFInternalLinkPostProcessor, PDFOutlineItemPostProcessor, PDFThumbnailItemPostProcessor } from 'pdf-link-like'; import { PDFViewerBacklinkVisualizer } from 'backlink-visualizer'; @@ -52,7 +52,7 @@ function onPDFInternalsPatchSuccess(plugin: PDFPlus) { // especially reflesh the "contextmenu" event handler (PDFViewerChild.prototype.onContextMenu/onThumbnailContext) viewer.unload(); viewer.load(); - if (file) viewer.loadFile(file); + if (file) viewer.loadFile(file, plugin.subpathWhenPatched); }); } @@ -262,7 +262,8 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { * Modified applySubpath() from Obsidian's app.js so that * - it can interpret the `rect` parameter as FitR, * - it supports `zoomToFitRect` setting, - * - and the `offset` & `rect` parameters can be parsed as float numbers, not integers + * - the `offset` & `rect` parameters can be parsed as float numbers, not integers, + * - and it can handle `search` parameter. */ applySubpath(old) { return function (subpath?: string) { @@ -282,10 +283,45 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { if (subpath) { const pdfViewer = self.pdfViewer; + const params = new URLSearchParams(subpath.startsWith('#') ? subpath.substring(1) : subpath); - const { dest, highlight } = ((subpath) => { - const params = new URLSearchParams(subpath.startsWith('#') ? subpath.substring(1) : subpath); + if (params.has('search') && self.findBar) { + const query = params.get('search')! + const settings: Partial = {}; + if (plugin.settings.searchLinkHighlightAll !== 'default') { + settings.highlightAll = plugin.settings.searchLinkHighlightAll === 'true'; + } + if (plugin.settings.searchLinkCaseSensitive !== 'default') { + settings.caseSensitive = plugin.settings.searchLinkCaseSensitive === 'true'; + } + if (plugin.settings.searchLinkMatchDiacritics !== 'default') { + settings.matchDiacritics = plugin.settings.searchLinkMatchDiacritics === 'true'; + } + if (plugin.settings.searchLinkEntireWord !== 'default') { + settings.entireWord = plugin.settings.searchLinkEntireWord === 'true'; + } + + const parseSearchSettings = (key: keyof PDFSearchSettings) => { + const kebabKey = camelCaseToKebabCase(key); + if (params.has(kebabKey)) { + const value = params.get(kebabKey); + if (value === 'true' || value === 'false') { + settings[key] = value === 'true'; + } + } + } + + parseSearchSettings('highlightAll'); + parseSearchSettings('caseSensitive'); + parseSearchSettings('matchDiacritics'); + parseSearchSettings('entireWord'); + + setTimeout(() => lib.search(self.findBar, query, settings)); + return; + } + + const { dest, highlight } = ((subpath) => { if (!params.has('page')) { return { dest: subpath, @@ -530,57 +566,59 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { const popupMetaEl = self.activeAnnotationPopupEl?.querySelector('.popupMeta'); if (popupMetaEl) { - // replace the copy button with a custom one - const copyButtonEl = popupMetaEl?.querySelector('.clickable-icon:last-child'); - if (copyButtonEl) { - copyButtonEl.remove(); // We need to remove the default event lisnter so we should use remove() instead of detach() - - popupMetaEl.createDiv('clickable-icon pdf-plus-copy-annotation-link', (iconEl) => { - setIcon(iconEl, 'lucide-copy'); - setTooltip(iconEl, 'Copy link'); - iconEl.addEventListener('click', async () => { - const palette = lib.getColorPaletteAssociatedWithNode(popupMetaEl) - if (!palette) return; - const template = plugin.settings.copyCommands[palette.actionIndex].template; - - lib.copyLink.copyLinkToAnnotation(self, false, template, page, id); - - setIcon(iconEl, 'lucide-check'); + popupMetaEl.createDiv('pdf-plus-annotation-icon-container', (iconContainerEl) => { + // replace the copy button with a custom one + const copyButtonEl = popupMetaEl?.querySelector('.clickable-icon:last-child'); + if (copyButtonEl) { + copyButtonEl.remove(); // We need to remove the default event lisnter so we should use remove() instead of detach() + + iconContainerEl.createDiv('clickable-icon pdf-plus-copy-annotation-link', (iconEl) => { + setIcon(iconEl, 'lucide-copy'); + setTooltip(iconEl, 'Copy link'); + iconEl.addEventListener('click', async () => { + const palette = lib.getColorPaletteAssociatedWithNode(popupMetaEl) + if (!palette) return; + const template = plugin.settings.copyCommands[palette.actionIndex].template; + + lib.copyLink.copyLinkToAnnotation(self, false, template, page, id); + + setIcon(iconEl, 'lucide-check'); + }); }); - }); - } + } - // add edit button - if (plugin.settings.enablePDFEdit - && plugin.settings.enableAnnotationContentEdit - && PDFAnnotationEditModal.isSubtypeSupported(annotationElement.data.subtype)) { - const subtype = annotationElement.data.subtype; - popupMetaEl.createDiv('clickable-icon pdf-plus-edit-annotation', (editButtonEl) => { - setIcon(editButtonEl, 'lucide-pencil'); - setTooltip(editButtonEl, 'Edit'); - editButtonEl.addEventListener('click', async () => { - if (self.file) { - PDFAnnotationEditModal - .forSubtype(subtype, plugin, self.file, page, id) - .open(); - } + // add edit button + if (plugin.settings.enablePDFEdit + && plugin.settings.enableAnnotationContentEdit + && PDFAnnotationEditModal.isSubtypeSupported(annotationElement.data.subtype)) { + const subtype = annotationElement.data.subtype; + iconContainerEl.createDiv('clickable-icon pdf-plus-edit-annotation', (editButtonEl) => { + setIcon(editButtonEl, 'lucide-pencil'); + setTooltip(editButtonEl, 'Edit'); + editButtonEl.addEventListener('click', async () => { + if (self.file) { + PDFAnnotationEditModal + .forSubtype(subtype, plugin, self.file, page, id) + .open(); + } + }); }); - }); - } + } - // add delete button - if (plugin.settings.enablePDFEdit && plugin.settings.enableAnnotationDeletion) { - popupMetaEl.createDiv('clickable-icon pdf-plus-delete-annotation', (deleteButtonEl) => { - setIcon(deleteButtonEl, 'lucide-trash'); - setTooltip(deleteButtonEl, 'Delete'); - deleteButtonEl.addEventListener('click', async () => { - if (self.file) { - new PDFAnnotationDeleteModal(plugin, self.file, page, id) - .openIfNeccessary(); - } + // add delete button + if (plugin.settings.enablePDFEdit && plugin.settings.enableAnnotationDeletion) { + iconContainerEl.createDiv('clickable-icon pdf-plus-delete-annotation', (deleteButtonEl) => { + setIcon(deleteButtonEl, 'lucide-trash'); + setTooltip(deleteButtonEl, 'Delete'); + deleteButtonEl.addEventListener('click', async () => { + if (self.file) { + new PDFAnnotationDeleteModal(plugin, self.file, page, id) + .openIfNeccessary(); + } + }); }); - }); - } + } + }); } if (plugin.settings.annotationPopupDrag && self.activeAnnotationPopupEl && self.file) { @@ -595,8 +633,6 @@ const patchPDFViewerChild = (plugin: PDFPlus, child: PDFViewerChild) => { }, destroyAnnotationPopup(old) { return function () { - // const self = this as PDFViewerChild; - // self.component?.unload(); plugin.lastAnnotationPopupChild = null; return old.call(this); } diff --git a/src/patchers/workspace.ts b/src/patchers/workspace.ts index 03263397..28420f5b 100644 --- a/src/patchers/workspace.ts +++ b/src/patchers/workspace.ts @@ -1,4 +1,4 @@ -import { OpenViewState, PaneType, Workspace, WorkspaceTabs, parseLinktext, Platform } from 'obsidian'; +import { OpenViewState, PaneType, Workspace, WorkspaceTabs, parseLinktext, Platform, WorkspaceSplit } from 'obsidian'; import { around } from 'monkey-around'; import PDFPlus from 'main'; @@ -53,7 +53,7 @@ export const patchWorkspace = (plugin: PDFPlus) => { if (plugin.settings.openLinkNextToExistingPDFTab || plugin.settings.paneTypeForFirstPDFLeaf) { const pdfLeaf = lib.getPDFView()?.leaf; if (pdfLeaf) { - if (plugin.settings.openLinkNextToExistingPDFTab) { + if (plugin.settings.openLinkNextToExistingPDFTab && pdfLeaf.parentSplit instanceof WorkspaceSplit) { const newLeaf = app.workspace.createLeafInParent(pdfLeaf.parentSplit, -1); return lib.workspace.openPDFLinkTextInLeaf(newLeaf, linktext, sourcePath, openViewState) } diff --git a/src/settings.ts b/src/settings.ts index 79380709..b985a712 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -215,6 +215,11 @@ export interface PDFPlusSettings { showBacklinkIconForRect: boolean; showBoundingRectForBacklinkedAnnot: boolean; hideReplyAnnotation: boolean; + showCopyLinkToSearchInContextMenu: boolean; + searchLinkHighlightAll: 'true' | 'false' | 'default'; + searchLinkCaseSensitive: 'true' | 'false' | 'default'; + searchLinkMatchDiacritics: 'true' | 'false' | 'default'; + searchLinkEntireWord: 'true' | 'false' | 'default'; } export const DEFAULT_SETTINGS: PDFPlusSettings = { @@ -254,11 +259,11 @@ export const DEFAULT_SETTINGS: PDFPlusSettings = { }, { name: 'Callout', - template: '> [!{{calloutType}}|{{colorName}}] {{linkWithDisplay}}\n> {{text}}\n', + template: '> [!{{calloutType}}|{{color}}] {{linkWithDisplay}}\n> {{text}}\n', }, { name: 'Quote in callout', - template: '> [!{{calloutType}}|{{colorName}}] {{linkWithDisplay}}\n> > {{text}}\n> \n> ', + template: '> [!{{calloutType}}|{{color}}] {{linkWithDisplay}}\n> > {{text}}\n> \n> ', } ], useAnotherCopyTemplateWhenNoSelection: false, @@ -414,6 +419,11 @@ export const DEFAULT_SETTINGS: PDFPlusSettings = { showBacklinkIconForRect: false, showBoundingRectForBacklinkedAnnot: false, hideReplyAnnotation: false, + showCopyLinkToSearchInContextMenu: true, + searchLinkHighlightAll: 'true', + searchLinkCaseSensitive: 'true', + searchLinkMatchDiacritics: 'default', + searchLinkEntireWord: 'false', }; @@ -450,9 +460,9 @@ export class PDFPlusSettingTab extends PluginSettingTab { return item; } - scrollTo(settingName: keyof PDFPlusSettings) { + scrollTo(settingName: keyof PDFPlusSettings, options?: { behavior: ScrollBehavior }) { const el = this.items[settingName]?.settingEl; - (el?.previousElementSibling ?? el)?.scrollIntoView(); + if (el) this.containerEl.scrollTo({ top: el.offsetTop - this.headerContainerEl.offsetHeight, ...options }); } addHeading(heading: string, icon?: IconName, processHeaderDom?: (dom: { headerEl: HTMLElement, iconEl: HTMLElement, titleEl: HTMLElement }) => void) { @@ -1035,11 +1045,14 @@ export class PDFPlusSettingTab extends PluginSettingTab { createLinkToSetting(id: keyof PDFPlusSettings, name?: string) { return createEl('a', '', (el) => { el.onclick = () => { - this.scrollTo(id); + this.scrollTo(id, { behavior: 'smooth' }); } window.setTimeout(() => { const setting = this.items[id]; - el.setText(name ?? setting?.nameEl.textContent ?? ''); + if (!name && setting) { + name = '"' + setting.nameEl.textContent + '"' + } + el.setText(name ?? ''); }); }); } @@ -1282,7 +1295,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { 'I recommend setting this as a custom color palette action in the setting below, like so:', '', '```markdown', - '> [!{{calloutType}}|{{colorName}}] {{linkWithDisplay}}', + '> [!{{calloutType}}|{{color}}] {{linkWithDisplay}}', '> {{text}}', '```', ], setting.descEl); @@ -1319,7 +1332,7 @@ export class PDFPlusSettingTab extends PluginSettingTab { this.addHeading('Right-click menu in PDF viewer', 'lucide-mouse-pointer-click') .setDesc('Customize the behavior of Obsidian\'s built-in right-click menu in PDF view.') this.addToggleSetting('replaceContextMenu', () => this.redisplay()) - .setName('Replace the built-in right-click menu and show color palette actions instead'); + .setName('Replace the built-in right-click menu with PDF++\'s custom menu'); if (!this.plugin.settings.replaceContextMenu) { this.addSetting() .setName('Display text format') @@ -1479,14 +1492,14 @@ export class PDFPlusSettingTab extends PluginSettingTab { '', 'In addition to the variables listed above, here you can use', '', - '- `link`: The link without display text, e.g. `[[file.pdf#page = 1 & selection=0, 1, 2, 3 & color=red]]`,', - '- `linkWithDisplay`: The link with display text, e.g. `[[file.pdf#page = 1 & selection=0, 1, 2, 3 & color=red | file, page 1]]`,', - '- `linktext`: The text content of the link without brackets and the display text, e.g. `file.pdf#page = 1 & selection=0, 1, 2, 3 & color=red`
(if the "Use \\[\\[Wikilinks\\]\\]" setting is turned off, `linktext` will be properly encoded for use in markdown links),', + '- `link`: The link without display text, e.g. `[[file.pdf#page=1&selection=0,1,2,3&color=red]]`,', + '- `linkWithDisplay`: The link with display text, e.g. `[[file.pdf#page=1&selection=0,1,2,3&color=red|file, page 1]]`,', + '- `linktext`: The text content of the link without brackets and the display text, e.g. `file.pdf#page=1&selection=0,1,2,3&color=red`
(if the "Use \\[\\[Wikilinks\\]\\]" setting is turned off, `linktext` will be properly encoded for use in markdown links),', '- `display`: The display text formatted according to the above setting, e.g. `file, page 1`,', - '- `linkToPage`: The link to the page without display text, e.g. `[[file.pdf#page = 1]]`,', - '- `linkToPageWithDisplay`: The link to the page with display text, e.g. `[[file.pdf#page = 1 | file, page 1]]`,', + '- `linkToPage`: The link to the page without display text, e.g. `[[file.pdf#page=1]]`,', + '- `linkToPageWithDisplay`: The link to the page with display text, e.g. `[[file.pdf#page=1|file, page 1]]`,', '- `calloutType`: The callout type you specify in the "Callout type name" setting above, in this case, ' + `"${this.plugin.settings.calloutType}", and`, - '- `colorName`: In the case of text selections, this is the name of the selected color in lowercase, e.g. `red`. If no color is specified, it will be an empty string. For text markup annotations (e.g. highlights and underlines), this is the RGB value of the color, e.g. `255, 208, 0`.', + '- `color` (or `colorName`): In the case of text selections, this is the name of the selected color in lowercase, e.g. `red`. If no color is specified, it will be an empty string. For text markup annotations (e.g. highlights and underlines), this is the RGB value of the color, e.g. `255,208,0`.', ], setting.descEl)) .addButton((button) => { button @@ -2039,6 +2052,46 @@ export class PDFPlusSettingTab extends PluginSettingTab { } + this.addHeading('Search from links', 'lucide-search') + .then((setting) => { + this.renderMarkdown([ + 'You can trigger full-text search by opening a link to a PDF file with a search query appended, e.g. `[[file.pdf#search=keyword]]`.', + ], setting.descEl); + }); + this.addToggleSetting('showCopyLinkToSearchInContextMenu') + .setName('Show "Copy link to search" in the right-click menu') + .setDesc(createFragment((el) => { + el.appendText('Requires the '); + el.appendChild(this.createLinkToSetting('replaceContextMenu')); + el.appendText(' option to be enabled.'); + })); + this.addHeading('Search options') + .then((setting) => { + this.renderMarkdown([ + 'The behavior of the search links can be customized globally by the following settings. ', + 'Alternatively, you can specify the behavior for each link by including the following query parameters in the link text: ', + '', + '- `&case-sensitive=true` or `&case-sensitive=false`', + '- `&highlight-all=true` or `&highlight-all=false`', + '- `&match-diacritics=true` or `&match-diacritics=false`', + '- `&entire-word=true` or `&entire-word=false`', + ], setting.descEl); + }); + const searchLinkDisplays = { + 'true': 'Yes', + 'false': 'No', + 'default': 'Follow default setting', + }; + this.addDropdownSetting('searchLinkCaseSensitive', searchLinkDisplays) + .setName('Case sensitive search'); + this.addDropdownSetting('searchLinkHighlightAll', searchLinkDisplays) + .setName('Highlight all search results'); + this.addDropdownSetting('searchLinkMatchDiacritics', searchLinkDisplays) + .setName('Match diacritics'); + this.addDropdownSetting('searchLinkEntireWord', searchLinkDisplays) + .setName('Match whole word'); + + this.addHeading('Integration with external apps (desktop-only)', 'lucide-share'); this.addToggleSetting('openPDFWithDefaultApp', () => this.redisplay()) .setName('Open PDF links with an external app') diff --git a/src/template.ts b/src/template.ts index c215652b..6437b326 100644 --- a/src/template.ts +++ b/src/template.ts @@ -40,6 +40,11 @@ export class PDFPlusTemplateProcessor extends TemplateProcessor { }) { const { app } = plugin; + // colorName is an alias for color + if ('colorName' in variables) { + variables.color = variables.colorName; + } + super(plugin, { ...variables, app, diff --git a/src/typings.d.ts b/src/typings.d.ts index be35ff54..26ee90ee 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -62,7 +62,8 @@ interface PDFViewerChild { opts: any; pdfViewer: ObsidianViewer; subpathHighlight: PDFTextHighlight | PDFAnnotationHighlight | null; - toolbar?: PDFToolbar; + toolbar: PDFToolbar; + findBar: PDFFindBar; highlightedText: [number, number][]; // [page, textContentItemindex][] annotationHighlight: HTMLElement | null; activeAnnotationPopupEl: HTMLElement | null; @@ -123,7 +124,9 @@ interface ObsidianViewer { pdfSidebar: PDFSidebar; pdfOutlineViewer: PDFOutlineViewer; pdfThumbnailViewer: PDFThumbnailViewer; - toolbar?: PDFToolbar; + toolbar: PDFToolbar; + findBar: PDFFindBar; + findController: PDFFindController; pdfLoadingTask: { promise: Promise }; setHeight(height?: number | 'page' | 'auto'): void; applySubpath(subpath: string): void; @@ -218,6 +221,66 @@ interface PDFToolbar { reset(): void; } +interface PDFFindBar { + app: App; + containerEl: HTMLElement; + barEl: HTMLElement; // div.pdf-findbar.pdf-toolbar.mod-hidden + findResultsCountEl: HTMLElement; // span.pdf-toolbar-label.pdf-find-results-count + findPreviousButtonEl: HTMLButtonElement; // button.pdf-toolbar-button + findNextButtonEl: HTMLButtonElement; // button.pdf-toolbar-button + settingsToggleEl: HTMLElement; // div.clickable-icon.pdf-findbar-settings-btn + settingsEl: HTMLElement; // div.pdf-findbar-settings + searchComponent: SearchComponent; + scope: Scope; + eventBus: EventBus; + opened: boolean; + searchSettings: PDFSearchSettings; + clickOutsideHandler: (evt: MouseEvent) => void; + /** Toggle whether to show the search settings menu ("Highlight all" etc) under the search bar. */ + toggleSetting(show: boolean): void; + /** Saves the current search settings to the local storage. */ + saveSettings(): void; + /** Just calls this.updateUIState(). */ + reset(): void; + /** + * Make the event bus dispatch a "find" event with the query retrieved from the search component. + * @param type + * @param findPrevious Defaults to false. + */ + dispatchEvent(type: '' | 'again' | 'highlightallchange' | 'casesensitivitychange' | 'diacriticmatchingchange' | 'entirewordchange', findPrevious?: boolean): void; + /** + * @param findState 0: FOUND, 1: NOT_FOUND, 2: WRAPPED, 3: PENDING. Defined in `window.pdfjsViewer.FindState`. + * @param unusedArg This parameter seems to be unused. + * @param counts See the explanation for `updateResultsCount()`. + */ + updateUIState(findState: number, unusedArg: any, counts?: PDFSearchMatchCounts): void; + /** + * @param counts Defaults to `{ current: 0, total: 0 }`. + */ + updateResultsCount(counts?: PDFSearchMatchCounts): void; + open(): void; + close(): void; + toggle(): void; + /** Show and focus on the search bar. */ + showSearch(): void; +} + +interface PDFSearchSettings { + highlightAll: boolean; + caseSensitive: boolean; + matchDiacritics: boolean; + entireWord: boolean; +} + +interface PDFSearchMatchCounts { + current: number; + total: number; +} + +interface PDFFindController { + +} + interface PDFViewer { pdfDocument: PDFDocumentProxy; pagesPromise: Promise | null; @@ -848,7 +911,9 @@ declare module 'obsidian' { interface WorkspaceLeaf { group: string | null; - readonly parentSplit: WorkspaceSplit; + /** As of Obsidian v1.5.8, this is just a read-only alias for `this.parent`. */ + readonly parentSplit?: WorkspaceParent; + parent?: WorkspaceParent; containerEl: HTMLElement; openLinkText(linktext: string, sourcePath: string, openViewState?: OpenViewState): Promise; highlight(): void; @@ -861,6 +926,11 @@ declare module 'obsidian' { interface WorkspaceTabs { children: WorkspaceItem[]; + selectTab(tab: WorkspaceItem): void; + } + + interface WorkspaceContainer { + focus(): void; } interface Menu { @@ -907,4 +977,8 @@ declare module 'obsidian' { interface HoverPopover { hide(): void; } + + interface SearchComponent { + containerEl: HTMLElement; + } } diff --git a/src/utils/index.ts b/src/utils/index.ts index 2f83b521..5b949969 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -15,6 +15,15 @@ export function getDirectPDFObj(dict: PDFDict, key: string) { return obj; } +// Thanks https://stackoverflow.com/a/54246501 +export function camelCaseToKebabCase(camelCaseStr: string) { + return camelCaseStr.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`); +} + +export function kebabCaseToCamelCase(kebabCaseStr: string) { + return kebabCaseStr.replace(/(-\w)/g, m => m[1].toUpperCase()); +} + /** Return an array of numbers from `from` (inclusive) to `to` (exclusive). */ export function range(from: number, to: number): number[] { return Array.from({ length: to - from }, (_, i) => from + i); diff --git a/styles.css b/styles.css index 66ee2c1e..8b7712e3 100644 --- a/styles.css +++ b/styles.css @@ -251,7 +251,7 @@ body:not(.pdf-plus-backlink-selection-underline) .pdf-plus-backlink-highlight-la opacity: var(--pdf-plus-highlight-opacity, 0.2); padding: var(--pdf-plus-highlight-padding-vertical-em, var(--pdf-plus-highlight-padding-default-em)) var(--pdf-plus-highlight-padding-horizontal-em, var(--pdf-plus-highlight-padding-default-em)); margin: calc(var(--pdf-plus-highlight-padding-vertical-em, var(--pdf-plus-highlight-padding-default-em)) * -1) calc(var(--pdf-plus-highlight-padding-horizontal-em, var(--pdf-plus-highlight-padding-default-em)) * -1); - border-radius: var(--radius-s); + border-radius: 0.1em; } body.pdf-plus-backlink-selection-underline { @@ -337,7 +337,22 @@ body.pdf-plus-backlink-selection-underline { } .popupWrapper { - --pdf-popup-width: 300px; + --pdf-popup-width: 310px; +} + +.pdf-plus-annotation-icon-container { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + margin-right: calc(var(--size-4-1) * -1); + margin-left: calc(var(--size-2-1) * -1); + + .clickable-icon { + margin-right: 0; + margin-left: 0; + } } .pdf-plus-draggable .popup {