mirror of
https://github.com/quorafind/Obsidian-Float-Search.git
synced 2026-07-22 07:30:25 +00:00
feat: support keyboard navigation
This commit is contained in:
parent
2d843bc32f
commit
4a13dbc005
11 changed files with 1031 additions and 11 deletions
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -5,7 +5,7 @@ on:
|
|||
types: [ created ]
|
||||
|
||||
env:
|
||||
PLUGIN_NAME: obsidian-canvas-presentation
|
||||
PLUGIN_NAME: obsidian-float-search
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -20,6 +20,3 @@ data.json
|
|||
|
||||
# Exclude macOS Finder (System Explorer) View States
|
||||
.DS_Store
|
||||
|
||||
src
|
||||
.github
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ esbuild.build({
|
|||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ['src/main.ts'],
|
||||
entryPoints: ['src/floatSearchIndex.ts'],
|
||||
bundle: true,
|
||||
external: [
|
||||
'obsidian',
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
253
src/floatSearchIndex.ts
Normal file
253
src/floatSearchIndex.ts
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
553
src/leafView.ts
Normal file
553
src/leafView.ts
Normal file
|
|
@ -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<Element, EmbeddedView>();
|
||||
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<T>(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;
|
||||
}
|
||||
}
|
||||
210
src/types/types-obsidian.d.ts
vendored
Normal file
210
src/types/types-obsidian.d.ts
vendored
Normal file
|
|
@ -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<TFile> {
|
||||
getSuggestions(query: string): TFile[] | Promise<TFile[]>;
|
||||
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<T extends keyof InternalPlugins>(id: T): InternalPlugins[T];
|
||||
};
|
||||
plugins: {
|
||||
manifests: Record<string, PluginManifest>;
|
||||
plugins: Record<string, Plugin> & {
|
||||
["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<string, string>; // file extensions to view types
|
||||
viewByType: Record<string, (leaf: WorkspaceLeaf) => 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<void>;
|
||||
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<void>;
|
||||
followLinkUnderCursor(newLeaf: boolean): void;
|
||||
modes: Record<string, MarkdownSubView>;
|
||||
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<TFile>;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
"0.0.1": "0.15.0"
|
||||
"0.0.1": "0.15.0",
|
||||
"1.0.0": "0.15.0"
|
||||
}
|
||||
Loading…
Reference in a new issue