mirror of
https://github.com/4source/open-editors-obsidian-plugin.git
synced 2026-07-22 11:40:26 +00:00
Add OpenEditorsView
This commit is contained in:
parent
4c46331bea
commit
efcde42e16
9 changed files with 362 additions and 20 deletions
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
137
src/OpenEditorsView.ts
Normal 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
123
src/TreeItem.ts
Normal 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
11
src/constants.ts
Normal 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';
|
||||
40
src/main.ts
40
src/main.ts
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
49
styles.css
49
styles.css
|
|
@ -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); */
|
||||
}
|
||||
Loading…
Reference in a new issue