diff --git a/.eslintrc b/.eslintrc index 0432787..5147a49 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,108 +1,115 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "env": { - "node": true - }, - "plugins": [ - "@typescript-eslint", - "@stylistic/eslint-plugin" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "parserOptions": { - "sourceType": "module" - }, - "rules": { - "@stylistic/indent": [ - "error", - "tab" - ], - "@stylistic/indent-binary-ops": [ - "error", - "tab" - ], - "@stylistic/quotes": [ - "error", - "single" - ], - "@stylistic/semi": [ - "error", - "always" - ], - "@stylistic/brace-style": [ - "error", - "stroustrup", - { - "allowSingleLine": true - } - ], - "@stylistic/comma-dangle": [ - "error", - "always-multiline" - ], - "@stylistic/dot-location": [ - "error", - "property" - ], - "@stylistic/function-call-argument-newline": [ - "error", - "consistent" - ], - "@stylistic/function-call-spacing": "error", - "@stylistic/implicit-arrow-linebreak": "error", - "@stylistic/key-spacing": "error", - "@stylistic/lines-around-comment": "error", - "@stylistic/new-parens": "error", - "@stylistic/no-confusing-arrow": "error", - "@stylistic/no-extra-semi": "error", - "@stylistic/no-floating-decimal": "error", - "@stylistic/no-mixed-operators": "error", - "@stylistic/no-mixed-spaces-and-tabs": "error", - "@stylistic/no-multi-spaces": "error", - "@stylistic/no-multiple-empty-lines": [ - "error", - { - "max": 1, - "maxEOF": 0, - "maxBOF": 0 - } - ], - "@stylistic/no-trailing-spaces": [ - "error", - { - "skipBlankLines": true - } - ], - "@stylistic/nonblock-statement-body-position": "error", - "@stylistic/operator-linebreak": [ - "error", - "after" - ], - "@stylistic/padded-blocks": [ - "error", - "never" - ], - "@stylistic/padding-line-between-statements": "error", - "@stylistic/rest-spread-spacing": "error", - "@stylistic/semi-spacing": "error", - "@stylistic/semi-style": "error", - "@stylistic/space-before-blocks": "error", - "@stylistic/space-before-function-paren": "error", - "@stylistic/arrow-spacing": "error", - "@stylistic/space-in-parens": "error", - "@stylistic/space-infix-ops": "error", - "@stylistic/space-unary-ops": "error", - "@stylistic/spaced-comment": [ - "error", - "always" - ], - "@stylistic/switch-colon-spacing": "error", - "@stylistic/template-curly-spacing": "error", - "@stylistic/type-generic-spacing": "error", - "@stylistic/type-named-tuple-spacing": "error" - } + "root": true, + "parser": "@typescript-eslint/parser", + "env": { + "node": true + }, + "plugins": [ + "@typescript-eslint", + "@stylistic/eslint-plugin" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parserOptions": { + "sourceType": "module" + }, + "rules": { + "@stylistic/indent": [ + "error", + "tab" + ], + "@stylistic/indent-binary-ops": [ + "error", + "tab" + ], + "@stylistic/quotes": [ + "error", + "single" + ], + "@stylistic/semi": [ + "error", + "always" + ], + "@stylistic/brace-style": [ + "error", + "stroustrup", + { + "allowSingleLine": true + } + ], + "@stylistic/comma-dangle": [ + "error", + "always-multiline" + ], + "@stylistic/dot-location": [ + "error", + "property" + ], + "@stylistic/function-call-argument-newline": [ + "error", + "consistent" + ], + "@stylistic/function-call-spacing": "error", + "@stylistic/implicit-arrow-linebreak": "error", + "@stylistic/key-spacing": "error", + "@stylistic/lines-around-comment": "error", + "@stylistic/new-parens": "error", + "@stylistic/no-confusing-arrow": "error", + "@stylistic/no-extra-semi": "error", + "@stylistic/no-floating-decimal": "error", + "@stylistic/no-mixed-operators": "error", + "@stylistic/no-mixed-spaces-and-tabs": "error", + "@stylistic/no-multi-spaces": "error", + "@stylistic/no-multiple-empty-lines": [ + "error", + { + "max": 1, + "maxEOF": 0, + "maxBOF": 0 + } + ], + "@stylistic/no-trailing-spaces": [ + "error", + { + "skipBlankLines": true + } + ], + "@stylistic/nonblock-statement-body-position": "error", + "@stylistic/operator-linebreak": [ + "error", + "after" + ], + "@stylistic/padded-blocks": [ + "error", + "never" + ], + "@stylistic/padding-line-between-statements": "error", + "@stylistic/rest-spread-spacing": "error", + "@stylistic/semi-spacing": "error", + "@stylistic/semi-style": "error", + "@stylistic/space-before-blocks": "error", + "@stylistic/space-before-function-paren": "error", + "@stylistic/arrow-spacing": "error", + "@stylistic/space-in-parens": "error", + "@stylistic/space-infix-ops": "error", + "@stylistic/space-unary-ops": "error", + "@stylistic/spaced-comment": [ + "error", + "always" + ], + "@stylistic/switch-colon-spacing": "error", + "@stylistic/template-curly-spacing": "error", + "@stylistic/type-generic-spacing": "error", + "@stylistic/type-named-tuple-spacing": "error", + "no-fallthrough": [ + "error", + { + "allowEmptyCase": true + } + ], + "no-console": ["error", {"allow": ["warn", "error", "debug"]}] + } } \ No newline at end of file diff --git a/src/OpenEditorsView.ts b/src/OpenEditorsView.ts index 9fe6a89..a770fde 100644 --- a/src/OpenEditorsView.ts +++ b/src/OpenEditorsView.ts @@ -1,4 +1,4 @@ -import { View, WorkspaceLeaf } from 'obsidian'; +import { View, WorkspaceLeaf, WorkspaceSplit } from 'obsidian'; import { ICON_CLOSE, ICON_CLOSE_GROUP, ICON_CLOSE_WINDOW, ICON_OPEN_EDITORS, VIEW_DISPLAY_OPEN_EDITORS, VIEW_TYPE_OPEN_EDITORS } from './constants'; import { TreeItem } from './TreeItem'; @@ -53,23 +53,31 @@ export class OpenEditorsView extends View { const main = layout['main']; if (main) { // Create a top-level tree item for the main window - const tree = new TreeItem(this.treeEl, 'Main window', main.id, undefined, [{ + const tree = new TreeItem(this.treeEl, 'Main window', main.id, main.type, undefined, [{ iconId: ICON_CLOSE_WINDOW, + ariaLabel: 'Close all', onClickCallback: () => { // Recursively detach all leaves in the tree - tree.rekursiveCall((tree) => { - this.app.workspace.getLeafById(tree.id)?.detach(); - }); + tree.rekursiveCallForType([{ + callback: (tree) => { + this.app.workspace.getLeafById(tree.id)?.detach(); + }, + type: 'leaf', + }]); }, - ariaLabel: 'Close all', }]); // Add the tree item to the list of windows this.windows.push(tree); // Recursively build the tree structure for all child elements - main.children.forEach((element: { id: string, type: string, children: object[], state: { title: string, type: string } }) => { - this.TreeWalker(element, tree); - }); + if (main.children.length > 1) { + this.TreeWalker(main, tree); + } + else { + main.children.forEach((element: { id: string, type: string, children: object[], state: { title: string, type: string } }) => { + this.TreeWalker(element, tree); + }); + } } // Handle floating windows (popout windows) @@ -93,15 +101,18 @@ export class OpenEditorsView extends View { // Handle popout window layouts case 'window': // Create a tree item for each floating window - parent = new TreeItem(this.treeEl, `Window ${count}`, layout.id, undefined, [{ + parent = new TreeItem(this.treeEl, `Window ${count}`, layout.id, layout.type, undefined, [{ iconId: ICON_CLOSE_WINDOW, + ariaLabel: 'Close all', onClickCallback: () => { // Recursively detach all leaves in the tree - parent?.rekursiveCall((parent) => { - this.app.workspace.getLeafById(parent.id)?.detach(); - }); + parent?.rekursiveCallForType([{ + callback: (parent) => { + this.app.workspace.getLeafById(parent.id)?.detach(); + }, + type: 'leaf', + }]); }, - ariaLabel: 'Close all', }]); // Add the tree item to the list of windows this.windows.push(parent); @@ -110,14 +121,17 @@ export class OpenEditorsView extends View { let groupCount = 1; // Recursively build the tree structure for all child elements layout.children.forEach((element: { id: string, type: string, children: object[], state: { title: string, type: string } }) => { - const group = parent?.addChild((containerEl) => new TreeItem(containerEl, `Group ${groupCount}`, element.id, undefined, [{ + const group = parent?.addChild((containerEl) => new TreeItem(containerEl, `Group ${groupCount}`, element.id, element.type, undefined, [{ iconId: ICON_CLOSE_GROUP, - onClickCallback: () => { - parent?.rekursiveCall((tree) => { - this.app.workspace.getLeafById(tree.id)?.detach(); - }); - }, ariaLabel: 'Close all', + onClickCallback: () => { + parent?.rekursiveCallForType([{ + callback: (tree) => { + this.app.workspace.getLeafById(tree.id)?.detach(); + }, + type: 'leaf', + }]); + }, }])); this.TreeWalker(element, group); groupCount++; @@ -136,17 +150,66 @@ export class OpenEditorsView extends View { case 'split': layout.children.forEach((element: { id: string, type: string, children: object[], state: { title: string, type: string } }) => { // Create a child tree item for each split group - const tree = parent?.addChild((container) => new TreeItem(container, `Group ${count}`, element.id, undefined, [{ - iconId: ICON_CLOSE_GROUP, + const group = parent?.addChild((container) => new TreeItem(container, `Group ${count}`, element.id, element.type, undefined, [{ + iconId: ICON_CLOSE, + ariaLabel: 'Close group', onClickCallback: () => { - tree?.rekursiveCall((tree) => { - this.app.workspace.getLeafById(tree.id)?.detach(); - }); + group?.rekursiveCallForType([{ + callback: (tree) => { + this.app.workspace.getLeafById(tree.id)?.detach(); + }, + type: 'leaf', + }]); + }, + }, { + iconId: ICON_CLOSE_GROUP, + ariaLabel: 'Close all editors', + onClickCallback: () => { + if (group) { + // Find leaf parent + let leafParent: WorkspaceSplit | undefined; + group.rekursiveCallForType([{ + callback: tree => { + const leaf = this.app.workspace.getLeafById(tree.id); + if (!leaf) { + console.debug('Leaf not found with the id ', tree.id); + return; + } + const parent = leaf?.parent; + if (!parent) { + console.debug('Parent of leaf not found!'); + return; + } + leafParent = parent; + }, + type: 'leaf', + }]); + if (!leafParent) { + console.warn('Parent of leaf not found!'); + return; + } + + // Add a new tab to parent + const newId = this.app.workspace.createLeafInParent(leafParent, 0).id; + if (!newId) { + console.warn('No id of new leaf!'); + return; + } + + // Recursively detach all leaves in the tree except for new tab + group?.rekursiveCallForType([{ + callback: (tree) => { + if (tree.id != newId) { + this.app.workspace.getLeafById(tree.id)?.detach(); + } + }, + type: 'leaf', + }]); + } }, - ariaLabel: 'Close all', }])); // Recursively process tabs as children - this.TreeWalker(element, tree); + this.TreeWalker(element, group); count++; }); break; @@ -158,7 +221,7 @@ export class OpenEditorsView extends View { break; // Handle individual leaves case 'leaf': - parent?.addChild((container) => new TreeItem(container, layout.state.title, layout.id, { + parent?.addChild((container) => new TreeItem(container, layout.state.title, layout.id, layout.type, { onClickCallback: () => { // Set the clicked leaf as the active leaf in the workspace const leaf = this.app.workspace.getLeafById(layout.id); @@ -171,10 +234,10 @@ export class OpenEditorsView extends View { }, }, [{ iconId: ICON_CLOSE, + ariaLabel: 'Close', onClickCallback: () => { this.app.workspace.getLeafById(layout.id)?.detach(); }, - ariaLabel: 'Close', }])); break; default: @@ -195,4 +258,4 @@ export class OpenEditorsView extends View { // Clear the tree container in the DOM this.treeEl.empty(); } -} +} \ No newline at end of file diff --git a/src/TreeItem.ts b/src/TreeItem.ts index b7954b0..999ef2c 100644 --- a/src/TreeItem.ts +++ b/src/TreeItem.ts @@ -3,6 +3,8 @@ import { IconName, setIcon } from 'obsidian'; export class TreeItem { // The ID of the tree item id: string; + // The type of the workspace item behind this tree item + type: string; // Tree items inside this tree item children: TreeItem[]; // Main container element for the tree item @@ -26,6 +28,7 @@ export class TreeItem { conteinerEl: HTMLDivElement, title: string, id: string, + type: string, handler?: { onClickCallback?: (ev: MouseEvent) => void, onDragCallback?: (ev: DragEvent, delegateTarget: HTMLElement) => void @@ -34,6 +37,7 @@ export class TreeItem { ) { this.children = []; this.id = id; + this.type = type; // Create the main tree item element this.treeItemEl = conteinerEl.createEl('div', { cls: 'tree-item' }); @@ -109,6 +113,23 @@ export class TreeItem { callback(this); } + /** + * Recursively apply a callback funstion to the current tree item and its children if the type matches the given type. + * @param callback - The function to call for each tree item. + * @param type - The type TreeItem needs to have + */ + rekursiveCallForType (actions: { callback: (tree: TreeItem) => void, type: string }[]) { + this.children.forEach((child) => { + child.rekursiveCallForType(actions); + }); + + actions.forEach(action => { + if (this.type === action.type) { + action.callback(this); + } + }); + } + /** * Add a child TreeItem to this tree item. * @param treeCreateCallback - A function that creates and returns a new TreeItem. diff --git a/src/constants.ts b/src/constants.ts index e8fa17d..bf6888f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -9,4 +9,5 @@ export const ICON_MOVE_TO_NEW_WINDOW = 'picture-in-picture'; export const ICON_OPEN_IN_NEW_WINDOW = 'picture-in-picture-2'; export const ICON_SPLIT_RIGHT = 'separator-vertical'; export const ICON_SPLIT_DOWN = 'separator-horizontal'; -export const ICON_REVEAL_IN_NAVIGATION = 'folder-open'; \ No newline at end of file +export const ICON_REVEAL_IN_NAVIGATION = 'folder-open'; +export const ICON_COMBINE_GROUP = 'shrink'; \ No newline at end of file