mirror of
https://github.com/vrtmrz/obsidian-tagfolder.git
synced 2026-07-22 06:10:24 +00:00
400 lines
12 KiB
TypeScript
400 lines
12 KiB
TypeScript
import { ItemView, Menu, Notice, type App } from "obsidian";
|
|
import { mount } from 'svelte'
|
|
import TagFolderPlugin from "./main";
|
|
import {
|
|
OrderDirection,
|
|
OrderKeyItem,
|
|
OrderKeyTag,
|
|
VIEW_TYPE_TAGFOLDER,
|
|
VIEW_TYPE_TAGFOLDER_LINK,
|
|
VIEW_TYPE_TAGFOLDER_LIST,
|
|
type TagFolderSettings,
|
|
type ViewItem
|
|
} from "./types";
|
|
import { maxDepth, selectedTags } from "./store";
|
|
import { ancestorToLongestTag, ancestorToTags, isSpecialTag, renderSpecialTag, joinPartialPath, removeIntermediatePath, trimTrailingSlash } from "./util";
|
|
import { askString } from "dialog";
|
|
|
|
function toggleObjectProp(obj: { [key: string]: unknown }, propName: string, value: string | false) {
|
|
if (value === false) {
|
|
const newTagInfoEntries = Object.entries(obj || {}).filter(([key]) => key != propName);
|
|
if (newTagInfoEntries.length == 0) {
|
|
return {};
|
|
} else {
|
|
return Object.fromEntries(newTagInfoEntries);
|
|
}
|
|
} else {
|
|
return { ...(obj ?? {}), [propName]: value };
|
|
}
|
|
}
|
|
|
|
type TagWranglerPlugin = {
|
|
rename: (tagName: string, toName?: string) => void | Promise<void>;
|
|
};
|
|
|
|
type AppWithCommunityPlugins = App & {
|
|
plugins?: {
|
|
plugins?: Record<string, unknown>;
|
|
};
|
|
};
|
|
|
|
function getTagWranglerPlugin(app: App): TagWranglerPlugin | undefined {
|
|
const plugin = (app as AppWithCommunityPlugins).plugins?.plugins?.["tag-wrangler"];
|
|
if (!plugin || typeof (plugin as TagWranglerPlugin).rename != "function") {
|
|
return undefined;
|
|
}
|
|
return plugin as TagWranglerPlugin;
|
|
}
|
|
|
|
function getRenameTargetTag(expandedTagsAll: string[]) {
|
|
const tagPath = trimTrailingSlash(expandedTagsAll[expandedTagsAll.length - 1] ?? "");
|
|
const tagParts = tagPath.split("/").filter((part) => part.trim() != "");
|
|
if (tagParts.length == 0 || tagParts.some((part) => isSpecialTag(part))) {
|
|
return undefined;
|
|
}
|
|
return tagParts.join("/");
|
|
}
|
|
|
|
export abstract class TagFolderViewBase extends ItemView {
|
|
component!: ReturnType<typeof mount>;
|
|
plugin!: TagFolderPlugin;
|
|
navigation = false;
|
|
async saveSettings(settings: TagFolderSettings) {
|
|
this.plugin.settings = { ...this.plugin.settings, ...settings };
|
|
await this.plugin.saveSettings();
|
|
this.plugin.updateFileCaches();
|
|
}
|
|
showOrder(evt: MouseEvent) {
|
|
const menu = new Menu();
|
|
|
|
menu.addItem((item) => {
|
|
item.setTitle("Tags")
|
|
.setIcon("hashtag")
|
|
.onClick((evt2) => {
|
|
const menu2 = new Menu();
|
|
for (const key in OrderKeyTag) {
|
|
for (const direction in OrderDirection) {
|
|
menu2.addItem((item) => {
|
|
const newSetting = `${key}_${direction}`;
|
|
item.setTitle(
|
|
OrderKeyTag[key] +
|
|
" " +
|
|
OrderDirection[direction]
|
|
).onClick(async () => {
|
|
//@ts-ignore
|
|
this.plugin.settings.sortTypeTag =
|
|
newSetting;
|
|
await this.plugin.saveSettings();
|
|
});
|
|
if (
|
|
newSetting ==
|
|
this.plugin.settings.sortTypeTag
|
|
) {
|
|
item.setIcon("checkmark");
|
|
}
|
|
return item;
|
|
});
|
|
}
|
|
}
|
|
menu2.showAtPosition({ x: evt.x, y: evt.y });
|
|
});
|
|
return item;
|
|
});
|
|
menu.addItem((item) => {
|
|
item.setTitle("Items")
|
|
.setIcon("document")
|
|
.onClick((evt2) => {
|
|
const menu2 = new Menu();
|
|
for (const key in OrderKeyItem) {
|
|
for (const direction in OrderDirection) {
|
|
menu2.addItem((item) => {
|
|
const newSetting = `${key}_${direction}`;
|
|
item.setTitle(
|
|
OrderKeyItem[key] +
|
|
" " +
|
|
OrderDirection[direction]
|
|
).onClick(async () => {
|
|
//@ts-ignore
|
|
this.plugin.settings.sortType = newSetting;
|
|
await this.plugin.saveSettings();
|
|
});
|
|
if (
|
|
newSetting == this.plugin.settings.sortType
|
|
) {
|
|
item.setIcon("checkmark");
|
|
}
|
|
return item;
|
|
});
|
|
}
|
|
}
|
|
menu2.showAtPosition({ x: evt.x, y: evt.y });
|
|
});
|
|
return item;
|
|
});
|
|
menu.showAtMouseEvent(evt);
|
|
}
|
|
|
|
showLevelSelect(evt: MouseEvent) {
|
|
const menu = new Menu();
|
|
const setLevel = async (level: number) => {
|
|
this.plugin.settings.expandLimit = level;
|
|
await this.plugin.saveSettings();
|
|
maxDepth.set(level);
|
|
};
|
|
for (const level of [2, 3, 4, 5]) {
|
|
menu.addItem((item) => {
|
|
item.setTitle(`Level ${level - 1}`).onClick(() => {
|
|
void setLevel(level);
|
|
});
|
|
if (this.plugin.settings.expandLimit == level)
|
|
item.setIcon("checkmark");
|
|
return item;
|
|
});
|
|
}
|
|
|
|
menu.addItem((item) => {
|
|
item.setTitle("No limit")
|
|
// .setIcon("hashtag")
|
|
.onClick(() => {
|
|
void setLevel(0);
|
|
});
|
|
if (this.plugin.settings.expandLimit == 0)
|
|
item.setIcon("checkmark");
|
|
|
|
return item;
|
|
});
|
|
menu.showAtMouseEvent(evt);
|
|
}
|
|
|
|
abstract getViewType(): string;
|
|
|
|
showMenu(evt: MouseEvent, trail: string[], targetTag?: string, targetItems?: ViewItem[]) {
|
|
|
|
const isTagTree = this.getViewType() == VIEW_TYPE_TAGFOLDER;
|
|
const menu = new Menu();
|
|
if (isTagTree) {
|
|
|
|
const expandedTagsAll = ancestorToLongestTag(ancestorToTags(joinPartialPath(removeIntermediatePath(trail)))).map(e => trimTrailingSlash(e));
|
|
const expandedTags = expandedTagsAll
|
|
.map(e => e.split("/")
|
|
.filter(ee => !isSpecialTag(ee))
|
|
.join("/")).filter(e => e != "")
|
|
.map((e) => "#" + e)
|
|
.join(" ")
|
|
.trim();
|
|
const displayExpandedTags = expandedTagsAll
|
|
.map(e => e.split("/")
|
|
.filter(ee => renderSpecialTag(ee))
|
|
.join("/")).filter(e => e != "")
|
|
.map((e) => "#" + e)
|
|
.join(" ")
|
|
.trim();
|
|
|
|
|
|
if (navigator && navigator.clipboard) {
|
|
menu.addItem((item) =>
|
|
item
|
|
.setTitle(`Copy tags:${expandedTags}`)
|
|
.setIcon("hashtag")
|
|
.onClick(async () => {
|
|
await navigator.clipboard.writeText(expandedTags);
|
|
new Notice("Copied");
|
|
})
|
|
);
|
|
}
|
|
menu.addItem((item) =>
|
|
item
|
|
.setTitle(`New note ${targetTag ? "in here" : "as like this"}`)
|
|
.setIcon("create-new")
|
|
.onClick(async () => {
|
|
await this.plugin.createNewNote(trail);
|
|
})
|
|
);
|
|
const renameTargetTag = targetTag ? getRenameTargetTag(expandedTagsAll) : undefined;
|
|
const tagWrangler = renameTargetTag ? getTagWranglerPlugin(this.app) : undefined;
|
|
if (renameTargetTag && tagWrangler) {
|
|
menu.addItem((item) =>
|
|
item
|
|
.setTitle(`Rename #${renameTargetTag}`)
|
|
.setIcon("pencil")
|
|
.onClick(async () => {
|
|
await tagWrangler.rename(renameTargetTag);
|
|
})
|
|
);
|
|
}
|
|
if (targetTag) {
|
|
if (this.plugin.settings.useTagInfo && this.plugin.tagInfo != null) {
|
|
const tag = targetTag;
|
|
|
|
if (tag in this.plugin.tagInfo && "key" in this.plugin.tagInfo[tag]) {
|
|
menu.addItem((item) =>
|
|
item.setTitle(`Unpin`)
|
|
.setIcon("pin")
|
|
.onClick(async () => {
|
|
this.plugin.tagInfo[tag] = toggleObjectProp(this.plugin.tagInfo[tag], "key", false);
|
|
this.plugin.applyTagInfo();
|
|
await this.plugin.saveTagInfo();
|
|
})
|
|
)
|
|
|
|
} else {
|
|
menu.addItem((item) => {
|
|
item.setTitle(`Pin`)
|
|
.setIcon("pin")
|
|
.onClick(async () => {
|
|
this.plugin.tagInfo[tag] = toggleObjectProp(this.plugin.tagInfo[tag], "key", "");
|
|
this.plugin.applyTagInfo();
|
|
await this.plugin.saveTagInfo();
|
|
})
|
|
})
|
|
}
|
|
menu.addItem((item) => {
|
|
item.setTitle(`Set an alternative label`)
|
|
.setIcon("pencil")
|
|
.onClick(async () => {
|
|
const oldAlt = tag in this.plugin.tagInfo ? (this.plugin.tagInfo[tag].alt ?? "") : "";
|
|
const label = await askString(this.app, "", "", oldAlt);
|
|
if (label === false) return;
|
|
this.plugin.tagInfo[tag] = toggleObjectProp(this.plugin.tagInfo[tag], "alt", label == "" ? false : label);
|
|
this.plugin.applyTagInfo();
|
|
await this.plugin.saveTagInfo();
|
|
})
|
|
});
|
|
menu.addItem((item) => {
|
|
item.setTitle(`Change the mark`)
|
|
.setIcon("pencil")
|
|
.onClick(async () => {
|
|
const oldMark = tag in this.plugin.tagInfo ? (this.plugin.tagInfo[tag].mark ?? "") : "";
|
|
const mark = await askString(this.app, "", "", oldMark);
|
|
if (mark === false) return;
|
|
this.plugin.tagInfo[tag] = toggleObjectProp(this.plugin.tagInfo[tag], "mark", mark == "" ? false : mark);
|
|
this.plugin.applyTagInfo();
|
|
await this.plugin.saveTagInfo();
|
|
})
|
|
});
|
|
menu.addItem((item) => {
|
|
item.setTitle(`Redirect this tag to ...`)
|
|
.setIcon("pencil")
|
|
.onClick(async () => {
|
|
const oldRedirect = tag in this.plugin.tagInfo ? (this.plugin.tagInfo[tag].redirect ?? "") : "";
|
|
const redirect = await askString(this.app, "", "", oldRedirect);
|
|
if (redirect === false) return;
|
|
this.plugin.tagInfo[tag] = toggleObjectProp(this.plugin.tagInfo[tag], "redirect", redirect == "" ? false : redirect);
|
|
this.plugin.applyTagInfo();
|
|
await this.plugin.saveTagInfo();
|
|
})
|
|
});
|
|
if (targetItems) {
|
|
menu.addItem(item => {
|
|
item.setTitle(`Open scroll view`)
|
|
.setIcon("sheets-in-box")
|
|
.onClick(async () => {
|
|
const files = targetItems.map(e => e.path);
|
|
await this.plugin.openScrollView(undefined, displayExpandedTags, expandedTagsAll.join(", "), files);
|
|
})
|
|
})
|
|
menu.addItem(item => {
|
|
item.setTitle(`Open list`)
|
|
.setIcon("sheets-in-box")
|
|
.onClick(() => {
|
|
selectedTags.set(
|
|
expandedTagsAll
|
|
);
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!targetTag && targetItems && targetItems.length == 1) {
|
|
const path = targetItems[0].path;
|
|
const file = this.app.vault.getAbstractFileByPath(path);
|
|
// Trigger
|
|
this.app.workspace.trigger(
|
|
"file-menu",
|
|
menu,
|
|
file,
|
|
"file-explorer"
|
|
);
|
|
menu.addSeparator();
|
|
menu.addItem((item) =>
|
|
item
|
|
.setTitle(`Open in new tab`)
|
|
.setSection("open")
|
|
.setIcon("lucide-file-plus")
|
|
.onClick(async () => {
|
|
await this.app.workspace.openLinkText(path, path, "tab");
|
|
})
|
|
);
|
|
menu.addItem((item) =>
|
|
item
|
|
.setTitle(`Open to the right`)
|
|
.setSection("open")
|
|
.setIcon("lucide-separator-vertical")
|
|
.onClick(async () => {
|
|
await this.app.workspace.openLinkText(path, path, "split");
|
|
})
|
|
);
|
|
} else if (!isTagTree && targetTag) {
|
|
const path = targetTag;
|
|
const file = this.app.vault.getAbstractFileByPath(path);
|
|
// Trigger
|
|
this.app.workspace.trigger(
|
|
"file-menu",
|
|
menu,
|
|
file,
|
|
"file-explorer"
|
|
);
|
|
menu.addSeparator();
|
|
menu.addItem((item) =>
|
|
item
|
|
.setTitle(`Open in new tab`)
|
|
.setSection("open")
|
|
.setIcon("lucide-file-plus")
|
|
.onClick(async () => {
|
|
await this.app.workspace.openLinkText(path, path, "tab");
|
|
})
|
|
);
|
|
menu.addItem((item) =>
|
|
item
|
|
.setTitle(`Open to the right`)
|
|
.setSection("open")
|
|
.setIcon("lucide-separator-vertical")
|
|
.onClick(async () => {
|
|
await this.app.workspace.openLinkText(path, path, "split");
|
|
})
|
|
);
|
|
}
|
|
if ("screenX" in evt) {
|
|
menu.showAtPosition({ x: evt.pageX, y: evt.pageY });
|
|
} else {
|
|
menu.showAtPosition({
|
|
// @ts-ignore
|
|
x: evt.nativeEvent.locationX,
|
|
// @ts-ignore
|
|
y: evt.nativeEvent.locationY,
|
|
});
|
|
}
|
|
evt.preventDefault();
|
|
// menu.showAtMouseEvent(evt);
|
|
}
|
|
|
|
switchView() {
|
|
let viewType = VIEW_TYPE_TAGFOLDER;
|
|
const currentType = this.getViewType();
|
|
if (currentType == VIEW_TYPE_TAGFOLDER) {
|
|
viewType = VIEW_TYPE_TAGFOLDER_LIST;
|
|
} else if (currentType == VIEW_TYPE_TAGFOLDER_LINK) {
|
|
return
|
|
} else if (currentType == VIEW_TYPE_TAGFOLDER_LIST) {
|
|
viewType = VIEW_TYPE_TAGFOLDER;
|
|
}
|
|
|
|
const leaves = this.app.workspace.getLeavesOfType(viewType).filter(e => !e.getViewState().pinned && e != this.leaf);
|
|
if (leaves.length) {
|
|
void this.app.workspace.revealLeaf(
|
|
leaves[0]
|
|
);
|
|
}
|
|
}
|
|
}
|