From 47b051b52d6b93ada6f120b6b8975fd5d44d0363 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 6 Jan 2023 16:54:00 +0900 Subject: [PATCH] Improved: - Now separated files pane is available on mobile device. - Now we can open the hierarchy even using separated pane without selecting that one. Fixed: - Code has been tidied up. - Now do nothing when clicking tags with alt-key. --- ScrollView.ts | 3 + ScrollViewComponent.svelte | 1 + ScrollViewMarkdownComponent.svelte | 2 +- TagFolderList.ts | 110 ++++ TagFolderView.ts | 64 +++ TagFolderViewBase.ts | 283 ++++++++++ TagFolderViewComponent.svelte | 29 +- TreeItemComponent.svelte | 19 +- main.ts | 796 +++-------------------------- types.ts | 28 +- util.ts | 20 +- 11 files changed, 621 insertions(+), 734 deletions(-) create mode 100644 TagFolderList.ts create mode 100644 TagFolderView.ts create mode 100644 TagFolderViewBase.ts diff --git a/ScrollView.ts b/ScrollView.ts index 4af5c48..cf544dd 100644 --- a/ScrollView.ts +++ b/ScrollView.ts @@ -42,10 +42,12 @@ export class ScrollView extends ItemView { getDisplayText() { return this.state.tagPath || "Tags scroll"; } + async setFile(filenames: ScrollViewFile[]) { this.state = { ...this.state, files: filenames }; await this.updateView(); } + async setState(state: ScrollViewState, result: ViewStateResult): Promise { this.state = { ...state }; this.title = state.title; @@ -61,6 +63,7 @@ export class ScrollView extends ItemView { isFileOpened(path: string) { return this.state.files.some(e => e.path == path); } + getScrollViewState(): ScrollViewState { return this.state; } diff --git a/ScrollViewComponent.svelte b/ScrollViewComponent.svelte index cd5efee..6beb068 100644 --- a/ScrollViewComponent.svelte +++ b/ScrollViewComponent.svelte @@ -64,6 +64,7 @@
{#each files as file} +
handleOpenFile(evt, file)} diff --git a/ScrollViewMarkdownComponent.svelte b/ScrollViewMarkdownComponent.svelte index 359ab3a..abd6f10 100644 --- a/ScrollViewMarkdownComponent.svelte +++ b/ScrollViewMarkdownComponent.svelte @@ -9,7 +9,7 @@ let el: HTMLElement; - function onAppearing(this: HTMLElement, ev: Event) { + function onAppearing(this: HTMLElement, _: Event) { if (file.content && el) { MarkdownRenderer.renderMarkdown(file.content, el, file.path, null); } diff --git a/TagFolderList.ts b/TagFolderList.ts new file mode 100644 index 0000000..cf7f8b3 --- /dev/null +++ b/TagFolderList.ts @@ -0,0 +1,110 @@ +import { Menu, TFile, ViewStateResult, WorkspaceLeaf } from "obsidian"; +import TagFolderViewComponent from "./TagFolderViewComponent.svelte"; +import { + TagFolderListState, + TreeItem, + VIEW_TYPE_TAGFOLDER_LIST +} from "./types"; +import { isSpecialTag } from "./util"; +import { treeRoot } from "./store"; +import TagFolderPlugin from "./main"; +import { TagFolderViewBase } from "./TagFolderViewBase"; + +export class TagFolderList extends TagFolderViewBase { + component: TagFolderViewComponent; + plugin: TagFolderPlugin; + icon: "stacked-levels"; + title: string; + + onPaneMenu(menu: Menu, source: string): void { + super.onPaneMenu(menu, source); + menu.addItem(item => { + item.setIcon("pin") + .setTitle("Pin") + .onClick(() => { + this.leaf.togglePinned(); + }) + }) + } + + getIcon(): string { + return "stacked-levels"; + } + + state: TagFolderListState = { tags: [], title: "" }; + + async setState(state: TagFolderListState, result: ViewStateResult): Promise { + this.state = { ...state }; + this.title = state.tags.join(","); + this.component.$set({ tags: state.tags, title: state.title ?? "" }) + result = {}; + return; + } + + getState() { + return this.state; + } + + constructor(leaf: WorkspaceLeaf, plugin: TagFolderPlugin) { + super(leaf); + this.plugin = plugin; + + this.showMenu = this.showMenu.bind(this); + this.showOrder = this.showOrder.bind(this); + this.newNote = this.newNote.bind(this); + this.showLevelSelect = this.showLevelSelect.bind(this); + this.switchView = this.switchView.bind(this); + } + + async newNote(evt: MouseEvent) { + + const expandedTags = this.state.tags + .map(e => e.split("/") + .filter(ee => !isSpecialTag(ee)) + .join("/")).filter(e => e != "") + .map((e) => "#" + e) + .join(" ") + .trim(); + + //@ts-ignore + const ww = await this.app.fileManager.createAndOpenMarkdownFile() as TFile; + await this.app.vault.append(ww, expandedTags); + } + + getViewType() { + return VIEW_TYPE_TAGFOLDER_LIST; + } + + getDisplayText() { + return `Files with ${this.state.title}`; + } + + async onOpen() { + this.component = new TagFolderViewComponent({ + target: this.contentEl, + props: { + openfile: this.plugin.focusFile, + hoverPreview: this.plugin.hoverPreview, + expandFolder: this.plugin.expandFolder, + title: "", + showMenu: this.showMenu, + showLevelSelect: this.showLevelSelect, + showOrder: this.showOrder, + newNote: this.newNote, + openScrollView: this.plugin.openScrollView, + items: [], + isViewSwitchable: this.plugin.settings.useMultiPaneList, + switchView: this.switchView + }, + }); + } + + async onClose() { + this.component.$destroy(); + } + + setTreeRoot(root: TreeItem) { + treeRoot.set(root); + } + +} diff --git a/TagFolderView.ts b/TagFolderView.ts new file mode 100644 index 0000000..ec6e12a --- /dev/null +++ b/TagFolderView.ts @@ -0,0 +1,64 @@ +import { WorkspaceLeaf } from "obsidian"; +import TagFolderViewComponent from "./TagFolderViewComponent.svelte"; +import { TreeItem, VIEW_TYPE_TAGFOLDER } from "./types"; +import { treeRoot } from "./store"; +import TagFolderPlugin from "./main"; +import { TagFolderViewBase } from "./TagFolderViewBase"; + +export class TagFolderView extends TagFolderViewBase { + icon: "stacked-levels"; + + getIcon(): string { + return "stacked-levels"; + } + + constructor(leaf: WorkspaceLeaf, plugin: TagFolderPlugin) { + super(leaf); + this.plugin = plugin; + + this.showMenu = this.showMenu.bind(this); + this.showOrder = this.showOrder.bind(this); + this.newNote = this.newNote.bind(this); + this.showLevelSelect = this.showLevelSelect.bind(this); + this.switchView = this.switchView.bind(this); + } + + newNote(evt: MouseEvent) { + //@ts-ignore + this.app.commands.executeCommandById("file-explorer:new-file"); + } + getViewType() { + return VIEW_TYPE_TAGFOLDER; + } + + getDisplayText() { + return "Tag Folder"; + } + + async onOpen() { + this.component = new TagFolderViewComponent({ + target: this.contentEl, + props: { + openfile: this.plugin.focusFile, + hoverPreview: this.plugin.hoverPreview, + expandFolder: this.plugin.expandFolder, + vaultname: this.app.vault.getName(), + showMenu: this.showMenu, + showLevelSelect: this.showLevelSelect, + showOrder: this.showOrder, + newNote: this.newNote, + openScrollView: this.plugin.openScrollView, + isViewSwitchable: this.plugin.settings.useMultiPaneList, + switchView: this.switchView + }, + }); + } + + async onClose() { + this.component.$destroy(); + } + + setTreeRoot(root: TreeItem) { + treeRoot.set(root); + } +} diff --git a/TagFolderViewBase.ts b/TagFolderViewBase.ts new file mode 100644 index 0000000..a3a0291 --- /dev/null +++ b/TagFolderViewBase.ts @@ -0,0 +1,283 @@ +import { ItemView, Menu, Notice, TFile } from "obsidian"; +import TagFolderViewComponent from "./TagFolderViewComponent.svelte"; +import TagFolderPlugin from "./main"; +import { + OrderDirection, + OrderKeyItem, + OrderKeyTag, + TagFolderItem, + VIEW_TYPE_TAGFOLDER, + VIEW_TYPE_TAGFOLDER_LIST +} from "./types"; +import { maxDepth, selectedTags } from "./store"; +import { ancestorToLongestTag, ancestorToTags, isSpecialTag, omittedTags, renderSpecialTag } from "./util"; +export abstract class TagFolderViewBase extends ItemView { + component: TagFolderViewComponent; + plugin: TagFolderPlugin; + + showOrder(evt: MouseEvent) { + const menu = new Menu(); + + menu.addItem((item) => { + item.setTitle("Tags") + .setIcon("hashtag") + .onClick(async (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(); + this.plugin.setRoot(this.plugin.root); + }); + if ( + newSetting == + this.plugin.settings.sortTypeTag + ) { + item.setIcon("checkmark"); + } + + menu2.showAtMouseEvent(evt); + return item; + }); + } + } + }); + return item; + }); + menu.addItem((item) => { + item.setTitle("Items") + .setIcon("document") + .onClick(async (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(); + this.plugin.setRoot(this.plugin.root); + }); + if ( + newSetting == this.plugin.settings.sortType + ) { + item.setIcon("checkmark"); + } + + menu2.showAtMouseEvent(evt); + return item; + }); + } + } + }); + 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); + this.plugin.setRoot(this.plugin.root); + }; + for (const level of [2, 3, 4, 5]) { + menu.addItem((item) => { + item.setTitle(`Level ${level - 1}`).onClick(() => { + setLevel(level); + }); + if (this.plugin.settings.expandLimit == level) + item.setIcon("checkmark"); + return item; + }); + } + + menu.addItem((item) => { + item.setTitle("No limit") + // .setIcon("hashtag") + .onClick(() => { + setLevel(0); + }); + if (this.plugin.settings.expandLimit == 0) + item.setIcon("checkmark"); + + return item; + }); + menu.showAtMouseEvent(evt); + } + + abstract getViewType(): string; + + showMenu(evt: MouseEvent, path: string, entry: TagFolderItem) { + + const entryPath = "tag" in entry ? [...ancestorToTags(entry.ancestors)] : ['root', ...entry.tags]; + + if ("tag" in entry) { + const oTags = omittedTags(entry, this.plugin.settings); + if (oTags != false) { + entryPath.push(...oTags); + } + } + entryPath.shift() + const expandedTagsAll = ancestorToLongestTag(ancestorToTags(entryPath)); + 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(); + const menu = new Menu(); + + 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 ${"tag" in entry ? "in here" : "as like this"}`) + .setIcon("create-new") + .onClick(async () => { + //@ts-ignore + const ww = await this.app.fileManager.createAndOpenMarkdownFile() as TFile; + await this.app.vault.append(ww, expandedTags); + }) + ); + if ("tag" in entry) { + if (this.plugin.settings.useTagInfo && this.plugin.tagInfo != null) { + const tag = entry.ancestors[entry.ancestors.length - 1]; + + if (tag in this.plugin.tagInfo && this.plugin.tagInfo[tag]) { + menu.addItem((item) => + item.setTitle(`Unpin`) + .setIcon("pin") + .onClick(async () => { + this.plugin.tagInfo = + { + ...this.plugin.tagInfo, + [tag]: undefined + }; + this.plugin.applyTagInfo(); + await this.plugin.saveTagInfo(); + }) + ) + + } else { + menu.addItem((item) => { + item.setTitle(`Pin`) + .setIcon("pin") + .onClick(async () => { + this.plugin.tagInfo = + { + ...this.plugin.tagInfo, + [tag]: { key: "" } + }; + this.plugin.applyTagInfo(); + await this.plugin.saveTagInfo(); + }) + }) + } + menu.addItem(item => { + item.setTitle(`Open scroll view`) + .setIcon("sheets-in-box") + .onClick(async () => { + const files = entry.allDescendants.map(e => e.path); + const tagPath = entry.ancestors.join("/"); + await this.plugin.openScrollView(null, displayExpandedTags, tagPath, files); + }) + }) + menu.addItem(item => { + item.setTitle(`Open list`) + .setIcon("sheets-in-box") + .onClick(async () => { + selectedTags.set( + entry.ancestors + ); + }) + }) + } + } + if ("path" in entry) { + const path = entry.path; + const file = this.app.vault.getAbstractFileByPath(path); + // Trigger + this.app.workspace.trigger( + "file-menu", + menu, + file, + "file-explorer" + ); + } + + if ("tags" in entry) { + menu.addSeparator(); + menu.addItem((item) => + item + .setTitle(`Open in new tab`) + .setIcon("lucide-file-plus") + .onClick(async () => { + app.workspace.openLinkText(entry.path, entry.path, "tab"); + }) + ); + menu.addItem((item) => + item + .setTitle(`Open to the right`) + .setIcon("lucide-separator-vertical") + .onClick(async () => { + app.workspace.openLinkText(entry.path, entry.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, + }); + } + // menu.showAtMouseEvent(evt); + } + + switchView() { + const viewType = this.getViewType() == VIEW_TYPE_TAGFOLDER ? VIEW_TYPE_TAGFOLDER_LIST : VIEW_TYPE_TAGFOLDER; + const leaves = this.app.workspace.getLeavesOfType(viewType).filter(e => !e.getViewState().pinned && e != this.leaf); + if (leaves.length) { + this.app.workspace.revealLeaf( + leaves[0] + ); + } + } +} diff --git a/TagFolderViewComponent.svelte b/TagFolderViewComponent.svelte index 58722c2..9a3848d 100644 --- a/TagFolderViewComponent.svelte +++ b/TagFolderViewComponent.svelte @@ -4,7 +4,7 @@ import TreeItemComponent from "./TreeItemComponent.svelte"; import { onMount } from "svelte"; import { setIcon } from "obsidian"; - import { ancestorToLongestTag, ancestorToTags, pickEntry } from "./util"; + import { pickEntry } from "./util"; export let items: Array = []; export let hoverPreview: (e: MouseEvent, path: string) => void; @@ -32,6 +32,9 @@ files: string[] ) => Promise; + export let isViewSwitchable: boolean; + export let switchView: () => void; + treeRoot.subscribe((root: TreeItem) => { if (tags.length == 0) { items = root?.children ?? []; @@ -71,12 +74,19 @@ function clearSearch() { search = ""; } + + function doSwitch() { + if (switchView) { + switchView(); + } + } let iconDivEl: HTMLDivElement; let documentIcon = ""; let folderIcon = ""; let upAndDownArrowsIcon = ""; let stackedLevels = ""; let searchIcon = ""; + let switchIcon = ""; onMount(async () => { setIcon(iconDivEl, "right-triangle", 24); @@ -91,6 +101,8 @@ setIcon(iconDivEl, "search", 20); searchIcon = iconDivEl.innerHTML; } + setIcon(iconDivEl, "lucide-arrow-left-right", 20); + switchIcon = iconDivEl.innerHTML; }); $: headerTitle = title == "" ? `Tags: ${vaultname}` : `Items: ${title}`; $: isMainTree = tags.length == 0; @@ -99,6 +111,7 @@