mirror of
https://github.com/quorafind/Obsidian-Float-Search.git
synced 2026-07-22 07:30:25 +00:00
917 lines
30 KiB
TypeScript
917 lines
30 KiB
TypeScript
import {
|
|
addIcon,
|
|
App,
|
|
Editor, ExtraButtonComponent,
|
|
Menu, MenuItem,
|
|
Modal, OpenViewState, PaneType,
|
|
Plugin, Scope, SearchView, setIcon,
|
|
TAbstractFile,
|
|
TFile, ViewStateResult,
|
|
Workspace,
|
|
WorkspaceContainer, WorkspaceItem,
|
|
WorkspaceLeaf
|
|
} from 'obsidian';
|
|
import { EmbeddedView, isEmebeddedLeaf, spawnLeafView } from "./leafView";
|
|
import { around } from "monkey-around";
|
|
|
|
type sortOrder =
|
|
"alphabetical"
|
|
| "alphabeticalReverse"
|
|
| "byModifiedTime"
|
|
| "byModifiedTimeReverse"
|
|
| "byCreatedTime"
|
|
| "byCreatedTimeReverse";
|
|
|
|
type searchType = "modal" | "sidebar" | PaneType;
|
|
|
|
interface viewType {
|
|
type: searchType;
|
|
icon: string;
|
|
}
|
|
|
|
interface searchState {
|
|
collapseAll?: boolean;
|
|
explainSearch?: boolean;
|
|
extraContext?: boolean;
|
|
matchingCase?: boolean;
|
|
query: string;
|
|
sortOrder?: sortOrder;
|
|
current?: boolean;
|
|
}
|
|
|
|
interface FloatSearchSettings {
|
|
searchViewState: searchState;
|
|
}
|
|
|
|
|
|
const DEFAULT_SETTINGS: FloatSearchSettings = {
|
|
searchViewState: {
|
|
collapseAll: false,
|
|
explainSearch: false,
|
|
extraContext: false,
|
|
matchingCase: false,
|
|
query: "",
|
|
sortOrder: "alphabetical",
|
|
}
|
|
};
|
|
|
|
const allViews: viewType[] = [{
|
|
type: "modal",
|
|
icon: "square-equal"
|
|
}, {
|
|
type: "sidebar",
|
|
icon: "panel-left-inactive"
|
|
}, {
|
|
type: "split",
|
|
icon: "split-square-horizontal"
|
|
}, {
|
|
type: "tab",
|
|
icon: "panel-top"
|
|
}, {
|
|
type: "window",
|
|
icon: "app-window"
|
|
}];
|
|
|
|
const initSearchViewWithLeaf = async (app: App, type: PaneType | 'sidebar', state?: searchState) => {
|
|
const leaf = type === 'sidebar' ? app.workspace.getLeftLeaf(false) : app.workspace.getLeaf(type);
|
|
leaf.setPinned(type !== 'sidebar');
|
|
await leaf.setViewState({
|
|
type: "search",
|
|
active: true,
|
|
state: state
|
|
});
|
|
|
|
setTimeout(() => {
|
|
const inputEl = leaf.containerEl.getElementsByTagName("input")[0];
|
|
inputEl.focus();
|
|
}, 0);
|
|
};
|
|
|
|
export default class FloatSearchPlugin extends Plugin {
|
|
settings: FloatSearchSettings;
|
|
private state: any;
|
|
private modal: FloatSearchModal;
|
|
|
|
private applyStateDebounceTimer = 0;
|
|
private applySettingsDebounceTimer = 0;
|
|
|
|
|
|
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);
|
|
}
|
|
|
|
async onload() {
|
|
await this.loadSettings();
|
|
this.initState();
|
|
this.registerIcons();
|
|
|
|
this.patchWorkspace();
|
|
this.patchWorkspaceLeaf();
|
|
this.patchSearchView();
|
|
|
|
this.registerObsidianURIHandler();
|
|
this.registerObsidianCommands();
|
|
this.registerEditorMenuHandler();
|
|
this.registerContextMenuHandler();
|
|
|
|
this.addRibbonIcon('search', 'Search Obsidian In Modal', () => this.initModal(this.state, true, true));
|
|
}
|
|
|
|
onunload() {
|
|
this.state = undefined;
|
|
this.modal?.close();
|
|
}
|
|
|
|
registerIcons() {
|
|
addIcon('panel-left-inactive', `<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:currentColor;stroke-opacity:1;stroke-miterlimit:4;" d="M 4.999687 3 L 19.000312 3 C 20.104688 3 21 3.895312 21 4.999687 L 21 19.000312 C 21 20.104688 20.104688 21 19.000312 21 L 4.999687 21 C 3.895312 21 3 20.104688 3 19.000312 L 3 4.999687 C 3 3.895312 3.895312 3 4.999687 3 Z M 4.999687 3 " transform="matrix(4.166667,0,0,4.166667,0,0)"/><path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:currentColor;stroke-opacity:1;stroke-miterlimit:4;" d="M 9 13.999688 L 9 15 " transform="matrix(4.166667,0,0,4.166667,0,0)"/><path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:currentColor;stroke-opacity:1;stroke-miterlimit:4;" d="M 9 19.000312 L 9 21 " transform="matrix(4.166667,0,0,4.166667,0,0)"/><path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:currentColor;stroke-opacity:1;stroke-miterlimit:4;" d="M 9 3 L 9 4.999687 " transform="matrix(4.166667,0,0,4.166667,0,0)"/><path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:currentColor;stroke-opacity:1;stroke-miterlimit:4;" d="M 9 9 L 9 10.000312 " transform="matrix(4.166667,0,0,4.166667,0,0)"/>`);
|
|
addIcon('app-window', `<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:currentColor;stroke-opacity:1;stroke-miterlimit:4;" d="M 4.000312 4.000312 L 19.999688 4.000312 C 21.105 4.000312 22.000312 4.895625 22.000312 6 L 22.000312 18 C 22.000312 19.104375 21.105 19.999688 19.999688 19.999688 L 4.000312 19.999688 C 2.895 19.999688 1.999687 19.104375 1.999687 18 L 1.999687 6 C 1.999687 4.895625 2.895 4.000312 4.000312 4.000312 Z M 4.000312 4.000312 " transform="matrix(4.166667,0,0,4.166667,0,0)"/><path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:currentColor;stroke-opacity:1;stroke-miterlimit:4;" d="M 10.000312 4.000312 L 10.000312 7.999687 " transform="matrix(4.166667,0,0,4.166667,0,0)"/><path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:currentColor;stroke-opacity:1;stroke-miterlimit:4;" d="M 1.999687 7.999687 L 22.000312 7.999687 " transform="matrix(4.166667,0,0,4.166667,0,0)"/><path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:currentColor;stroke-opacity:1;stroke-miterlimit:4;" d="M 6 4.000312 L 6 7.999687 " transform="matrix(4.166667,0,0,4.166667,0,0)"/>`);
|
|
addIcon('panel-top', `<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:currentColor;stroke-opacity:1;stroke-miterlimit:4;" d="M 4.999687 3 L 19.000312 3 C 20.104688 3 21 3.895312 21 4.999687 L 21 19.000312 C 21 20.104688 20.104688 21 19.000312 21 L 4.999687 21 C 3.895312 21 3 20.104688 3 19.000312 L 3 4.999687 C 3 3.895312 3.895312 3 4.999687 3 Z M 4.999687 3 " transform="matrix(4.166667,0,0,4.166667,0,0)"/><path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:currentColor;stroke-opacity:1;stroke-miterlimit:4;" d="M 3 9 L 21 9 " transform="matrix(4.166667,0,0,4.166667,0,0)"/>`);
|
|
addIcon('square-equal', `<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:currentColor;stroke-opacity:1;stroke-miterlimit:4;" d="M 4.999687 3 L 19.000312 3 C 20.104688 3 21 3.895312 21 4.999687 L 21 19.000312 C 21 20.104688 20.104688 21 19.000312 21 L 4.999687 21 C 3.895312 21 3 20.104688 3 19.000312 L 3 4.999687 C 3 3.895312 3.895312 3 4.999687 3 Z M 4.999687 3 " transform="matrix(4.166667,0,0,4.166667,0,0)"/><path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:currentColor;stroke-opacity:1;stroke-miterlimit:4;" d="M 7.000312 10.000312 L 16.999688 10.000312 " transform="matrix(4.166667,0,0,4.166667,0,0)"/><path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:currentColor;stroke-opacity:1;stroke-miterlimit:4;" d="M 7.000312 13.999688 L 16.999688 13.999688 " transform="matrix(4.166667,0,0,4.166667,0,0)"/>`);
|
|
}
|
|
|
|
initState() {
|
|
this.state = this.settings.searchViewState as searchState;
|
|
}
|
|
|
|
initModal(state: searchState, stateSave: boolean = false, clearQuery: boolean = false) {
|
|
this.modal = new FloatSearchModal((state) => {
|
|
this.state = state;
|
|
if (stateSave) this.applyStateUpdate();
|
|
this.settings.searchViewState = state as searchState;
|
|
this.applySettingsUpdate();
|
|
}, this, {...state, query: clearQuery ? "" : state.query});
|
|
this.modal.open();
|
|
}
|
|
|
|
patchWorkspace() {
|
|
let layoutChanging = false;
|
|
const uninstaller = around(Workspace.prototype, {
|
|
getLeaf: (next) =>
|
|
function (...args) {
|
|
const activeLeaf = this.activeLeaf;
|
|
if (activeLeaf) {
|
|
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();
|
|
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);
|
|
};
|
|
},
|
|
pushUndoHistory(old: any) {
|
|
return function (leaf: WorkspaceLeaf, id: string, ...args: any[]) {
|
|
const viewState = leaf.getViewState();
|
|
if (viewState.type === "search") {
|
|
return;
|
|
}
|
|
return old.call(this, leaf, id, ...args);
|
|
};
|
|
}
|
|
});
|
|
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 (isEmebeddedLeaf(this) && !pinned) this.setPinned(true);
|
|
};
|
|
},
|
|
openFile(old) {
|
|
return function (file: TFile, openState?: OpenViewState) {
|
|
if (isEmebeddedLeaf(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,
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
const view = old.call(this, file, openState);
|
|
setTimeout(() => {
|
|
const fsCtnEl = (this.parent.containerEl as HTMLElement).parentElement;
|
|
if (!(fsCtnEl?.classList.contains("fs-content"))) return;
|
|
if (file.extension != "canvas") return;
|
|
|
|
const canvas = this.view.canvas;
|
|
setTimeout(() => {
|
|
if (canvas && openState?.eState?.match) {
|
|
let node = canvas.data.nodes?.find((e: any) => e.text === openState.eState.match.content);
|
|
node = canvas.nodes.get(node.id);
|
|
|
|
canvas.selectOnly(node);
|
|
canvas.zoomToSelection();
|
|
}
|
|
}, 20);
|
|
}, 1);
|
|
|
|
return view;
|
|
};
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
patchSearchView() {
|
|
const updateCurrentState = (state: searchState) => {
|
|
this.state = state;
|
|
this.settings.searchViewState = state as searchState;
|
|
this.applySettingsUpdate();
|
|
};
|
|
|
|
const checkCurrentViewType = (leaf: WorkspaceLeaf) => {
|
|
const isModal = document.querySelector('.float-search-modal') !== null;
|
|
const currentLeafRoot = leaf.getRoot();
|
|
if (currentLeafRoot?.side && (currentLeafRoot?.side === "left" || currentLeafRoot?.side === 'right')) return "sidebar";
|
|
if (leaf.getContainer()?.type === "window") return "window";
|
|
return isModal ? "modal" : "split";
|
|
};
|
|
|
|
const initViewMenu = (menu: Menu, current: searchType, originLeaf?: WorkspaceLeaf) => {
|
|
menu.dom.toggleClass("float-search-view-menu", true);
|
|
let availableViews = allViews.filter((view) => {
|
|
if (current === "split") {
|
|
return view.type !== "tab";
|
|
} else {
|
|
return view.type !== current;
|
|
}
|
|
});
|
|
for (const view of availableViews) {
|
|
menu.addItem((item: MenuItem) => {
|
|
item.setTitle(`${view.type} view`).setIcon(`${view.icon}`).onClick(async () => {
|
|
if (view.type === "modal") {
|
|
originLeaf?.detach();
|
|
setTimeout(() => {
|
|
this.initModal(this.state, true, false);
|
|
}, 10);
|
|
} else if (view.type === "sidebar") {
|
|
await initSearchViewWithLeaf(this.app, view.type, this.state);
|
|
} else {
|
|
if (current === "window") {
|
|
originLeaf?.detach();
|
|
setTimeout(async () => {
|
|
await initSearchViewWithLeaf(this.app, <"tab" | "split">view.type, this.state);
|
|
}, 10);
|
|
} else {
|
|
await initSearchViewWithLeaf(this.app, view.type, this.state);
|
|
}
|
|
|
|
}
|
|
if (current === "modal") {
|
|
this.modal.close();
|
|
} else {
|
|
originLeaf?.detach();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
return menu;
|
|
};
|
|
|
|
const patchSearch = () => {
|
|
const searchView = this.app.workspace.getLeavesOfType("search")[0]?.view as any;
|
|
|
|
if (!searchView) return false;
|
|
|
|
const searchViewConstructor = searchView.constructor;
|
|
|
|
this.register(
|
|
around(searchViewConstructor.prototype, {
|
|
onOpen(old) {
|
|
return function () {
|
|
old.call(this);
|
|
const viewSwitchEl = createDiv({cls: "float-search-view-switch"});
|
|
const targetEl = this.filterSectionToggleEl;
|
|
const viewSwitchButton = new ExtraButtonComponent(viewSwitchEl);
|
|
viewSwitchButton.setIcon('layout-template').setTooltip("Switch to File View");
|
|
viewSwitchButton.onClick(() => {
|
|
const currentType = checkCurrentViewType(this.leaf);
|
|
const layoutMenu = initViewMenu(new Menu(), currentType, this.leaf);
|
|
const viewSwitchButtonPos = viewSwitchEl.getBoundingClientRect();
|
|
layoutMenu.showAtPosition({x: viewSwitchButtonPos.x, y: viewSwitchButtonPos.y + 30});
|
|
});
|
|
targetEl.parentElement.insertBefore(viewSwitchEl, targetEl);
|
|
};
|
|
},
|
|
startSearch(old) {
|
|
return function () {
|
|
old.call(this);
|
|
const viewState = this.getState();
|
|
updateCurrentState({
|
|
...viewState,
|
|
query: this.searchComponent.getValue(),
|
|
});
|
|
};
|
|
}
|
|
})
|
|
);
|
|
searchView.leaf?.rebuildView();
|
|
console.log("Metadata-Style: all property view get patched");
|
|
return true;
|
|
};
|
|
this.app.workspace.onLayoutReady(() => {
|
|
if (!patchSearch()) {
|
|
const evt = this.app.workspace.on("layout-change", () => {
|
|
patchSearch() && this.app.workspace.offref(evt);
|
|
});
|
|
this.registerEvent(evt);
|
|
}
|
|
});
|
|
}
|
|
|
|
registerObsidianURIHandler() {
|
|
this.registerObsidianProtocolHandler("fs", (path) => this.initModal({
|
|
...this.state,
|
|
query: path.query,
|
|
current: false
|
|
}, true, true));
|
|
}
|
|
|
|
private createCommand(options: {
|
|
id: string;
|
|
name: string;
|
|
queryBuilder: (file: any) => string;
|
|
}): void {
|
|
this.addCommand({
|
|
id: options.id,
|
|
name: options.name,
|
|
checkCallback: (checking: boolean) => {
|
|
const activeLeaf = this.app.workspace.activeLeaf;
|
|
if (!activeLeaf) return;
|
|
|
|
const viewType = activeLeaf.view.getViewType();
|
|
if (viewType === "markdown" || viewType === "canvas") {
|
|
if (!checking) {
|
|
const currentFile = activeLeaf.view.file;
|
|
const query = options.queryBuilder(currentFile);
|
|
this.initModal({...this.state, query, current: true}, true, false);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
registerObsidianCommands() {
|
|
this.addCommand({
|
|
id: 'search-obsidian-globally',
|
|
name: 'Search Obsidian Globally',
|
|
callback: () => this.initModal({...this.state, query: "", current: false}, false, true)
|
|
});
|
|
|
|
|
|
this.addCommand({
|
|
id: 'search-obsidian-globally-state',
|
|
name: 'Search Obsidian Globally (With Last State)',
|
|
callback: () => this.initModal({...this.state, query: this.state.query, current: false}, true, false)
|
|
});
|
|
|
|
|
|
this.createCommand({
|
|
id: 'search-in-backlink',
|
|
name: 'Search In Backlink Of Current File',
|
|
queryBuilder: (file) => {
|
|
return " /\\[\\[" + (file.extension === "canvas" ? file.name : file.basename) + "(\\|[^\\]]*)?\\]\\]/";
|
|
}
|
|
});
|
|
|
|
this.createCommand({
|
|
id: 'search-in-current-file',
|
|
name: 'Search In Current File',
|
|
queryBuilder: (file) => {
|
|
return " path:" + `"${file.path}"`;
|
|
}
|
|
});
|
|
|
|
for (const type of ['split', 'tab', 'window'] as PaneType[]) {
|
|
this.addCommand({
|
|
id: `open-search-view-${type}`,
|
|
name: `Open Search View (${type})`,
|
|
callback: async () => {
|
|
const existingLeaf = this.app.workspace.getLeavesOfType("search");
|
|
switch (type) {
|
|
case "window":
|
|
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":
|
|
const isExistingLeaf = existingLeaf.find((leaf) => !leaf.parentSplit.parent.side);
|
|
if (isExistingLeaf) {
|
|
this.app.workspace.revealLeaf(isExistingLeaf);
|
|
return;
|
|
}
|
|
await initSearchViewWithLeaf(this.app, type);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
registerEditorMenuHandler() {
|
|
this.registerEvent(
|
|
this.app.workspace.on('editor-menu', (menu: Menu, editor: Editor) => {
|
|
if (!editor) {
|
|
return;
|
|
}
|
|
if (editor.getSelection().length === 0) {
|
|
return;
|
|
}
|
|
const selection = editor.getSelection().trim();
|
|
let searchWord = selection;
|
|
|
|
if (selection.length > 8) {
|
|
searchWord = selection.substring(0, 3) + "..." + selection.substring(selection.length - 3, selection.length);
|
|
} else {
|
|
searchWord = selection;
|
|
}
|
|
|
|
menu.addItem((item) => {
|
|
// Add sub menu
|
|
item.setTitle('Search "' + searchWord + '"' + " in Float Search").setIcon("search")
|
|
.onClick(() => this.initModal({...this.state, query: selection, current: false}, true, false));
|
|
});
|
|
}));
|
|
}
|
|
|
|
registerContextMenuHandler() {
|
|
this.registerEvent(
|
|
this.app.workspace.on("file-menu", (menu: Menu, file: TAbstractFile, source: string, leaf?: WorkspaceLeaf) => {
|
|
const popover = leaf ? EmbeddedView.forLeaf(leaf) : undefined;
|
|
if (file instanceof TFile && !popover && !leaf) {
|
|
menu.addItem(item => {
|
|
item
|
|
.setIcon("popup-open")
|
|
.setTitle("Open in Float Preview")
|
|
.onClick(async () => {
|
|
if (this.modal) {
|
|
await this.modal.initFileView(file, undefined);
|
|
|
|
return;
|
|
}
|
|
|
|
this.initModal({...this.state, current: false}, true, true);
|
|
setTimeout(async () => {
|
|
await this.modal.initFileView(file, undefined);
|
|
}, 20);
|
|
})
|
|
.setSection?.("open");
|
|
});
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
async loadSettings() {
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
}
|
|
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
}
|
|
}
|
|
|
|
|
|
class FloatSearchModal extends Modal {
|
|
private readonly plugin: FloatSearchPlugin;
|
|
private searchEmbeddedView: EmbeddedView;
|
|
private fileEmbeddedView: EmbeddedView;
|
|
|
|
searchLeaf: WorkspaceLeaf;
|
|
fileLeaf: WorkspaceLeaf | undefined;
|
|
|
|
private cb: (state: any) => void;
|
|
private state: any;
|
|
|
|
private fileState: any;
|
|
|
|
private searchCtnEl: HTMLElement;
|
|
private instructionsEl: HTMLElement;
|
|
private fileEl: HTMLElement;
|
|
private viewType: string;
|
|
|
|
constructor(cb: (state: any) => void, plugin: FloatSearchPlugin, state: any, viewType: string = "search") {
|
|
super(plugin.app);
|
|
this.plugin = plugin;
|
|
this.cb = cb;
|
|
this.state = state;
|
|
this.viewType = viewType;
|
|
}
|
|
|
|
async onOpen() {
|
|
const {contentEl, containerEl, modalEl} = this;
|
|
|
|
this.searchCtnEl = contentEl.createDiv({cls: "float-search-modal-search-ctn"});
|
|
this.instructionsEl = modalEl.createDiv({cls: "float-search-modal-instructions"});
|
|
|
|
this.initInstructions(this.instructionsEl);
|
|
this.initCss(contentEl, modalEl, containerEl);
|
|
await this.initSearchView(this.searchCtnEl);
|
|
this.initInput();
|
|
this.initContent();
|
|
}
|
|
|
|
onClose() {
|
|
const {contentEl} = this;
|
|
|
|
this.cb(this.searchLeaf.view.getState());
|
|
|
|
this.searchLeaf.detach();
|
|
this.fileLeaf?.detach();
|
|
this.searchEmbeddedView.unload();
|
|
this.fileEmbeddedView?.unload();
|
|
contentEl.empty();
|
|
}
|
|
|
|
initInstructions(instructionsEl: HTMLElement) {
|
|
const navigateInstructionsEl = instructionsEl.createDiv({cls: "float-search-modal-instructions-navigate"});
|
|
const collapseInstructionsEl = instructionsEl.createDiv({cls: "float-search-modal-instructions-collapse"});
|
|
const enterInstructionsEl = instructionsEl.createDiv({cls: "float-search-modal-instructions-enter"});
|
|
const altEnterInstructionsEl = instructionsEl.createDiv({cls: "float-search-modal-instructions-alt-enter"});
|
|
|
|
const tabInstructionsEl = instructionsEl.createDiv({cls: "float-search-modal-instructions-tab"});
|
|
const switchInstructionsEl = instructionsEl.createDiv({cls: "float-search-modal-instructions-switch"});
|
|
|
|
const navigateIconEl = navigateInstructionsEl.createSpan({cls: "float-search-modal-instructions-key"});
|
|
const navigateTextEl = navigateInstructionsEl.createSpan({cls: "float-search-modal-instructions-text"});
|
|
navigateIconEl.setText("↑↓");
|
|
navigateTextEl.setText("Navigate");
|
|
|
|
const collapseIconEl = collapseInstructionsEl.createSpan({cls: "float-search-modal-instructions-key"});
|
|
const collapseTextEl = collapseInstructionsEl.createSpan({cls: "float-search-modal-instructions-text"});
|
|
collapseIconEl.setText("Shift+↑↓");
|
|
collapseTextEl.setText("Collapse/Expand");
|
|
|
|
const enterIconEl = enterInstructionsEl.createSpan({cls: "float-search-modal-instructions-key"});
|
|
const enterTextEl = enterInstructionsEl.createSpan({cls: "float-search-modal-instructions-text"});
|
|
enterIconEl.setText("↵");
|
|
enterTextEl.setText("Open in background");
|
|
|
|
const altEnterIconEl = altEnterInstructionsEl.createSpan({cls: "float-search-modal-instructions-key"});
|
|
const altEnterTextEl = altEnterInstructionsEl.createSpan({cls: "float-search-modal-instructions-text"});
|
|
altEnterIconEl.setText("Alt+↵");
|
|
altEnterTextEl.setText("Open File and Close");
|
|
|
|
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");
|
|
tabTextEl.setText("Preview/Close Preview");
|
|
|
|
const switchIconEl = switchInstructionsEl.createSpan({cls: "float-search-modal-instructions-key"});
|
|
const switchTextEl = switchInstructionsEl.createSpan({cls: "float-search-modal-instructions-text"});
|
|
switchIconEl.setText("Ctrl+G");
|
|
switchTextEl.setText("Switch Between Search and File View");
|
|
|
|
const clickInstructionsEl = instructionsEl.createDiv({cls: "float-search-modal-instructions-click"});
|
|
const clickIconEl = clickInstructionsEl.createSpan({cls: "float-search-modal-instructions-key"});
|
|
const clickTextEl = clickInstructionsEl.createSpan({cls: "float-search-modal-instructions-text"});
|
|
clickIconEl.setText("Alt+Click");
|
|
clickTextEl.setText("Close Modal While In File View");
|
|
}
|
|
|
|
initCss(contentEl: HTMLElement, modalEl: HTMLElement, containerEl: HTMLElement) {
|
|
contentEl.classList.add("float-search-modal-content");
|
|
modalEl.classList.add("float-search-modal");
|
|
containerEl.classList.add("float-search-modal-container");
|
|
}
|
|
|
|
async initSearchView(contentEl: HTMLElement) {
|
|
const [createdLeaf, embeddedView] = spawnLeafView(this.plugin, contentEl);
|
|
this.searchLeaf = createdLeaf;
|
|
this.searchEmbeddedView = embeddedView;
|
|
|
|
this.searchLeaf.setPinned(true);
|
|
await this.searchLeaf.setViewState({
|
|
type: "search",
|
|
});
|
|
|
|
setTimeout(async () => {
|
|
await this.searchLeaf.view.setState(this.state, {
|
|
history: false,
|
|
});
|
|
this.state?.current ? (this.searchLeaf.view as SearchView).searchComponent.inputEl.setSelectionRange(0, 0) : (this.searchLeaf.view as SearchView).searchComponent.inputEl.setSelectionRange(0, this.state?.query?.length);
|
|
}, 0);
|
|
|
|
return;
|
|
}
|
|
|
|
initInput() {
|
|
const inputEl = this.contentEl.getElementsByTagName("input")[0];
|
|
inputEl.focus();
|
|
inputEl.onkeydown = (e) => {
|
|
const currentView = this.searchLeaf.view as SearchView;
|
|
switch (e.key) {
|
|
case "ArrowDown":
|
|
if (e.shiftKey) {
|
|
currentView.onKeyShowMoreAfter(e);
|
|
if (currentView.dom.focusedItem) {
|
|
if (currentView.dom.focusedItem.collapsible) {
|
|
currentView.dom.focusedItem.setCollapse(false);
|
|
}
|
|
}
|
|
break;
|
|
} else {
|
|
currentView.onKeyArrowDownInFocus(e);
|
|
break;
|
|
}
|
|
case "ArrowUp":
|
|
if (e.shiftKey) {
|
|
currentView.onKeyShowMoreBefore(e);
|
|
if (currentView.dom.focusedItem) {
|
|
if (currentView.dom.focusedItem.collapseEl) {
|
|
currentView.dom.focusedItem.setCollapse(true);
|
|
}
|
|
}
|
|
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;
|
|
case "Tab":
|
|
e.preventDefault();
|
|
if (e.shiftKey) {
|
|
if (this.fileLeaf) {
|
|
this.fileLeaf?.detach();
|
|
this.fileLeaf = undefined;
|
|
this.fileEmbeddedView?.unload();
|
|
this.modalEl.toggleClass("float-search-width", false);
|
|
this.fileEl.detach();
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (currentView.dom.focusedItem) {
|
|
const item = currentView.dom.focusedItem;
|
|
const file = item.parent.file instanceof TFile ? item.parent.file : item.file;
|
|
|
|
item.parent.file instanceof TFile ? this.initFileView(file, {
|
|
match: {
|
|
content: item.content,
|
|
matches: item.matches
|
|
}
|
|
}) : this.initFileView(file, undefined);
|
|
}
|
|
break;
|
|
case "e":
|
|
if (e.ctrlKey) {
|
|
e.preventDefault();
|
|
if (this.fileLeaf) {
|
|
const estate = this.fileLeaf.getViewState();
|
|
estate.state.mode = "preview" === estate.state.mode ? "source" : "preview";
|
|
this.fileLeaf.setViewState(estate, {
|
|
focus: !0
|
|
});
|
|
setTimeout(() => {
|
|
(this.searchLeaf.view as SearchView).searchComponent.inputEl.focus();
|
|
}, 0);
|
|
}
|
|
}
|
|
break;
|
|
case "g":
|
|
if (this.fileLeaf && e.ctrlKey) {
|
|
e.preventDefault();
|
|
this.plugin.app.workspace.setActiveLeaf(this.fileLeaf, {
|
|
focus: true,
|
|
});
|
|
}
|
|
break;
|
|
case "C":
|
|
if (e.ctrlKey && e.shiftKey) {
|
|
e.preventDefault();
|
|
const text = currentView.dom.focusedItem.el.innerText;
|
|
navigator.clipboard.writeText(text);
|
|
}
|
|
break;
|
|
|
|
}
|
|
};
|
|
}
|
|
|
|
initContent() {
|
|
const {contentEl} = this;
|
|
contentEl.onclick = (e) => {
|
|
const resultElement = contentEl.getElementsByClassName('search-results-children')[0];
|
|
if (resultElement.children.length < 2) {
|
|
return;
|
|
}
|
|
|
|
let targetElement = e.target as HTMLElement | null;
|
|
|
|
if (e.altKey || !this.fileLeaf) {
|
|
while (targetElement) {
|
|
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;
|
|
}
|
|
|
|
targetElement = targetElement.parentElement;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (this.fileLeaf) {
|
|
const currentView = this.searchLeaf.view as SearchView;
|
|
|
|
if ((this.searchCtnEl as Node).contains(targetElement as Node)) {
|
|
while (targetElement) {
|
|
if (targetElement.classList.contains('tree-item')) {
|
|
break;
|
|
}
|
|
targetElement = targetElement.parentElement;
|
|
}
|
|
if (!targetElement) return;
|
|
|
|
const fileInnerEl = targetElement?.getElementsByClassName("tree-item-inner")[0] as HTMLElement;
|
|
const innerText = fileInnerEl.innerText;
|
|
const file = this.plugin.app.metadataCache.getFirstLinkpathDest(innerText, "");
|
|
|
|
if (file) {
|
|
const item = currentView.dom.resultDomLookup.get(file);
|
|
currentView.dom.setFocusedItem(item);
|
|
this.initFileView(file, undefined);
|
|
(this.searchLeaf.view as SearchView).searchComponent.inputEl.focus();
|
|
}
|
|
}
|
|
|
|
|
|
return;
|
|
}
|
|
};
|
|
}
|
|
|
|
async initFileView(file: TFile, state: any) {
|
|
if (this.fileLeaf) {
|
|
await this.fileLeaf.openFile(file, {
|
|
active: false,
|
|
eState: state
|
|
});
|
|
|
|
if (this.fileState?.match?.matches[0] === state?.match?.matches[0] && state && this.fileState) {
|
|
setTimeout(() => {
|
|
if (this.fileLeaf) {
|
|
this.plugin.app.workspace.setActiveLeaf(this.fileLeaf, {
|
|
focus: true,
|
|
});
|
|
}
|
|
}, 0);
|
|
} else {
|
|
this.fileState = state;
|
|
setTimeout(() => {
|
|
(this.searchLeaf.view as SearchView).searchComponent.inputEl.focus();
|
|
}, 0);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
const {contentEl} = this;
|
|
this.fileEl = contentEl.createDiv({cls: "float-search-modal-file-ctn"});
|
|
this.modalEl.toggleClass("float-search-width", true);
|
|
this.fileEl.onkeydown = (e) => {
|
|
if (e.ctrlKey && e.key === "g") {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
(this.searchLeaf.view as SearchView).searchComponent.inputEl.focus();
|
|
}
|
|
|
|
if (e.key === "Tab" && e.ctrlKey) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
(this.searchLeaf.view as SearchView).searchComponent.inputEl.focus();
|
|
}
|
|
};
|
|
|
|
if (!this.fileEl) return;
|
|
|
|
const [createdLeaf, embeddedView] = spawnLeafView(this.plugin, this.fileEl);
|
|
this.fileLeaf = createdLeaf;
|
|
this.fileEmbeddedView = embeddedView;
|
|
|
|
this.fileLeaf.setPinned(true);
|
|
await this.fileLeaf.openFile(file, {
|
|
active: false,
|
|
eState: state
|
|
});
|
|
this.fileState = state;
|
|
|
|
(this.searchLeaf.view as SearchView).searchComponent.inputEl.focus();
|
|
|
|
}
|
|
}
|