#74 - Integration with Bookmarks core plugin and support for indirect drag & drop arrangement
- functionality completed!!! - increased coverage of the new functionality with unit tests - more unit tests possible - basic manual tests done - next step: real-life usage tests
This commit is contained in:
parent
cd933cb4f0
commit
b854ce14ce
|
@ -27,6 +27,7 @@
|
||||||
"jest": "^28.1.1",
|
"jest": "^28.1.1",
|
||||||
"monkey-around": "^2.3.0",
|
"monkey-around": "^2.3.0",
|
||||||
"obsidian": "^0.15.4",
|
"obsidian": "^0.15.4",
|
||||||
|
"obsidian-1.4.11": "npm:obsidian@1.4.11",
|
||||||
"ts-jest": "^28.0.5",
|
"ts-jest": "^28.0.5",
|
||||||
"tslib": "2.4.0",
|
"tslib": "2.4.0",
|
||||||
"typescript": "4.7.4"
|
"typescript": "4.7.4"
|
||||||
|
|
|
@ -615,10 +615,8 @@ export const determineBookmarksOrderIfNeeded = (folderItems: Array<FolderItemFor
|
||||||
export const folderSort = function (sortingSpec: CustomSortSpec, ctx: ProcessingContext) {
|
export const folderSort = function (sortingSpec: CustomSortSpec, ctx: ProcessingContext) {
|
||||||
let fileExplorer = this.fileExplorer
|
let fileExplorer = this.fileExplorer
|
||||||
|
|
||||||
// shallow copy of groups
|
// shallow copy of groups and expand folder-specific macros on them
|
||||||
sortingSpec.groupsShadow = sortingSpec.groups?.map((group) => Object.assign({} as CustomSortGroup, group))
|
sortingSpec.groupsShadow = sortingSpec.groups?.map((group) => Object.assign({} as CustomSortGroup, group))
|
||||||
|
|
||||||
// expand folder-specific macros
|
|
||||||
const parentFolderName: string|undefined = this.file.name
|
const parentFolderName: string|undefined = this.file.name
|
||||||
expandMacros(sortingSpec, parentFolderName)
|
expandMacros(sortingSpec, parentFolderName)
|
||||||
|
|
||||||
|
@ -655,10 +653,15 @@ export const folderSort = function (sortingSpec: CustomSortSpec, ctx: Processing
|
||||||
};
|
};
|
||||||
|
|
||||||
// Returns a sorted copy of the input array, intentionally to keep it intact
|
// 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> {
|
export const sortFolderItemsForBookmarking = function (folder: TFolder, items: Array<TAbstractFile>, sortingSpec: CustomSortSpec|null|undefined, ctx: ProcessingContext, uiSortOrder: string): Array<TAbstractFile> {
|
||||||
if (sortingSpec) {
|
if (sortingSpec) {
|
||||||
const folderItemsByPath: { [key: string]: TAbstractFile } = {}
|
const folderItemsByPath: { [key: string]: TAbstractFile } = {}
|
||||||
|
|
||||||
|
// shallow copy of groups and expand folder-specific macros on them
|
||||||
|
sortingSpec.groupsShadow = sortingSpec.groups?.map((group) => Object.assign({} as CustomSortGroup, group))
|
||||||
|
const parentFolderName: string|undefined = folder.name
|
||||||
|
expandMacros(sortingSpec, parentFolderName)
|
||||||
|
|
||||||
const folderItems: Array<FolderItemForSorting> = items.map((entry: TFile | TFolder) => {
|
const folderItems: Array<FolderItemForSorting> = items.map((entry: TFile | TFolder) => {
|
||||||
folderItemsByPath[entry.path] = entry
|
folderItemsByPath[entry.path] = entry
|
||||||
const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec, ctx)
|
const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec, ctx)
|
||||||
|
|
76
src/main.ts
76
src/main.ts
|
@ -46,7 +46,8 @@ import {
|
||||||
import {getStarredPlugin} from "./utils/StarredPluginSignature";
|
import {getStarredPlugin} from "./utils/StarredPluginSignature";
|
||||||
import {
|
import {
|
||||||
BookmarksPluginInterface,
|
BookmarksPluginInterface,
|
||||||
getBookmarksPlugin
|
getBookmarksPlugin,
|
||||||
|
groupNameForPath
|
||||||
} from "./utils/BookmarksCorePluginSignature";
|
} from "./utils/BookmarksCorePluginSignature";
|
||||||
import {getIconFolderPlugin} from "./utils/ObsidianIconFolderPluginSignature";
|
import {getIconFolderPlugin} from "./utils/ObsidianIconFolderPluginSignature";
|
||||||
import {lastPathComponent} from "./utils/utils";
|
import {lastPathComponent} from "./utils/utils";
|
||||||
|
@ -357,6 +358,17 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const unbookmarkThisMenuItem = (item: MenuItem) => {
|
||||||
|
item.setTitle('Custom sort: UNbookmark from sorting.');
|
||||||
|
item.setIcon('hashtag');
|
||||||
|
item.onClick(() => {
|
||||||
|
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||||
|
if (bookmarksPlugin) {
|
||||||
|
bookmarksPlugin.unbookmarkFolderItem(file)
|
||||||
|
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
const bookmarkAllMenuItem = (item: MenuItem) => {
|
const bookmarkAllMenuItem = (item: MenuItem) => {
|
||||||
item.setTitle('Custom sort: bookmark+siblings for sorting.');
|
item.setTitle('Custom sort: bookmark+siblings for sorting.');
|
||||||
item.setIcon('hashtag');
|
item.setIcon('hashtag');
|
||||||
|
@ -369,15 +381,73 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const unbookmarkAllMenuItem = (item: MenuItem) => {
|
||||||
|
item.setTitle('Custom sort: UNbookmark+all siblings from sorting.');
|
||||||
|
item.setIcon('hashtag');
|
||||||
|
item.onClick(() => {
|
||||||
|
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||||
|
if (bookmarksPlugin) {
|
||||||
|
const orderedChildren: Array<TAbstractFile> = file.parent.children.map((entry: TFile | TFolder) => entry)
|
||||||
|
bookmarksPlugin.unbookmarkSiblings(orderedChildren)
|
||||||
|
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const itemAlreadyBookmarkedForSorting: boolean = bookmarksPlugin.isBookmarkedForSorting(file)
|
const itemAlreadyBookmarkedForSorting: boolean = bookmarksPlugin.isBookmarkedForSorting(file)
|
||||||
if (!itemAlreadyBookmarkedForSorting) {
|
if (!itemAlreadyBookmarkedForSorting) {
|
||||||
menu.addItem(bookmarkThisMenuItem)
|
menu.addItem(bookmarkThisMenuItem)
|
||||||
|
} else {
|
||||||
|
menu.addItem(unbookmarkThisMenuItem)
|
||||||
}
|
}
|
||||||
menu.addItem(bookmarkAllMenuItem)
|
menu.addItem(bookmarkAllMenuItem)
|
||||||
|
menu.addItem(unbookmarkAllMenuItem)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (requireApiVersion('1.4.11')) {
|
||||||
|
this.registerEvent(
|
||||||
|
// "files-menu" event was exposed in 1.4.11
|
||||||
|
// @ts-ignore
|
||||||
|
app.workspace.on("files-menu", (menu: Menu, files: TAbstractFile[], source: string, leaf?: WorkspaceLeaf) => {
|
||||||
|
if (!this.settings.bookmarksContextMenus) return; // Don't show the context menus at all
|
||||||
|
|
||||||
|
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||||
|
if (!bookmarksPlugin) return; // Don't show context menu if bookmarks plugin not available and not enabled
|
||||||
|
|
||||||
|
const bookmarkSelectedMenuItem = (item: MenuItem) => {
|
||||||
|
item.setTitle('Custom sort: bookmark selected for sorting.');
|
||||||
|
item.setIcon('hashtag');
|
||||||
|
item.onClick(() => {
|
||||||
|
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||||
|
if (bookmarksPlugin) {
|
||||||
|
files.forEach((file) => {
|
||||||
|
bookmarksPlugin.bookmarkFolderItem(file)
|
||||||
|
})
|
||||||
|
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const unbookmarkSelectedMenuItem = (item: MenuItem) => {
|
||||||
|
item.setTitle('Custom sort: UNbookmark selected from sorting.');
|
||||||
|
item.setIcon('hashtag');
|
||||||
|
item.onClick(() => {
|
||||||
|
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||||
|
if (bookmarksPlugin) {
|
||||||
|
files.forEach((file) => {
|
||||||
|
bookmarksPlugin.unbookmarkFolderItem(file)
|
||||||
|
})
|
||||||
|
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
menu.addItem(bookmarkSelectedMenuItem)
|
||||||
|
menu.addItem(unbookmarkSelectedMenuItem)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
this.registerEvent(
|
this.registerEvent(
|
||||||
app.vault.on("rename", (file: TAbstractFile, oldPath: string) => {
|
app.vault.on("rename", (file: TAbstractFile, oldPath: string) => {
|
||||||
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
|
||||||
|
@ -504,6 +574,7 @@ export default class CustomSortPlugin extends Plugin {
|
||||||
const has: HasSortingOrGrouping = collectSortingAndGroupingTypes(sortSpec)
|
const has: HasSortingOrGrouping = collectSortingAndGroupingTypes(sortSpec)
|
||||||
|
|
||||||
return sortFolderItemsForBookmarking(
|
return sortFolderItemsForBookmarking(
|
||||||
|
folder,
|
||||||
folder.children,
|
folder.children,
|
||||||
sortSpec,
|
sortSpec,
|
||||||
this.createProcessingContextForSorting(has),
|
this.createProcessingContextForSorting(has),
|
||||||
|
@ -669,7 +740,8 @@ 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() ? pathToFlatString(normalizePath(value)) : '';
|
value = groupNameForPath(value.trim()).trim()
|
||||||
|
this.plugin.settings.bookmarksGroupToConsumeAsOrderingReference = value ? pathToFlatString(normalizePath(value)) : '';
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
Integration with Bookmarks core plugin:
|
||||||
|
- support two approaches _at the same time_:
|
||||||
|
- (A) structured bookmarks inside a dedicated bookmarks group, and
|
||||||
|
- (B) a flat list of bookmarks inside the dedicated bookmarks group
|
||||||
|
|
||||||
|
For (A):
|
||||||
|
- preferred
|
||||||
|
- a folder is represented by a group in bookmarks
|
||||||
|
- a file is represented by a file-with-block
|
||||||
|
- this also applied to non-md files, like jpg and others
|
||||||
|
- guarantees _'hiding'_ the bookmarks-for-sorting from regular bookmarks usage scenarios
|
||||||
|
- bookmark entries for sorting are encapsulated in the dedicated group
|
||||||
|
- they don't interfere with bookmarking of files and folders via standard bookmarking
|
||||||
|
- only exact location of file bookmark / group matches for sorting order in file explorer
|
||||||
|
- the contextual bookmark menus always work in (A) mode
|
||||||
|
- the contextual menus create / modify the bookmarks structure on-the-fly
|
||||||
|
|
||||||
|
For (B):
|
||||||
|
- discouraged, yet supported (exception for some edge cases)
|
||||||
|
- typically a result of manual bookmarks management
|
||||||
|
- for small number of items seems reasonable
|
||||||
|
- for flat vaults it could look same as for (A)
|
||||||
|
- groups don't have a 'path' attribute, their path is determined by their location
|
||||||
|
- bookmarked folders represent folders if inside the bookmarks group for sorting
|
||||||
|
- yet in this way they interfere with regular bookmarks scenario
|
||||||
|
- file bookmarks work correctly in non-interfering way thanks to the _'artificial block reference'_
|
||||||
|
- file bookmarks not having the _'artificial block ref'_ work as well
|
||||||
|
- if they are in the designated bookmarks group
|
||||||
|
- if there isn't a duplicate, which has the _'artificial block ref'_
|
||||||
|
- yet in this way they interfere with regular bookmarks scenario
|
||||||
|
|
||||||
|
-[ ] TODO: review again the 'item moved' and 'item deleted' scenarios (they look ok, check if they don't delete/move too much)
|
||||||
|
- [x] fundamental question 1: should 'move' create a bookmark entry/structure if it is not covered by bookmarks?
|
||||||
|
- Answer: the moved item is removed from bookmarks. If it is a group with descendants not transparent for sorting,
|
||||||
|
it is renamed to become transparent for sorting.
|
||||||
|
By design, the order of items is property of the parent folder (the container) and not the items
|
||||||
|
- [x] fundamental question 2: should 'move' create a bookmark entry if moved item was not bookmarked, yet is moved to a folder covered by bookmarks?
|
||||||
|
- Answer: same as for previous point.
|
||||||
|
- [x] review from (A) and (B) perspective
|
||||||
|
- Answer: scenario (A) is fully handled by 'item moved' and 'item deleted'.
|
||||||
|
scenario (B) is partially handled for 'item moved'. Details to be read from code (too complex to cover here)
|
||||||
|
- [x] consider deletion of item outside of bookmarks sorting container group
|
||||||
|
Answer: bookmark items outside of bookmarks sorting container are not manipulated by custom-sort plugin
|
||||||
|
to not interfere with standard Bookmarks scenarios
|
||||||
|
- [x] consider moving an item outside of bookmarks group
|
||||||
|
- Answer: question not relevant. Items are moved in file explorer and bookmarks only reflect that, if needed.
|
||||||
|
Hence there is no concept of 'moving an item outside of bookmarks group' - bookmarks group only exists in bookmarks
|
||||||
|
- [x] edge case: bookmarked item is a group, the deleted/moved is a file, not a folder --> what to do?
|
||||||
|
- Answer: for moved files, only file bookmarks are scanned (and handles), for moved folders, only groups are scanned (and handled).
|
||||||
|
- [x] delete all instances at any level of bookmarks structure in 'delete' handler
|
||||||
|
- Answer: only instances of (A) or (B) are deleted. Items outside of bookmarks container for sorting or
|
||||||
|
in invalid locations in bookmarks hierarchy are ignored
|
||||||
|
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -9,6 +9,8 @@ import {
|
||||||
extractParentFolderPath,
|
extractParentFolderPath,
|
||||||
lastPathComponent
|
lastPathComponent
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
import {Arr} from "tern";
|
||||||
|
import * as process from "process";
|
||||||
|
|
||||||
const BookmarksPlugin_getBookmarks_methodName = 'getBookmarks'
|
const BookmarksPlugin_getBookmarks_methodName = 'getBookmarks'
|
||||||
|
|
||||||
|
@ -21,45 +23,48 @@ type Path = string
|
||||||
type BookmarkedItem = BookmarkedFile | BookmarkedFolder | BookmarkedGroup
|
type BookmarkedItem = BookmarkedFile | BookmarkedFolder | BookmarkedGroup
|
||||||
|
|
||||||
// Either a file, a folder or header/block inside a file
|
// Either a file, a folder or header/block inside a file
|
||||||
interface BookmarkWithPath {
|
interface BookmarkItemSuperset {
|
||||||
path: Path
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BookmarkedFile {
|
|
||||||
type: 'file'
|
|
||||||
path: Path
|
path: Path
|
||||||
|
title?: string
|
||||||
|
ctime: number
|
||||||
subpath?: string // Anchor within the file (heading and/or block ref)
|
subpath?: string // Anchor within the file (heading and/or block ref)
|
||||||
title?: string
|
|
||||||
ctime: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BookmarkedFolder {
|
interface BookmarkWithPath extends Pick<BookmarkItemSuperset, 'path'> {
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BookmarkedFile extends BookmarkItemSuperset {
|
||||||
|
type: 'file'
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BookmarkedFolder extends Omit<BookmarkItemSuperset, 'subpath'> {
|
||||||
type: 'folder'
|
type: 'folder'
|
||||||
path: Path
|
|
||||||
title?: string
|
|
||||||
ctime: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BookmarkedGroup {
|
interface BookmarkedGroup extends Omit<BookmarkItemSuperset, 'subpath'|'path'> {
|
||||||
type: 'group'
|
type: 'group'
|
||||||
items: Array<BookmarkedItem>
|
items: Array<BookmarkedItem>
|
||||||
title?: string
|
|
||||||
ctime: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BookmarkedItemPath = string
|
export type BookmarkedItemPath = string
|
||||||
|
|
||||||
export interface OrderedBookmarkedItem {
|
export interface OrderedBookmarkedItemWithMetadata {
|
||||||
file: boolean
|
isGroup?: boolean
|
||||||
folder: boolean
|
|
||||||
group: boolean
|
|
||||||
path: BookmarkedItemPath
|
path: BookmarkedItemPath
|
||||||
|
hasSortingIndicator?: boolean
|
||||||
order: number
|
order: number
|
||||||
bookmarkPathOverlap: number|true // how much the location in bookmarks hierarchy matches the actual file/folder path
|
bookmarkPathMatches?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OrderedBookmarks {
|
export type OrderedBookmarkedItem = Pick<OrderedBookmarkedItemWithMetadata, 'order'>
|
||||||
[key: BookmarkedItemPath]: OrderedBookmarkedItem
|
export type Order = number
|
||||||
|
|
||||||
|
export interface OrderedBookmarks {
|
||||||
|
[key: BookmarkedItemPath]: Order
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderedBookmarksWithMetadata {
|
||||||
|
[key: BookmarkedItemPath]: OrderedBookmarkedItemWithMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Bookmarks_PluginInstance extends PluginInstance {
|
interface Bookmarks_PluginInstance extends PluginInstance {
|
||||||
|
@ -72,8 +77,10 @@ interface Bookmarks_PluginInstance extends PluginInstance {
|
||||||
export interface BookmarksPluginInterface {
|
export interface BookmarksPluginInterface {
|
||||||
determineBookmarkOrder(path: string): number|undefined
|
determineBookmarkOrder(path: string): number|undefined
|
||||||
bookmarkFolderItem(item: TAbstractFile): void
|
bookmarkFolderItem(item: TAbstractFile): void
|
||||||
|
unbookmarkFolderItem(item: TAbstractFile): void
|
||||||
saveDataAndUpdateBookmarkViews(updateBookmarkViews: boolean): void
|
saveDataAndUpdateBookmarkViews(updateBookmarkViews: boolean): void
|
||||||
bookmarkSiblings(siblings: Array<TAbstractFile>, inTheTop?: boolean): void
|
bookmarkSiblings(siblings: Array<TAbstractFile>, inTheTop?: boolean): void
|
||||||
|
unbookmarkSiblings(siblings: Array<TAbstractFile>): void
|
||||||
updateSortingBookmarksAfterItemRenamed(renamedItem: TAbstractFile, oldPath: string): void
|
updateSortingBookmarksAfterItemRenamed(renamedItem: TAbstractFile, oldPath: string): void
|
||||||
updateSortingBookmarksAfterItemDeleted(deletedItem: TAbstractFile): void
|
updateSortingBookmarksAfterItemDeleted(deletedItem: TAbstractFile): void
|
||||||
isBookmarkedForSorting(item: TAbstractFile): boolean
|
isBookmarkedForSorting(item: TAbstractFile): boolean
|
||||||
|
@ -82,6 +89,25 @@ export interface BookmarksPluginInterface {
|
||||||
bookmarksIncludeItemsInFolder(folderPath: string): boolean
|
bookmarksIncludeItemsInFolder(folderPath: string): boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkSubtreeForOnlyTransparentGroups = (items: Array<BookmarkedItem>): boolean => {
|
||||||
|
if (!items || items?.length === 0) return true
|
||||||
|
for (let it of items) {
|
||||||
|
if (it.type !== 'group' || !it.title || !isGroupTransparentForSorting(it.title)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// it is a group transparent for sorting
|
||||||
|
const isEmptyOrTransparent: boolean = checkSubtreeForOnlyTransparentGroups(it.items)
|
||||||
|
if (!isEmptyOrTransparent) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants = (group: BookmarkedGroup): boolean => {
|
||||||
|
return checkSubtreeForOnlyTransparentGroups(group.items)
|
||||||
|
}
|
||||||
|
|
||||||
class BookmarksPluginWrapper implements BookmarksPluginInterface {
|
class BookmarksPluginWrapper implements BookmarksPluginInterface {
|
||||||
|
|
||||||
plugin: Bookmarks_PluginInstance|undefined
|
plugin: Bookmarks_PluginInstance|undefined
|
||||||
|
@ -96,16 +122,16 @@ class BookmarksPluginWrapper implements BookmarksPluginInterface {
|
||||||
// Intentionally not returning 0 to allow simple syntax of processing the result
|
// Intentionally not returning 0 to allow simple syntax of processing the result
|
||||||
//
|
//
|
||||||
// Parameterless invocation enforces cache population, if empty
|
// Parameterless invocation enforces cache population, if empty
|
||||||
determineBookmarkOrder = (path?: string): number | undefined => {
|
determineBookmarkOrder = (path?: string): Order | undefined => {
|
||||||
if (!bookmarksCache) {
|
if (!bookmarksCache) {
|
||||||
[bookmarksCache, bookmarksFoldersCoverage] = getOrderedBookmarks(this.plugin!, this.groupNameForSorting)
|
[bookmarksCache, bookmarksFoldersCoverage] = getOrderedBookmarks(this.plugin!, this.groupNameForSorting)
|
||||||
bookmarksCacheTimestamp = Date.now()
|
bookmarksCacheTimestamp = Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path && path.length > 0) {
|
if (path && path.length > 0) {
|
||||||
const bookmarkedItemPosition: number | undefined = bookmarksCache?.[path]?.order
|
const bookmarkedItemPosition: Order | undefined = bookmarksCache?.[path]
|
||||||
|
|
||||||
return (bookmarkedItemPosition !== undefined && bookmarkedItemPosition >= 0) ? (bookmarkedItemPosition + 1) : undefined
|
return (bookmarkedItemPosition && bookmarkedItemPosition > 0) ? bookmarkedItemPosition : undefined
|
||||||
} else {
|
} else {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
@ -115,6 +141,10 @@ class BookmarksPluginWrapper implements BookmarksPluginInterface {
|
||||||
this.bookmarkSiblings([item], true)
|
this.bookmarkSiblings([item], true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unbookmarkFolderItem = (item: TAbstractFile) => {
|
||||||
|
this.unbookmarkSiblings([item])
|
||||||
|
}
|
||||||
|
|
||||||
saveDataAndUpdateBookmarkViews = (updateBookmarkViews: boolean = true) => {
|
saveDataAndUpdateBookmarkViews = (updateBookmarkViews: boolean = true) => {
|
||||||
this.plugin!.onItemsChanged(true)
|
this.plugin!.onItemsChanged(true)
|
||||||
if (updateBookmarkViews) {
|
if (updateBookmarkViews) {
|
||||||
|
@ -138,7 +168,13 @@ class BookmarksPluginWrapper implements BookmarksPluginInterface {
|
||||||
if (bookmarksContainer) { // for sanity, the group should be always created if missing
|
if (bookmarksContainer) { // for sanity, the group should be always created if missing
|
||||||
siblings.forEach((aSibling) => {
|
siblings.forEach((aSibling) => {
|
||||||
const siblingName = lastPathComponent(aSibling.path)
|
const siblingName = lastPathComponent(aSibling.path)
|
||||||
if (!bookmarksContainer.items.find((it) =>
|
const groupTransparentForSorting = bookmarksContainer.items.find((it) => (
|
||||||
|
it.type === 'group' && groupNameForPath(it.title||'') === siblingName && isGroupTransparentForSorting(it.title)
|
||||||
|
))
|
||||||
|
if (groupTransparentForSorting) {
|
||||||
|
// got a group transparent for sorting
|
||||||
|
groupTransparentForSorting.title = groupNameForPath(groupTransparentForSorting.title||'')
|
||||||
|
} else if (!bookmarksContainer.items.find((it) =>
|
||||||
((it.type === 'folder' || it.type === 'file') && it.path === aSibling.path) ||
|
((it.type === 'folder' || it.type === 'file') && it.path === aSibling.path) ||
|
||||||
(it.type === 'group' && it.title === siblingName))) {
|
(it.type === 'group' && it.title === siblingName))) {
|
||||||
const newEntry: BookmarkedItem = (aSibling instanceof TFolder) ? createBookmarkGroupEntry(siblingName) : createBookmarkFileEntry(aSibling.path);
|
const newEntry: BookmarkedItem = (aSibling instanceof TFolder) ? createBookmarkGroupEntry(siblingName) : createBookmarkFileEntry(aSibling.path);
|
||||||
|
@ -152,6 +188,46 @@ class BookmarksPluginWrapper implements BookmarksPluginInterface {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unbookmarkSiblings = (siblings: Array<TAbstractFile>) => {
|
||||||
|
if (siblings.length === 0) return // for sanity
|
||||||
|
|
||||||
|
const bookmarksContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(
|
||||||
|
siblings[0].path,
|
||||||
|
DontCreateIfMissing,
|
||||||
|
this.plugin!,
|
||||||
|
this.groupNameForSorting
|
||||||
|
)
|
||||||
|
|
||||||
|
if (bookmarksContainer) { // for sanity
|
||||||
|
const bookmarkedItemsToRemove: Array<BookmarkedItem> = []
|
||||||
|
siblings.forEach((aSibling) => {
|
||||||
|
const siblingName = lastPathComponent(aSibling.path)
|
||||||
|
const aGroup = bookmarksContainer.items.find(
|
||||||
|
(it) => (it.type === 'group' && groupNameForPath(it.title||'') === siblingName)
|
||||||
|
)
|
||||||
|
if (aGroup) {
|
||||||
|
const canBeRemoved = bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants(aGroup as BookmarkedGroup)
|
||||||
|
if (canBeRemoved) {
|
||||||
|
bookmarksContainer.items.remove(aGroup)
|
||||||
|
cleanupBookmarkTreeFromTransparentEmptyGroups(bookmarksContainer, this.plugin!, this.groupNameForSorting)
|
||||||
|
} else {
|
||||||
|
if (!isGroupTransparentForSorting(aGroup.title)) {
|
||||||
|
aGroup.title = groupNameTransparentForSorting(aGroup.title||'')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const aFileOrFolder = bookmarksContainer.items.find(
|
||||||
|
(it) => ((it.type === 'folder' || it.type === 'file') && it.path === aSibling.path)
|
||||||
|
)
|
||||||
|
if (aFileOrFolder) {
|
||||||
|
bookmarksContainer.items.remove(aFileOrFolder)
|
||||||
|
cleanupBookmarkTreeFromTransparentEmptyGroups(bookmarksContainer, this.plugin!, this.groupNameForSorting)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateSortingBookmarksAfterItemRenamed = (renamedItem: TAbstractFile, oldPath: string): void => {
|
updateSortingBookmarksAfterItemRenamed = (renamedItem: TAbstractFile, oldPath: string): void => {
|
||||||
updateSortingBookmarksAfterItemRenamed(this.plugin!, renamedItem, oldPath, this.groupNameForSorting)
|
updateSortingBookmarksAfterItemRenamed(this.plugin!, renamedItem, oldPath, this.groupNameForSorting)
|
||||||
}
|
}
|
||||||
|
@ -177,7 +253,6 @@ class BookmarksPluginWrapper implements BookmarksPluginInterface {
|
||||||
}
|
}
|
||||||
|
|
||||||
bookmarksIncludeItemsInFolder = (folderPath: string): boolean => {
|
bookmarksIncludeItemsInFolder = (folderPath: string): boolean => {
|
||||||
console.error(`C: for ${folderPath} is ${bookmarksFoldersCoverage?.[folderPath]}`)
|
|
||||||
return !! bookmarksFoldersCoverage?.[folderPath]
|
return !! bookmarksFoldersCoverage?.[folderPath]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,7 +265,8 @@ export const getBookmarksPlugin = (bookmarksGroupName?: string, forceFlushCache?
|
||||||
if (installedBookmarksPlugin && installedBookmarksPlugin.enabled && installedBookmarksPlugin.instance) {
|
if (installedBookmarksPlugin && installedBookmarksPlugin.enabled && installedBookmarksPlugin.instance) {
|
||||||
const bookmarksPluginInstance: Bookmarks_PluginInstance = installedBookmarksPlugin.instance as Bookmarks_PluginInstance
|
const bookmarksPluginInstance: Bookmarks_PluginInstance = installedBookmarksPlugin.instance as Bookmarks_PluginInstance
|
||||||
// defensive programming, in case Obsidian changes its internal APIs
|
// defensive programming, in case Obsidian changes its internal APIs
|
||||||
if (typeof bookmarksPluginInstance?.[BookmarksPlugin_getBookmarks_methodName] === 'function') {
|
if (typeof bookmarksPluginInstance?.[BookmarksPlugin_getBookmarks_methodName] === 'function' &&
|
||||||
|
Array.isArray(bookmarksPluginInstance?.[BookmarksPlugin_items_collectionName])) {
|
||||||
bookmarksPlugin.plugin = bookmarksPluginInstance
|
bookmarksPlugin.plugin = bookmarksPluginInstance
|
||||||
bookmarksPlugin.groupNameForSorting = bookmarksGroupName
|
bookmarksPlugin.groupNameForSorting = bookmarksGroupName
|
||||||
if (ensureCachePopulated && !bookmarksCache) {
|
if (ensureCachePopulated && !bookmarksCache) {
|
||||||
|
@ -231,11 +307,16 @@ const invalidateExpiredBookmarksCache = (force?: boolean): void => {
|
||||||
type TraverseCallback = (item: BookmarkedItem, parentsGroupsPath: string) => boolean | void
|
type TraverseCallback = (item: BookmarkedItem, parentsGroupsPath: string) => boolean | void
|
||||||
|
|
||||||
const traverseBookmarksCollection = (items: Array<BookmarkedItem>, callbackConsumeItem: TraverseCallback) => {
|
const traverseBookmarksCollection = (items: Array<BookmarkedItem>, callbackConsumeItem: TraverseCallback) => {
|
||||||
|
if (!Array.isArray(items)) return
|
||||||
const recursiveTraversal = (collection: Array<BookmarkedItem>, groupsPath: string) => {
|
const recursiveTraversal = (collection: Array<BookmarkedItem>, groupsPath: string) => {
|
||||||
|
if (!Array.isArray(collection)) return
|
||||||
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 (callbackConsumeItem(item, groupsPath)) return;
|
if (callbackConsumeItem(item, groupsPath)) return;
|
||||||
if ('group' === item.type) recursiveTraversal(item.items, `${groupsPath}${groupsPath?'/':''}${item.title}`);
|
if ('group' === item.type) {
|
||||||
|
const groupNameToUseInPath: string = groupNameForPath(item.title || '')
|
||||||
|
recursiveTraversal(item.items, `${groupsPath}${groupsPath?'/':''}${groupNameToUseInPath}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
recursiveTraversal(items, '');
|
recursiveTraversal(items, '');
|
||||||
|
@ -243,69 +324,94 @@ const traverseBookmarksCollection = (items: Array<BookmarkedItem>, callbackConsu
|
||||||
|
|
||||||
const ARTIFICIAL_ANCHOR_SORTING_BOOKMARK_INDICATOR = '#^-'
|
const ARTIFICIAL_ANCHOR_SORTING_BOOKMARK_INDICATOR = '#^-'
|
||||||
|
|
||||||
const bookmarkLocationAndPathOverlap = (bookmarkParentGroupPath: string, fileOrFolderPath: string): number => {
|
|
||||||
return fileOrFolderPath?.startsWith(bookmarkParentGroupPath) ? bookmarkParentGroupPath.length : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const ROOT_FOLDER_PATH = '/'
|
const ROOT_FOLDER_PATH = '/'
|
||||||
|
|
||||||
|
const TRANSPARENT_FOR_SORTING_PREFIX = '\\\\'
|
||||||
|
|
||||||
|
const isGroupTransparentForSorting = (name?: string): boolean => {
|
||||||
|
return !!name?.startsWith(TRANSPARENT_FOR_SORTING_PREFIX)
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupNameTransparentForSorting = (name: string): string => {
|
||||||
|
return isGroupTransparentForSorting(name) ? name : `${TRANSPARENT_FOR_SORTING_PREFIX}${name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const groupNameForPath = (name: string): string => {
|
||||||
|
return isGroupTransparentForSorting(name) ? name.substring(TRANSPARENT_FOR_SORTING_PREFIX.length) : name
|
||||||
|
}
|
||||||
|
|
||||||
const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroupName?: string): [OrderedBookmarks, FoldersCoverage] | [undefined, undefined] => {
|
const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance, bookmarksGroupName?: string): [OrderedBookmarks, FoldersCoverage] | [undefined, undefined] => {
|
||||||
console.log(`getOrderedBookmarks()`)
|
|
||||||
let bookmarksItems: Array<BookmarkedItem> | undefined = plugin?.[BookmarksPlugin_items_collectionName]
|
let bookmarksItems: Array<BookmarkedItem> | undefined = plugin?.[BookmarksPlugin_items_collectionName]
|
||||||
let bookmarksCoveredFolders: FoldersCoverage = {}
|
let bookmarksCoveredFolders: FoldersCoverage = {}
|
||||||
if (bookmarksItems) {
|
if (bookmarksItems && Array.isArray(bookmarksItems)) {
|
||||||
if (bookmarksGroupName) {
|
if (bookmarksGroupName) {
|
||||||
// scanning only top level items because by desing the bookmarks group for sorting is a top level item
|
// scanning only top level items because by design the bookmarks group for sorting is a top level item
|
||||||
const bookmarksGroup: BookmarkedGroup|undefined = bookmarksItems.find(
|
const bookmarksGroup: BookmarkedGroup|undefined = bookmarksItems.find(
|
||||||
(item) => item.type === 'group' && item.title === bookmarksGroupName
|
(item) => item.type === 'group' && item.title === bookmarksGroupName
|
||||||
) as BookmarkedGroup
|
) as BookmarkedGroup
|
||||||
bookmarksItems = bookmarksGroup ? bookmarksGroup.items : undefined
|
bookmarksItems = bookmarksGroup ? bookmarksGroup.items : undefined
|
||||||
}
|
}
|
||||||
if (bookmarksItems) {
|
if (bookmarksItems) {
|
||||||
const orderedBookmarks: OrderedBookmarks = {}
|
const orderedBookmarksWithMetadata: OrderedBookmarksWithMetadata = {}
|
||||||
let order: number = 0
|
let order: number = 1 // Intentionally start > 0 to allow easy check: if (order) ...
|
||||||
const consumeItem = (item: BookmarkedItem, parentGroupsPath: string) => {
|
const consumeItem = (item: BookmarkedItem, parentGroupsPath: string) => {
|
||||||
const isFile: boolean = item.type === 'file'
|
if ('group' === item.type) {
|
||||||
const hasSortspecAnchor: boolean = isFile && (item as BookmarkedFile).subpath === ARTIFICIAL_ANCHOR_SORTING_BOOKMARK_INDICATOR
|
if (!isGroupTransparentForSorting(item.title)) {
|
||||||
const isFolder: boolean = item.type === 'folder'
|
const path: string = `${parentGroupsPath}${parentGroupsPath ? '/' : ''}${item.title}`
|
||||||
const isGroup: boolean = item.type === 'group'
|
const alreadyConsumed = orderedBookmarksWithMetadata[path]
|
||||||
if ((isFile && hasSortspecAnchor) || isFolder || isGroup) {
|
if (alreadyConsumed) {
|
||||||
const pathOfGroup: string = `${parentGroupsPath}${parentGroupsPath?'/':''}${item.title}`
|
if (alreadyConsumed.isGroup) return // Defensive programming
|
||||||
const path = isGroup ? pathOfGroup : (item as BookmarkWithPath).path
|
if (alreadyConsumed.hasSortingIndicator) return
|
||||||
const alreadyConsumed = orderedBookmarks[path]
|
|
||||||
|
|
||||||
const parentFolderPathOfBookmarkedItem = isGroup ? parentGroupsPath : extractParentFolderPath(path)
|
|
||||||
console.log(`Add ${path}`)
|
|
||||||
bookmarksCoveredFolders[parentFolderPathOfBookmarkedItem.length > 0 ? parentFolderPathOfBookmarkedItem : ROOT_FOLDER_PATH] = true
|
|
||||||
console.log(bookmarksCoveredFolders)
|
|
||||||
// for groups (they represent folders from sorting perspective) bookmark them unconditionally
|
|
||||||
// the idea of better match is not applicable
|
|
||||||
if (alreadyConsumed && isGroup && alreadyConsumed.group) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// for files and folders (folder can be only manually bookmarked, the plugin uses groups to represent folders)
|
orderedBookmarksWithMetadata[path] = {
|
||||||
// the most closely matching location in bookmarks hierarchy is preferred
|
|
||||||
let pathOverlapLength: number|undefined
|
|
||||||
if (alreadyConsumed && (isFile || isFolder)) {
|
|
||||||
pathOverlapLength = bookmarkLocationAndPathOverlap(parentGroupsPath, path)
|
|
||||||
if (pathOverlapLength <= alreadyConsumed.bookmarkPathOverlap) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
orderedBookmarks[path] = {
|
|
||||||
path: path,
|
path: path,
|
||||||
order: order++,
|
order: order++,
|
||||||
file: isFile,
|
isGroup: true
|
||||||
folder: isFile,
|
}
|
||||||
group: isGroup,
|
}
|
||||||
bookmarkPathOverlap: isGroup || (pathOverlapLength ?? bookmarkLocationAndPathOverlap(parentGroupsPath, path))
|
} else if ('file' === item.type || 'folder' === item.type) {
|
||||||
|
const itemWithPath = (item as BookmarkWithPath)
|
||||||
|
const itemFile = 'file' === item.type ? (item as BookmarkedFile) : undefined
|
||||||
|
const alreadyConsumed = orderedBookmarksWithMetadata[itemWithPath.path]
|
||||||
|
const hasSortingIndicator: boolean|undefined = itemFile ? itemFile.subpath === ARTIFICIAL_ANCHOR_SORTING_BOOKMARK_INDICATOR : undefined
|
||||||
|
const parentFolderPathOfBookmarkedItem = extractParentFolderPath(itemWithPath.path)
|
||||||
|
const bookmarkPathMatches: boolean = parentGroupsPath === parentFolderPathOfBookmarkedItem
|
||||||
|
const bookmarkPathIsRoot: boolean = !(parentGroupsPath?.length > 0)
|
||||||
|
|
||||||
|
// Bookmarks not in root (group) or in matching path are ignored
|
||||||
|
if (!bookmarkPathMatches && !bookmarkPathIsRoot) return
|
||||||
|
|
||||||
|
// For bookmarks in root or in matching path, apply the prioritized duplicate elimination logic
|
||||||
|
if (alreadyConsumed) {
|
||||||
|
if (hasSortingIndicator) {
|
||||||
|
if (alreadyConsumed.hasSortingIndicator && alreadyConsumed.bookmarkPathMatches) return
|
||||||
|
if (alreadyConsumed.hasSortingIndicator && !bookmarkPathMatches) return
|
||||||
|
} else { // no sorting indicator on new
|
||||||
|
if (alreadyConsumed.hasSortingIndicator) return
|
||||||
|
if (!bookmarkPathMatches || alreadyConsumed.bookmarkPathMatches || alreadyConsumed.isGroup) return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
orderedBookmarksWithMetadata[itemWithPath.path] = {
|
||||||
|
path: itemWithPath.path,
|
||||||
|
order: order++,
|
||||||
|
isGroup: false,
|
||||||
|
bookmarkPathMatches: bookmarkPathMatches,
|
||||||
|
hasSortingIndicator: hasSortingIndicator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
traverseBookmarksCollection(bookmarksItems, consumeItem)
|
traverseBookmarksCollection(bookmarksItems, consumeItem)
|
||||||
|
|
||||||
|
const orderedBookmarks: OrderedBookmarks = {}
|
||||||
|
|
||||||
|
for (let path in orderedBookmarksWithMetadata) {
|
||||||
|
orderedBookmarks[path] = orderedBookmarksWithMetadata[path].order
|
||||||
|
const parentFolderPath: Path = extractParentFolderPath(path)
|
||||||
|
bookmarksCoveredFolders[parentFolderPath.length > 0 ? parentFolderPath : ROOT_FOLDER_PATH] = true
|
||||||
|
}
|
||||||
return [orderedBookmarks, bookmarksCoveredFolders]
|
return [orderedBookmarks, bookmarksCoveredFolders]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -322,18 +428,16 @@ const createBookmarkGroupEntry = (title: string): BookmarkedGroup => {
|
||||||
return { type: "group", ctime: Date.now(), items: [], title: title }
|
return { type: "group", ctime: Date.now(), items: [], title: title }
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BookmarkedParentFolder {
|
export interface BookmarkedParentFolder {
|
||||||
|
pathOfGroup?: Path // undefined when the container is the root of bookmarks
|
||||||
group?: BookmarkedGroup // undefined when the item is at root level of bookmarks
|
group?: BookmarkedGroup // undefined when the item is at root level of bookmarks
|
||||||
items: Array<BookmarkedItem> // reference to group.items or to root collection of bookmarks
|
items: Array<BookmarkedItem> // reference to group.items or to root collection of bookmarks
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ItemInBookmarks {
|
|
||||||
parentItemsCollection: Array<BookmarkedItem>
|
|
||||||
item: BookmarkedItem
|
|
||||||
}
|
|
||||||
|
|
||||||
const findGroupForItemPathInBookmarks = (itemPath: string, createIfMissing: boolean, plugin: Bookmarks_PluginInstance, bookmarksGroup?: string): BookmarkedParentFolder|undefined => {
|
const findGroupForItemPathInBookmarks = (itemPath: string, createIfMissing: boolean, plugin: Bookmarks_PluginInstance, bookmarksGroup?: string): BookmarkedParentFolder|undefined => {
|
||||||
let items = plugin[BookmarksPlugin_items_collectionName]
|
let items = plugin?.[BookmarksPlugin_items_collectionName]
|
||||||
|
|
||||||
|
if (!Array.isArray(items)) return undefined
|
||||||
|
|
||||||
if (!itemPath || !itemPath.trim()) return undefined // for sanity
|
if (!itemPath || !itemPath.trim()) return undefined // for sanity
|
||||||
|
|
||||||
|
@ -347,11 +451,13 @@ const findGroupForItemPathInBookmarks = (itemPath: string, createIfMissing: bool
|
||||||
|
|
||||||
let group: BookmarkedGroup|undefined = undefined
|
let group: BookmarkedGroup|undefined = undefined
|
||||||
|
|
||||||
parentPathComponents.forEach((pathSegment) => {
|
parentPathComponents.forEach((pathSegment, index) => {
|
||||||
let group: BookmarkedGroup|undefined = items.find((it) => it.type === 'group' && it.title === pathSegment) as BookmarkedGroup
|
group = items.find((it) => it.type === 'group' && groupNameForPath(it.title||'') === pathSegment) as BookmarkedGroup
|
||||||
if (!group) {
|
if (!group) {
|
||||||
if (createIfMissing) {
|
if (createIfMissing) {
|
||||||
group = createBookmarkGroupEntry(pathSegment)
|
const theSortingBookmarksContainerGroup = (bookmarksGroup && index === 0)
|
||||||
|
const groupName: string = theSortingBookmarksContainerGroup ? pathSegment : groupNameTransparentForSorting(pathSegment)
|
||||||
|
group = createBookmarkGroupEntry(groupName)
|
||||||
items.push(group)
|
items.push(group)
|
||||||
} else {
|
} else {
|
||||||
return undefined
|
return undefined
|
||||||
|
@ -363,19 +469,50 @@ const findGroupForItemPathInBookmarks = (itemPath: string, createIfMissing: bool
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: items,
|
items: items,
|
||||||
group: group
|
group: group,
|
||||||
|
pathOfGroup: parentPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateIfMissing = true
|
const CreateIfMissing = true
|
||||||
const DontCreateIfMissing = false
|
const DontCreateIfMissing = false
|
||||||
|
|
||||||
|
const renameGroup = (group: BookmarkedGroup, newName: string, makeTransparentForSorting: boolean|undefined) => {
|
||||||
|
if (makeTransparentForSorting === true) {
|
||||||
|
group.title = groupNameTransparentForSorting(newName)
|
||||||
|
} else if (makeTransparentForSorting === false) {
|
||||||
|
group.title = newName
|
||||||
|
} else { // no transparency status, retain the status as-is
|
||||||
|
group.title = isGroupTransparentForSorting(group.title) ? groupNameTransparentForSorting(newName) : newName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanupBookmarkTreeFromTransparentEmptyGroups = (parentGroup: BookmarkedParentFolder|undefined, plugin: Bookmarks_PluginInstance, bookmarksGroup?: string) => {
|
||||||
|
|
||||||
|
if (!parentGroup) return // invalid invocation - exit
|
||||||
|
if (!parentGroup.group) return // root folder of the bookmarks - do not touch items in root folder
|
||||||
|
|
||||||
|
if (checkSubtreeForOnlyTransparentGroups(parentGroup.items)) {
|
||||||
|
parentGroup.group.items = []
|
||||||
|
|
||||||
|
const parentContainerOfGroup = findGroupForItemPathInBookmarks(
|
||||||
|
parentGroup.pathOfGroup || '',
|
||||||
|
DontCreateIfMissing,
|
||||||
|
plugin,
|
||||||
|
bookmarksGroup
|
||||||
|
)
|
||||||
|
if (parentContainerOfGroup) {
|
||||||
|
parentContainerOfGroup.group?.items?.remove(parentGroup.group)
|
||||||
|
cleanupBookmarkTreeFromTransparentEmptyGroups(parentContainerOfGroup, plugin, bookmarksGroup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updateSortingBookmarksAfterItemRenamed = (plugin: Bookmarks_PluginInstance, renamedItem: TAbstractFile, oldPath: string, bookmarksGroup?: string) => {
|
const updateSortingBookmarksAfterItemRenamed = (plugin: Bookmarks_PluginInstance, renamedItem: TAbstractFile, oldPath: string, bookmarksGroup?: string) => {
|
||||||
|
|
||||||
if (renamedItem.path === oldPath) return; // sanity
|
if (renamedItem.path === oldPath) return; // sanity
|
||||||
|
|
||||||
const aFolder: boolean = renamedItem instanceof TFolder
|
const aFolder: boolean = renamedItem instanceof TFolder
|
||||||
const aFolderWithChildren: boolean = aFolder && (renamedItem as TFolder).children.length > 0
|
|
||||||
const aFile: boolean = !aFolder
|
const aFile: boolean = !aFolder
|
||||||
const oldParentPath: string = extractParentFolderPath(oldPath)
|
const oldParentPath: string = extractParentFolderPath(oldPath)
|
||||||
const oldName: string = lastPathComponent(oldPath)
|
const oldName: string = lastPathComponent(oldPath)
|
||||||
|
@ -384,35 +521,58 @@ const updateSortingBookmarksAfterItemRenamed = (plugin: Bookmarks_PluginInstance
|
||||||
const moved: boolean = oldParentPath !== newParentPath
|
const moved: boolean = oldParentPath !== newParentPath
|
||||||
const renamed: boolean = oldName !== newName
|
const renamed: boolean = oldName !== newName
|
||||||
|
|
||||||
|
// file renames are handled automatically by Obsidian in bookmarks, no need for additional actions
|
||||||
|
if (aFile && renamed) return
|
||||||
|
|
||||||
const originalContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(oldPath, DontCreateIfMissing, plugin, bookmarksGroup)
|
const originalContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(oldPath, DontCreateIfMissing, plugin, bookmarksGroup)
|
||||||
|
|
||||||
if (!originalContainer) return;
|
if (!originalContainer) return;
|
||||||
|
|
||||||
const item: BookmarkedItem|undefined = originalContainer.items.find((it) => {
|
const item: BookmarkedItem|undefined = aFolder ?
|
||||||
if (aFolder && it.type === 'group' && it.title === oldName) return true;
|
originalContainer.items.find((it) => (
|
||||||
if (aFile && it.type === 'file' && it.path === oldPath) return true;
|
it.type === 'group' && groupNameForPath(it.title||'') === oldName
|
||||||
})
|
))
|
||||||
|
: // aFile
|
||||||
|
originalContainer.items.find((it) => (
|
||||||
|
it.type === 'file' && it.path === renamedItem.path
|
||||||
|
))
|
||||||
|
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
// The renamed/moved item was located in bookmarks, apply the necessary bookmarks updates
|
// The renamed/moved item was located in bookmarks, actions depend on item type
|
||||||
|
if (aFile) {
|
||||||
|
if (moved) { // sanity
|
||||||
|
originalContainer.group?.items.remove(item)
|
||||||
|
cleanupBookmarkTreeFromTransparentEmptyGroups(originalContainer, plugin, bookmarksGroup)
|
||||||
|
}
|
||||||
|
} else { // a group
|
||||||
|
const aGroup: BookmarkedGroup = item as BookmarkedGroup
|
||||||
|
|
||||||
let itemRemovedFromBookmarks: boolean = false
|
if (bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants(aGroup)) {
|
||||||
|
if (moved) { // sanity
|
||||||
|
originalContainer.group?.items.remove(aGroup)
|
||||||
|
cleanupBookmarkTreeFromTransparentEmptyGroups(originalContainer, plugin, bookmarksGroup)
|
||||||
|
} else if (renamed) {
|
||||||
|
renameGroup(aGroup, newName, undefined)
|
||||||
|
}
|
||||||
|
} else { // group has some descendants not transparent for sorting
|
||||||
if (moved) {
|
if (moved) {
|
||||||
originalContainer.items.remove(item)
|
originalContainer.group?.items.remove(aGroup)
|
||||||
const createTargetLocation: boolean = aFolderWithChildren
|
const targetContainer: BookmarkedParentFolder | undefined = findGroupForItemPathInBookmarks(renamedItem.path, CreateIfMissing, plugin, bookmarksGroup)
|
||||||
const targetContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(renamedItem.path, createTargetLocation, plugin, bookmarksGroup)
|
|
||||||
if (targetContainer) {
|
if (targetContainer) {
|
||||||
targetContainer.items.push(item)
|
targetContainer.group?.items.push(aGroup)
|
||||||
} else {
|
// the group in new location becomes by design transparent for sorting.
|
||||||
itemRemovedFromBookmarks = true // open question: remove from bookmarks indeed, if target location was not under bookmarks control?
|
// The sorting order is a property of the parent folder, not the item itself
|
||||||
|
renameGroup(aGroup, groupNameForPath(aGroup.title||''), true)
|
||||||
}
|
}
|
||||||
|
cleanupBookmarkTreeFromTransparentEmptyGroups(originalContainer, plugin, bookmarksGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aFolder && renamed && !itemRemovedFromBookmarks) {
|
if (renamed) {
|
||||||
// Renames of files are handled automatically by Bookmarks core plugin, only need to handle folder rename
|
// unrealistic scenario when a folder is moved and renamed at the same time
|
||||||
// because folders are represented (for sorting purposes) by groups with exact name
|
renameGroup(aGroup, newName, undefined)
|
||||||
(item as BookmarkedGroup).title = newName
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -422,19 +582,42 @@ const updateSortingBookmarksAfterItemDeleted = (plugin: Bookmarks_PluginInstance
|
||||||
if (deletedItem instanceof TFile) return;
|
if (deletedItem instanceof TFile) return;
|
||||||
|
|
||||||
let items = plugin[BookmarksPlugin_items_collectionName]
|
let items = plugin[BookmarksPlugin_items_collectionName]
|
||||||
|
|
||||||
|
if (!Array.isArray(items)) return
|
||||||
|
|
||||||
const aFolder: boolean = deletedItem instanceof TFolder
|
const aFolder: boolean = deletedItem instanceof TFolder
|
||||||
const aFile: boolean = !aFolder
|
const aFile: boolean = !aFolder
|
||||||
|
|
||||||
const originalContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(deletedItem.path, DontCreateIfMissing, plugin, bookmarksGroup)
|
// Delete all instances of deleted item from two handled locations:
|
||||||
|
// - in bookmark groups hierarchy matching the item path in file explorer
|
||||||
|
// - in the bookmark group designated as container for bookmarks (immediate children)
|
||||||
|
const bookmarksContainer: BookmarkedParentFolder|undefined = findGroupForItemPathInBookmarks(deletedItem.path, DontCreateIfMissing, plugin, bookmarksGroup)
|
||||||
|
const itemInRootFolder = !!extractParentFolderPath(deletedItem.path)
|
||||||
|
const bookmarksRootContainer: BookmarkedParentFolder|undefined =
|
||||||
|
(bookmarksGroup && !itemInRootFolder) ? findGroupForItemPathInBookmarks('intentionally-in-root-path', DontCreateIfMissing, plugin, bookmarksGroup) : undefined
|
||||||
|
|
||||||
if (!originalContainer) return;
|
if (!bookmarksContainer && !bookmarksRootContainer) return;
|
||||||
|
|
||||||
const item: BookmarkedItem|undefined = originalContainer.items.find((it) => {
|
[bookmarksContainer, bookmarksRootContainer].forEach((container) => {
|
||||||
if (aFolder && it.type === 'group' && it.title === deletedItem.name) return true;
|
const bookmarkEntriesToRemove: Array<BookmarkedItem> = []
|
||||||
if (aFile && it.type === 'file' && it.path === deletedItem.path) return true;
|
container?.items.forEach((it) => {
|
||||||
|
if (aFolder && it.type === 'group' && groupNameForPath(it.title||'') === deletedItem.name) {
|
||||||
|
bookmarkEntriesToRemove.push(it)
|
||||||
|
}
|
||||||
|
if (aFile && it.type === 'file' && it.path === deletedItem.path) {
|
||||||
|
bookmarkEntriesToRemove.push(it)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
bookmarkEntriesToRemove.forEach((itemToRemove) =>{
|
||||||
|
container?.group?.items.remove(itemToRemove)
|
||||||
|
})
|
||||||
|
cleanupBookmarkTreeFromTransparentEmptyGroups(container, plugin, bookmarksGroup)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
if (!item) return;
|
|
||||||
|
export const _unitTests = {
|
||||||
originalContainer.items.remove(item)
|
getOrderedBookmarks: getOrderedBookmarks,
|
||||||
|
bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants: bookmarkedGroupEmptyOrOnlyTransparentForSortingDescendants,
|
||||||
|
cleanupBookmarkTreeFromTransparentEmptyGroups: cleanupBookmarkTreeFromTransparentEmptyGroups,
|
||||||
|
findGroupForItemPathInBookmarks: findGroupForItemPathInBookmarks
|
||||||
}
|
}
|
||||||
|
|
15
yarn.lock
15
yarn.lock
|
@ -763,6 +763,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/tern" "*"
|
"@types/tern" "*"
|
||||||
|
|
||||||
|
"@types/codemirror@5.60.8":
|
||||||
|
version "5.60.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-5.60.8.tgz#b647d04b470e8e1836dd84b2879988fc55c9de68"
|
||||||
|
integrity sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==
|
||||||
|
dependencies:
|
||||||
|
"@types/tern" "*"
|
||||||
|
|
||||||
"@types/estree@*":
|
"@types/estree@*":
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2"
|
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2"
|
||||||
|
@ -2396,6 +2403,14 @@ npm-run-path@^4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key "^3.0.0"
|
path-key "^3.0.0"
|
||||||
|
|
||||||
|
"obsidian-1.4.11@npm:obsidian@1.4.11":
|
||||||
|
version "1.4.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/obsidian/-/obsidian-1.4.11.tgz#5cba594c83a74ebad58b630c610265018abdadaa"
|
||||||
|
integrity sha512-BCVYTvaXxElJMl6MMbDdY/CGK+aq18SdtDY/7vH8v6BxCBQ6KF4kKxL0vG9UZ0o5qh139KpUoJHNm+6O5dllKA==
|
||||||
|
dependencies:
|
||||||
|
"@types/codemirror" "5.60.8"
|
||||||
|
moment "2.29.4"
|
||||||
|
|
||||||
obsidian@^0.15.4:
|
obsidian@^0.15.4:
|
||||||
version "0.15.9"
|
version "0.15.9"
|
||||||
resolved "https://registry.yarnpkg.com/obsidian/-/obsidian-0.15.9.tgz#b6e0b566952643db6b55f7e74fbba6d7140fe4a2"
|
resolved "https://registry.yarnpkg.com/obsidian/-/obsidian-0.15.9.tgz#b6e0b566952643db6b55f7e74fbba6d7140fe4a2"
|
||||||
|
|
Loading…
Reference in New Issue