#74 - Integration with Bookmarks core plugin and support for indirect drag & drop arrangement
Tons of updates: - feature complete - context menu for 'bookmark this' and 'bookmark+siblings' for sorting - order of bookmarked siblings reflects the current sorting in File Explorer, whatever it is (!!!) - unit tests probably missing for the additions - list of TODO: updated in main.ts and other files inline
This commit is contained in:
parent
71ab76652c
commit
99afdebba8
|
@ -16,7 +16,6 @@ import {
|
||||||
ObsidianIconFolder_PluginInstance,
|
ObsidianIconFolder_PluginInstance,
|
||||||
ObsidianIconFolderPlugin_Data
|
ObsidianIconFolderPlugin_Data
|
||||||
} from "../utils/ObsidianIconFolderPluginSignature";
|
} from "../utils/ObsidianIconFolderPluginSignature";
|
||||||
import {determineBookmarkOrder} from "../utils/BookmarksCorePluginSignature";
|
|
||||||
|
|
||||||
const mockTFile = (basename: string, ext: string, size?: number, ctime?: number, mtime?: number): TFile => {
|
const mockTFile = (basename: string, ext: string, size?: number, ctime?: number, mtime?: number): TFile => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -57,6 +57,8 @@ export interface FolderItemForSorting {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SorterFn = (a: FolderItemForSorting, b: FolderItemForSorting) => number
|
export type SorterFn = (a: FolderItemForSorting, b: FolderItemForSorting) => number
|
||||||
|
export type PlainSorterFn = (a: TAbstractFile, b: TAbstractFile) => number
|
||||||
|
export type PlainFileOnlySorterFn = (a: TFile, b: TFile) => number
|
||||||
export type CollatorCompareFn = (a: string, b: string) => number
|
export type CollatorCompareFn = (a: string, b: string) => number
|
||||||
|
|
||||||
// Syntax sugar
|
// Syntax sugar
|
||||||
|
@ -129,13 +131,32 @@ let Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
||||||
[CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
|
[CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// OS - Obsidian Sort
|
||||||
|
const OS_alphabetical = 'alphabetical'
|
||||||
|
const OS_alphabeticalReverse = 'alphabeticalReverse'
|
||||||
|
const OS_byModifiedTime = 'byModifiedTime'
|
||||||
|
const OS_byModifiedTimeReverse = 'byModifiedTimeReverse'
|
||||||
|
const OS_byCreatedTime = 'byCreatedTime'
|
||||||
|
const OS_byCreatedTimeReverse = 'byCreatedTimeReverse'
|
||||||
|
|
||||||
|
export const ObsidianStandardDefaultSortingName = OS_alphabetical
|
||||||
|
|
||||||
const StandardObsidianToCustomSort: {[key: string]: CustomSortOrder} = {
|
const StandardObsidianToCustomSort: {[key: string]: CustomSortOrder} = {
|
||||||
"alphabetical": CustomSortOrder.alphabetical,
|
[OS_alphabetical]: CustomSortOrder.alphabetical,
|
||||||
"alphabeticalReverse": CustomSortOrder.alphabeticalReverse,
|
[OS_alphabeticalReverse]: CustomSortOrder.alphabeticalReverse,
|
||||||
"byModifiedTime": CustomSortOrder.byModifiedTimeReverse, // In Obsidian labeled as 'Modified time (new to old)'
|
[OS_byModifiedTime]: CustomSortOrder.byModifiedTimeReverse, // In Obsidian labeled as 'Modified time (new to old)'
|
||||||
"byModifiedTimeReverse": CustomSortOrder.byModifiedTime, // In Obsidian labeled as 'Modified time (old to new)'
|
[OS_byModifiedTimeReverse]: CustomSortOrder.byModifiedTime, // In Obsidian labeled as 'Modified time (old to new)'
|
||||||
"byCreatedTime": CustomSortOrder.byCreatedTimeReverse, // In Obsidian labeled as 'Created time (new to old)'
|
[OS_byCreatedTime]: CustomSortOrder.byCreatedTimeReverse, // In Obsidian labeled as 'Created time (new to old)'
|
||||||
"byCreatedTimeReverse": CustomSortOrder.byCreatedTime // In Obsidian labeled as 'Created time (old to new)'
|
[OS_byCreatedTimeReverse]: CustomSortOrder.byCreatedTime // In Obsidian labeled as 'Created time (old to new)'
|
||||||
|
}
|
||||||
|
|
||||||
|
const StandardObsidianToPlainSortFn: {[key: string]: PlainFileOnlySorterFn} = {
|
||||||
|
[OS_alphabetical]: (a: TFile, b: TFile) => CollatorCompare(a.basename, b.basename),
|
||||||
|
[OS_alphabeticalReverse]: (a: TFile, b: TFile) => -StandardObsidianToPlainSortFn[OS_alphabetical](a,b),
|
||||||
|
[OS_byModifiedTime]: (a: TFile, b: TFile) => b.stat.mtime - a.stat.mtime,
|
||||||
|
[OS_byModifiedTimeReverse]: (a: TFile, b: TFile) => -StandardObsidianToPlainSortFn[OS_byModifiedTime](a,b),
|
||||||
|
[OS_byCreatedTime]: (a: TFile, b: TFile) => b.stat.ctime - a.stat.ctime,
|
||||||
|
[OS_byCreatedTimeReverse]: (a: TFile, b: TFile) => -StandardObsidianToPlainSortFn[OS_byCreatedTime](a,b)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Standard Obsidian comparator keeps folders in the top sorted alphabetically
|
// Standard Obsidian comparator keeps folders in the top sorted alphabetically
|
||||||
|
@ -150,6 +171,20 @@ const StandardObsidianComparator = (order: CustomSortOrder): SorterFn => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Equivalent of StandardObsidianComparator working directly on TAbstractFile items
|
||||||
|
export const StandardPlainObsidianComparator = (order: string): PlainSorterFn => {
|
||||||
|
const fileSorterFn = StandardObsidianToPlainSortFn[order] || StandardObsidianToCustomSort[OS_alphabetical]
|
||||||
|
return (a: TAbstractFile, b: TAbstractFile): number => {
|
||||||
|
const aIsFolder: boolean = a instanceof TFolder
|
||||||
|
const bIsFolder: boolean = b instanceof TFolder
|
||||||
|
return aIsFolder || bIsFolder
|
||||||
|
?
|
||||||
|
(aIsFolder && !bIsFolder ? -1 : (bIsFolder && !aIsFolder ? 1 : CollatorCompare(a.name,b.name)))
|
||||||
|
:
|
||||||
|
fileSorterFn(a as TFile, b as TFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const getSorterFnFor = (sorting: CustomSortOrder, currentUIselectedSorting?: string): SorterFn => {
|
export const getSorterFnFor = (sorting: CustomSortOrder, currentUIselectedSorting?: string): SorterFn => {
|
||||||
if (sorting === CustomSortOrder.standardObsidian) {
|
if (sorting === CustomSortOrder.standardObsidian) {
|
||||||
sorting = StandardObsidianToCustomSort[currentUIselectedSorting ?? 'alphabetical'] ?? CustomSortOrder.alphabetical
|
sorting = StandardObsidianToCustomSort[currentUIselectedSorting ?? 'alphabetical'] ?? CustomSortOrder.alphabetical
|
||||||
|
@ -545,3 +580,36 @@ export const folderSort = function (sortingSpec: CustomSortSpec, ctx: Processing
|
||||||
this.children = items;
|
this.children = items;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Returns a sorted copy of the input array, intentionally to keep it intact
|
||||||
|
export const sortFolderItemsForBookmarking = function (items: Array<TAbstractFile>, sortingSpec: CustomSortSpec|null|undefined, ctx: ProcessingContext, uiSortOrder: string): Array<TAbstractFile> {
|
||||||
|
if (sortingSpec) {
|
||||||
|
const folderItemsByPath: { [key: string]: TAbstractFile } = {}
|
||||||
|
|
||||||
|
const folderItems: Array<FolderItemForSorting> = items.map((entry: TFile | TFolder) => {
|
||||||
|
folderItemsByPath[entry.path] = entry
|
||||||
|
const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec, ctx)
|
||||||
|
return itemForSorting
|
||||||
|
})
|
||||||
|
|
||||||
|
// Finally, for advanced sorting by modified date, for some folders the modified date has to be determined
|
||||||
|
determineFolderDatesIfNeeded(folderItems, sortingSpec)
|
||||||
|
|
||||||
|
if (ctx.bookmarksPlugin?.instance) {
|
||||||
|
determineBookmarksOrderIfNeeded(folderItems, sortingSpec, ctx.bookmarksPlugin.instance, ctx.bookmarksPlugin.groupNameForSorting)
|
||||||
|
}
|
||||||
|
|
||||||
|
const comparator: SorterFn = getComparator(sortingSpec, uiSortOrder)
|
||||||
|
|
||||||
|
folderItems.sort(comparator)
|
||||||
|
|
||||||
|
const sortedItems: Array<TAbstractFile> = folderItems.map((entry) => folderItemsByPath[entry.path])
|
||||||
|
|
||||||
|
return sortedItems
|
||||||
|
} else { // No custom sorting or the custom sort disabled - apply standard Obsidian sorting (internally 1:1 recreated implementation)
|
||||||
|
const folderItems: Array<TAbstractFile> = items.map((entry: TFile | TFolder) => entry)
|
||||||
|
const plainSorterFn: PlainSorterFn = StandardPlainObsidianComparator(uiSortOrder)
|
||||||
|
folderItems.sort(plainSorterFn)
|
||||||
|
return folderItems
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
129
src/main.ts
129
src/main.ts
|
@ -16,9 +16,14 @@ import {
|
||||||
Vault, WorkspaceLeaf
|
Vault, WorkspaceLeaf
|
||||||
} from 'obsidian';
|
} from 'obsidian';
|
||||||
import {around} from 'monkey-around';
|
import {around} from 'monkey-around';
|
||||||
import {folderSort, ProcessingContext} from './custom-sort/custom-sort';
|
import {
|
||||||
|
folderSort,
|
||||||
|
ObsidianStandardDefaultSortingName,
|
||||||
|
ProcessingContext,
|
||||||
|
sortFolderItemsForBookmarking
|
||||||
|
} from './custom-sort/custom-sort';
|
||||||
import {SortingSpecProcessor, SortSpecsCollection} from './custom-sort/sorting-spec-processor';
|
import {SortingSpecProcessor, SortSpecsCollection} from './custom-sort/sorting-spec-processor';
|
||||||
import {CustomSortOrder, CustomSortSpec} from './custom-sort/custom-sort-types';
|
import {CustomSortSpec} from './custom-sort/custom-sort-types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addIcons,
|
addIcons,
|
||||||
|
@ -30,8 +35,14 @@ import {
|
||||||
ICON_SORT_SUSPENDED_SYNTAX_ERROR
|
ICON_SORT_SUSPENDED_SYNTAX_ERROR
|
||||||
} from "./custom-sort/icons";
|
} from "./custom-sort/icons";
|
||||||
import {getStarredPlugin} from "./utils/StarredPluginSignature";
|
import {getStarredPlugin} from "./utils/StarredPluginSignature";
|
||||||
import {getBookmarksPlugin} from "./utils/BookmarksCorePluginSignature";
|
import {
|
||||||
|
getBookmarksPlugin,
|
||||||
|
bookmarkFolderItem,
|
||||||
|
saveDataAndUpdateBookmarkViews,
|
||||||
|
bookmarkSiblings
|
||||||
|
} from "./utils/BookmarksCorePluginSignature";
|
||||||
import {getIconFolderPlugin} from "./utils/ObsidianIconFolderPluginSignature";
|
import {getIconFolderPlugin} from "./utils/ObsidianIconFolderPluginSignature";
|
||||||
|
import {lastPathComponent} from "./utils/utils";
|
||||||
|
|
||||||
interface CustomSortPluginSettings {
|
interface CustomSortPluginSettings {
|
||||||
additionalSortspecFile: string
|
additionalSortspecFile: string
|
||||||
|
@ -317,14 +328,25 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
item.setTitle('Custom sort: bookmark for sorting.');
|
item.setTitle('Custom sort: bookmark for sorting.');
|
||||||
item.setIcon('hashtag');
|
item.setIcon('hashtag');
|
||||||
item.onClick(() => {
|
item.onClick(() => {
|
||||||
console.log(`custom-sort: bookmark this clicked ${source}`)
|
const bookmarksPlugin = getBookmarksPlugin(plugin.app)
|
||||||
|
console.log(`custom-sort: bookmark this clicked ${source} and the leaf is`)
|
||||||
|
if (bookmarksPlugin) {
|
||||||
|
bookmarkFolderItem(file, bookmarksPlugin, plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||||
|
saveDataAndUpdateBookmarkViews(bookmarksPlugin, plugin.app)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const bookmarkAllMenuItem = (item: MenuItem) => {
|
const bookmarkAllMenuItem = (item: MenuItem) => {
|
||||||
item.setTitle('Custom sort: bookmark all siblings for sorting.');
|
item.setTitle('Custom sort: bookmark+siblings for sorting.');
|
||||||
item.setIcon('hashtag');
|
item.setIcon('hashtag');
|
||||||
item.onClick(() => {
|
item.onClick(() => {
|
||||||
console.log(`custom-sort: bookmark all siblings clicked ${source}`)
|
console.log(`custom-sort: bookmark all siblings clicked ${source}`)
|
||||||
|
const bookmarksPlugin = getBookmarksPlugin(plugin.app)
|
||||||
|
if (bookmarksPlugin) {
|
||||||
|
const orderedChildren: Array<TAbstractFile> = plugin.orderedFolderItemsForBookmarking(file.parent)
|
||||||
|
bookmarkSiblings(orderedChildren, bookmarksPlugin, plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||||
|
saveDataAndUpdateBookmarkViews(bookmarksPlugin, plugin.app)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -358,6 +380,32 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
determineSortSpecForFolder(folderPath: string, folderName?: string): CustomSortSpec|null|undefined {
|
||||||
|
folderName = folderName ?? lastPathComponent(folderPath)
|
||||||
|
let sortSpec: CustomSortSpec | null | undefined = this.sortSpecCache?.sortSpecByPath?.[folderPath]
|
||||||
|
sortSpec = sortSpec ?? this.sortSpecCache?.sortSpecByName?.[folderName]
|
||||||
|
|
||||||
|
if (!sortSpec && this.sortSpecCache?.sortSpecByWildcard) {
|
||||||
|
// when no sorting spec found directly by folder path, check for wildcard-based match
|
||||||
|
sortSpec = this.sortSpecCache?.sortSpecByWildcard.folderMatch(folderPath, folderName)
|
||||||
|
}
|
||||||
|
return sortSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
createProcessingContextForSorting(): ProcessingContext {
|
||||||
|
const ctx: ProcessingContext = {
|
||||||
|
_mCache: this.app.metadataCache,
|
||||||
|
starredPluginInstance: getStarredPlugin(this.app),
|
||||||
|
bookmarksPlugin: {
|
||||||
|
instance: this.settings.automaticBookmarksIntegration ? getBookmarksPlugin(this.app) : undefined,
|
||||||
|
groupNameForSorting: this.settings.bookmarksGroupToConsumeAsOrderingReference
|
||||||
|
},
|
||||||
|
iconFolderPluginInstance: getIconFolderPlugin(this.app),
|
||||||
|
plugin: this
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
// For the idea of monkey-patching credits go to https://github.com/nothingislost/obsidian-bartender
|
// For the idea of monkey-patching credits go to https://github.com/nothingislost/obsidian-bartender
|
||||||
patchFileExplorerFolder(patchableFileExplorer?: FileExplorerView): boolean {
|
patchFileExplorerFolder(patchableFileExplorer?: FileExplorerView): boolean {
|
||||||
let plugin = this;
|
let plugin = this;
|
||||||
|
@ -371,10 +419,6 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(Folder.prototype, {
|
const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(Folder.prototype, {
|
||||||
sort(old: any) {
|
sort(old: any) {
|
||||||
return function (...args: any[]) {
|
return function (...args: any[]) {
|
||||||
console.log(this)
|
|
||||||
console.log(this.fileExplorer.sortOrder)
|
|
||||||
|
|
||||||
|
|
||||||
// quick check for plugin status
|
// quick check for plugin status
|
||||||
if (plugin.settings.suspended) {
|
if (plugin.settings.suspended) {
|
||||||
return old.call(this, ...args);
|
return old.call(this, ...args);
|
||||||
|
@ -385,38 +429,12 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
setIcon(plugin.ribbonIconEl, ICON_SORT_ENABLED_ACTIVE)
|
setIcon(plugin.ribbonIconEl, ICON_SORT_ENABLED_ACTIVE)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if custom sort is not specified, use the UI-selected
|
|
||||||
const folder: TFolder = this.file
|
const folder: TFolder = this.file
|
||||||
let sortSpec: CustomSortSpec | null | undefined = plugin.sortSpecCache?.sortSpecByPath?.[folder.path]
|
let sortSpec: CustomSortSpec | null | undefined = plugin.determineSortSpecForFolder(folder.path, folder.name)
|
||||||
sortSpec = sortSpec ?? plugin.sortSpecCache?.sortSpecByName?.[folder.name]
|
|
||||||
|
|
||||||
if (!sortSpec && plugin.sortSpecCache?.sortSpecByWildcard) {
|
|
||||||
// when no sorting spec found directly by folder path, check for wildcard-based match
|
|
||||||
sortSpec = plugin.sortSpecCache?.sortSpecByWildcard.folderMatch(folder.path, folder.name)
|
|
||||||
/* SM??? if (sortSpec?.defaultOrder === CustomSortOrder.standardObsidian) {
|
|
||||||
explicitlyStandardSort = true
|
|
||||||
sortSpec = null // A folder is explicitly excluded from custom sorting plugin
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: ensure that explicitly configured standard sort excludes the auto-applied on-the-fly
|
|
||||||
|
|
||||||
if (sortSpec) {
|
if (sortSpec) {
|
||||||
console.log(`Sortspec for folder ${folder.path}`)
|
return folderSort.call(this, sortSpec, plugin.createProcessingContextForSorting());
|
||||||
console.log(sortSpec)
|
|
||||||
const ctx: ProcessingContext = {
|
|
||||||
_mCache: plugin.app.metadataCache,
|
|
||||||
starredPluginInstance: getStarredPlugin(plugin.app),
|
|
||||||
bookmarksPlugin: {
|
|
||||||
instance: plugin.settings.automaticBookmarksIntegration ? getBookmarksPlugin(this.app) : undefined,
|
|
||||||
groupNameForSorting: plugin.settings.bookmarksGroupToConsumeAsOrderingReference
|
|
||||||
},
|
|
||||||
iconFolderPluginInstance: getIconFolderPlugin(this.app),
|
|
||||||
plugin: plugin
|
|
||||||
}
|
|
||||||
return folderSort.call(this, sortSpec, ctx);
|
|
||||||
} else {
|
} else {
|
||||||
console.log(`NO Sortspec for folder ${folder.path}`)
|
|
||||||
return old.call(this, ...args);
|
return old.call(this, ...args);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -429,6 +447,16 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
orderedFolderItemsForBookmarking(folder: TFolder): Array<TAbstractFile> {
|
||||||
|
let sortSpec: CustomSortSpec | null | undefined = undefined
|
||||||
|
if (!this.settings.suspended) {
|
||||||
|
sortSpec = this.determineSortSpecForFolder(folder.path, folder.name)
|
||||||
|
}
|
||||||
|
let uiSortOrder: string = this.getFileExplorer()?.sortOrder || ObsidianStandardDefaultSortingName
|
||||||
|
|
||||||
|
return sortFolderItemsForBookmarking(folder.children, sortSpec, this.createProcessingContextForSorting(), uiSortOrder)
|
||||||
|
}
|
||||||
|
|
||||||
// Credits go to https://github.com/nothingislost/obsidian-bartender
|
// Credits go to https://github.com/nothingislost/obsidian-bartender
|
||||||
getFileExplorer(): FileExplorerView | undefined {
|
getFileExplorer(): FileExplorerView | undefined {
|
||||||
let fileExplorer: FileExplorerView | undefined = this.app.workspace.getLeavesOfType("file-explorer")?.first()
|
let fileExplorer: FileExplorerView | undefined = this.app.workspace.getLeavesOfType("file-explorer")?.first()
|
||||||
|
@ -437,7 +465,6 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStatusBar() {
|
updateStatusBar() {
|
||||||
|
@ -455,6 +482,10 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pathToFlatString = (path: string): string => {
|
||||||
|
return path.replace('/','_').replace('\\', '_')
|
||||||
|
}
|
||||||
|
|
||||||
class CustomSortSettingTab extends PluginSettingTab {
|
class CustomSortSettingTab extends PluginSettingTab {
|
||||||
plugin: CustomSortPlugin;
|
plugin: CustomSortPlugin;
|
||||||
|
|
||||||
|
@ -577,33 +608,23 @@ class CustomSortSettingTab extends PluginSettingTab {
|
||||||
.setPlaceholder('e.g. Group for sorting')
|
.setPlaceholder('e.g. Group for sorting')
|
||||||
.setValue(this.plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
.setValue(this.plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.bookmarksGroupToConsumeAsOrderingReference = value.trim();
|
this.plugin.settings.bookmarksGroupToConsumeAsOrderingReference = value.trim() ? pathToFlatString(normalizePath(value)) : '';
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TODO: clear bookmarks cache upon each tap on ribbon or on the command of 'sorting-on'
|
// TODO: clear bookmarks cache upon each tap on ribbon or on the command of 'sorting-on'
|
||||||
|
|
||||||
// TODO: clear bookmarks cache upon each context menu - before and after (maybe after is not needed, implicitlty empty after first clearing)
|
// TODO: clear bookmarks cache upon each context menu - before and after (maybe after is not needed, implicitly empty after first clearing)
|
||||||
|
|
||||||
// TODO: if a folder doesn't have any bookmarked items, it should remain under control of standard obsidian sorting
|
|
||||||
|
|
||||||
// TODO: in discussion sections add (and pin) announcement "DRAG & DROP ORDERING AVAILABLE VIA THE BOOKMARKS CORE PLUGIN INTEGRATION"
|
// TODO: in discussion sections add (and pin) announcement "DRAG & DROP ORDERING AVAILABLE VIA THE BOOKMARKS CORE PLUGIN INTEGRATION"
|
||||||
|
|
||||||
// TODO: in community, add update message with announcement of drag & drop support via Bookmarks plugin
|
// TODO: in community, add update message with announcement of drag & drop support via Bookmarks plugin
|
||||||
|
|
||||||
// TODO: if folder has explicit sorting: standard, don't apply bookmarks
|
// TODO: context menu only if bookmarks plugin enabled and new setting (yet to be exposed) doesn't disable it
|
||||||
|
|
||||||
// TODO: fix error
|
// TODO: defensive programming with ?. and equivalents to protect against crash if Obsidian API changes
|
||||||
// bookmarks integration - for root folder and for other folders
|
// Better the plugin to fail an operation than crash with errors
|
||||||
// (check for the case:
|
|
||||||
// target-folder: /*
|
|
||||||
// sorting: standard
|
|
||||||
|
|
||||||
// TODO: unbookmarked items in partially bookmarked -> can it apply the system sort ???
|
// TODO: remove console.log (many places added)
|
||||||
|
|
||||||
// TODO: unblock syntax 'sorting: standard' also for groups --> since I have access to currently configured sorting :-)
|
|
||||||
|
|
||||||
// TODO: bug? On auto-bookmark integration strange behavior
|
|
||||||
|
|
|
@ -53,5 +53,7 @@ declare module 'obsidian' {
|
||||||
createFolderDom(folder: TFolder): FileExplorerFolder;
|
createFolderDom(folder: TFolder): FileExplorerFolder;
|
||||||
|
|
||||||
requestSort(): void;
|
requestSort(): void;
|
||||||
|
|
||||||
|
sortOrder: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import {App, InstalledPlugin, PluginInstance} from "obsidian";
|
import {App, InstalledPlugin, Plugin, PluginInstance, TAbstractFile, TFolder} from "obsidian";
|
||||||
|
import {lastPathComponent} from "./utils";
|
||||||
|
|
||||||
const BookmarksPlugin_getBookmarks_methodName = 'getBookmarks'
|
const BookmarksPlugin_getBookmarks_methodName = 'getBookmarks'
|
||||||
|
|
||||||
|
const BookmarksPlugin_items_collectionName = 'items'
|
||||||
|
|
||||||
type Path = string
|
type Path = string
|
||||||
|
|
||||||
// Only relevant types of bookmarked items considered here
|
// Only relevant types of bookmarked items considered here
|
||||||
|
@ -16,20 +19,23 @@ interface BookmarkWithPath {
|
||||||
interface BookmarkedFile {
|
interface BookmarkedFile {
|
||||||
type: 'file'
|
type: 'file'
|
||||||
path: Path
|
path: Path
|
||||||
subpath?: string // Anchor within the file
|
subpath?: string // Anchor within the file (heading and/or block ref)
|
||||||
title?: string
|
title?: string
|
||||||
|
ctime: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BookmarkedFolder {
|
interface BookmarkedFolder {
|
||||||
type: 'folder'
|
type: 'folder'
|
||||||
path: Path
|
path: Path
|
||||||
title?: string
|
title?: string
|
||||||
|
ctime: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BookmarkedGroup {
|
interface BookmarkedGroup {
|
||||||
type: 'group'
|
type: 'group'
|
||||||
items: Array<BookmarkedItem>
|
items: Array<BookmarkedItem>
|
||||||
title?: string
|
title?: string
|
||||||
|
ctime: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookmarkedItemPath = string
|
export type BookmarkedItemPath = string
|
||||||
|
@ -37,6 +43,7 @@ export type BookmarkedItemPath = string
|
||||||
export interface OrderedBookmarkedItem {
|
export interface OrderedBookmarkedItem {
|
||||||
file: boolean
|
file: boolean
|
||||||
folder: boolean
|
folder: boolean
|
||||||
|
group: boolean
|
||||||
path: BookmarkedItemPath
|
path: BookmarkedItemPath
|
||||||
order: number
|
order: number
|
||||||
}
|
}
|
||||||
|
@ -47,6 +54,8 @@ interface OrderedBookmarks {
|
||||||
|
|
||||||
export interface Bookmarks_PluginInstance extends PluginInstance {
|
export interface Bookmarks_PluginInstance extends PluginInstance {
|
||||||
[BookmarksPlugin_getBookmarks_methodName]: () => Array<BookmarkedItem> | undefined
|
[BookmarksPlugin_getBookmarks_methodName]: () => Array<BookmarkedItem> | undefined
|
||||||
|
[BookmarksPlugin_items_collectionName]: Array<BookmarkedItem>
|
||||||
|
saveData(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
let bookmarksCache: OrderedBookmarks | undefined = undefined
|
let bookmarksCache: OrderedBookmarks | undefined = undefined
|
||||||
|
@ -86,48 +95,55 @@ export const getBookmarksPlugin = (app?: App): Bookmarks_PluginInstance | undefi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type TraverseCallback = (item: BookmarkedItem, groupPath: string) => boolean | void
|
type TraverseCallback = (item: BookmarkedItem, parentsGroupsPath: string) => boolean | void
|
||||||
|
|
||||||
const traverseBookmarksCollection = (items: Array<BookmarkedItem>, callback: TraverseCallback) => {
|
const traverseBookmarksCollection = (items: Array<BookmarkedItem>, callback: TraverseCallback) => {
|
||||||
const recursiveTraversal = (collection: Array<BookmarkedItem>, groupPath: string) => {
|
const recursiveTraversal = (collection: Array<BookmarkedItem>, groupsPath: string) => {
|
||||||
for (let idx = 0, collectionRef = collection; idx < collectionRef.length; idx++) {
|
for (let idx = 0, collectionRef = collection; idx < collectionRef.length; idx++) {
|
||||||
const item = collectionRef[idx];
|
const item = collectionRef[idx];
|
||||||
if (callback(item, groupPath)) return;
|
if (callback(item, groupsPath)) return;
|
||||||
if ('group' === item.type) recursiveTraversal(item.items, `${groupPath}${groupPath ? '/' : ''}${item.title}`);
|
if ('group' === item.type) recursiveTraversal(item.items, `${groupsPath}${groupsPath?'/':''}${item.title}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
recursiveTraversal(items, '');
|
recursiveTraversal(items, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroup?: string): OrderedBookmarks | undefined => {
|
const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroupName?: string): OrderedBookmarks | undefined => {
|
||||||
const bookmarks: Array<BookmarkedItem> | undefined = plugin?.[BookmarksPlugin_getBookmarks_methodName]()
|
console.log(`Populating bookmarks cache with group scope ${bookmarksGroupName}`)
|
||||||
|
let bookmarks: Array<BookmarkedItem> | undefined = plugin?.[BookmarksPlugin_getBookmarks_methodName]()
|
||||||
if (bookmarks) {
|
if (bookmarks) {
|
||||||
const orderedBookmarks: OrderedBookmarks = {}
|
if (bookmarksGroupName) {
|
||||||
let order: number = 0
|
const bookmarksGroup: BookmarkedGroup|undefined = bookmarks.find(
|
||||||
const groupNamePrefix: string = bookmarksGroup ? `${bookmarksGroup}/` : ''
|
(item) => item.type === 'group' && item.title === bookmarksGroupName) as BookmarkedGroup
|
||||||
const consumeItem = (item: BookmarkedItem, groupPath: string) => {
|
bookmarks = bookmarksGroup ? bookmarksGroup.items : undefined
|
||||||
if (groupNamePrefix && !groupPath.startsWith(groupNamePrefix)) {
|
}
|
||||||
return
|
if (bookmarks) {
|
||||||
}
|
const orderedBookmarks: OrderedBookmarks = {}
|
||||||
const isFile: boolean = item.type === 'file'
|
let order: number = 0
|
||||||
const isAnchor: boolean = isFile && !!(item as BookmarkedFile).subpath
|
const consumeItem = (item: BookmarkedItem, parentGroupsPath: string) => {
|
||||||
const isFolder: boolean = item.type === 'folder'
|
const isFile: boolean = item.type === 'file'
|
||||||
if ((isFile && !isAnchor) || isFolder) {
|
const isAnchor: boolean = isFile && !!(item as BookmarkedFile).subpath
|
||||||
const path = (item as BookmarkWithPath).path
|
const isFolder: boolean = item.type === 'folder'
|
||||||
// Consume only the first occurrence of a path in bookmarks, even if many duplicates can exist
|
const isGroup: boolean = item.type === 'group'
|
||||||
const alreadyConsumed = orderedBookmarks[path]
|
if ((isFile && !isAnchor) || isFolder || isGroup) {
|
||||||
if (!alreadyConsumed) {
|
const pathOfGroup: string = `${parentGroupsPath}${parentGroupsPath?'/':''}${item.title}`
|
||||||
orderedBookmarks[path] = {
|
const path = isGroup ? pathOfGroup : (item as BookmarkWithPath).path
|
||||||
path: path,
|
// Consume only the first occurrence of a path in bookmarks, even if many duplicates can exist
|
||||||
order: order++,
|
const alreadyConsumed = orderedBookmarks[path]
|
||||||
file: isFile,
|
if (!alreadyConsumed) {
|
||||||
folder: isFile
|
orderedBookmarks[path] = {
|
||||||
|
path: path,
|
||||||
|
order: order++,
|
||||||
|
file: isFile,
|
||||||
|
folder: isFile,
|
||||||
|
group: isGroup
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
traverseBookmarksCollection(bookmarks, consumeItem)
|
||||||
|
return orderedBookmarks
|
||||||
}
|
}
|
||||||
traverseBookmarksCollection(bookmarks, consumeItem)
|
|
||||||
return orderedBookmarks
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,3 +161,57 @@ export const determineBookmarkOrder = (path: string, plugin: Bookmarks_PluginIns
|
||||||
|
|
||||||
return (bookmarkedItemPosition !== undefined && bookmarkedItemPosition >= 0) ? (bookmarkedItemPosition + 1) : undefined
|
return (bookmarkedItemPosition !== undefined && bookmarkedItemPosition >= 0) ? (bookmarkedItemPosition + 1) : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EXPERIMENTAL - operates on internal structures of core Bookmarks plugin
|
||||||
|
|
||||||
|
const createBookmarkFileEntry = (path: string): BookmarkedFile => {
|
||||||
|
return { type: "file", ctime: Date.now(), path: path }
|
||||||
|
}
|
||||||
|
|
||||||
|
const createBookmarkGroupEntry = (title: string): BookmarkedGroup => {
|
||||||
|
return { type: "group", ctime: Date.now(), items: [], title: title }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bookmarkFolderItem = (item: TAbstractFile, plugin: Bookmarks_PluginInstance, bookmarksGroup?: string) => {
|
||||||
|
bookmarkSiblings([item], plugin, bookmarksGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bookmarkSiblings = (siblings: Array<TAbstractFile>, plugin: Bookmarks_PluginInstance, bookmarksGroup?: string) => {
|
||||||
|
let items = plugin[BookmarksPlugin_items_collectionName]
|
||||||
|
|
||||||
|
if (siblings.length === 0) return // for sanity
|
||||||
|
|
||||||
|
const parentPathComponents: Array<string> = siblings[0].path.split('/')!
|
||||||
|
parentPathComponents.pop()
|
||||||
|
|
||||||
|
if (bookmarksGroup) {
|
||||||
|
parentPathComponents.unshift(bookmarksGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
parentPathComponents.forEach((pathSegment) => {
|
||||||
|
let group: BookmarkedGroup|undefined = items.find((it) => it.type === 'group' && it.title === pathSegment) as BookmarkedGroup
|
||||||
|
if (!group) {
|
||||||
|
group = createBookmarkGroupEntry(pathSegment)
|
||||||
|
items.push(group)
|
||||||
|
}
|
||||||
|
items = group.items
|
||||||
|
})
|
||||||
|
|
||||||
|
siblings.forEach((aSibling) => {
|
||||||
|
const siblingName = lastPathComponent(aSibling.path)
|
||||||
|
if (!items.find((it) =>
|
||||||
|
((it.type === 'folder' || it.type === 'file') && it.path === aSibling.path) ||
|
||||||
|
(it.type === 'group' && it.title === siblingName))) {
|
||||||
|
const newEntry: BookmarkedItem = (aSibling instanceof TFolder) ? createBookmarkGroupEntry(siblingName) : createBookmarkFileEntry(aSibling.path)
|
||||||
|
items.push(newEntry)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const saveDataAndUpdateBookmarkViews = (plugin: Bookmarks_PluginInstance, app: App) => {
|
||||||
|
plugin.saveData()
|
||||||
|
const bookmarksLeafs = app.workspace.getLeavesOfType('bookmarks')
|
||||||
|
bookmarksLeafs?.forEach((leaf) => {
|
||||||
|
(leaf.view as any)?.update?.()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -6,3 +6,8 @@ export function isDefined(o: any): boolean {
|
||||||
export function last<T>(o: Array<T>): T | undefined {
|
export function last<T>(o: Array<T>): T | undefined {
|
||||||
return o?.length > 0 ? o[o.length - 1] : undefined
|
return o?.length > 0 ? o[o.length - 1] : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function lastPathComponent(path: string): string {
|
||||||
|
const pathComponents = (path ?? '').split('/')
|
||||||
|
return pathComponents.pop()!
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue