Skip to content

Commit

Permalink
New Features
Browse files Browse the repository at this point in the history
- Use the Tag Wrangler context menu for tags in the body of a note (editor AND preview mode)
- Open or create a tag page by alt/opt clicking a tag in any note (or the tag pane)
- Drag-and-drop tags to rename/reorganize them
- Drag tags from the tag pane or a note preview to an editor pane to insert them as text.
- A donation link is now available
  • Loading branch information
pjeby committed Oct 4, 2023
1 parent 9a95a3c commit 3b9f8d9
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 45 deletions.
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Obsidian Tag Wrangler Plugin

> NEW in 0.5.5 - you can now drag tags from the tag pane to an editor pane to insert them as text.
> NEW in 0.6.0
> - Open or create a tag page by alt/opt clicking a tag in any note (or the tag pane)
> - Use the Tag Wrangler context menu for tags in the body of a note (editor or preview mode)
> - Drag-and-drop tags to rename/reorganize them
> - Drag tags from the tag pane or a note preview to an editor pane to insert them as text.
This plugin adds a context menu for tags in the [Obsidian.md](https://obsidian.md) tag pane, with the following actions available:

Expand Down Expand Up @@ -35,7 +38,7 @@ People often debate the merits of using tags vs. page links to organize your not

To create a tag page, just right click any tag in the tag pane, then select "Create Tag Page". A new note will be created with an alias of the selected tag. You can rename the note or move it anywhere you like in the vault, as long as it retains the alias linking it to the tag. (Renaming a tag associated with a tag page (see "Renaming Tags", below) will automatically update the alias.)

To open an *existing* tag page, you can Alt-click any tag in the tag pane or any note, whether in editing or reading view. Ctrl/Cmd-click or middle click will open the tag page in a new pane. (Note: if no tag page exists, the normal click behavior of globally searching for the tag will apply.)
To open or create a tag page, you can Alt-click (Option-click on Mac) any tag in the tag pane or any note, whether in editing or reading view. Ctrl/Cmd-click or middle click plus Alt/Option will open the tag page in a new pane. (Note: if no tag page exists, you'll be prompted for whether you want to create it. If you cancel, the normal click behavior of globally searching for the tag will apply.)

Or, you can enter the tag's name in the Obsidian "quick switcher" (default hotkey: Ctrl/Cmd-O) to open the page from the keyboard. You can also hover-preview any tag in the tag pane or any markdown views to pop up a preview of the tag page.

Expand Down Expand Up @@ -119,6 +122,10 @@ Rather, this kind of thing will happen if the `#Bar/baz` tag is the first tag be

This is just how Obsidian tags work, and not something that Tag Wrangler can work around. But you can easily fix the problem by renaming anything that's in the "wrong" case to the "right" case. It just means that (as is already the case in Obsidian) you can't have more than one casing of the same tag name displayed in the tag pane, and that now you can easily rename tags to a consistent casing, if desired.

### Canvas Support

Please note that tag renaming is not supported for tags in Obsidian Canvas files yet, as Obsidian itself doesn't fully support such tags yet either. (That is, tags in canvas text do *not* appear in the tag pane counts or in Obsidian's internal indexes, so from Tag Wrangler's perspective they aren't findable and don't exist.) If some future version of Obsidian addresses this, this limitation *may* be removable then, depending on how the issue is addressed.



## Developer Notes
Expand Down
3 changes: 2 additions & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
"name": "Tag Wrangler",
"author": "PJ Eby",
"authorUrl": "https://github.com/pjeby",
"version": "0.5.13",
"version": "0.6.0",
"minAppVersion": "1.2.8",
"description": "Rename, merge, toggle, and search tags from the tag pane",
"fundingUrl": "https://dirtsimple.org/tips/tag-wrangler",
"isDesktopOnly": false
}
4 changes: 4 additions & 0 deletions src/Tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export class Tag {
return name.startsWith("#") ? name : "#"+name;
}

static toName(tag) {
return this.toTag(tag).slice(1);
}

static canonical(name) {
return Tag.toTag(name).toLowerCase();
}
Expand Down
155 changes: 114 additions & 41 deletions src/plugin.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {Component, Keymap, Menu, Notice, parseFrontMatterAliases, Plugin, Scope} from "obsidian";
import {Component, Keymap, Menu, Notice, parseFrontMatterAliases, Plugin} from "obsidian";
import {renameTag, findTargets} from "./renaming";
import {Tag} from "./Tag";
import {around} from "monkey-around";
import {Confirm} from "@ophidian/core";

const tagHoverMain = "tag-wrangler:tag-pane";

Expand Down Expand Up @@ -47,7 +48,14 @@ export default class TagWrangler extends Plugin {
app.workspace.trigger("tag-page:did-create", tp_evt);
}

async onload(){
onload(){
this.registerEvent(
app.workspace.on("editor-menu", (menu, editor) => {
const token = editor.getClickableTokenAt(editor.getCursor());
if (token?.type === "tag") this.setupMenu(menu, token.text);
})
)

this.register(
onElement(document, "contextmenu", ".tag-pane-tag", this.onMenu.bind(this), {capture: true})
);
Expand Down Expand Up @@ -117,6 +125,31 @@ export default class TagWrangler extends Plugin {
}, {capture: false})
);

const dropHandler = (e, targetEl, info = app.dragManager.draggable, drop) => {
if (info?.source !== "tag-wrangler" || e.defaultPrevented ) return;
const tag = targetEl.find(".tag-pane-tag-text, tag-pane-tag-text, .tag-pane-tag .tree-item-inner-text")?.textContent;
const dest = tag+"/"+Tag.toName(info.title).split("/").pop();
if (Tag.canonical(tag) === Tag.canonical(info.title)) return;
e.dataTransfer.dropEffect = "move";
e.preventDefault();
if (drop) {
this.rename(Tag.toName(info.title), dest);
} else {
app.dragManager.updateHover(targetEl, "is-being-dragged-over");
app.dragManager.setAction(`Rename to ${dest}`);
}
}

this.register(onElement(document.body, "dragover", ".tag-pane-tag.tree-item-self", dropHandler, {capture: true}));
this.register(onElement(document.body, "dragenter", ".tag-pane-tag.tree-item-self", dropHandler, {capture: true}));
// This has to be registered on the window so that it will still get the .draggable
this.registerDomEvent(window, "drop", e => {
const targetEl = e.target?.matchParent(".tag-pane-tag.tree-item-self", e.currentTarget);
if (!targetEl) return;
const info = app.dragManager.draggable;
if (info && !e.defaultPrevented) dropHandler(e, targetEl, info, true);
}, {capture: true});

// Track Tag Pages
const metaCache = this.app.metadataCache;
const plugin = this;
Expand Down Expand Up @@ -177,70 +210,76 @@ export default class TagWrangler extends Plugin {
}

onMenu(e, tagEl) {
if (!e.obsidian_contextmenu) {
e.obsidian_contextmenu = new Menu(this.app);
let menu = e.obsidian_contextmenu;
if (!menu) {
menu = e.obsidian_contextmenu = new Menu();
setTimeout(() => menu.showAtPosition({x: e.pageX, y: e.pageY}), 0);
}

const
tagName = tagEl.find(".tag-pane-tag-text, .tag-pane-tag .tree-item-inner-text").textContent,
isHierarchy = tagEl.parentElement.parentElement.find(".collapse-icon")
;
this.setupMenu(menu, tagName, isHierarchy);
if (isHierarchy) {
const
tagParent = tagName.split("/").slice(0, -1).join("/"),
tagView = this.leafView(tagEl.matchParent(".workspace-leaf")),
tagContainer = tagParent ? tagView.tagDoms["#" + tagParent.toLowerCase()]: tagView.root
;
function toggle(collapse) {
for(const tag of tagContainer.children ?? tagContainer.vChildren.children) tag.setCollapsed(collapse);
}
menu.addItem(item("tag-hierarchy", "vertical-three-dots", "Collapse tags at this level", () => toggle(true )))
.addItem(item("tag-hierarchy", "expand-vertically" , "Expand tags at this level" , () => toggle(false)))
}
}

setupMenu(menu, tagName, isHierarchy=false) {
tagName = Tag.toTag(tagName).slice(1);
const
tagPage = this.tagPage(tagName),
isHierarchy = tagEl.parentElement.parentElement.find(".collapse-icon"),
searchPlugin = this.app.internalPlugins.getPluginById("global-search"),
search = searchPlugin && searchPlugin.instance,
query = search && search.getGlobalSearchQuery(),
random = this.app.plugins.plugins["smart-random-note"],
menu = e.obsidian_contextmenu.addItem(item("pencil", "Rename #"+tagName, () => this.rename(tagName)));
random = this.app.plugins.plugins["smart-random-note"]
;
menu.addItem(item("tag-rename", "pencil", "Rename #"+tagName, () => this.rename(tagName)))

menu.addSeparator();
if (tagPage) {
menu.addItem(
item("popup-open", "Open tag page", (e) => this.openTagPage(tagPage, false, Keymap.isModEvent(e)))
item("tag-page", "popup-open", "Open tag page", (e) => this.openTagPage(tagPage, false, Keymap.isModEvent(e)))
)
} else {
menu.addItem(
item("create-new", "Create tag page", (e) => this.createTagPage(tagName, Keymap.isModEvent(e)))
item("tag-page", "create-new", "Create tag page", (e) => this.createTagPage(tagName, Keymap.isModEvent(e)))
)
}

if (search) {
menu.addSeparator().addItem(
item("magnifying-glass", "New search for #"+tagName, () => search.openGlobalSearch("tag:" + tagName))
menu.addItem(
item("tag-search", "magnifying-glass", "New search for #"+tagName, () => search.openGlobalSearch("tag:#" + tagName))
);
if (query) {
menu.addItem(
item("sheets-in-box", "Require #"+tagName+" in search" , () => search.openGlobalSearch(query+" tag:" + tagName))
item("tag-search", "sheets-in-box", "Require #"+tagName+" in search" , () => search.openGlobalSearch(query+" tag:#" + tagName))
);
}
menu.addItem(
item("crossed-star" , "Exclude #"+tagName+" from search", () => search.openGlobalSearch(query+" -tag:" + tagName))
item("tag-search", "crossed-star" , "Exclude #"+tagName+" from search", () => search.openGlobalSearch(query+" -tag:#" + tagName))
);
}

if (random) {
menu.addSeparator().addItem(
item("dice", "Open random note", async () => {
menu.addItem(
item("tag-random", "dice", "Open random note", async () => {
const targets = await findTargets(this.app, new Tag(tagName));
random.openRandomNote(targets.map(f=> this.app.vault.getAbstractFileByPath(f.filename)));
})
);
}

this.app.workspace.trigger("tag-wrangler:contextmenu", menu, tagName, {search, query, isHierarchy, tagPage});

if (isHierarchy) {
const
tagParent = tagName.split("/").slice(0, -1).join("/"),
tagView = this.leafView(tagEl.matchParent(".workspace-leaf")),
tagContainer = tagParent ? tagView.tagDoms["#" + tagParent.toLowerCase()]: tagView.root
;
function toggle(collapse) {
for(const tag of tagContainer.children ?? tagContainer.vChildren.children) tag.setCollapsed(collapse);
}
menu.addSeparator()
.addItem(item("vertical-three-dots", "Collapse tags at this level", () => toggle(true )))
.addItem(item("expand-vertically" , "Expand tags at this level" , () => toggle(false)))
}
}

leafView(containerEl) {
Expand All @@ -252,18 +291,15 @@ export default class TagWrangler extends Plugin {
}


async rename(tagName) {
const scope = new Scope;
this.app.keymap.pushScope(scope);
try { await renameTag(this.app, tagName); }
async rename(tagName, toName=tagName) {
try { await renameTag(this.app, tagName, toName); }
catch (e) { console.error(e); new Notice("error: " + e); }
this.app.keymap.popScope(scope);
}

}

function item(icon, title, click) {
return i => i.setIcon(icon).setTitle(title).onClick(click);
function item(section, icon, title, click) {
return i => { i.setIcon(icon).setTitle(title).onClick(click); if (section) i.setSection(section); }
}


Expand All @@ -288,18 +324,55 @@ class TagPageUIHandler extends Component {
});
}, {capture: false})
);

if (hoverSource === "preview") {
this.register(
onElement(document, "contextmenu", selector, (e, targetEl) => {
let menu = e.obsidian_contextmenu;
if (!menu) {
menu = e.obsidian_contextmenu = new Menu();
setTimeout(() => menu.showAtPosition({x: e.pageX, y: e.pageY}), 0);
}
this.plugin.setupMenu(menu, toTag(targetEl));
})
);
this.register(
onElement(document, "dragstart", selector, (event, targetEl) => {
const tagName = toTag(targetEl);
event.dataTransfer.setData("text/plain", Tag.toTag(tagName));
app.dragManager.onDragStart(event, {
source: "tag-wrangler",
type: "text",
title: tagName,
icon: "hashtag",
})
}, {capture: false})
);
}

this.register(
// Open tag page w/alt click (current pane) or ctrl/cmd/middle click (new pane)
onElement(document, "click", selector, (event, targetEl) => {
onElement(document, hoverSource === "editor" ? "mousedown" : "click", selector, (event, targetEl) => {
const {altKey} = event;
if (!Keymap.isModEvent(event) && !altKey) return;
const tagName = toTag(targetEl), tp = tagName && this.plugin.tagPage(tagName);
if (tp) {
this.plugin.openTagPage(tp, false, Keymap.isModEvent(event));
event.preventDefault();
event.stopPropagation();
return false;
} else {
new Confirm()
.setTitle("Create Tag Page")
.setContent(`A tag page for ${tagName} does not exist. Create it?`)
.confirm()
.then(v => {
if (v) return this.plugin.createTagPage(tagName, Keymap.isModEvent(event));
const search = app.internalPlugins.getPluginById("global-search")?.instance;
search?.openGlobalSearch("tag:#" + tagName)
})
;
}
event.preventDefault();
event.stopImmediatePropagation();
return false;
}, {capture: true})
);
}
Expand Down
3 changes: 3 additions & 0 deletions versions.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"0.6.0": "1.2.8",
"0.5.13": "1.2.8",
"0.5.12": "1.2.8",
"0.5.10": "0.15.9",
"0.5.5": "0.15.9",
"0.5.3": "0.14.5",
"0.5.2": "0.13.19",
Expand Down

0 comments on commit 3b9f8d9

Please sign in to comment.