Add OpenEditorsView

This commit is contained in:
4Source 2024-12-14 21:58:05 +01:00
parent 4c46331bea
commit efcde42e16
9 changed files with 362 additions and 20 deletions

View file

@ -1,8 +1,8 @@
{
"id": "obsidian-plugin-template",
"name": "Obsidian plugin template",
"id": "open-editors",
"name": "Open Editors",
"minAppVersion": "0.15.0",
"description": "",
"description": "Adds a view which let you manage your opened tabs.",
"author": "4Source",
"authorUrl": "https://github.com/4Source",
"isDesktopOnly": false,

4
package-lock.json generated
View file

@ -1,11 +1,11 @@
{
"name": "obsidian-plugin-template",
"name": "open-editors",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "obsidian-plugin-template",
"name": "open-editors",
"version": "0.0.1",
"license": "MIT",
"devDependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-plugin-template",
"description": "",
"name": "open-editors",
"description": "Obsidian plugin",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",

137
src/OpenEditorsView.ts Normal file
View file

@ -0,0 +1,137 @@
import { View, WorkspaceLeaf } from 'obsidian';
import { ICON_CLOSE, ICON_CLOSE_GROUP, ICON_CLOSE_WINDOW, ICON_OPEN_EDITORS, VIEW_TYPE_OPEN_EDITORS } from './constants';
import { TreeItem } from './TreeItem';
export class OpenEditorsView extends View {
windows: TreeItem[];
treeEl: HTMLDivElement;
constructor (leaf: WorkspaceLeaf) {
super(leaf);
this.windows = [];
this.icon = ICON_OPEN_EDITORS;
}
getViewType (): string {
return VIEW_TYPE_OPEN_EDITORS;
}
getDisplayText (): string {
return 'Open Editors';
}
async onOpen () {
// Create the HTML container
this.containerEl.empty();
const container = this.containerEl.createEl('div', { cls: (VIEW_TYPE_OPEN_EDITORS + '-container') });
this.treeEl = container.createEl('div', { attr: { 'style': '' } });
// If layout changes clear the current tree and recreate it from layout
this.registerEvent(this.app.workspace.on('layout-change', async () => {
this.clearTree();
this.createTree();
}));
}
createTree () {
const layout = this.app.workspace.getLayout();
// Expand the tree with the groups and leafs from the main window
const main = layout['main'];
if (main) {
const tree = new TreeItem(this.treeEl, 'Main window', main.id, undefined, [{
iconId: ICON_CLOSE_WINDOW,
onClickCallback: () => {
tree.rekursiveCall((tree) => {
this.app.workspace.getLeafById(tree.id)?.detach();
});
},
ariaLabel: 'Close all',
}]);
this.windows.push(tree);
main.children.forEach((element: { id: string, type: string, children: object[], state: { title: string } }) => {
TreeWalker(element, tree);
});
}
// Expand the tree with the groups and leafs from popout windows
const floatingWindow = layout['floating'];
if (floatingWindow) {
let count = 1;
floatingWindow.children.forEach((element: { id: string, type: string, children: object[] }) => {
const tree = new TreeItem(this.treeEl, `Window ${count}`, element.id, undefined, [{
iconId: ICON_CLOSE_WINDOW,
onClickCallback: () => {
tree.rekursiveCall((tree) => {
this.app.workspace.getLeafById(tree.id)?.detach();
});
},
ariaLabel: 'Close all',
}]);
this.windows.push(tree);
element.children.forEach((element_: { id: string, type: string, children: object[], state: { title: string } }) => {
TreeWalker(element_, tree);
});
count++;
});
}
}
clearTree () {
this.windows.forEach(window => window.deleteTree());
this.windows = [];
this.treeEl.empty();
}
}
function TreeWalker (layout: { id: string, type: string, children: object[], state: { title: string } }, parent: TreeItem) {
let count = 0;
switch (layout.type) {
case 'split':
count = 1;
layout.children.forEach((element: { id: string, type: string, children: object[], state: { title: string } }) => {
const tree = parent.addChild((container) => new TreeItem(container, `Group ${count}`, element.id, undefined, [{
iconId: ICON_CLOSE_GROUP,
onClickCallback: () => {
tree.rekursiveCall((tree) => {
this.app.workspace.getLeafById(tree.id)?.detach();
});
},
ariaLabel: 'Close all',
}]));
TreeWalker(element, tree);
count++;
});
break;
case 'tabs':
count = 1;
layout.children.forEach((element: { id: string, type: string, children: object[], state: { title: string } }) => {
TreeWalker(element, parent);
count++;
});
break;
case 'leaf':
parent.addChild((container) => new TreeItem(container, layout.state.title, layout.id, {
onClickCallback: () => {
const leaf = this.app.workspace.getLeafById(layout.id);
if (!leaf) {
console.warn('Leaf not found with the id ', layout.id);
return;
}
this.app.workspace.setActiveLeaf(leaf);
// TODO: Leaf is in popout Window bringt to forground
},
}, [{
iconId: ICON_CLOSE,
onClickCallback: () => {
this.app.workspace.getLeafById(layout.id)?.detach();
},
ariaLabel: 'Close',
}]));
break;
default:
console.warn('Unknown layout component', layout);
break;
}
}

123
src/TreeItem.ts Normal file
View file

@ -0,0 +1,123 @@
import { IconName, setIcon } from 'obsidian';
export class TreeItem {
id: string;
parent: TreeItem | undefined;
children: TreeItem[];
treeItemEl: HTMLDivElement;
selfEl: HTMLDivElement;
childrenEl: HTMLDivElement;
iconEl: HTMLDivElement;
constructor (conteiner: HTMLDivElement, title: string, id: string, handler?: { onClickCallback?: (ev: MouseEvent) => void, onDragCallback?: (ev: DragEvent, delegateTarget: HTMLElement) => void }, actions?: { iconId: IconName, onClickCallback: (ev: MouseEvent) => void, ariaLabel?: string }[]) {
this.children = [];
this.id = id;
this.treeItemEl = conteiner.createEl('div', { cls: 'tree-item' });
this.selfEl = this.treeItemEl.createEl('div', { cls: 'open-editors-tree-item-self tree-item-self is-clickable' });
this.childrenEl = this.treeItemEl.createEl('div', { cls: 'tree-item-children' });
this.selfEl.onClickEvent((ev) => {
if (this.selfEl.hasClass('mod-collapsible')) {
if (this.iconEl.hasClass('is-collapsed')) {
this.selfEl.removeClass('is-collapsed');
this.iconEl.removeClass('is-collapsed');
this.childrenEl.setCssProps({ 'display': 'block' });
}
else {
this.selfEl.addClass('is-collapsed');
this.iconEl.addClass('is-collapsed');
this.childrenEl.setCssProps({ 'display': 'none' });
}
}
if (handler?.onClickCallback) {
handler.onClickCallback(ev);
}
});
if (handler?.onDragCallback) {
this.selfEl.on('drag', 'selector', (ev, delegateTarget) => {
console.log(ev, delegateTarget);
if (handler?.onDragCallback) {
handler.onDragCallback(ev, delegateTarget);
}
});
}
this.selfEl.createEl('div', { cls: 'open-editors-tree-item-inner tree-item-inner' })
.createEl('div', { cls: 'tree-item-inner-text' })
.createEl('span', { cls: 'tree-item-inner-text', text: title });
const flairOuter = this.selfEl.createEl('div', { cls: 'open-editors-tree-item-flair-outer tree-item-flair-outer' });
if (actions) {
actions.forEach(action => {
const button = flairOuter.createEl('div', { cls: 'tree-item-flair mod-clickable tree-iten-action-icon' });
setIcon(button.createEl('span', { cls: '' }), action.iconId);
if (action.ariaLabel) {
button.ariaLabel = action.ariaLabel;
// button.setAttr('data-tooltip-position', 'top');
}
button.onClickEvent((ev) => {
ev.stopPropagation();
action.onClickCallback(ev);
});
});
}
}
rekursiveCall (callback: (tree: TreeItem) => void) {
this.children.forEach((child) => {
child.rekursiveCall(callback);
});
callback(this);
}
addChild (treeCreateCallback: (container: HTMLDivElement) => TreeItem): TreeItem {
if (!this.selfEl.hasClass('mod-collapsible')) {
this.selfEl.addClass('mod-collapsible');
this.iconEl = this.selfEl.createEl('div', { cls: 'open-editors-tree-item-icon tree-item-icon collapse-icon' });
setIcon(this.iconEl, 'right-triangle');
}
const child = treeCreateCallback(this.childrenEl);
child.parent = this;
this.children.push(child);
return child;
}
removeChild (id: string) {
const child = this.children.find((value) => value.id == id);
if (child) {
this.children.remove(child);
child.removeAllChildren();
child.removeDom();
}
if (this.children.length <= 0 && this.selfEl.hasClass('mod-collapsible')) {
this.selfEl.removeClass('mod-collapsible');
this.iconEl?.remove();
}
}
removeAllChildren () {
while (this.children.length > 0) {
const child = this.children.pop();
if (child) {
child.removeAllChildren();
child.removeDom();
}
}
this.selfEl.removeClass('mod-collapsible');
this.iconEl?.remove();
}
deleteTree () {
this.removeAllChildren();
this.removeDom();
}
private removeDom () {
this.treeItemEl?.remove();
this.selfEl?.remove();
this.childrenEl?.remove();
this.iconEl?.remove();
}
}

11
src/constants.ts Normal file
View file

@ -0,0 +1,11 @@
export const VIEW_TYPE_OPEN_EDITORS = 'open-editors';
export const ICON_OPEN_EDITORS = 'layers';
export const ICON_CLOSE = 'x-square'; // x
export const ICON_CLOSE_GROUP = 'copy-x'; // folder-x
export const ICON_CLOSE_WINDOW = 'copy-x'; // monitor-x
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';

View file

@ -1,15 +1,22 @@
import { Plugin } from 'obsidian';
import { Plugin, WorkspaceLeaf } from 'obsidian';
import { DEFAULT_SETTINGS, Settings } from './settings/SettingsInterface';
import { MyPluginSettingTab } from './settings/SettingsTab';
import { ICON_OPEN_EDITORS, VIEW_TYPE_OPEN_EDITORS } from './constants';
import { OpenEditorsView } from './OpenEditorsView';
export default class MyPlugin extends Plugin {
export default class OpenEditorsPlugin extends Plugin {
settings: Settings;
async onload () {
await this.loadSettings();
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new MyPluginSettingTab(this.app, this));
// this.addSettingTab(new OpenEditorsSettingTab(this.app, this));
this.registerView(VIEW_TYPE_OPEN_EDITORS, (leaf) => new OpenEditorsView(leaf));
this.addRibbonIcon(ICON_OPEN_EDITORS, 'Open Open Editors', () => {
this.activateView(VIEW_TYPE_OPEN_EDITORS);
});
}
onunload () {
@ -23,4 +30,29 @@ export default class MyPlugin extends Plugin {
async saveSettings () {
await this.saveData(this.settings);
}
async activateView (viewType: string) {
const { workspace } = this.app;
let leaf: WorkspaceLeaf | null = null;
const leaves = workspace.getLeavesOfType(viewType);
if (leaves.length > 0) {
// A leaf with our view already exists, use that
leaf = leaves[0];
}
else {
// Our view could not be found in the workspace, create a new leaf
// in the right sidebar for it
leaf = workspace.getRightLeaf(false);
if (leaf) {
await leaf.setViewState({ type: viewType, active: true });
}
}
// "Reveal" the leaf in case it is in a collapsed sidebar
if (leaf) {
workspace.revealLeaf(leaf);
}
}
}

View file

@ -1,12 +1,12 @@
import { App, PluginSettingTab, Setting } from 'obsidian';
import MyPlugin from 'src/main';
import OpenEditorsPlugin from 'src/main';
export class MyPluginSettingTab extends PluginSettingTab {
plugin: MyPlugin;
export class OpenEditorsSettingTab extends PluginSettingTab {
plugin: OpenEditorsPlugin;
constructor (
app: App,
plugin: MyPlugin
plugin: OpenEditorsPlugin
) {
super(app, plugin);
this.plugin = plugin;

View file

@ -1,8 +1,47 @@
/*
.open-editors-container {
font-size: var(--font-ui-small);
padding: var(--size-4-3) var(--size-4-3) var(--size-4-8);
overflow: auto;
}
This CSS file will be included with your plugin, and
available in the app when your plugin is enabled.
.tree-iten-action-icon {
vertical-align: middle;
display: flex;
align-items: center;
margin-inline-end: var(--size-4-2);
}
If your plugin does not need CSS, delete this file.
.tree-iten-action-icon:last-child {
vertical-align: middle;
display: flex;
align-items: center;
margin-inline-end: 0;
}
*/
.tree-iten-action-icon>span>svg {
height: var(--icon-s) !important;
width: var(--icon-s) !important;
}
.tree-iten-action-icon:hover {
background-color: var(--interactive-hover);
}
.open-editors-tree-item-self {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.open-editors-tree-item-inner {
margin-top: var(--size-4-1) !important;
margin-bottom: var(--size-4-1) !important;
}
.open-editors-tree-item-icon {
margin-top: var(--size-4-1) !important;
margin-bottom: var(--size-4-1) !important;
}
.open-editors-tree-item-flair-outer {
/* padding-top: var(--size-4-1); */
}