From 4a13dbc005f30ae342d642dcd1459370a7951c6b Mon Sep 17 00:00:00 2001 From: Quorafind Date: Wed, 15 Mar 2023 17:23:56 +0800 Subject: [PATCH] feat: support keyboard navigation --- .github/workflows/release.yml | 2 +- .gitignore | 3 - README.md | 4 +- esbuild.config.mjs | 2 +- manifest.json | 4 +- package.json | 4 +- src/floatSearchIndex.ts | 253 ++++++++++++++++ src/leafView.ts | 553 ++++++++++++++++++++++++++++++++++ src/types/types-obsidian.d.ts | 210 +++++++++++++ styles.css | 4 + versions.json | 3 +- 11 files changed, 1031 insertions(+), 11 deletions(-) create mode 100644 src/floatSearchIndex.ts create mode 100644 src/leafView.ts create mode 100644 src/types/types-obsidian.d.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6c345a4..3a3a67c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,7 @@ on: types: [ created ] env: - PLUGIN_NAME: obsidian-canvas-presentation + PLUGIN_NAME: obsidian-float-search jobs: build: diff --git a/.gitignore b/.gitignore index d1ccdc1..f386483 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,3 @@ data.json # Exclude macOS Finder (System Explorer) View States .DS_Store - -src -.github diff --git a/README.md b/README.md index d7ee86a..3c07202 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Obsidian Float Search -Float search modal. +You can use search view in modal now. + +- Set hotkey for open float search quickly. ## Support diff --git a/esbuild.config.mjs b/esbuild.config.mjs index b33ec53..3104a06 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -15,7 +15,7 @@ esbuild.build({ banner: { js: banner, }, - entryPoints: ['src/main.ts'], + entryPoints: ['src/floatSearchIndex.ts'], bundle: true, external: [ 'obsidian', diff --git a/manifest.json b/manifest.json index 8bc148d..33fe0e8 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,9 @@ { "id": "float-search", "name": "Float Search", - "version": "0.0.1", + "version": "1.0.0", "minAppVersion": "0.15.0", - "description": "This is a sample plugin for Obsidian. This plugin demonstrates some of the capabilities of the Obsidian API.", + "description": "You can use search view in modal now.", "author": "Boninall", "authorUrl": "https://github.com/Quorafind", "fundingUrl": { diff --git a/package.json b/package.json index b35c2c2..d253033 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "float-search", - "version": "0.0.1", - "description": "This is a sample plugin for Obsidian (https://obsidian.md)", + "version": "1.0.0", + "description": "You can use search view in modal now.", "main": "main.js", "scripts": { "dev": "node esbuild.config.mjs", diff --git a/src/floatSearchIndex.ts b/src/floatSearchIndex.ts new file mode 100644 index 0000000..aee405b --- /dev/null +++ b/src/floatSearchIndex.ts @@ -0,0 +1,253 @@ +import { + App, + Modal, OpenViewState, + Plugin, SearchView, + TFile, + Workspace, + WorkspaceContainer, WorkspaceItem, + WorkspaceLeaf +} from 'obsidian'; +import { EmbeddedView, isDailyNoteLeaf, spawnLeafView } from "./leafView"; +import { around } from "monkey-around"; + +export default class FloatSearchPlugin extends Plugin { + + async onload() { + this.patchWorkspace(); + this.patchWorkspaceLeaf(); + + this.addCommand({ + id: 'float-search', + name: 'Search Obsidian In Modal', + callback: () => { + new FloatSearchModal(this.app, this).open(); + } + }); + + this.addRibbonIcon('search', 'Search Obsidian In Modal', () => { + new FloatSearchModal(this.app, this).open(); + }); + } + + onunload() { + + } + + patchWorkspace() { + let layoutChanging = false; + const uninstaller = around(Workspace.prototype, { + getLeaf: (next) => + function (...args) { + const activeLeaf = this.activeLeaf; + if(activeLeaf) { + if(activeLeaf.pinned === true && activeLeaf.view.getViewType() === "search") { + const newLeaf = app.workspace.getUnpinnedLeaf(); + if(newLeaf) { + this.setActiveLeaf(newLeaf); + } + } + return next.call(this, ...args); + } + return next.call(this, ...args); + }, + changeLayout(old) { + return async function (workspace: unknown) { + layoutChanging = true; + try { + // Don't consider hover popovers part of the workspace while it's changing + await old.call(this, workspace); + } finally { + layoutChanging = false; + } + }; + }, + iterateLeaves(old) { + type leafIterator = (item: WorkspaceLeaf) => boolean | void; + return function (arg1, arg2) { + // Fast exit if desired leaf found + if (old.call(this, arg1, arg2)) return true; + + // Handle old/new API parameter swap + const cb: leafIterator = (typeof arg1 === "function" ? arg1 : arg2) as leafIterator; + const parent: WorkspaceItem = (typeof arg1 === "function" ? arg2 : arg1) as WorkspaceItem; + + if (!parent) return false; // <- during app startup, rootSplit can be null + if (layoutChanging) return false; // Don't let HEs close during workspace change + + // 0.14.x doesn't have WorkspaceContainer; this can just be an instanceof check once 15.x is mandatory: + if (parent === app.workspace.rootSplit || (WorkspaceContainer && parent instanceof WorkspaceContainer)) { + for(const popover of EmbeddedView.popoversForWindow((parent as WorkspaceContainer).win)) { + // Use old API here for compat w/0.14.x + if (old.call(this, cb, popover.rootSplit)) return true; + } + } + return false; + }; + }, + onDragLeaf(old) { + return function (event: MouseEvent, leaf: WorkspaceLeaf) { + return old.call(this, event, leaf); + }; + } + }); + this.register(uninstaller); + } + + // Used for patch workspaceleaf pinned behaviors + patchWorkspaceLeaf() { + this.register( + around(WorkspaceLeaf.prototype, { + getRoot(old) { + return function () { + const top = old.call(this); + return top?.getRoot === this.getRoot ? top : top?.getRoot(); + }; + }, + setPinned(old) { + return function (pinned: boolean) { + old.call(this, pinned); + if(isDailyNoteLeaf(this) && !pinned) this.setPinned(true); + } + }, + openFile(old) { + return function (file: TFile, openState?: OpenViewState) { + if (isDailyNoteLeaf(this)) { + setTimeout( + around(Workspace.prototype, { + recordMostRecentOpenedFile(old) { + return function (_file: TFile) { + // Don't update the quick switcher's recent list + if (_file !== file) { + return old.call(this, _file); + } + }; + }, + }), + 1, + ); + const recentFiles = this.app.plugins.plugins["recent-files-obsidian"]; + if (recentFiles) + setTimeout( + around(recentFiles, { + shouldAddFile(old) { + return function (_file: TFile) { + // Don't update the Recent Files plugin + return _file !== file && old.call(this, _file); + }; + }, + }), + 1, + ); + } + return old.call(this, file, openState); + } + } + }), + ); + } +} + + +class FloatSearchModal extends Modal { + private readonly plugin: FloatSearchPlugin; + private embeddedView: EmbeddedView; + private leaf: WorkspaceLeaf; + + constructor(app: App, plugin: FloatSearchPlugin) { + super(app); + this.plugin = plugin; + } + + async onOpen() { + const { contentEl } = this; + + this.initCss(contentEl); + await this.initView(contentEl); + this.initInput(); + this.initContent(); + } + + onClose() { + const { contentEl } = this; + this.leaf.detach(); + this.embeddedView.unload(); + contentEl.empty(); + } + + initCss(contentEl: HTMLElement) { + contentEl.classList.add("float-search-modal"); + contentEl.parentElement?.classList.add("float-search-modal-parent"); + contentEl.parentElement?.parentElement?.classList.add("float-search-modal-container"); + } + + async initView(contentEl: HTMLElement) { + const [createdLeaf, embeddedView] = spawnLeafView(this.plugin, contentEl); + this.leaf = createdLeaf; + this.embeddedView = embeddedView; + + this.leaf.setPinned(true); + await this.leaf.setViewState({ + type: "search", + }); + } + + initInput() { + const inputEl = this.contentEl.getElementsByTagName("input")[0]; + inputEl.focus(); + inputEl.onkeydown = (e) => { + const currentView = this.leaf.view as SearchView; + switch (e.key) { + case "ArrowDown": + if (e.shiftKey) { + currentView.onKeyShowMoreAfter(e); + break; + } else { + currentView.onKeyArrowDownInFocus(e); + break; + } + case "ArrowUp": + if (e.shiftKey) { + currentView.onKeyShowMoreBefore(e); + break; + } else { + currentView.onKeyArrowUpInFocus(e); + break; + } + case "ArrowLeft": + currentView.onKeyArrowLeftInFocus(e); + break; + case "ArrowRight": + currentView.onKeyArrowRightInFocus(e); + break; + case "Enter": + currentView.onKeyEnterInFocus(e); + if(e.altKey && currentView.dom.focusedItem) { + this.close(); + } + break; + } + } + } + + initContent() { + const { contentEl } = this; + contentEl.onclick = (e) => { + const resultElement = contentEl.getElementsByClassName('search-results-children')[0]; + if(resultElement.children.length < 2) { + return; + } + + const target = e.target as HTMLElement; + const classList = target.classList; + + const navElement = contentEl.getElementsByClassName('nav-header')[0]; + if (e.target !== navElement && navElement.contains(e.target as Node)) { + return; + } + + if(!(classList.contains("tree-item-icon") || classList.contains("float-search-modal") || classList.contains("right-triangle") || target.parentElement?.classList.contains("right-triangle") || classList.contains("search-input-container") || target?.parentElement?.classList.contains("search-input-container") || classList.contains("search-result-hover-button"))) { + this.close(); + } + } + } +} diff --git a/src/leafView.ts b/src/leafView.ts new file mode 100644 index 0000000..d99ecf5 --- /dev/null +++ b/src/leafView.ts @@ -0,0 +1,553 @@ +// Original code from https://github.com/nothingislost/obsidian-hover-editor/blob/9ec3449be9ab3433dc46c4c3acfde1da72ff0261/src/popover.ts +// You can use this file as a basic leaf view create method in anywhere +// Please rememeber if you want to use this file, you should patch the types-obsidian.d.ts file +// And also monkey around the Obsidian original method. +import { + Component, + EphemeralState, + HoverPopover, + MarkdownEditView, + parseLinktext, + PopoverState, + requireApiVersion, + resolveSubpath, + TFile, + View, + Workspace, + WorkspaceLeaf, + WorkspaceSplit, + WorkspaceTabs, +} from "obsidian"; +import type { OpenViewState } from "src/types/types-obsidian"; +import type FloatSearchPlugin from "src/floatSearchIndex"; + + +export interface EmbeddedViewParent { + hoverPopover: EmbeddedView | null; + containerEl?: HTMLElement; + view?: View; + dom?: HTMLElement; +} +const popovers = new WeakMap(); +type ConstructableWorkspaceSplit = new (ws: Workspace, dir: "horizontal"|"vertical") => WorkspaceSplit; + +export function isDailyNoteLeaf(leaf: WorkspaceLeaf) { + // Work around missing enhance.js API by checking match condition instead of looking up parent + return leaf.containerEl.matches(".fs-block.fs-leaf-view .workspace-leaf"); +} + +function genId(size: number): string { + const chars = []; + for (let n = 0; n < size; n++) chars.push(((16 * Math.random()) | 0).toString(16)); + return chars.join(""); +} + +function nosuper(base: new (...args: unknown[]) => T): new () => T { + const derived = function () { + return Object.setPrototypeOf(new Component, new.target.prototype); + }; + derived.prototype = base.prototype; + return Object.setPrototypeOf(derived, base); +} + +export const spawnLeafView = (plugin: FloatSearchPlugin, initiatingEl?: HTMLElement, leaf?: WorkspaceLeaf, onShowCallback?: () => unknown): [WorkspaceLeaf, EmbeddedView] => { + // When Obsidian doesn't set any leaf active, use leaf instead. + let parent = app.workspace.activeLeaf as unknown as EmbeddedViewParent; + if (!parent) parent = leaf as unknown as EmbeddedViewParent; + + if (!initiatingEl) initiatingEl = parent?.containerEl; + + const hoverPopover = new EmbeddedView(parent, initiatingEl!, plugin, undefined, onShowCallback); + return [hoverPopover.attachLeaf(), hoverPopover]; + +} + +export class EmbeddedView extends nosuper(HoverPopover) { + onTarget: boolean; + setActive: (event: MouseEvent) => void; + + lockedOut: boolean; + abortController? = this.addChild(new Component()); + detaching = false; + opening = false; + + rootSplit: WorkspaceSplit = new (WorkspaceSplit as ConstructableWorkspaceSplit)(window.app.workspace, "vertical"); + + isPinned = true; + + titleEl: HTMLElement; + containerEl: HTMLElement; + + // It is currently not useful. + // leafInHoverEl: WorkspaceLeaf; + + oldPopover = this.parent?.hoverPopover; + document: Document = this.targetEl?.ownerDocument ?? window.activeDocument ?? window.document; + + id = genId(8); + bounce?: NodeJS.Timeout; + boundOnZoomOut: () => void; + + originalPath: string; // these are kept to avoid adopting targets w/a different link + originalLinkText: string; + static activePopover?: EmbeddedView; + + static activeWindows() { + const windows: Window[] = [window]; + const { floatingSplit } = app.workspace; + if (floatingSplit) { + for (const split of floatingSplit.children) { + if (split.win) windows.push(split.win); + } + } + return windows; + } + + static containerForDocument(doc: Document) { + if (doc !== document && app.workspace.floatingSplit) + for (const container of app.workspace.floatingSplit.children) { + if (container.doc === doc) return container; + } + return app.workspace.rootSplit; + } + + static activePopovers() { + return this.activeWindows().flatMap(this.popoversForWindow); + } + + static popoversForWindow(win?: Window) { + return (Array.prototype.slice.call(win?.document?.body.querySelectorAll(".fs-leaf-view") ?? []) as HTMLElement[]) + .map(el => popovers.get(el)!) + .filter(he => he); + } + + static forLeaf(leaf: WorkspaceLeaf | undefined) { + // leaf can be null such as when right clicking on an internal link + const el = leaf && document.body.matchParent.call(leaf.containerEl, ".fs-leaf-view"); // work around matchParent race condition + return el ? popovers.get(el) : undefined; + } + + static iteratePopoverLeaves(ws: Workspace, cb: (leaf: WorkspaceLeaf) => boolean | void) { + for (const popover of this.activePopovers()) { + if (popover.rootSplit && ws.iterateLeaves(cb, popover.rootSplit)) return true; + } + return false; + } + + hoverEl: HTMLElement = this.document.defaultView!.createDiv({ + cls: "fs-block fs-leaf-view", + attr: { id: "fs-" + this.id }, + }); + + constructor( + parent: EmbeddedViewParent, + public targetEl: HTMLElement, + public plugin: FloatSearchPlugin, + waitTime?: number, + public onShowCallback?: () => unknown, + ) { + // + super(); + + if (waitTime === undefined) { + waitTime = 300; + } + this.onTarget = true; + + this.parent = parent; + this.waitTime = waitTime; + this.state = PopoverState.Showing; + const { hoverEl } = this; + + this.abortController!.load(); + this.show(); + this.onShow(); + + this.setActive = this._setActive.bind(this); + // if (hoverEl) { + // hoverEl.addEventListener("mousedown", this.setActive); + // } + // custom logic begin + popovers.set(this.hoverEl, this); + this.hoverEl.addClass("fs-block"); + this.containerEl = this.hoverEl.createDiv("fs-content"); + this.buildWindowControls(); + this.setInitialDimensions(); + } + + _setActive(evt: MouseEvent) { + evt.preventDefault(); + evt.stopPropagation(); + this.plugin.app.workspace.setActiveLeaf(this.leaves()[0], {focus: true}) + } + + getDefaultMode() { + // return this.parent?.view?.getMode ? this.parent.view.getMode() : "source"; + return "source"; + } + + updateLeaves() { + if (this.onTarget && this.targetEl && !this.document.contains(this.targetEl)) { + this.onTarget = false; + this.transition(); + } + let leafCount = 0; + this.plugin.app.workspace.iterateLeaves(leaf => { + leafCount++; + }, this.rootSplit); + + if (leafCount === 0) { + this.hide(); // close if we have no leaves + } + this.hoverEl.setAttribute("data-leaf-count", leafCount.toString()); + } + + leaves() { + const leaves: WorkspaceLeaf[] = []; + this.plugin.app.workspace.iterateLeaves(leaf => { + leaves.push(leaf); + }, this.rootSplit); + return leaves; + } + + setInitialDimensions() { + + this.hoverEl.style.height = 'auto'; + this.hoverEl.style.width = "100%"; + } + + transition() { + if (this.shouldShow()) { + if (this.state === PopoverState.Hiding) { + this.state = PopoverState.Shown; + clearTimeout(this.timer); + } + } else { + if (this.state === PopoverState.Showing) { + this.hide(); + } else { + if (this.state === PopoverState.Shown) { + this.state = PopoverState.Hiding; + this.timer = window.setTimeout(() => { + if (this.shouldShow()) { + this.transition(); + } else { + this.hide(); + } + }, this.waitTime); + } + } + } + } + + + buildWindowControls() { + this.titleEl = this.document.defaultView!.createDiv("popover-titlebar"); + this.titleEl.createDiv("popover-title"); + + this.containerEl.prepend(this.titleEl); + + } + + attachLeaf(): WorkspaceLeaf { + this.rootSplit.getRoot = () => this.plugin.app.workspace[this.document === document ? "rootSplit" : "floatingSplit"]!; + this.rootSplit.getContainer = () => EmbeddedView.containerForDocument(this.document); + + this.titleEl.insertAdjacentElement("afterend", this.rootSplit.containerEl); + const leaf = this.plugin.app.workspace.createLeafInParent(this.rootSplit, 0); + + this.updateLeaves(); + return leaf; + } + + onload(): void { + super.onload(); + this.registerEvent(this.plugin.app.workspace.on("layout-change", this.updateLeaves, this)); + this.registerEvent(app.workspace.on("layout-change", () => { + // Ensure that top-level items in a popover are not tabbed + // @ts-ignore + this.rootSplit.children.forEach((item: any, index: any) => { + if (item instanceof WorkspaceTabs) { + this.rootSplit.replaceChild(index, item.children[0]); + } + }) + })); + } + + onShow() { + // Once we've been open for closeDelay, use the closeDelay as a hiding timeout + const closeDelay = 600; + setTimeout(() => (this.waitTime = closeDelay), closeDelay); + + this.oldPopover?.hide(); + this.oldPopover = null; + + this.hoverEl.toggleClass("is-new", true); + + this.document.body.addEventListener( + "click", + () => { + this.hoverEl.toggleClass("is-new", false); + }, + { once: true, capture: true }, + ); + + if (this.parent) { + this.parent.hoverPopover = this; + } + + // Remove original view header; + const viewHeaderEl = this.hoverEl.querySelector(".view-header"); + viewHeaderEl?.remove(); + + const sizer = this.hoverEl.querySelector(".workspace-leaf"); + if(sizer) this.hoverEl.appendChild(sizer); + + // Remove original inline tilte; + const inlineTitle = this.hoverEl.querySelector(".inline-title"); + if(inlineTitle) inlineTitle.remove(); + + this.onShowCallback?.(); + this.onShowCallback = undefined; // only call it once + } + + detect(el: HTMLElement) { + // TODO: may not be needed? the mouseover/out handers handle most detection use cases + const { targetEl } = this; + + if (targetEl) { + this.onTarget = el === targetEl || targetEl.contains(el); + } + } + + shouldShow() { + return this.shouldShowSelf() || this.shouldShowChild(); + } + + shouldShowChild(): boolean { + return EmbeddedView.activePopovers().some(popover => { + if (popover !== this && popover.targetEl && this.hoverEl.contains(popover.targetEl)) { + return popover.shouldShow(); + } + return false; + }); + } + + shouldShowSelf() { + // Don't let obsidian show() us if we've already started closing + // return !this.detaching && (this.onTarget || this.onHover); + return ( + !this.detaching && + !!( + this.onTarget || + (this.state == PopoverState.Shown) || + this.document.querySelector(`body>.modal-container, body > #he${this.id} ~ .menu, body > #he${this.id} ~ .suggestion-container`) + ) + ); + } + + show() { + // native obsidian logic start + // if (!this.targetEl || this.document.body.contains(this.targetEl)) { + this.state = PopoverState.Shown; + this.timer = 0; + + this.targetEl.appendChild(this.hoverEl); + this.onShow(); + app.workspace.onLayoutChange(); + + // initializingHoverPopovers.remove(this); + // activeHoverPopovers.push(this); + // initializePopoverChecker(); + this.load(); + // } + // native obsidian logic end + + // if this is an image view, set the dimensions to the natural dimensions of the image + // an interactjs reflow will be triggered to constrain the image to the viewport if it's + // too large + if (this.hoverEl.dataset.imgHeight && this.hoverEl.dataset.imgWidth) { + this.hoverEl.style.height = parseFloat(this.hoverEl.dataset.imgHeight) + this.titleEl.offsetHeight + "px"; + this.hoverEl.style.width = parseFloat(this.hoverEl.dataset.imgWidth) + "px"; + } + } + + onHide() { + this.oldPopover = null; + if (this.parent?.hoverPopover === this) { + this.parent.hoverPopover = null; + } + } + + hide() { + this.onTarget = false; + this.detaching = true; + // Once we reach this point, we're committed to closing + + // in case we didn't ever call show() + + + // A timer might be pending to call show() for the first time, make sure + // it doesn't bring us back up after we close + if (this.timer) { + clearTimeout(this.timer); + this.timer = 0; + } + + // Hide our HTML element immediately, even if our leaves might not be + // detachable yet. This makes things more responsive and improves the + // odds of not showing an empty popup that's just going to disappear + // momentarily. + this.hoverEl.hide(); + + // If a file load is in progress, we need to wait until it's finished before + // detaching leaves. Because we set .detaching, The in-progress openFile() + // will call us again when it finishes. + if (this.opening) return; + + // Leave this code here to observe the state of the leaves + const leaves = this.leaves(); + if (leaves.length) { + // Detach all leaves before we unload the popover and remove it from the DOM. + // Each leaf.detach() will trigger layout-changed and the updateLeaves() + // method will then call hide() again when the last one is gone. + // leaves[0].detach(); + // leaves[0].detach(); + this.targetEl.empty(); + } else { + this.parent = null; + this.abortController?.unload(); + this.abortController = undefined; + return this.nativeHide(); + } + } + + nativeHide() { + const { hoverEl, targetEl } = this; + this.state = PopoverState.Hidden; + hoverEl.detach(); + + if (targetEl) { + const parent = targetEl.matchParent(".fs-leaf-view"); + if (parent) popovers.get(parent)?.transition(); + } + + this.onHide(); + this.unload(); + } + + resolveLink(linkText: string, sourcePath: string): TFile | null { + const link = parseLinktext(linkText); + const tFile = link ? this.plugin.app.metadataCache.getFirstLinkpathDest(link.path, sourcePath) : null; + return tFile; + } + + async openLink(linkText: string, sourcePath: string, eState?: EphemeralState, createInLeaf?: WorkspaceLeaf) { + let file = this.resolveLink(linkText, sourcePath); + const link = parseLinktext(linkText); + if (!file && createInLeaf) { + const folder = this.plugin.app.fileManager.getNewFileParent(sourcePath); + file = await this.plugin.app.fileManager.createNewMarkdownFile(folder, link.path); + } + + if (!file) { + // this.displayCreateFileAction(linkText, sourcePath, eState); + return; + } + const { viewRegistry } = this.plugin.app; + const viewType = viewRegistry.typeByExtension[file.extension]; + if (!viewType || !viewRegistry.viewByType[viewType]) { + // this.displayOpenFileAction(file); + return; + } + + eState = Object.assign(this.buildEphemeralState(file, link), eState); + const parentMode = this.getDefaultMode(); + const state = this.buildState(parentMode, eState); + const leaf = await this.openFile(file, state, createInLeaf); + const leafViewType = leaf?.view?.getViewType(); + // console.log(leaf); + if (leafViewType === "image") { + // TODO: temporary workaround to prevent image popover from disappearing immediately when using live preview + if ( + this.parent?.hasOwnProperty("editorEl") && + (this.parent as unknown as MarkdownEditView).editorEl!.hasClass("is-live-preview") + ) { + this.waitTime = 3000; + } + const img = leaf!.view.contentEl.querySelector("img")!; + this.hoverEl.dataset.imgHeight = String(img.naturalHeight); + this.hoverEl.dataset.imgWidth = String(img.naturalWidth); + this.hoverEl.dataset.imgRatio = String(img.naturalWidth / img.naturalHeight); + } else if (leafViewType === "pdf") { + this.hoverEl.style.height = "800px"; + this.hoverEl.style.width = "600px"; + } + if (state.state?.mode === "source") { + this.whenShown(() => { + // Not sure why this is needed, but without it we get issue #186 + if (requireApiVersion("1.0"))(leaf?.view as any)?.editMode?.reinit?.(); + leaf?.view?.setEphemeralState(state.eState); + }); + } + } + + whenShown(callback: () => any) { + // invoke callback once the popover is visible + if (this.detaching) return; + const existingCallback = this.onShowCallback; + this.onShowCallback = () => { + if (this.detaching) return; + callback(); + if (typeof existingCallback === "function") existingCallback(); + }; + if (this.state === PopoverState.Shown) { + this.onShowCallback(); + this.onShowCallback = undefined; + } + } + + async openFile(file: TFile, openState?: OpenViewState, useLeaf?: WorkspaceLeaf) { + if (this.detaching) return; + const leaf = useLeaf ?? this.attachLeaf(); + this.opening = true; + + try { + await leaf.openFile(file, openState); + } catch (e) { + console.error(e); + } finally { + this.opening = false; + if (this.detaching) this.hide(); + } + this.plugin.app.workspace.setActiveLeaf(leaf); + + return leaf; + } + + buildState(parentMode: string, eState?: EphemeralState) { + return { + active: false, // Don't let Obsidian force focus if we have autofocus off + state: { mode: "source" }, // Don't set any state for the view, because this leaf is stayed on another view. + eState: eState, + }; + } + + buildEphemeralState( + file: TFile, + link?: { + path: string; + subpath: string; + }, + ) { + const cache = this.plugin.app.metadataCache.getFileCache(file); + const subpath = cache ? resolveSubpath(cache, link?.subpath || "") : undefined; + const eState: EphemeralState = { subpath: link?.subpath }; + if (subpath) { + eState.line = subpath.start.line; + eState.startLoc = subpath.start; + eState.endLoc = subpath.end || undefined; + } + return eState; + } +} diff --git a/src/types/types-obsidian.d.ts b/src/types/types-obsidian.d.ts new file mode 100644 index 0000000..bd942cd --- /dev/null +++ b/src/types/types-obsidian.d.ts @@ -0,0 +1,210 @@ +import "obsidian"; +import { + EditorPosition, + EphemeralState, + Loc, + MarkdownPreviewRenderer, MarkdownSubView, + Plugin, + PluginManifest, + SuggestModal, + TFile, TFolder, + View, WorkspaceItem, + WorkspaceLeaf +} from "obsidian"; +import { EmbeddedViewParent } from "../leafView"; + +interface InternalPlugins { + switcher: QuickSwitcherPlugin; + "page-preview": InternalPlugin; + graph: GraphPlugin; +} +interface OpenViewState { + eState?: EphemeralState; + state?: { mode: string }; + active?: boolean; +} + +declare class QuickSwitcherModal extends SuggestModal { + getSuggestions(query: string): TFile[] | Promise; + renderSuggestion(value: TFile, el: HTMLElement): unknown; + onChooseSuggestion(item: TFile, evt: MouseEvent | KeyboardEvent): unknown; +} +interface InternalPlugin { + disable(): void; + enable(): void; + enabled: boolean; + _loaded: boolean; + instance: { name: string; id: string }; +} +interface GraphPlugin extends InternalPlugin { + views: { localgraph: (leaf: WorkspaceLeaf) => GraphView }; +} + +interface GraphView extends View { + engine: typeof Object; + renderer: { worker: { terminate(): void } }; +} +interface QuickSwitcherPlugin extends InternalPlugin { + instance: { + name: string; + id: string; + QuickSwitcherModal: typeof QuickSwitcherModal; + }; +} + +declare global { + const i18next: { + t(id: string): string; + }; + interface Window { + activeWindow: Window; + activeDocument: Document; + } +} + +declare module "obsidian" { + interface App { + internalPlugins: { + plugins: InternalPlugins; + getPluginById(id: T): InternalPlugins[T]; + }; + plugins: { + manifests: Record; + plugins: Record & { + ["recent-files-obsidian"]: Plugin & { + shouldAddFile(file: TFile): boolean; + }; + }; + getPlugin(id: string): Plugin; + getPlugin(id: "calendar"): CalendarPlugin; + }; + dom: { appContainerEl: HTMLElement }; + viewRegistry: ViewRegistry; + openWithDefaultApp(path: string): void; + } + interface ViewRegistry { + typeByExtension: Record; // file extensions to view types + viewByType: Record View>; // file extensions to view types + } + interface CalendarPlugin { + view: View; + } + interface WorkspaceParent { + insertChild(index: number, child: WorkspaceItem, resize?: boolean): void; + replaceChild(index: number, child: WorkspaceItem, resize?: boolean): void; + removeChild(leaf: WorkspaceLeaf, resize?: boolean): void; + containerEl: HTMLElement; + children: any; + } + interface MarkdownEditView { + editorEl: HTMLElement; + } + class MarkdownPreviewRendererStatic extends MarkdownPreviewRenderer { + static registerDomEvents(el: HTMLElement, handlerInstance: unknown, cb: (el: HTMLElement) => unknown): void; + } + + interface WorkspaceLeaf { + openLinkText(linkText: string, path: string, state?: unknown): Promise; + updateHeader(): void; + containerEl: HTMLDivElement; + working: boolean; + parentSplit: WorkspaceParent; + activeTime: number; + } + interface Workspace { + recordHistory(leaf: WorkspaceLeaf, pushHistory: boolean): void; + iterateLeaves(callback: (item: WorkspaceLeaf) => boolean | void, item: WorkspaceItem | WorkspaceItem[]): boolean; + iterateLeaves(item: WorkspaceItem | WorkspaceItem[], callback: (item: WorkspaceLeaf) => boolean | void): boolean; + getDropLocation(event: MouseEvent): { + target: WorkspaceItem; + sidedock: boolean; + }; + recursiveGetTarget(event: MouseEvent, parent: WorkspaceParent): WorkspaceItem; + recordMostRecentOpenedFile(file: TFile): void; + onDragLeaf(event: MouseEvent, leaf: WorkspaceLeaf): void; + onLayoutChange(): void; // tell Obsidian leaves have been added/removed/etc. + activeLeafEvents(): void; + floatingSplit: any; + } + interface Editor { + getClickableTokenAt(pos: EditorPosition): { + text: string; + type: string; + start: EditorPosition; + end: EditorPosition; + }; + } + interface View { + iconEl: HTMLElement; + file: TFile; + setMode(mode: MarkdownSubView): Promise; + followLinkUnderCursor(newLeaf: boolean): void; + modes: Record; + getMode(): string; + headerEl: HTMLElement; + contentEl: HTMLElement; + } + + interface SearchView extends View { + onKeyArrowRightInFocus(e: KeyboardEvent): void; + onKeyArrowLeftInFocus(e: KeyboardEvent): void; + onKeyArrowUpInFocus(e: KeyboardEvent): void; + onKeyArrowDownInFocus(e: KeyboardEvent): void; + onKeyEnterInFocus(e: KeyboardEvent): void; + onKeyShowMoreBefore(e: KeyboardEvent): void; + onKeyShowMoreAfter(e: KeyboardEvent): void; + dom: any; + } + + interface EmptyView extends View { + actionListEl: HTMLElement; + emptyTitleEl: HTMLElement; + } + + interface FileManager { + createNewMarkdownFile(folder: TFolder, fileName: string): Promise; + } + enum PopoverState { + Showing, + Shown, + Hiding, + Hidden, + } + interface Menu { + items: MenuItem[]; + dom: HTMLElement; + hideCallback: () => unknown; + } + interface MenuItem { + iconEl: HTMLElement; + dom: HTMLElement; + } + interface EphemeralState { + focus?: boolean; + subpath?: string; + line?: number; + startLoc?: Loc; + endLoc?: Loc; + scroll?: number; + } + interface HoverParent { + type?: string; + } + interface HoverPopover { + parent: EmbeddedViewParent | null; + targetEl: HTMLElement; + hoverEl: HTMLElement; + position(pos?: MousePos): void; + hide(): void; + show(): void; + shouldShowSelf(): boolean; + timer: number; + waitTime: number; + shouldShow(): boolean; + transition(): void; + } + interface MousePos { + x: number; + y: number; + } +} diff --git a/styles.css b/styles.css index 5c5538a..79b58bd 100644 --- a/styles.css +++ b/styles.css @@ -22,6 +22,10 @@ If your plugin does not need CSS, delete this file. height: 100% !important; } +.fs-block .workspace-leaf-resize-handle { + display: none; +} + .modal-container.float-search-modal-container.mod-dim { z-index: 30; } diff --git a/versions.json b/versions.json index b5fbc7b..b23a413 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,4 @@ { - "0.0.1": "0.15.0" + "0.0.1": "0.15.0", + "1.0.0": "0.15.0" } \ No newline at end of file