diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 523d81d..3a3a67c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,8 +7,6 @@ on: env: PLUGIN_NAME: obsidian-float-search -permissions: write-all - jobs: build: runs-on: ubuntu-latest diff --git a/manifest.json b/manifest.json index 42ca387..1872520 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "float-search", "name": "Floating Search", - "version": "3.4.5", + "version": "3.4.10", "minAppVersion": "0.15.0", "description": "You can use search view in modal/leaf/popout window now.", "author": "Boninall", diff --git a/package.json b/package.json index b865b46..c77738d 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,27 @@ { - "name": "float-search", - "version": "3.4.5", - "description": "You can use search view in modal/leaf/popout window now.", - "main": "main.js", - "scripts": { - "dev": "node esbuild.config.mjs", - "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", - "version": "node version-bump.mjs && git add manifest.json versions.json" - }, - "keywords": [], - "author": "Boninall", - "license": "GPL-3.0", - "devDependencies": { - "@types/node": "^16.18.50", - "@typescript-eslint/eslint-plugin": "5.29.0", - "@typescript-eslint/parser": "5.29.0", - "builtin-modules": "3.3.0", - "esbuild": "0.14.47", - "obsidian": "latest", - "tslib": "2.4.0", - "typescript": "4.7.4" - }, - "dependencies": { - "monkey-around": "^2.3.0" - } + "name": "float-search", + "version": "3.4.10", + "description": "You can use search view in modal/leaf/popout window now.", + "main": "main.js", + "scripts": { + "dev": "node esbuild.config.mjs", + "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", + "version": "node version-bump.mjs && git add manifest.json versions.json" + }, + "keywords": [], + "author": "Boninall", + "license": "GPL-3.0", + "devDependencies": { + "@types/node": "^16.18.50", + "@typescript-eslint/eslint-plugin": "5.29.0", + "@typescript-eslint/parser": "5.29.0", + "builtin-modules": "3.3.0", + "esbuild": "0.14.47", + "obsidian": "latest", + "tslib": "2.4.0", + "typescript": "4.7.4" + }, + "dependencies": { + "monkey-around": "^2.3.0" + } } diff --git a/src/floatSearchIndex.ts b/src/floatSearchIndex.ts index ad7208f..97092a8 100644 --- a/src/floatSearchIndex.ts +++ b/src/floatSearchIndex.ts @@ -1,18 +1,26 @@ import { addIcon, App, - Editor, ExtraButtonComponent, - Menu, MenuItem, - Modal, OpenViewState, PaneType, - Plugin, SearchView, setIcon, Setting, + Editor, + ExtraButtonComponent, Keymap, + Menu, + MenuItem, + Modal, + OpenViewState, + PaneType, + Plugin, + Scope, + SearchView, setIcon, Setting, TAbstractFile, - TFile, ViewStateResult, + TFile, Workspace, - WorkspaceContainer, WorkspaceItem, + WorkspaceContainer, + WorkspaceItem, WorkspaceLeaf } from 'obsidian'; import { EmbeddedView, isEmebeddedLeaf, spawnLeafView } from "./leafView"; import { around } from "monkey-around"; +import { debounce } from "obsidian"; type sortOrder = "alphabetical" @@ -82,6 +90,7 @@ const initSearchViewWithLeaf = async (app: App, type: PaneType | 'sidebar', stat active: true, state: state }); + setTimeout(() => { const inputEl = leaf.containerEl.getElementsByTagName("input")[0]; inputEl.focus(); @@ -93,38 +102,30 @@ export default class FloatSearchPlugin extends Plugin { private state: any; private modal: FloatSearchModal; - private applyStateDebounceTimer = 0; - private applySettingsDebounceTimer = 0; - patchedDomChildren = false; + public applySettingsUpdate = debounce(async () => { + await this.saveSettings(); + }, 1000); - public applySettingsUpdate() { - clearTimeout(this.applySettingsDebounceTimer); - this.applySettingsDebounceTimer = window.setTimeout(async () => { - await this.saveSettings(); - }, 1000); - } - - private applyStateUpdate() { - this.applyStateDebounceTimer = window.setTimeout(() => { - clearTimeout(this.applyStateDebounceTimer); - this.state = { - ...this.state, - query: "", - }; - }, 30000); - } + private applyStateUpdate = debounce(() => { + this.state = { + ...this.state, + query: "", + }; + }, 30000); async onload() { await this.loadSettings(); this.initState(); this.registerIcons(); - this.patchWorkspace(); - this.patchWorkspaceLeaf(); - this.patchSearchView(); - this.patchVchildren(); + this.app.workspace.onLayoutReady(() => { + this.patchWorkspace(); + this.patchWorkspaceLeaf(); + this.patchSearchView(); + this.patchVchildren(); + }); this.registerObsidianURIHandler(); this.registerObsidianCommands(); @@ -175,17 +176,21 @@ export default class FloatSearchPlugin extends Plugin { patchWorkspace() { let layoutChanging = false; + const self = this; const uninstaller = around(Workspace.prototype, { getLeaf: (next) => function (...args) { - const activeLeaf = this.activeLeaf; + const activeLeaf = (this as Workspace).activeLeaf; if (activeLeaf) { + // @ts-ignore const fsCtnEl = (activeLeaf.parent.containerEl as HTMLElement).parentElement; if (fsCtnEl?.hasClass("fs-content")) { if (activeLeaf.view.getViewType() === "markdown") { return activeLeaf; } - const newLeaf = app.workspace.getUnpinnedLeaf(); + + const newLeaf = self.app.workspace.getMostRecentLeaf(); + if (newLeaf) { this.setActiveLeaf(newLeaf); } @@ -219,15 +224,26 @@ export default class FloatSearchPlugin extends Plugin { 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)) { + + if (parent === self.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; + if (old.call(this, cb, popover.rootSplit)) return false; + } } return false; }; }, + setActiveLeaf(old) { + return function (leaf: any, params?: any) { + if (isEmebeddedLeaf(leaf)) { + old.call(this, leaf, params); + leaf.activeTime = 1700000000000; + } + return old.call(this, leaf, params); + }; + }, onDragLeaf(old) { return function (event: MouseEvent, leaf: WorkspaceLeaf) { return old.call(this, event, leaf); @@ -378,17 +394,22 @@ export default class FloatSearchPlugin extends Plugin { const patchSearch = () => { const searchView = this.app.workspace.getLeavesOfType("search")[0]?.view as any; + const self = this; if (!searchView) return false; const searchViewConstructor = searchView.constructor; - const self = this; this.register( around(searchViewConstructor.prototype, { onOpen(old) { return function () { old.call(this); + + (this.scope as Scope).register(['Mod'], 'w', () => { + this.leaf?.detach(); + }); + const viewSwitchEl = createDiv({cls: "float-search-view-switch"}); const targetEl = this.filterSectionToggleEl; const viewSwitchButton = new ExtraButtonComponent(viewSwitchEl); @@ -400,7 +421,6 @@ export default class FloatSearchPlugin extends Plugin { layoutMenu.showAtPosition({x: viewSwitchButtonPos.x, y: viewSwitchButtonPos.y + 30}); }); targetEl.parentElement.insertBefore(viewSwitchEl, targetEl); - if (!this.hidePathToggle) { this.hidePathToggle = new Setting(this.searchParamsContainerEl).setName('Show file path').addToggle((toggle) => { toggle.toggleEl.toggleClass('mod-small', true); @@ -422,11 +442,21 @@ export default class FloatSearchPlugin extends Plugin { query: this.searchComponent.getValue(), }); }; + }, + setState(old) { + return async function (state: any, result: any) { + old.call(this, state, result); + if (self.app.workspace.layoutReady) { + updateCurrentState({ + ...state, + query: this.searchComponent.getValue(), + }); + } + }; } }) ); searchView.leaf?.rebuildView(); - console.log("Metadata-Style: all property view get patched"); return true; }; this.app.workspace.onLayoutReady(() => { @@ -490,7 +520,7 @@ export default class FloatSearchPlugin extends Plugin { ...this.state, query: path.query, current: false - }, true, true)); + }, true, false)); } private createCommand(options: { @@ -537,8 +567,8 @@ export default class FloatSearchPlugin extends Plugin { this.addCommand({ id: 'search-obsidian-globally-state', - name: 'Search obsidian globally (with last state)', - callback: () => this.initModal({...this.state, query: "", current: false}, true, false) + name: 'Search Obsidian Globally (With Last State)', + callback: () => this.initModal({...this.state, query: this.state.query, current: false}, true, false) }); @@ -562,7 +592,35 @@ export default class FloatSearchPlugin extends Plugin { this.addCommand({ id: `open-search-view-${type}`, name: `Open search view (${type})`, - callback: async () => initSearchViewWithLeaf(this.app, type) + callback: async () => { + const existingLeaf = this.app.workspace.getLeavesOfType("search"); + switch (type) { + case "window": + // @ts-ignore + const isExistingWindowLeaf = existingLeaf.find((leaf) => leaf.parentSplit.parent.type === "window"); + if (isExistingWindowLeaf) { + this.app.workspace.revealLeaf(isExistingWindowLeaf); + return; + } + await initSearchViewWithLeaf(this.app, type); + break; + case "tab": + case "split": + // @ts-ignore + const isExistingLeaf = existingLeaf.find((leaf) => !leaf.parentSplit.parent.side); + if (isExistingLeaf) { + this.app.workspace.revealLeaf(isExistingLeaf); + isExistingLeaf.setViewState({ + type: "search", + active: true, + state: this.state + }); + return; + } + await initSearchViewWithLeaf(this.app, type); + break; + } + } }); } } @@ -649,6 +707,8 @@ class FloatSearchModal extends Modal { private fileEl: HTMLElement; private viewType: string; + private focusdItem: any; + constructor(cb: (state: any) => void, plugin: FloatSearchPlugin, state: any, viewType: string = "search") { super(plugin.app); this.plugin = plugin; @@ -711,6 +771,11 @@ class FloatSearchModal extends Modal { altEnterIconEl.setText("Alt+↵"); altEnterTextEl.setText("Open File and Close"); + + const ctrlEnterIconEl = altEnterInstructionsEl.createSpan({cls: "float-search-modal-instructions-key"}); + const ctrlEnterTextEl = altEnterInstructionsEl.createSpan({cls: "float-search-modal-instructions-text"}); + altEnterIconEl.setText("Ctrl+↵"); + altEnterTextEl.setText("Create File When Not Exist"); const tabIconEl = tabInstructionsEl.createSpan({cls: "float-search-modal-instructions-key"}); const tabTextEl = tabInstructionsEl.createSpan({cls: "float-search-modal-instructions-text"}); tabIconEl.setText("Tab/Shift+Tab"); @@ -767,10 +832,16 @@ class FloatSearchModal extends Modal { if (currentView.dom.focusedItem.collapsible) { currentView.dom.focusedItem.setCollapse(false); } + + this.focusdItem = currentView.dom.focusedItem; + } break; } else { currentView.onKeyArrowDownInFocus(e); + + this.focusdItem = currentView.dom.focusedItem; + break; } case "ArrowUp": @@ -780,10 +851,19 @@ class FloatSearchModal extends Modal { if (currentView.dom.focusedItem.collapseEl) { currentView.dom.focusedItem.setCollapse(true); } + + this.focusdItem = currentView.dom.focusedItem; + } break; } else { currentView.onKeyArrowUpInFocus(e); + + this.focusdItem = currentView.dom.focusedItem; + if (!currentView.dom.focusedItem.content) { + this.focusdItem = undefined; + } + break; } case "ArrowLeft": @@ -793,6 +873,14 @@ class FloatSearchModal extends Modal { currentView.onKeyArrowRightInFocus(e); break; case "Enter": + if (Keymap.isModifier(e, 'Mod') && Keymap.isModifier(e, 'Shift') && !this.focusdItem) { + e.preventDefault(); + const fileName = inputEl.value.trim(); + const real = fileName.replace(/[/\\?%*:|"<>]/g, '-'); + this.plugin.app.workspace.openLinkText(real, "", true); + this.close(); + break; + } currentView.onKeyEnterInFocus(e); if (e.altKey && currentView.dom.focusedItem) { this.close(); @@ -874,6 +962,9 @@ class FloatSearchModal extends Modal { if (targetElement.classList.contains('tree-item-icon')) { break; } + if (targetElement.classList.contains('search-result-hover-button')) { + break; + } if (targetElement.classList.contains('tree-item')) { this.close(); break; diff --git a/src/leafView.ts b/src/leafView.ts index 3c22146..ea5301a 100644 --- a/src/leafView.ts +++ b/src/leafView.ts @@ -3,551 +3,552 @@ // 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, + Component, EphemeralState, - HoverPopover, - MarkdownEditView, - parseLinktext, - PopoverState, - requireApiVersion, - resolveSubpath, - TFile, - View, - Workspace, - WorkspaceLeaf, - WorkspaceSplit, - WorkspaceTabs, + 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; + hoverPopover: EmbeddedView | null; + containerEl?: HTMLElement; + view?: View; + dom?: HTMLElement; } + const popovers = new WeakMap(); -type ConstructableWorkspaceSplit = new (ws: Workspace, dir: "horizontal"|"vertical") => WorkspaceSplit; +type ConstructableWorkspaceSplit = new (ws: Workspace, dir: "horizontal" | "vertical") => WorkspaceSplit; export function isEmebeddedLeaf(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"); + // 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(""); + 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); + 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; + // 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; + if (!initiatingEl) initiatingEl = parent?.containerEl; - const hoverPopover = new EmbeddedView(parent, initiatingEl!, plugin, undefined, onShowCallback); - return [hoverPopover.attachLeaf(), hoverPopover]; + 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; - } + 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/styles.css b/styles.css index 0620733..c0b7905 100644 --- a/styles.css +++ b/styles.css @@ -7,7 +7,11 @@ If your plugin does not need CSS, delete this file. */ .float-search-modal { +<<<<<<< HEAD width: 600px; +======= + width: 700px; +>>>>>>> 285693757c1023d96948f67c0ca5efe185cae1e8 padding-bottom: 0; } @@ -110,6 +114,7 @@ If your plugin does not need CSS, delete this file. stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; +<<<<<<< HEAD } body:not(.show-file-path) .search-result-file-title .search-result-file-path { @@ -142,4 +147,6 @@ body:not(.show-file-path) .search-result-file-title .search-result-file-path { .show-file-path .search-result-file-title .search-result-file-path:hover { background-color: var(--background-secondary); +======= +>>>>>>> 285693757c1023d96948f67c0ca5efe185cae1e8 } diff --git a/versions.json b/versions.json index 259362b..d3075e2 100644 --- a/versions.json +++ b/versions.json @@ -22,5 +22,10 @@ "3.4.2": "0.15.0", "3.4.3": "0.15.0", "3.4.4": "0.15.0", - "3.4.5": "0.15.0" + "3.4.5": "0.15.0", + "3.4.6": "0.15.0", + "3.4.7": "0.15.0", + "3.4.8": "0.15.0", + "3.4.9": "0.15.0", + "3.4.10": "0.15.0" } \ No newline at end of file