2024-03-02 14:25:27 +00:00
|
|
|
import Folder from '@joplin/lib/models/Folder';
|
|
|
|
|
import Tag from '@joplin/lib/models/Tag';
|
|
|
|
|
import BaseModel from '@joplin/lib/BaseModel';
|
|
|
|
|
import Setting from '@joplin/lib/models/Setting';
|
|
|
|
|
import { _ } from '@joplin/lib/locale';
|
|
|
|
|
import { FolderEntity } from '@joplin/lib/services/database/types';
|
2025-08-18 13:55:55 +01:00
|
|
|
import {
|
|
|
|
|
getDisplayParentId,
|
|
|
|
|
getTrashFolderId,
|
|
|
|
|
} from '@joplin/lib/services/trash';
|
2017-10-07 23:17:10 +01:00
|
|
|
const ListWidget = require('tkwidgets/ListWidget.js');
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
|
2025-08-18 13:55:55 +01:00
|
|
|
export default class FolderListWidget extends ListWidget {
|
2024-03-02 14:25:27 +00:00
|
|
|
private folders_: FolderEntity[] = [];
|
|
|
|
|
|
|
|
|
|
public constructor() {
|
2017-10-07 23:17:10 +01:00
|
|
|
super();
|
|
|
|
|
|
2017-10-22 18:12:16 +01:00
|
|
|
this.tags_ = [];
|
2017-10-23 21:34:04 +01:00
|
|
|
this.searches_ = [];
|
2017-10-22 18:12:16 +01:00
|
|
|
this.selectedFolderId_ = null;
|
|
|
|
|
this.selectedTagId_ = null;
|
2017-10-23 21:34:04 +01:00
|
|
|
this.selectedSearchId_ = null;
|
2017-10-22 18:12:16 +01:00
|
|
|
this.notesParentType_ = 'Folder';
|
2017-10-15 12:38:22 +01:00
|
|
|
this.updateIndexFromSelectedFolderId_ = false;
|
2017-10-22 18:12:16 +01:00
|
|
|
this.updateItems_ = false;
|
2018-05-09 12:39:27 +01:00
|
|
|
this.trimItemTitle = false;
|
2022-09-05 13:37:51 +02:00
|
|
|
this.showIds = false;
|
2017-10-15 12:38:22 +01:00
|
|
|
|
2024-04-05 12:16:49 +01:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
2024-03-02 14:25:27 +00:00
|
|
|
this.itemRenderer = (item: any) => {
|
2020-03-13 23:46:14 +00:00
|
|
|
const output = [];
|
2017-10-23 22:48:29 +01:00
|
|
|
if (item === '-') {
|
|
|
|
|
output.push('-'.repeat(this.innerWidth));
|
|
|
|
|
} else if (item.type_ === Folder.modelType()) {
|
2025-08-18 13:55:55 +01:00
|
|
|
const depth = this.folderDepth(this.folders, item.id);
|
|
|
|
|
output.push(' '.repeat(depth));
|
|
|
|
|
|
|
|
|
|
// Add collapse/expand indicator
|
|
|
|
|
const hasChildren = this.folderHasChildren_(this.folders, item.id);
|
|
|
|
|
if (hasChildren) {
|
|
|
|
|
const collapsedFolders = Setting.value('collapsedFolderIds');
|
|
|
|
|
const isCollapsed = collapsedFolders.includes(item.id);
|
|
|
|
|
output.push(isCollapsed ? '[+] ' : '[-] ');
|
|
|
|
|
} else {
|
|
|
|
|
output.push(' '); // Space for alignment
|
|
|
|
|
}
|
2022-09-05 13:37:51 +02:00
|
|
|
|
|
|
|
|
if (this.showIds) {
|
|
|
|
|
output.push(Folder.shortId(item.id));
|
|
|
|
|
}
|
|
|
|
|
output.push(Folder.displayTitle(item));
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
if (Setting.value('showNoteCounts') && !item.deleted_time && item.id !== getTrashFolderId()) {
|
2022-06-26 09:55:49 -07:00
|
|
|
let noteCount = item.note_count;
|
2023-01-11 18:37:22 +00:00
|
|
|
if (this.folderHasChildren_(this.folders, item.id)) {
|
2022-06-26 09:55:49 -07:00
|
|
|
for (let i = 0; i < this.folders.length; i++) {
|
|
|
|
|
if (this.folders[i].parent_id === item.id) {
|
2024-04-05 12:16:49 +01:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
2024-03-02 14:25:27 +00:00
|
|
|
noteCount -= (this.folders[i] as any).note_count;
|
2022-06-26 09:55:49 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
output.push(noteCount);
|
|
|
|
|
}
|
2017-10-22 18:12:16 +01:00
|
|
|
} else if (item.type_ === Tag.modelType()) {
|
2019-09-19 22:51:18 +01:00
|
|
|
output.push(`[${Folder.displayTitle(item)}]`);
|
2017-10-23 21:34:04 +01:00
|
|
|
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
|
2017-10-23 22:48:29 +01:00
|
|
|
output.push(_('Search:'));
|
|
|
|
|
output.push(item.title);
|
2018-05-09 12:39:27 +01:00
|
|
|
}
|
2019-07-30 09:35:42 +02:00
|
|
|
|
2017-10-22 18:12:16 +01:00
|
|
|
return output.join(' ');
|
2017-10-09 18:05:01 +00:00
|
|
|
};
|
2017-10-07 23:17:10 +01:00
|
|
|
}
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public folderDepth(folders: FolderEntity[], folderId: string) {
|
2018-05-09 12:39:27 +01:00
|
|
|
let output = 0;
|
|
|
|
|
while (true) {
|
|
|
|
|
const folder = BaseModel.byId(folders, folderId);
|
2025-08-18 13:55:55 +01:00
|
|
|
const folderParentId = getDisplayParentId(
|
|
|
|
|
folder,
|
|
|
|
|
folders.find((f) => f.id === folder.parent_id),
|
|
|
|
|
);
|
2024-03-02 14:25:27 +00:00
|
|
|
if (!folder || !folderParentId) return output;
|
2018-05-09 12:39:27 +01:00
|
|
|
output++;
|
2024-03-02 14:25:27 +00:00
|
|
|
folderId = folderParentId;
|
2018-05-09 12:39:27 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public get selectedFolderId() {
|
2017-10-07 23:17:10 +01:00
|
|
|
return this.selectedFolderId_;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public set selectedFolderId(v) {
|
2017-10-07 23:17:10 +01:00
|
|
|
this.selectedFolderId_ = v;
|
2019-07-30 09:35:42 +02:00
|
|
|
this.updateIndexFromSelectedItemId();
|
2017-10-22 18:12:16 +01:00
|
|
|
this.invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public get selectedSearchId() {
|
2017-10-23 21:34:04 +01:00
|
|
|
return this.selectedSearchId_;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public set selectedSearchId(v) {
|
2017-10-23 21:34:04 +01:00
|
|
|
this.selectedSearchId_ = v;
|
2019-07-30 09:35:42 +02:00
|
|
|
this.updateIndexFromSelectedItemId();
|
2017-10-23 21:34:04 +01:00
|
|
|
this.invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public get selectedTagId() {
|
2017-10-22 18:12:16 +01:00
|
|
|
return this.selectedTagId_;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public set selectedTagId(v) {
|
2017-10-22 18:12:16 +01:00
|
|
|
this.selectedTagId_ = v;
|
2019-07-30 09:35:42 +02:00
|
|
|
this.updateIndexFromSelectedItemId();
|
2017-10-22 18:12:16 +01:00
|
|
|
this.invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public get notesParentType() {
|
2017-10-22 18:12:16 +01:00
|
|
|
return this.notesParentType_;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public set notesParentType(v) {
|
2017-10-22 18:12:16 +01:00
|
|
|
this.notesParentType_ = v;
|
2019-07-30 09:35:42 +02:00
|
|
|
this.updateIndexFromSelectedItemId();
|
2017-10-23 21:34:04 +01:00
|
|
|
this.invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public get searches() {
|
2017-10-23 21:34:04 +01:00
|
|
|
return this.searches_;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public set searches(v) {
|
2017-10-23 21:34:04 +01:00
|
|
|
this.searches_ = v;
|
|
|
|
|
this.updateItems_ = true;
|
2019-07-30 09:35:42 +02:00
|
|
|
this.updateIndexFromSelectedItemId();
|
2017-10-22 18:12:16 +01:00
|
|
|
this.invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public get tags() {
|
2017-10-22 18:12:16 +01:00
|
|
|
return this.tags_;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public set tags(v) {
|
2017-10-22 18:12:16 +01:00
|
|
|
this.tags_ = v;
|
|
|
|
|
this.updateItems_ = true;
|
2019-07-30 09:35:42 +02:00
|
|
|
this.updateIndexFromSelectedItemId();
|
2017-10-22 18:12:16 +01:00
|
|
|
this.invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public get folders() {
|
2017-10-22 18:12:16 +01:00
|
|
|
return this.folders_;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public set folders(v) {
|
2017-10-22 18:12:16 +01:00
|
|
|
this.folders_ = v;
|
|
|
|
|
this.updateItems_ = true;
|
2019-07-30 09:35:42 +02:00
|
|
|
this.updateIndexFromSelectedItemId();
|
2022-09-05 13:37:51 +02:00
|
|
|
this.invalidate();
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public toggleShowIds() {
|
2022-09-05 13:37:51 +02:00
|
|
|
this.showIds = !this.showIds;
|
2017-10-22 18:12:16 +01:00
|
|
|
this.invalidate();
|
|
|
|
|
}
|
2018-09-23 19:33:44 +01:00
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public folderHasChildren_(folders: FolderEntity[], folderId: string) {
|
2018-09-23 19:33:44 +01:00
|
|
|
for (let i = 0; i < folders.length; i++) {
|
2020-03-13 23:46:14 +00:00
|
|
|
const folder = folders[i];
|
2025-08-18 13:55:55 +01:00
|
|
|
const folderParentId = getDisplayParentId(
|
|
|
|
|
folder,
|
|
|
|
|
folders.find((f) => f.id === folder.parent_id),
|
|
|
|
|
);
|
2024-03-02 14:25:27 +00:00
|
|
|
if (folderParentId === folderId) return true;
|
2018-09-23 19:33:44 +01:00
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2019-07-30 09:35:42 +02:00
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public render() {
|
2017-10-22 18:12:16 +01:00
|
|
|
if (this.updateItems_) {
|
2025-08-18 13:55:55 +01:00
|
|
|
this.logger().debug(
|
|
|
|
|
'Rebuilding items...',
|
|
|
|
|
this.notesParentType,
|
|
|
|
|
this.selectedJoplinItemId,
|
|
|
|
|
this.selectedSearchId,
|
|
|
|
|
);
|
2017-10-23 22:48:29 +01:00
|
|
|
const wasSelectedItemId = this.selectedJoplinItemId;
|
2017-10-23 21:34:04 +01:00
|
|
|
const previousParentType = this.notesParentType;
|
2017-10-23 22:48:29 +01:00
|
|
|
|
2024-04-05 12:16:49 +01:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
2024-03-02 14:25:27 +00:00
|
|
|
let newItems: any[] = [];
|
|
|
|
|
const orderFolders = (parentId: string) => {
|
2018-09-23 19:33:44 +01:00
|
|
|
for (let i = 0; i < this.folders.length; i++) {
|
|
|
|
|
const f = this.folders[i];
|
2025-08-18 13:55:55 +01:00
|
|
|
const originalParent = this.folders_.find(
|
|
|
|
|
(f) => f.id === f.parent_id,
|
|
|
|
|
);
|
2024-03-02 14:25:27 +00:00
|
|
|
|
|
|
|
|
const folderParentId = getDisplayParentId(f, originalParent); // f.parent_id ? f.parent_id : '';
|
2019-01-10 19:17:38 +00:00
|
|
|
if (folderParentId === parentId) {
|
2018-09-23 19:33:44 +01:00
|
|
|
newItems.push(f);
|
2025-08-18 13:55:55 +01:00
|
|
|
// Only recurse into children if the folder is not collapsed
|
|
|
|
|
if (this.folderHasChildren_(this.folders, f.id)) {
|
|
|
|
|
const collapsedFolders = Setting.value('collapsedFolderIds');
|
|
|
|
|
if (!collapsedFolders.includes(f.id)) {
|
|
|
|
|
orderFolders(f.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-09-23 19:33:44 +01:00
|
|
|
}
|
|
|
|
|
}
|
2019-07-30 09:35:42 +02:00
|
|
|
};
|
2018-09-23 19:33:44 +01:00
|
|
|
|
|
|
|
|
orderFolders('');
|
2017-10-23 22:48:29 +01:00
|
|
|
|
|
|
|
|
if (this.tags.length) {
|
|
|
|
|
if (newItems.length) newItems.push('-');
|
|
|
|
|
newItems = newItems.concat(this.tags);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.searches.length) {
|
|
|
|
|
if (newItems.length) newItems.push('-');
|
|
|
|
|
newItems = newItems.concat(this.searches);
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-29 16:22:53 +00:00
|
|
|
this.items = newItems;
|
2017-10-23 22:48:29 +01:00
|
|
|
|
2017-10-23 21:34:04 +01:00
|
|
|
this.notesParentType = previousParentType;
|
2019-07-30 09:35:42 +02:00
|
|
|
this.updateIndexFromSelectedItemId(wasSelectedItemId);
|
2017-10-22 18:12:16 +01:00
|
|
|
this.updateItems_ = false;
|
|
|
|
|
}
|
2017-10-29 16:22:53 +00:00
|
|
|
|
|
|
|
|
super.render();
|
2017-10-15 12:38:22 +01:00
|
|
|
}
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public get selectedJoplinItemId() {
|
2017-10-23 21:34:04 +01:00
|
|
|
if (!this.notesParentType) return '';
|
|
|
|
|
if (this.notesParentType === 'Folder') return this.selectedFolderId;
|
|
|
|
|
if (this.notesParentType === 'Tag') return this.selectedTagId;
|
|
|
|
|
if (this.notesParentType === 'Search') return this.selectedSearchId;
|
2019-09-19 22:51:18 +01:00
|
|
|
throw new Error(`Unknown parent type: ${this.notesParentType}`);
|
2017-10-23 21:34:04 +01:00
|
|
|
}
|
2017-10-15 12:38:22 +01:00
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public get selectedJoplinItem() {
|
2017-10-23 22:48:29 +01:00
|
|
|
const id = this.selectedJoplinItemId;
|
|
|
|
|
const index = this.itemIndexByKey('id', id);
|
|
|
|
|
return this.itemAt(index);
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 14:25:27 +00:00
|
|
|
public updateIndexFromSelectedItemId(itemId: string = null) {
|
2017-10-23 22:48:29 +01:00
|
|
|
if (itemId === null) itemId = this.selectedJoplinItemId;
|
2017-10-23 21:34:04 +01:00
|
|
|
const index = this.itemIndexByKey('id', itemId);
|
|
|
|
|
this.currentIndex = index >= 0 ? index : 0;
|
2017-10-07 23:17:10 +01:00
|
|
|
}
|
2025-08-18 13:55:55 +01:00
|
|
|
|
|
|
|
|
public toggleFolderCollapse() {
|
|
|
|
|
const item = this.currentItem;
|
|
|
|
|
if (item && item.type_ === Folder.modelType() && this.folderHasChildren_(this.folders, item.id)) {
|
|
|
|
|
const collapsedFolders = Setting.value('collapsedFolderIds');
|
|
|
|
|
const isCollapsed = collapsedFolders.includes(item.id);
|
|
|
|
|
if (isCollapsed) {
|
|
|
|
|
const newCollapsed = collapsedFolders.filter((id: string) => id !== item.id);
|
|
|
|
|
Setting.setValue('collapsedFolderIds', newCollapsed);
|
|
|
|
|
} else {
|
|
|
|
|
Setting.setValue('collapsedFolderIds', [...collapsedFolders, item.id]);
|
|
|
|
|
}
|
|
|
|
|
this.updateItems_ = true;
|
|
|
|
|
this.invalidate();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public expandToFolder(folderId: string) {
|
|
|
|
|
// Find all parent folders and expand them
|
|
|
|
|
const parentsToExpand: string[] = [];
|
|
|
|
|
let currentId = folderId;
|
|
|
|
|
|
|
|
|
|
while (currentId) {
|
|
|
|
|
const folder = BaseModel.byId(this.folders, currentId);
|
|
|
|
|
if (!folder) break;
|
|
|
|
|
|
|
|
|
|
const parentId = getDisplayParentId(
|
|
|
|
|
folder,
|
|
|
|
|
this.folders.find((f) => f.id === folder.parent_id),
|
|
|
|
|
);
|
|
|
|
|
if (parentId) {
|
|
|
|
|
parentsToExpand.unshift(parentId);
|
|
|
|
|
currentId = parentId;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Expand all parent folders
|
|
|
|
|
const collapsedFolders = Setting.value('collapsedFolderIds');
|
|
|
|
|
const newCollapsed = collapsedFolders.filter((id: string) => !parentsToExpand.includes(id));
|
|
|
|
|
Setting.setValue('collapsedFolderIds', newCollapsed);
|
|
|
|
|
|
|
|
|
|
this.updateItems_ = true;
|
|
|
|
|
this.invalidate();
|
|
|
|
|
}
|
2017-10-07 23:17:10 +01:00
|
|
|
}
|