mirror of
https://github.com/dragonish/obsidian-heading-decorator.git
synced 2026-07-22 05:42:05 +00:00
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:
parent
777d02b69e
commit
55729c1e13
7 changed files with 617 additions and 394 deletions
|
|
@ -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";
|
||||
|
|
|
|||
151
common/dom.ts
151
common/dom.ts
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
187
components/file-explorer.ts
Normal 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
221
components/outline.ts
Normal 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));
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue