diff --git a/release/oz-calendar/styles.css b/release/oz-calendar/styles.css
index 96761f4..8cb3c7c 100644
--- a/release/oz-calendar/styles.css
+++ b/release/oz-calendar/styles.css
@@ -327,3 +327,41 @@ settings:
.oz-calendar-note-line-dragging {
opacity: 0.5;
}
+
+.oz-calendar-top-section {
+ display: flex;
+ flex-direction: column;
+ flex: 2 1 0;
+ min-height: 0;
+}
+
+.oz-calendar-open-tabs-container {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 0;
+ min-height: 0;
+ border-top: 1px solid var(--background-modifier-border);
+}
+
+.oz-calendar-open-tabs-header {
+ padding: 6px 8px;
+ font-weight: 600;
+ font-size: var(--nav-item-size);
+ color: var(--oz-calendar-header-date-color);
+ flex-shrink: 0;
+}
+
+.oz-calendar-open-tabs-list {
+ flex: 1 1 auto;
+ overflow-y: auto;
+ padding: 0 5px 5px 5px;
+ min-height: 0;
+}
+
+.oz-calendar-plugin-view.fixed .oz-calendar-top-section {
+ overflow: hidden;
+}
+
+.oz-calendar-plugin-view.fixed .oz-calendar-open-tabs-list {
+ overflow-y: auto;
+}
diff --git a/src/components/calendar.tsx b/src/components/calendar.tsx
index b3aac8a..d3d2a0e 100644
--- a/src/components/calendar.tsx
+++ b/src/components/calendar.tsx
@@ -3,6 +3,7 @@ import Calendar, { CalendarTileProperties } from 'react-calendar';
import { RxDotFilled } from 'react-icons/rx';
import OZCalendarPlugin from '../main';
import NoteListComponent from './noteList';
+import OpenTabsComponent from './openTabs';
import dayjs from 'dayjs';
import useForceUpdate from 'hooks/forceUpdate';
import { DayChangeCommandAction } from 'types';
@@ -100,32 +101,32 @@ export default function MyCalendar(params: { plugin: OZCalendarPlugin }) {
return (
-
{
- if (props.action === 'next') {
- setActiveStartDate(dayjs(activeStartDate).add(1, 'month').toDate());
- } else if (props.action === 'next2') {
- setActiveStartDate(dayjs(activeStartDate).add(12, 'month').toDate());
- } else if (props.action === 'prev') {
- setActiveStartDate(dayjs(activeStartDate).add(-1, 'month').toDate());
- } else if (props.action === 'prev2') {
- setActiveStartDate(dayjs(activeStartDate).add(-12, 'month').toDate());
- }
- }}
- formatMonthYear={(locale, date) => dayjs(date).format('MMM YYYY')}
- />
- <>
+
+
{
+ if (props.action === 'next') {
+ setActiveStartDate(dayjs(activeStartDate).add(1, 'month').toDate());
+ } else if (props.action === 'next2') {
+ setActiveStartDate(dayjs(activeStartDate).add(12, 'month').toDate());
+ } else if (props.action === 'prev') {
+ setActiveStartDate(dayjs(activeStartDate).add(-1, 'month').toDate());
+ } else if (props.action === 'prev2') {
+ setActiveStartDate(dayjs(activeStartDate).add(-12, 'month').toDate());
+ }
+ }}
+ formatMonthYear={(locale, date) => dayjs(date).format('MMM YYYY')}
+ />
- >
+
+
);
}
diff --git a/src/components/openTabs.tsx b/src/components/openTabs.tsx
new file mode 100644
index 0000000..fec0fb8
--- /dev/null
+++ b/src/components/openTabs.tsx
@@ -0,0 +1,179 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { TFile } from 'obsidian';
+import { HiOutlineDocumentText } from 'react-icons/hi';
+import OZCalendarPlugin from '../main';
+import { openFile } from '../util/utils';
+
+interface OpenTabNote {
+ displayName: string;
+ path: string;
+}
+
+interface OpenTabsComponentParams {
+ plugin: OZCalendarPlugin;
+}
+
+export default function OpenTabsComponent(params: OpenTabsComponentParams) {
+ const { plugin } = params;
+
+ const getOpenTabNotes = (): OpenTabNote[] => {
+ const seen = new Set();
+ const notes: OpenTabNote[] = [];
+ plugin.app.workspace.iterateAllLeaves((leaf) => {
+ const state = leaf.getViewState();
+ if (state.type !== 'markdown') return;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const file: TFile | undefined = (leaf.view as any)?.file;
+ if (!file || seen.has(file.path)) return;
+ seen.add(file.path);
+ notes.push({ displayName: file.basename, path: file.path });
+ });
+ return notes;
+ };
+
+ const [openTabs, setOpenTabs] = useState(getOpenTabNotes());
+
+ useEffect(() => {
+ const update = () => setOpenTabs(getOpenTabNotes());
+ const refs = [
+ plugin.app.workspace.on('layout-change', update),
+ plugin.app.workspace.on('active-leaf-change', update),
+ plugin.app.workspace.on('file-open', update),
+ ];
+ return () => {
+ refs.forEach((ref) => plugin.app.workspace.offref(ref));
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const openFilePath = (e: React.MouseEvent, filePath: string) => {
+ const abstractFile = plugin.app.vault.getAbstractFileByPath(filePath);
+ const openFileBehaviour = plugin.settings.openFileBehaviour;
+ if (abstractFile && abstractFile instanceof TFile) {
+ let openInNewLeaf: boolean = openFileBehaviour === 'new-tab';
+ let openInNewTabGroup: boolean = openFileBehaviour === 'new-tab-group';
+ if (openFileBehaviour === 'obsidian-default') {
+ openInNewLeaf = (e.ctrlKey || e.metaKey) && !(e.shiftKey || e.altKey);
+ openInNewTabGroup = (e.ctrlKey || e.metaKey) && (e.shiftKey || e.altKey);
+ }
+ openFile({
+ file: abstractFile,
+ plugin: plugin,
+ newLeaf: openInNewLeaf,
+ leafBySplit: openInNewTabGroup,
+ });
+ }
+ };
+
+ const handleDragStart = (e: React.DragEvent, note: OpenTabNote) => {
+ // Use Obsidian's internal DragManager so the editor's drop handler
+ // recognizes the drag source. Standard HTML5 dataTransfer types alone
+ // (text/plain, text/uri-list) make the browser show the "not allowed"
+ // cursor when dragging into the editor; the editor's drop handler only
+ // preventDefault()s on dragover for drag payloads that went through
+ // dragManager.onDragStart.
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const dragManager = (plugin.app as any).dragManager;
+ if (dragManager && typeof dragManager.dragLink === 'function') {
+ const wikiLink = `[[${note.displayName}]]`;
+ const dragData = dragManager.dragLink(e.nativeEvent, wikiLink, note.path);
+ dragManager.onDragStart(e.nativeEvent, dragData);
+ } else {
+ const obsidianUri = `obsidian://open?vault=${encodeURIComponent(
+ plugin.app.vault.getName()
+ )}&file=${encodeURIComponent(note.path)}`;
+ e.dataTransfer.effectAllowed = 'link';
+ e.dataTransfer.setData('text/uri-list', obsidianUri);
+ e.dataTransfer.setData('text/plain', `[[${note.displayName}]]`);
+ }
+ e.currentTarget.classList.add('oz-calendar-note-line-dragging');
+ };
+
+ const handleDragEnd = (e: React.DragEvent) => {
+ e.currentTarget.classList.remove('oz-calendar-note-line-dragging');
+ };
+
+ // Hover preview (Ctrl/Cmd pressed -> native HoverPopover, no delay).
+ const isModPressed = (e: React.MouseEvent): boolean => e.ctrlKey || e.metaKey;
+
+ const activeTargetRef = useRef(null);
+
+ const forceCloseHoverPreview = () => {
+ const targetEl = activeTargetRef.current;
+ activeTargetRef.current = null;
+ if (targetEl) {
+ targetEl.dispatchEvent(new MouseEvent('mouseleave', { bubbles: false }));
+ }
+ document.querySelectorAll('.popover.hover-popover').forEach((el) => {
+ el.dispatchEvent(new MouseEvent('mouseleave', { bubbles: false }));
+ });
+ };
+
+ const handleNoteMouseEnter = (e: React.MouseEvent, note: OpenTabNote) => {
+ if (!isModPressed(e)) return;
+ forceCloseHoverPreview();
+ activeTargetRef.current = e.currentTarget;
+ plugin.app.workspace.trigger('hover-link', {
+ event: e.nativeEvent,
+ source: 'oz-calendar',
+ hoverParent: { hoverPopover: null },
+ targetEl: e.currentTarget,
+ linktext: note.path,
+ sourcePath: note.path,
+ });
+ };
+
+ const handleNoteMouseLeave = () => {
+ if (activeTargetRef.current) {
+ forceCloseHoverPreview();
+ }
+ };
+
+ useEffect(() => {
+ const onKeyUp = (ev: KeyboardEvent) => {
+ if (!ev.ctrlKey && !ev.metaKey) {
+ forceCloseHoverPreview();
+ }
+ };
+ const onBlur = () => forceCloseHoverPreview();
+ window.addEventListener('keyup', onKeyUp);
+ window.addEventListener('blur', onBlur);
+ return () => {
+ window.removeEventListener('keyup', onKeyUp);
+ window.removeEventListener('blur', onBlur);
+ forceCloseHoverPreview();
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+
Open tabs
+
+ {openTabs.length === 0 && (
+
No open tabs
+ )}
+ {openTabs.map((note) => (
+
openFilePath(e, note.path)}
+ onMouseEnter={(e) => handleNoteMouseEnter(e, note)}
+ onMouseLeave={handleNoteMouseLeave}
+ onDragStart={(e) => handleDragStart(e, note)}
+ onDragEnd={handleDragEnd}>
+
+ {note.displayName}
+
+ ))}
+
+
+ );
+}
diff --git a/src/main.ts b/src/main.ts
index 47c1cbb..525d5f1 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -323,7 +323,7 @@ export default class OZCalendarPlugin extends Plugin {
for (let mdFile of mdFiles) {
if (this.settings.dateSource === 'yaml') {
// Get the file Cache
- let fileCache = app.metadataCache.getFileCache(mdFile);
+ let fileCache = this.app.metadataCache.getFileCache(mdFile);
// Check if there is Frontmatter
if (fileCache && fileCache.frontmatter) {
let fm = fileCache.frontmatter;
diff --git a/src/settings/suggest.ts b/src/settings/suggest.ts
index a930900..3af1004 100644
--- a/src/settings/suggest.ts
+++ b/src/settings/suggest.ts
@@ -3,6 +3,8 @@
import { ISuggestOwner, Scope } from 'obsidian';
import { createPopper, Instance as PopperInstance } from '@popperjs/core';
+declare const app: any;
+
const wrapAround = (value: number, size: number): number => {
return ((value % size) + size) % size;
};
diff --git a/src/settings/suggestor.ts b/src/settings/suggestor.ts
index b4c9df0..905be7e 100644
--- a/src/settings/suggestor.ts
+++ b/src/settings/suggestor.ts
@@ -3,6 +3,8 @@
import { TAbstractFile, TFolder } from 'obsidian';
import { TextInputSuggest } from './suggest';
+declare const app: any;
+
export class FolderSuggest extends TextInputSuggest {
getSuggestions(inputStr: string): TFolder[] {
const abstractFiles = app.vault.getAllLoadedFiles();
diff --git a/styles.css b/styles.css
index 96761f4..8cb3c7c 100644
--- a/styles.css
+++ b/styles.css
@@ -327,3 +327,41 @@ settings:
.oz-calendar-note-line-dragging {
opacity: 0.5;
}
+
+.oz-calendar-top-section {
+ display: flex;
+ flex-direction: column;
+ flex: 2 1 0;
+ min-height: 0;
+}
+
+.oz-calendar-open-tabs-container {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 0;
+ min-height: 0;
+ border-top: 1px solid var(--background-modifier-border);
+}
+
+.oz-calendar-open-tabs-header {
+ padding: 6px 8px;
+ font-weight: 600;
+ font-size: var(--nav-item-size);
+ color: var(--oz-calendar-header-date-color);
+ flex-shrink: 0;
+}
+
+.oz-calendar-open-tabs-list {
+ flex: 1 1 auto;
+ overflow-y: auto;
+ padding: 0 5px 5px 5px;
+ min-height: 0;
+}
+
+.oz-calendar-plugin-view.fixed .oz-calendar-top-section {
+ overflow: hidden;
+}
+
+.oz-calendar-plugin-view.fixed .oz-calendar-open-tabs-list {
+ overflow-y: auto;
+}