review fixes

This commit is contained in:
Nick 2026-02-18 10:21:02 +01:00
parent d24635f0dc
commit 8e3d2f6f9c
8 changed files with 159 additions and 94 deletions

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 Nick (Reifat)
Copyright (c) 2026 Nick (Reifat)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -3,7 +3,7 @@
"name": "Daily notes sorter",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Automatically sort daily notes and files by date in the file explorer. Supports multiple date formats (YYYY-MM-DD, DD.MM.YYYY, DD.MM.YY, MM/DD/YYYY) and flexible folder configuration.",
"description": "Automatically sort daily notes and files by date in the file explorer. Supports multiple date formats and flexible folder configuration.",
"author": "Nick",
"authorUrl": "https://github.com/Reifat",
"isDesktopOnly": false

View file

@ -14,6 +14,8 @@ export class FileExplorerUtils {
private sorter: Sorter;
private settings: { items: FolderItem[]; sortAscending?: boolean };
private pluginInstance: { saveSettings: () => Promise<void> } | undefined; // Reference to plugin instance for saving settings
private fileExplorerInitialized = false;
private fileExplorerInitializationInProgress = false;
public sortAscending: boolean = true; // Public for access from closure
constructor(
@ -102,10 +104,12 @@ export class FileExplorerUtils {
* Gets FileExplorerView instance from workspace
*/
getFileExplorer(): FileExplorerView | undefined {
const fileExplorer: FileExplorerView | undefined = this.app.workspace
.getLeavesOfType('file-explorer')
.first()?.view as unknown as FileExplorerView;
return fileExplorer;
const view = this.app.workspace.getLeavesOfType('file-explorer').first()?.view;
if (view && typeof (view as Partial<FileExplorerView>).requestSort === 'function') {
return view as FileExplorerView;
}
return undefined;
}
/**
@ -125,18 +129,55 @@ export class FileExplorerUtils {
const checkFileExplorer = (): void => {
const fileExplorer = this.getFileExplorer();
if (fileExplorer) {
clearInterval(timer);
window.clearInterval(timer);
callback(fileExplorer);
resolve();
} else if (Date.now() - startTime > timeout) {
clearInterval(timer);
window.clearInterval(timer);
reject(new Error('[FileExplorerUtils] FileExplorer was not loaded, reason is time up!'));
}
};
const timer = setInterval(checkFileExplorer, interval);
const timer = window.setInterval(checkFileExplorer, interval);
});
}
async ensureFileExplorerInitialized(
onInitialized: (fileExplorer: FileExplorerView) => void,
): Promise<void> {
if (this.fileExplorerInitialized || this.fileExplorerInitializationInProgress) {
return;
}
this.fileExplorerInitializationInProgress = true;
return this.waitForFileExplorer(
(fileExplorer) => {
if (this.fileExplorerInitialized) {
return;
}
this.patchFileExplorerFolder(fileExplorer);
onInitialized(fileExplorer);
this.fileExplorerInitialized = true;
// Use a small delay to ensure the patch is applied.
window.setTimeout(() => {
this.applySort();
}, 100);
},
100,
1500,
)
.catch(() => {
// File Explorer may still be deferred/not visible yet. We'll retry on next layout change.
console.debug(
"[FileExplorerUtils] File Explorer may still be deferred/not visible yet. We'll retry on next layout change.",
);
})
.finally(() => {
this.fileExplorerInitializationInProgress = false;
});
}
/**
* Checks availability and patchability of FileExplorer
*/

View file

@ -46,22 +46,16 @@ export default class DailyNotesSorter extends Plugin {
// Initialize UI for FileExplorer
this.explorerUI = new ExplorerUI(this.app, this.fileExplorerUtils);
this.fileExplorerUtils
.waitForFileExplorer((fileExplorer) => {
this.fileExplorerUtils.patchFileExplorerFolder(fileExplorer);
this.explorerUI.initialize(fileExplorer);
// Apply sorting after plugin load
// Use a small delay to ensure the patch is applied
setTimeout(() => {
this.fileExplorerUtils.applySort();
}, 100);
})
.catch((err: unknown) => {
console.error('[DailyNotesSorter] Error initializing FileExplorer:', err);
// Note: We don't show a Notice here as the plugin might still work
// if FileExplorer loads later
});
void this.fileExplorerUtils.ensureFileExplorerInitialized((fileExplorer) => {
this.explorerUI.initialize(fileExplorer);
});
this.registerEvent(
this.app.workspace.on('layout-change', () => {
void this.fileExplorerUtils.ensureFileExplorerInitialized((fileExplorer) => {
this.explorerUI.initialize(fileExplorer);
});
}),
);
this.addSettingTab(new SorterSettings(this.app, this));
}

View file

@ -6,6 +6,8 @@ export class AutocompleteInput {
private inputEl: HTMLInputElement;
private onValueChange: (value: string) => void;
private getSuggestions: () => string[];
private blurTimeoutId: number | null = null;
private disconnectObserver: MutationObserver | null = null;
constructor(
container: HTMLElement,
@ -18,13 +20,14 @@ export class AutocompleteInput {
this.getSuggestions = getSuggestions;
this.suggestionsList = this.createSuggestionsList(container);
this.attachEventListeners();
this.setupAutoCleanupOnDetach();
}
/**
* Updates suggestion list if it's visible
*/
public refresh(): void {
if (!this.suggestionsList.hasClass('hidden')) {
if (!this.suggestionsList.hasClass('dns-hidden')) {
const query = this.inputEl.value.trim();
if (query) {
this.handleInput();
@ -39,35 +42,58 @@ export class AutocompleteInput {
* Checks if suggestion list is visible
*/
public isVisible(): boolean {
return !this.suggestionsList.hasClass('hidden');
return !this.suggestionsList.hasClass('dns-hidden');
}
private createSuggestionsList(container: HTMLElement): HTMLElement {
return container.createDiv({
cls: 'suggestions-list_1 hidden',
cls: 'dns-suggestions-list dns-hidden',
});
}
private attachEventListeners(): void {
this.inputEl.addEventListener('input', () => {
this.handleInput();
});
this.inputEl.addEventListener('focus', () => {
this.handleFocus();
});
this.inputEl.addEventListener('blur', () => {
this.handleBlur();
});
// Recalculate height on window resize
window.addEventListener('resize', () => {
if (!this.suggestionsList.hasClass('hidden')) {
this.adjustListHeight();
}
});
this.inputEl.addEventListener('input', this.handleInput);
this.inputEl.addEventListener('focus', this.handleFocus);
this.inputEl.addEventListener('blur', this.handleBlur);
window.addEventListener('resize', this.handleResize);
}
private handleInput(): void {
private setupAutoCleanupOnDetach(): void {
const rootNode = this.inputEl.ownerDocument.body;
this.disconnectObserver = new MutationObserver(() => {
if (!this.inputEl.isConnected) {
this.dispose();
}
});
this.disconnectObserver.observe(rootNode, { childList: true, subtree: true });
}
private dispose(): void {
if (!this.disconnectObserver) {
return;
}
this.inputEl.removeEventListener('input', this.handleInput);
this.inputEl.removeEventListener('focus', this.handleFocus);
this.inputEl.removeEventListener('blur', this.handleBlur);
window.removeEventListener('resize', this.handleResize);
if (this.blurTimeoutId !== null) {
window.clearTimeout(this.blurTimeoutId);
this.blurTimeoutId = null;
}
this.disconnectObserver.disconnect();
this.disconnectObserver = null;
}
private handleResize = (): void => {
if (!this.suggestionsList.hasClass('dns-hidden')) {
this.adjustListHeight();
}
};
private handleInput = (): void => {
const query = this.inputEl.value.trim();
this.suggestionsList.empty();
@ -82,9 +108,9 @@ export class AutocompleteInput {
} else {
this.hideSuggestions();
}
}
};
private handleFocus(): void {
private handleFocus = (): void => {
if (this.inputEl.value.trim()) {
const query = this.inputEl.value.trim();
const filtered = this.filterSuggestions(query);
@ -94,14 +120,18 @@ export class AutocompleteInput {
this.showSuggestions(filtered);
}
}
}
};
private handleBlur(): void {
private handleBlur = (): void => {
// Delay for handling click on suggestion
setTimeout(() => {
if (this.blurTimeoutId !== null) {
window.clearTimeout(this.blurTimeoutId);
}
this.blurTimeoutId = window.setTimeout(() => {
this.hideSuggestions();
this.blurTimeoutId = null;
}, 100);
}
};
private filterSuggestions(query: string): string[] {
const lowerQuery = query.toLowerCase();
@ -112,7 +142,7 @@ export class AutocompleteInput {
private showSuggestions(suggestions: string[]): void {
// Always clear list before adding new elements
this.suggestionsList.empty();
this.suggestionsList.removeClass('hidden');
this.suggestionsList.removeClass('dns-hidden');
// Limit number of displayed suggestions for performance
const maxSuggestions = 20;
@ -132,7 +162,7 @@ export class AutocompleteInput {
// Show message if there are more suggestions
if (suggestions.length > maxSuggestions) {
this.suggestionsList.createDiv({
cls: 'suggestion-item_1 suggestion-more',
cls: 'dns-suggestion-item dns-suggestion-more',
text: `... and ${suggestions.length - maxSuggestions} more`,
});
}
@ -149,7 +179,7 @@ export class AutocompleteInput {
*/
private adjustListHeight(): void {
// Check if list is visible
if (this.suggestionsList.hasClass('hidden')) {
if (this.suggestionsList.hasClass('dns-hidden')) {
return;
}
@ -158,7 +188,7 @@ export class AutocompleteInput {
'--suggestions-list-height': 'auto',
'--suggestions-list-overflow': 'auto',
});
this.suggestionsList.removeClass('no-scroll');
this.suggestionsList.removeClass('dns-no-scroll');
// Get actual content height
const contentHeight = this.suggestionsList.scrollHeight;
@ -176,14 +206,14 @@ export class AutocompleteInput {
'--suggestions-list-height': `${contentHeight}px`,
'--suggestions-list-overflow': 'hidden',
});
this.suggestionsList.addClass('no-scroll');
this.suggestionsList.addClass('dns-no-scroll');
} else if (contentHeight > 0) {
// Content doesn't fit, set maximum height with scrolling
this.suggestionsList.setCssProps({
'--suggestions-list-height': `${maxAllowedHeight}px`,
'--suggestions-list-overflow': 'auto',
});
this.suggestionsList.removeClass('no-scroll');
this.suggestionsList.removeClass('dns-no-scroll');
}
}
@ -199,7 +229,7 @@ export class AutocompleteInput {
}
const item = this.suggestionsList.createDiv({
cls: 'suggestion-item_1',
cls: 'dns-suggestion-item',
text: suggestion,
});
@ -211,6 +241,6 @@ export class AutocompleteInput {
}
private hideSuggestions(): void {
this.suggestionsList.addClass('hidden');
this.suggestionsList.addClass('dns-hidden');
}
}

View file

@ -37,10 +37,10 @@ export class ExplorerUI {
);
// Style the button using CSS class and custom color
setTimeout(() => {
window.setTimeout(() => {
const button = fileExplorer.headerDom.navButtonsEl?.lastElementChild as HTMLElement | null;
if (button) {
button.addClass('file-explorer-sort-button');
button.addClass('dns-file-explorer-sort-button');
const svg = button.querySelector('svg') as {
setCssProps?: (props: Record<string, string>) => void;
} | null;
@ -72,7 +72,7 @@ export class ExplorerUI {
// Reopen FileExplorer leaves with saved state
// Use setTimeout to ensure detach completes before reopening
setTimeout(() => {
window.setTimeout(() => {
// Choose the view state to restore: use the first saved state
const preferred = leavesData[0];

View file

@ -154,7 +154,7 @@ export class SorterSettings extends PluginSettingTab {
}
const itemDiv = this.itemsContainer.createDiv({
cls: 'setting-item-custom-plugin',
cls: 'dns-setting-item',
});
const inputSetting = this.createInputSetting(itemDiv, item.path || '', index);
@ -213,12 +213,12 @@ export class SorterSettings extends PluginSettingTab {
if (inputEl) {
if (!isValidPath) {
inputEl.addClass('input-error');
inputEl.removeClass('input-valid');
inputEl.addClass('dns-input-error');
inputEl.removeClass('dns-input-valid');
isValid = false;
} else {
inputEl.removeClass('input-error');
inputEl.addClass('input-valid');
inputEl.removeClass('dns-input-error');
inputEl.addClass('dns-input-valid');
}
} else {
if (!isValidPath) {
@ -239,11 +239,11 @@ export class SorterSettings extends PluginSettingTab {
if (inputEl) {
if (!isValid) {
inputEl.addClass('input-error');
inputEl.removeClass('input-valid');
inputEl.addClass('dns-input-error');
inputEl.removeClass('dns-input-valid');
} else {
inputEl.removeClass('input-error');
inputEl.addClass('input-valid');
inputEl.removeClass('dns-input-error');
inputEl.addClass('dns-input-valid');
}
}
@ -379,11 +379,11 @@ export class SorterSettings extends PluginSettingTab {
*/
private renderApplyButton(container: HTMLElement): void {
const buttonContainer = container.createDiv({
cls: 'apply-button-container',
cls: 'dns-apply-button-container',
});
const applyButton = buttonContainer.createEl('button', {
cls: 'apply-button',
cls: 'dns-apply-button',
text: APPLY_BUTTON_TEXT,
});
@ -415,8 +415,8 @@ export class SorterSettings extends PluginSettingTab {
// Remove all validation indicators after successful application
this.inputElements.forEach((inputEl) => {
inputEl.removeClass('input-error');
inputEl.removeClass('input-valid');
inputEl.removeClass('dns-input-error');
inputEl.removeClass('dns-input-valid');
});
new Notice('Settings applied successfully!');

View file

@ -1,12 +1,12 @@
.setting-item-custom-plugin {
.dns-setting-item {
position: relative; /* Child elements are now positioned relative to this container */
width: 100%; /* Ensure the container takes the required width */
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2);
border-radius: 5px;
}
.suggestions-list_1 {
position: absolute; /* Positioned relative to setting-item-custom-plugin */
.dns-suggestions-list {
position: absolute; /* Positioned relative to dns-setting-item */
top: 100%; /* Starts immediately below the input field */
left: 0; /* Aligned to the left edge of the parent */
width: 100%; /* Matches the width of the input field */
@ -22,7 +22,7 @@
min-height: 0;
}
.suggestion-item_1 {
.dns-suggestion-item {
padding: 10px;
cursor: pointer;
color: var(--text-normal);
@ -30,38 +30,38 @@
border-bottom: 1px solid var(--background-modifier-border);
}
.suggestion-item_1:hover {
.dns-suggestion-item:hover {
background-color: var(--background-modifier-hover);
}
.suggestion-item_1:last-child {
.dns-suggestion-item:last-child {
border-bottom: none;
}
.suggestion-more {
.dns-suggestion-more {
font-style: italic;
opacity: 0.7;
}
.suggestions-list_1.no-scroll {
.dns-suggestions-list.dns-no-scroll {
overflow-y: hidden;
}
.hidden {
.dns-hidden {
display: none;
}
/* Validation styles */
.input-error {
border: 2px solid var(--text-error) !important;
background-color: var(--background-modifier-error) !important;
.dns-setting-item input.dns-input-error {
border: 2px solid var(--text-error);
background-color: var(--background-modifier-error);
}
.input-valid {
border: 2px solid var(--text-success) !important;
.dns-setting-item input.dns-input-valid {
border: 2px solid var(--text-success);
}
.apply-button-container {
.dns-apply-button-container {
display: flex;
justify-content: flex-end;
margin-top: 20px;
@ -69,7 +69,7 @@
border-top: 1px solid var(--background-modifier-border);
}
.apply-button {
.dns-apply-button {
padding: 8px 20px;
background-color: var(--interactive-accent);
color: var(--text-on-accent);
@ -79,16 +79,16 @@
font-weight: 500;
}
.apply-button:hover {
.dns-apply-button:hover {
background-color: var(--interactive-accent-hover);
}
.apply-button:disabled {
.dns-apply-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* File Explorer sort button styles */
.file-explorer-sort-button svg {
.dns-file-explorer-sort-button svg {
color: var(--sort-button-icon-color, #fff);
}