enh(plugin): make the decorator settings changes on the third-party plugin take effect immediately

When changing the relevant decorator settings, re-render the
decorator on the third-party plugin.
This commit is contained in:
dragonish 2025-05-29 00:00:00 +08:00
parent 777d02b69e
commit 55729c1e13
No known key found for this signature in database
GPG key ID: 6F42FA9E807A5177
7 changed files with 617 additions and 394 deletions

View file

@ -104,12 +104,15 @@ export const sourceHeadingDecoratorClassName =
"source-custom-heading-decorator";
export const outlineHeadingDecoratorClassName =
"outline-custom-heading-decorator";
export const outlineContainerClassName = "outline-custom-heading-container";
export const quietOutlineHeadingDecoratorClassName =
"quiet-outline-custom-heading-decorator";
export const quietOutlineContainerClassName =
"quiet-outline-custom-heading-container";
export const fileExplorerHeadingDecoratorClassName =
"file-explorer-custom-heading-decorator";
export const fileExplorerContainerClassName =
"file-explorer-custom-heading-container";
export const beforeDecoratorClassName = "before-heading-decorator";
export const beforeInsideDecoratorClassName = "before-inside-heading-decorator";
export const afterDecoratorClassName = "after-heading-decorator";

View file

@ -1,4 +1,3 @@
import { htmlToMarkdown } from "obsidian";
import {
headingDecoratorClassName,
readingHeadingDecoratorClassName,
@ -6,9 +5,6 @@ import {
beforeInsideDecoratorClassName,
afterDecoratorClassName,
afterInsideDecoratorClassName,
outlineHeadingDecoratorClassName,
fileExplorerHeadingDecoratorClassName,
compareMarkdownText,
} from "./data";
/**
@ -118,150 +114,3 @@ export function decorateHTMLElement(
}
}
}
/**
* Get the tree item level of a given element.
*
* @param element The element to get the tree item level for.
* @returns The tree item level.
*/
export function getTreeItemLevel(element: Element): number {
let level = 0;
let current = element.closest(".tree-item");
while (current) {
level++;
current = current.parentElement?.closest(".tree-item") || null;
}
return level;
}
/**
* Get the text of a given tree item.
*
* @param element The HTML element to get the text for.
* @returns The text of the tree item.
*/
export function getTreeItemText(element: HTMLElement): string {
const inner = element.querySelector<HTMLElement>(
".tree-item-self .tree-item-inner"
);
return inner ? inner.innerText : "";
}
/**
* Decorate an outline HTML element with a given content, opacity and position.
*
* @param element The HTML element to decorate.
* @param content The content to decorate with.
* @param opacity The opacity of the decorator.
* @param position The position of the decorator.
*/
export function decorateOutlineElement(
element: HTMLElement,
content: string,
opacity: OpacityOptions,
position: PostionOptions
): void {
if (content) {
const inner = element.querySelector<HTMLElement>(
".tree-item-self .tree-item-inner"
);
if (inner) {
inner.dataset.headingDecorator = content;
inner.dataset.decoratorOpacity = `${opacity}%`;
inner.classList.add(
outlineHeadingDecoratorClassName,
getPositionClassName(position, true)
);
}
}
}
/**
* Cancel an outline decorator from an HTML element.
*
* @param element The HTML element to cancel the decorator.
*/
export function cancelOutlineDecorator(element: HTMLElement): void {
const inner = element.querySelector<HTMLElement>(
".tree-item-self .tree-item-inner"
);
if (inner) {
delete inner.dataset.headingDecorator;
delete inner.dataset.decoratorOpacity;
inner.classList.remove(
outlineHeadingDecoratorClassName,
beforeDecoratorClassName,
afterDecoratorClassName
);
}
}
/**
* Get the level of a file heading item based on its margin-left value.
*
* @param element The file heading item element.
* @param marginMultiplier The multiplier used to calculate the level.
* @returns The level of the file heading item.
*/
export function getFileHeadingItemLevel(
element: HTMLElement,
marginMultiplier: number
): number {
const marginLeft = element.style.marginLeft;
if (marginLeft) {
const value = parseInt(marginLeft.replace("px", ""));
return Math.floor(value / marginMultiplier) + 1;
}
return 0;
}
/**
* Decorate a file heading element with a custom content and style.
*
* @param element The file heading element to decorate.
* @param content The content to decorate with.
* @param opacity The opacity of the decorator.
* @param position The position of the decorator.
*/
export function decorateFileHeadingElement(
element: HTMLElement,
content: string,
opacity: OpacityOptions,
position: PostionOptions
): void {
if (content) {
element.dataset.headingDecorator = content;
element.dataset.decoratorOpacity = `${opacity}%`;
element.classList.add(
fileExplorerHeadingDecoratorClassName,
getPositionClassName(position, true)
);
}
}
/**
* Cancel a file heading decorator from an element.
*
* @param element The file heading element to cancel decorator.
*/
export function cancelFileHeadingDecorator(element: HTMLElement): void {
delete element.dataset.headingDecorator;
delete element.dataset.decoratorOpacity;
element.classList.remove(
fileExplorerHeadingDecoratorClassName,
beforeDecoratorClassName,
afterDecoratorClassName
);
}
/**
* Compare heading text.
*
* @param source The source heading text.
* @param outline The outline heading text.
* @returns true if the two headings are equal, false otherwise.
*/
export function compareHeadingText(source: string, outline: string): boolean {
return compareMarkdownText(htmlToMarkdown(source), htmlToMarkdown(outline));
}

View file

@ -40,15 +40,19 @@ export class ViewChildComponent extends Component {
}
}
render(): void {
this.observer?.disconnect();
this.decorationCallback();
if (this.containerEl) {
this.observer?.observe(this.containerEl, this.config);
}
}
onload(): void {
this.decorationCallback();
this.observer = new MutationObserver(() => {
this.observer?.disconnect();
this.decorationCallback();
if (this.containerEl) {
this.observer?.observe(this.containerEl, this.config);
}
this.render();
});
if (this.containerEl) {

187
components/file-explorer.ts Normal file
View file

@ -0,0 +1,187 @@
import { HeadingCache } from "obsidian";
import type { HeadingDecoratorSettings } from "../common/data";
import {
fileExplorerHeadingDecoratorClassName,
fileExplorerContainerClassName,
beforeDecoratorClassName,
afterDecoratorClassName,
getOrderedCustomIdents,
getUnorderedLevelHeadings,
} from "../common/data";
import { Querier, Counter } from "../common/counter";
/**
* File Explorer Heading Decorator.
*
* @param settings The heading decorator settings.
* @param container The container element.
* @param headingElements The heading elements.
* @param cacheHeadings The cache headings.
*/
export function fileExplorerHandler(
settings: HeadingDecoratorSettings,
container: HTMLElement,
headingElements: NodeListOf<HTMLElement>,
cacheHeadings: HeadingCache[]
): void {
const {
opacity,
position,
ordered,
orderedDelimiter,
orderedTrailingDelimiter,
orderedStyleType,
orderedSpecifiedString,
orderedCustomIdents,
orderedIgnoreSingle,
orderedIgnoreMaximum = 6,
orderedBasedOnExisting,
orderedAllowZeroLevel,
unorderedLevelHeadings,
} = settings;
container.classList.add(fileExplorerContainerClassName);
let ignoreTopLevel = 0;
if (ordered && (orderedIgnoreSingle || orderedBasedOnExisting)) {
const queier = new Querier(orderedAllowZeroLevel);
for (const cacheHeading of cacheHeadings) {
queier.handler(cacheHeading.level);
ignoreTopLevel = queier.query(orderedIgnoreSingle, orderedIgnoreMaximum);
if (ignoreTopLevel === 0) {
break;
}
}
}
const counter = new Counter({
ordered,
delimiter: orderedDelimiter,
trailingDelimiter: orderedTrailingDelimiter,
styleType: orderedStyleType,
customIdents: getOrderedCustomIdents(orderedCustomIdents),
specifiedString: orderedSpecifiedString,
ignoreTopLevel,
allowZeroLevel: orderedAllowZeroLevel,
levelHeadings: getUnorderedLevelHeadings(unorderedLevelHeadings),
});
const marginMultiplier =
parseInt(
getComputedStyle(document.body).getPropertyValue(
"--clickable-heading-margin-multiplier"
)
) || 10;
for (
let i = 0, j = 0;
i < headingElements.length && j < cacheHeadings.length;
i++, j++
) {
const readLevel = getFileHeadingItemLevel(
headingElements[i],
marginMultiplier
);
const readText = headingElements[i].innerText;
let cacheLevel = cacheHeadings[j].level;
while (
j < cacheHeadings.length - 1 &&
(cacheLevel !== readLevel || cacheHeadings[j].heading !== readText)
) {
counter.handler(cacheLevel);
j++;
cacheLevel = cacheHeadings[j].level;
}
const decoratorContent = counter.decorator(cacheLevel);
decorateFileHeadingElement(
headingElements[i],
decoratorContent,
opacity,
position
);
}
}
/**
* Cancel the decoration of file headings in a container.
*
* @param container The container element containing the file headings.
*/
export function cancelFileExplorerDecoration(container: HTMLElement): void {
if (container.classList.contains(fileExplorerContainerClassName)) {
container.classList.remove(fileExplorerContainerClassName);
const headingElements = container.querySelectorAll<HTMLElement>(
".file-heading-container .clickable-heading"
);
headingElements.forEach((ele) => {
cancelFileHeadingDecorator(ele);
});
}
}
/**
* Decorate a file heading element with a custom content and style.
*
* @param element The file heading element to decorate.
* @param content The content to decorate with.
* @param opacity The opacity of the decorator.
* @param position The position of the decorator.
*/
function decorateFileHeadingElement(
element: HTMLElement,
content: string,
opacity: OpacityOptions,
position: PostionOptions
): void {
if (content) {
element.dataset.headingDecorator = content;
element.dataset.decoratorOpacity = `${opacity}%`;
//? Remove potential residual class names
element.classList.remove(
position === "after" ? beforeDecoratorClassName : afterDecoratorClassName
);
element.classList.add(
fileExplorerHeadingDecoratorClassName,
position === "after" ? afterDecoratorClassName : beforeDecoratorClassName
);
}
}
/**
* Cancel a file heading decorator from an element.
*
* @param element The file heading element to cancel decorator.
*/
function cancelFileHeadingDecorator(element: HTMLElement): void {
delete element.dataset.headingDecorator;
delete element.dataset.decoratorOpacity;
element.classList.remove(
fileExplorerHeadingDecoratorClassName,
beforeDecoratorClassName,
afterDecoratorClassName
);
}
/**
* Get the level of a file heading item based on its margin-left value.
*
* @param element The file heading item element.
* @param marginMultiplier The multiplier used to calculate the level.
* @returns The level of the file heading item.
*/
function getFileHeadingItemLevel(
element: HTMLElement,
marginMultiplier: number
): number {
const marginLeft = element.style.marginLeft;
if (marginLeft) {
const value = parseInt(marginLeft.replace("px", ""));
return Math.floor(value / marginMultiplier) + 1;
}
return 0;
}

221
components/outline.ts Normal file
View file

@ -0,0 +1,221 @@
import { HeadingCache, htmlToMarkdown } from "obsidian";
import type { HeadingDecoratorSettings } from "../common/data";
import {
outlineHeadingDecoratorClassName,
outlineContainerClassName,
beforeDecoratorClassName,
afterDecoratorClassName,
getOrderedCustomIdents,
getUnorderedLevelHeadings,
diffLevel,
compareMarkdownText,
} from "../common/data";
import { Querier, Counter } from "../common/counter";
/**
* Handle the outline rendering based on the given settings and heading elements.
*
* @param settings The heading decorator settings.
* @param container The container element to render the outline.
* @param headingElements The heading elements to render the outline.
* @param cacheHeadings The cache headings to render the outline.
*/
export function outlineHandler(
settings: HeadingDecoratorSettings,
container: HTMLElement,
headingElements: NodeListOf<HTMLElement>,
cacheHeadings: HeadingCache[]
): void {
const {
opacity,
position,
ordered,
orderedDelimiter,
orderedTrailingDelimiter,
orderedStyleType,
orderedSpecifiedString,
orderedCustomIdents,
orderedIgnoreSingle,
orderedIgnoreMaximum = 6,
orderedBasedOnExisting,
orderedAllowZeroLevel,
unorderedLevelHeadings,
} = settings;
container.classList.add(outlineContainerClassName);
let ignoreTopLevel = 0;
if (ordered && (orderedIgnoreSingle || orderedBasedOnExisting)) {
const queier = new Querier(orderedAllowZeroLevel);
for (const cacheHeading of cacheHeadings) {
queier.handler(cacheHeading.level);
ignoreTopLevel = queier.query(orderedIgnoreSingle, orderedIgnoreMaximum);
if (ignoreTopLevel === 0) {
break;
}
}
}
const counter = new Counter({
ordered,
delimiter: orderedDelimiter,
trailingDelimiter: orderedTrailingDelimiter,
styleType: orderedStyleType,
customIdents: getOrderedCustomIdents(orderedCustomIdents),
specifiedString: orderedSpecifiedString,
ignoreTopLevel,
allowZeroLevel: orderedAllowZeroLevel,
levelHeadings: getUnorderedLevelHeadings(unorderedLevelHeadings),
});
let lastCacheLevel = 0;
let lastReadLevel = 0;
for (
let i = 0, j = 0;
i < headingElements.length && j < cacheHeadings.length;
i++, j++
) {
const readLevel = getTreeItemLevel(headingElements[i]);
const readText = getTreeItemText(headingElements[i]);
let cacheLevel = cacheHeadings[j].level;
if (i > 0) {
const diff = diffLevel(readLevel, lastReadLevel);
while (
j < cacheHeadings.length - 1 &&
(diffLevel(cacheLevel, lastCacheLevel) !== diff ||
!compareHeadingText(cacheHeadings[j].heading, readText))
) {
counter.handler(cacheLevel);
j++;
cacheLevel = cacheHeadings[j].level;
}
}
const decoratorContent = counter.decorator(cacheLevel);
decorateOutlineElement(
headingElements[i],
decoratorContent,
opacity,
position
);
lastCacheLevel = cacheLevel;
lastReadLevel = readLevel;
}
}
/**
* Cancel the outline decoration for a given container.
*
* @param container The container element that holds the outline elements.
*/
export function cancelOutlineDecoration(container: HTMLElement): void {
if (container.classList.contains(outlineContainerClassName)) {
container.classList.remove(outlineContainerClassName);
const headingElements =
container.querySelectorAll<HTMLElement>(".tree-item");
headingElements.forEach((ele) => {
cancelOutlineDecorator(ele);
});
}
}
/**
* Decorate an outline HTML element with a given content, opacity and position.
*
* @param element The HTML element to decorate.
* @param content The content to decorate with.
* @param opacity The opacity of the decorator.
* @param position The position of the decorator.
*/
function decorateOutlineElement(
element: HTMLElement,
content: string,
opacity: OpacityOptions,
position: PostionOptions
): void {
if (content) {
const inner = element.querySelector<HTMLElement>(
".tree-item-self .tree-item-inner"
);
if (inner) {
inner.dataset.headingDecorator = content;
inner.dataset.decoratorOpacity = `${opacity}%`;
//? Remove potential residual class names
inner.classList.remove(
position === "after"
? beforeDecoratorClassName
: afterDecoratorClassName
);
inner.classList.add(
outlineHeadingDecoratorClassName,
position === "after"
? afterDecoratorClassName
: beforeDecoratorClassName
);
}
}
}
/**
* Cancel an outline decorator from an HTML element.
*
* @param element The HTML element to cancel the decorator.
*/
function cancelOutlineDecorator(element: HTMLElement): void {
const inner = element.querySelector<HTMLElement>(
".tree-item-self .tree-item-inner"
);
if (inner) {
delete inner.dataset.headingDecorator;
delete inner.dataset.decoratorOpacity;
inner.classList.remove(
outlineHeadingDecoratorClassName,
beforeDecoratorClassName,
afterDecoratorClassName
);
}
}
/**
* Get the tree item level of a given element.
*
* @param element The element to get the tree item level for.
* @returns The tree item level.
*/
function getTreeItemLevel(element: Element): number {
let level = 0;
let current = element.closest(".tree-item");
while (current) {
level++;
current = current.parentElement?.closest(".tree-item") || null;
}
return level;
}
/**
* Get the text of a given tree item.
*
* @param element The HTML element to get the text for.
* @returns The text of the tree item.
*/
function getTreeItemText(element: HTMLElement): string {
const inner = element.querySelector<HTMLElement>(
".tree-item-self .tree-item-inner"
);
return inner ? inner.innerText : "";
}
/**
* Compare heading text.
*
* @param source The source heading text.
* @param outline The outline heading text.
* @returns true if the two headings are equal, false otherwise.
*/
function compareHeadingText(source: string, outline: string): boolean {
return compareMarkdownText(htmlToMarkdown(source), htmlToMarkdown(outline));
}

View file

@ -13,7 +13,6 @@ import {
headingsSelector,
getUnorderedLevelHeadings,
getOrderedCustomIdents,
diffLevel,
getBoolean,
checkEnabledCSS,
stringToRegex,
@ -21,18 +20,7 @@ import {
} from "../common/data";
import { Counter, Querier } from "../common/counter";
import { Heading } from "../common/heading";
import {
decorateHTMLElement,
decorateOutlineElement,
cancelOutlineDecorator,
decorateFileHeadingElement,
cancelFileHeadingDecorator,
queryHeadingLevelByElement,
getTreeItemLevel,
getTreeItemText,
getFileHeadingItemLevel,
compareHeadingText,
} from "../common/dom";
import { decorateHTMLElement, queryHeadingLevelByElement } from "../common/dom";
import {
HeadingEditorViewPlugin,
headingDecorationsField,
@ -40,10 +28,15 @@ import {
updateEditorMode,
} from "./editor";
import { ViewChildComponent } from "./child";
import { outlineHandler, cancelOutlineDecoration } from "./outline";
import {
quietOutlineHandler,
cancelQuietOutlineDecoration,
} from "./quiet-outline";
import {
fileExplorerHandler,
cancelFileExplorerDecoration,
} from "./file-explorer";
import { HeadingSettingTab } from "./setting-tab";
interface ObsidianEditor extends Editor {
@ -54,15 +47,18 @@ interface ObsidianWorkspaceLeaf extends WorkspaceLeaf {
id: string;
}
interface SettingsChangeState {
outline?: boolean;
quietOutline?: boolean;
fileExplorer?: boolean;
type OnChangeCallback = (path: string, newValue: unknown) => void;
interface RevocableProxy {
proxy: HeadingPluginSettings;
revoke: () => void;
}
export class HeadingPlugin extends Plugin {
settings: HeadingPluginSettings;
private revokes: (() => void)[] = [];
private outlineIdSet: Set<string> = new Set();
private outlineComponents: ViewChildComponent[] = [];
@ -72,6 +68,94 @@ export class HeadingPlugin extends Plugin {
private fileExplorerIdSet: Set<string> = new Set();
private fileExplorerComponents: ViewChildComponent[] = [];
private debouncedRerenderPreviewMarkdown = debounce(
this.rerenderPreviewMarkdown.bind(this),
1000,
true
);
private debouncedRerenderOutlineDecorator = debounce(
this.rerenderOutlineDecorator.bind(this),
1000,
true
);
private debouncedRerenderQuietOutlineDecorator = debounce(
this.rerenderQuietOutlineDecorator.bind(this),
1000,
true
);
private debouncedRerenderFileExplorerDecorator = debounce(
this.rerenderFileExplorerDecorator.bind(this),
1000,
true
);
private createDeepRevocableProxy<T extends HeadingPluginSettings>(
obj: T,
onChange: OnChangeCallback,
revokes: (() => void)[],
cache = new WeakMap<object, RevocableProxy>(),
path = ""
): RevocableProxy {
if (typeof obj !== "object" || obj === null) {
return { proxy: obj, revoke: () => {} };
}
// If it has already been proxy, return the cached proxy directly.
if (cache.has(obj)) {
return cache.get(obj)!;
}
const revocable = Proxy.revocable(obj, {
get: (target, prop, receiver) => {
const value = Reflect.get(target, prop, receiver);
if (typeof value === "object" && value !== null) {
const newPath = path ? `${path}.${String(prop)}` : String(prop);
return this.createDeepRevocableProxy(
value as unknown as HeadingPluginSettings,
onChange,
revokes,
cache,
newPath
).proxy;
}
return value;
},
set: (target, prop, value, receiver) => {
const oldValue = Reflect.get(target, prop, receiver);
const result = Reflect.set(target, prop, value, receiver);
if (oldValue !== value) {
const changedPath = path ? `${path}.${String(prop)}` : String(prop);
onChange(changedPath, value);
}
return result;
},
});
revokes.push(revocable.revoke);
cache.set(obj, revocable);
return revocable;
}
private async loadSettings() {
const rawSettings = Object.assign<HeadingPluginSettings, unknown>(
defalutSettings(),
await this.loadData()
);
this.revokes.forEach((revoke) => revoke());
this.revokes = [];
const { proxy } = this.createDeepRevocableProxy(
rawSettings,
this.settingsChanged.bind(this),
this.revokes
);
this.settings = proxy;
}
async onload() {
await this.loadSettings();
@ -294,42 +378,55 @@ export class HeadingPlugin extends Plugin {
this.unloadOutlineComponents();
this.unloadQuietOutlineComponents();
this.unloadFileExplorerComponents();
this.revokes.forEach((revoke) => revoke());
this.revokes = [];
}
async loadSettings() {
this.settings = Object.assign({}, defalutSettings(), await this.loadData());
}
async saveSettings(state?: SettingsChangeState) {
async saveSettings() {
await this.saveData(this.settings);
}
if (state) {
if (state.outline != undefined) {
if (state.outline) {
this.loadOutlineComponents();
} else {
this.unloadOutlineComponents();
}
private settingsChanged(path: string, value: unknown) {
if (path === "enabledInOutline") {
if (value) {
this.loadOutlineComponents();
} else {
this.unloadOutlineComponents();
}
if (state.quietOutline != undefined) {
if (state.quietOutline) {
this.loadQuietOutlineComponents();
} else {
this.unloadQuietOutlineComponents();
}
} else if (path === "enabledInQuietOutline") {
if (value) {
this.loadQuietOutlineComponents();
} else {
this.unloadQuietOutlineComponents();
}
if (state.fileExplorer != undefined) {
if (state.fileExplorer) {
this.loadFileExplorerComponents();
} else {
this.unloadFileExplorerComponents();
}
} else if (path === "enabledInFileExplorer") {
if (value) {
this.loadFileExplorerComponents();
} else {
this.unloadFileExplorerComponents();
}
} else if (path.startsWith("outlineSettings")) {
this.debouncedRerenderOutlineDecorator();
} else if (path.startsWith("quietOutlineSettings")) {
this.debouncedRerenderQuietOutlineDecorator();
} else if (path.startsWith("fileExplorerSettings")) {
this.debouncedRerenderFileExplorerDecorator();
} else if (
path === "enabledInReading" ||
path.startsWith("readingSettings")
) {
this.debouncedRerenderPreviewMarkdown();
} else if (
path === "metadataKeyword" ||
path.startsWith("fileRegexBlacklist") ||
path.startsWith("folderBlacklist")
) {
this.debouncedRerenderPreviewMarkdown();
this.debouncedRerenderOutlineDecorator();
this.debouncedRerenderQuietOutlineDecorator();
this.debouncedRerenderFileExplorerDecorator();
}
this.debouncedSaveSettings();
}
private loadOutlineComponents() {
@ -452,103 +549,36 @@ export class HeadingPlugin extends Plugin {
frontmatter
);
const {
enabledInEachNote,
opacity,
position,
ordered,
orderedDelimiter,
orderedTrailingDelimiter,
orderedStyleType,
orderedSpecifiedString,
orderedCustomIdents,
orderedIgnoreSingle,
orderedIgnoreMaximum = 6,
orderedBasedOnExisting,
orderedAllowZeroLevel,
unorderedLevelHeadings,
} = this.settings.outlineSettings;
const { enabledInEachNote } = this.settings.outlineSettings;
let enabled = true;
if (metadataEnabled == null) {
if (enabledInEachNote != undefined && !enabledInEachNote) {
return;
enabled = false;
}
if (this.getEnabledFromBlacklist(state.file)) {
return;
enabled = false;
}
} else if (!metadataEnabled) {
return;
enabled = false;
}
let ignoreTopLevel = 0;
if (ordered && (orderedIgnoreSingle || orderedBasedOnExisting)) {
const queier = new Querier(orderedAllowZeroLevel);
for (const cacheHeading of cacheHeadings) {
queier.handler(cacheHeading.level);
ignoreTopLevel = queier.query(
orderedIgnoreSingle,
orderedIgnoreMaximum
);
if (ignoreTopLevel === 0) {
break;
}
}
}
const counter = new Counter({
ordered,
delimiter: orderedDelimiter,
trailingDelimiter: orderedTrailingDelimiter,
styleType: orderedStyleType,
customIdents: getOrderedCustomIdents(orderedCustomIdents),
specifiedString: orderedSpecifiedString,
ignoreTopLevel,
allowZeroLevel: orderedAllowZeroLevel,
levelHeadings: getUnorderedLevelHeadings(unorderedLevelHeadings),
});
let lastCacheLevel = 0;
let lastReadLevel = 0;
for (
let i = 0, j = 0;
i < headingElements.length && j < cacheHeadings.length;
i++, j++
) {
const readLevel = getTreeItemLevel(headingElements[i]);
const readText = getTreeItemText(headingElements[i]);
let cacheLevel = cacheHeadings[j].level;
if (i > 0) {
const diff = diffLevel(readLevel, lastReadLevel);
while (
j < cacheHeadings.length - 1 &&
(diffLevel(cacheLevel, lastCacheLevel) !== diff ||
!compareHeadingText(cacheHeadings[j].heading, readText))
) {
counter.handler(cacheLevel);
j++;
cacheLevel = cacheHeadings[j].level;
}
}
const decoratorContent = counter.decorator(cacheLevel);
decorateOutlineElement(
headingElements[i],
decoratorContent,
opacity,
position
if (enabled) {
outlineHandler(
this.settings.outlineSettings,
viewContent,
headingElements,
cacheHeadings
);
lastCacheLevel = cacheLevel;
lastReadLevel = readLevel;
} else {
cancelOutlineDecoration(viewContent);
}
},
() => {
const headingElements =
viewContent.querySelectorAll<HTMLElement>(".tree-item");
headingElements.forEach((ele) => {
cancelOutlineDecorator(ele);
});
if (viewContent) {
cancelOutlineDecoration(viewContent);
}
}
);
@ -628,16 +658,15 @@ export class HeadingPlugin extends Plugin {
enabled = false;
}
if (!enabled) {
if (enabled) {
quietOutlineHandler(
this.settings.quietOutlineSettings,
containerElement,
headingELements
);
} else {
cancelQuietOutlineDecoration(containerElement);
return;
}
quietOutlineHandler(
this.settings.quietOutlineSettings,
containerElement,
headingELements
);
},
() => {
const containerElement =
@ -718,108 +747,38 @@ export class HeadingPlugin extends Plugin {
frontmatter
);
const {
enabledInEachNote,
opacity,
position,
ordered,
orderedDelimiter,
orderedTrailingDelimiter,
orderedStyleType,
orderedSpecifiedString,
orderedCustomIdents,
orderedIgnoreSingle,
orderedIgnoreMaximum = 6,
orderedBasedOnExisting,
orderedAllowZeroLevel,
unorderedLevelHeadings,
} = this.settings.fileExplorerSettings;
const { enabledInEachNote } = this.settings.fileExplorerSettings;
let enabled = true;
if (metadataEnabled == null) {
if (enabledInEachNote != undefined && !enabledInEachNote) {
return;
enabled = false;
}
if (this.getEnabledFromBlacklist(filePath)) {
return;
enabled = false;
}
} else if (!metadataEnabled) {
return;
enabled = false;
}
let ignoreTopLevel = 0;
if (ordered && (orderedIgnoreSingle || orderedBasedOnExisting)) {
const queier = new Querier(orderedAllowZeroLevel);
for (const cacheHeading of cacheHeadings) {
queier.handler(cacheHeading.level);
ignoreTopLevel = queier.query(
orderedIgnoreSingle,
orderedIgnoreMaximum
);
if (ignoreTopLevel === 0) {
break;
}
}
}
const counter = new Counter({
ordered,
delimiter: orderedDelimiter,
trailingDelimiter: orderedTrailingDelimiter,
styleType: orderedStyleType,
customIdents: getOrderedCustomIdents(orderedCustomIdents),
specifiedString: orderedSpecifiedString,
ignoreTopLevel,
allowZeroLevel: orderedAllowZeroLevel,
levelHeadings: getUnorderedLevelHeadings(unorderedLevelHeadings),
});
const marginMultiplier =
parseInt(
getComputedStyle(document.body).getPropertyValue(
"--clickable-heading-margin-multiplier"
)
) || 10;
for (
let i = 0, j = 0;
i < headingElements.length && j < cacheHeadings.length;
i++, j++
) {
const readLevel = getFileHeadingItemLevel(
headingElements[i],
marginMultiplier
);
const readText = headingElements[i].innerText;
let cacheLevel = cacheHeadings[j].level;
while (
j < cacheHeadings.length - 1 &&
(cacheLevel !== readLevel ||
cacheHeadings[j].heading !== readText)
) {
counter.handler(cacheLevel);
j++;
cacheLevel = cacheHeadings[j].level;
}
const decoratorContent = counter.decorator(cacheLevel);
decorateFileHeadingElement(
headingElements[i],
decoratorContent,
opacity,
position
if (enabled) {
fileExplorerHandler(
this.settings.fileExplorerSettings,
navFile,
headingElements,
cacheHeadings
);
} else {
cancelFileExplorerDecoration(navFile);
}
});
},
() => {
const headingElements =
navFilesContainer.querySelectorAll<HTMLElement>(
".nav-file-title .file-heading-container .clickable-heading"
);
headingElements.forEach((ele) => {
cancelFileHeadingDecorator(ele);
const containerElements =
navFilesContainer.querySelectorAll<HTMLElement>(".nav-file-title");
containerElements.forEach((ele) => {
cancelFileExplorerDecoration(ele);
});
}
);
@ -834,12 +793,6 @@ export class HeadingPlugin extends Plugin {
});
}
private debouncedSaveSettings = debounce(
this.rerenderPreviewMarkdown.bind(this),
1000,
true
);
/**
* Rerender Preview Markdown.
* @param file
@ -853,6 +806,18 @@ export class HeadingPlugin extends Plugin {
}
}
private rerenderOutlineDecorator() {
this.outlineComponents.forEach((vc) => vc.render());
}
private rerenderQuietOutlineDecorator() {
this.quietOutlineComponents.forEach((vc) => vc.render());
}
private rerenderFileExplorerDecorator() {
this.fileExplorerComponents.forEach((vc) => vc.render());
}
async getPluginData(): Promise<HeadingPluginData> {
const {
metadataKeyword,

View file

@ -121,9 +121,7 @@ export class HeadingSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.enabledInOutline)
.onChange(async (value) => {
this.plugin.settings.enabledInOutline = value;
await this.plugin.saveSettings({
outline: this.plugin.settings.enabledInOutline,
});
await this.plugin.saveSettings();
})
);
@ -145,9 +143,7 @@ export class HeadingSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.enabledInQuietOutline)
.onChange(async (value) => {
this.plugin.settings.enabledInQuietOutline = value;
await this.plugin.saveSettings({
quietOutline: this.plugin.settings.enabledInQuietOutline,
});
await this.plugin.saveSettings();
})
);
@ -182,9 +178,7 @@ export class HeadingSettingTab extends PluginSettingTab {
.setValue(this.plugin.settings.enabledInFileExplorer)
.onChange(async (value) => {
this.plugin.settings.enabledInFileExplorer = value;
await this.plugin.saveSettings({
fileExplorer: this.plugin.settings.enabledInFileExplorer,
});
await this.plugin.saveSettings();
})
);