mirror of
https://github.com/ozntel/oz-calendar.git
synced 2026-07-22 07:40:24 +00:00
feat: 添加打开标签页列表功能并修复全局app引用问题
1. 新增OpenTabsComponent组件,在日历视图底部展示当前打开的标签页 2. 为标签页列表添加拖拽、点击打开、悬浮预览等交互功能 3. 修复部分文件中直接使用全局app变量的问题,统一使用插件实例的app属性 4. 添加对应的CSS样式来布局日历和标签页区域
This commit is contained in:
parent
897c0890ca
commit
bb8633dace
7 changed files with 289 additions and 28 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={'oz-calendar-plugin-view ' + fixedCalendarClass}>
|
||||
<Calendar
|
||||
onChange={setSelectedDay}
|
||||
value={selectedDay}
|
||||
maxDetail="month"
|
||||
minDetail="month"
|
||||
showWeekNumbers={plugin.settings.showWeekNumbers}
|
||||
view="month"
|
||||
tileContent={customTileContent}
|
||||
tileClassName={customTileClass}
|
||||
calendarType={plugin.settings.calendarType}
|
||||
showFixedNumberOfWeeks={plugin.settings.fixedCalendar}
|
||||
activeStartDate={activeStartDate}
|
||||
onActiveStartDateChange={(props) => {
|
||||
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')}
|
||||
/>
|
||||
<>
|
||||
<div className="oz-calendar-top-section">
|
||||
<Calendar
|
||||
onChange={setSelectedDay}
|
||||
value={selectedDay}
|
||||
maxDetail="month"
|
||||
minDetail="month"
|
||||
showWeekNumbers={plugin.settings.showWeekNumbers}
|
||||
view="month"
|
||||
tileContent={customTileContent}
|
||||
tileClassName={customTileClass}
|
||||
calendarType={plugin.settings.calendarType}
|
||||
showFixedNumberOfWeeks={plugin.settings.fixedCalendar}
|
||||
activeStartDate={activeStartDate}
|
||||
onActiveStartDateChange={(props) => {
|
||||
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')}
|
||||
/>
|
||||
<div id="oz-calendar-divider"></div>
|
||||
<NoteListComponent
|
||||
selectedDay={selectedDay}
|
||||
|
|
@ -135,7 +136,8 @@ export default function MyCalendar(params: { plugin: OZCalendarPlugin }) {
|
|||
forceValue={forceValue}
|
||||
createNote={createNote}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
<OpenTabsComponent plugin={plugin} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
179
src/components/openTabs.tsx
Normal file
179
src/components/openTabs.tsx
Normal file
|
|
@ -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<string>();
|
||||
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<OpenTabNote[]>(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<HTMLDivElement, 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<HTMLDivElement>, 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<HTMLDivElement>) => {
|
||||
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<HTMLElement | null>(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<HTMLDivElement>, 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 (
|
||||
<div className="oz-calendar-open-tabs-container">
|
||||
<div className="oz-calendar-open-tabs-header">Open tabs</div>
|
||||
<div className="oz-calendar-open-tabs-list">
|
||||
{openTabs.length === 0 && (
|
||||
<div className="oz-calendar-note-no-note">No open tabs</div>
|
||||
)}
|
||||
{openTabs.map((note) => (
|
||||
<div
|
||||
className={
|
||||
'oz-calendar-note-line' +
|
||||
(plugin.settings.fileNameOverflowBehaviour == 'hide'
|
||||
? ' oz-calendar-overflow-hide'
|
||||
: '')
|
||||
}
|
||||
id={`oz-open-tab-${note.path}`}
|
||||
key={note.path}
|
||||
draggable={true}
|
||||
onClick={(e) => openFilePath(e, note.path)}
|
||||
onMouseEnter={(e) => handleNoteMouseEnter(e, note)}
|
||||
onMouseLeave={handleNoteMouseLeave}
|
||||
onDragStart={(e) => handleDragStart(e, note)}
|
||||
onDragEnd={handleDragEnd}>
|
||||
<HiOutlineDocumentText className="oz-calendar-note-line-icon" />
|
||||
<span>{note.displayName}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
import { TAbstractFile, TFolder } from 'obsidian';
|
||||
import { TextInputSuggest } from './suggest';
|
||||
|
||||
declare const app: any;
|
||||
|
||||
export class FolderSuggest extends TextInputSuggest<TFolder> {
|
||||
getSuggestions(inputStr: string): TFolder[] {
|
||||
const abstractFiles = app.vault.getAllLoadedFiles();
|
||||
|
|
|
|||
38
styles.css
38
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue