From f54b8b831bc7fcb934457682e63c5854b3c31cd8 Mon Sep 17 00:00:00 2001 From: kotaindah55 Date: Mon, 17 Mar 2025 10:41:52 +0200 Subject: [PATCH] feat: use TagMenu to cover all tag-based formatting Additionally, ColorMenu has to be removed, and use Formatter directly rather than caching some token informations to the menu. --- src/editor-mode/ui-components/ColorMenu.ts | 171 ------------------- src/editor-mode/ui-components/TagMenu.ts | 185 +++++++++++++++++++++ 2 files changed, 185 insertions(+), 171 deletions(-) delete mode 100644 src/editor-mode/ui-components/ColorMenu.ts create mode 100644 src/editor-mode/ui-components/TagMenu.ts diff --git a/src/editor-mode/ui-components/ColorMenu.ts b/src/editor-mode/ui-components/ColorMenu.ts deleted file mode 100644 index 359e7da..0000000 --- a/src/editor-mode/ui-components/ColorMenu.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { EditorView } from "@codemirror/view"; -import { Menu } from "obsidian"; -import { ColorConfig, IndexCache, PlainRange, Token } from "src/types"; -import { appFacet, settingsFacet } from "src/editor-mode/facets"; -import { getActiveCanvasNodeCoords } from "src/editor-mode/utils"; - -export class ColorMenu extends Menu { - openRange: PlainRange; - tagRange: PlainRange; - closeRange: PlainRange; - itemIndexCache: IndexCache; - moveCursorAfterTag: boolean; - view: EditorView; - private constructor(view: EditorView, openRange: PlainRange, tagRange: PlainRange, closeRange: PlainRange, moveCursorAfterTag: boolean, itemIndexCache: IndexCache) { - super(); - this.view = view; - this.openRange = openRange; - this.tagRange = tagRange; - this.closeRange = closeRange; - this.moveCursorAfterTag = moveCursorAfterTag; - this.dom.addClass("highlight-colors-modal"); - this.itemIndexCache = itemIndexCache; - } - get openLen() { - return this.openRange.to - this.openRange.from; - } - get closeLen() { - return this.closeRange.to - this.closeRange.from; - } - get tagLen() { - return this.tagRange.to - this.tagRange.from; - } - addColorConfigs(configs: ColorConfig[]) { - configs.forEach(({ name, tag, showInMenu }) => { - if (!showInMenu) { return } - this.setItem(name, "palette", "menu-item-" + tag, () => { this.changeColor(tag) }) - }); - } - addAccent() { - this.setItem("Accent", "palette", "menu-item-accent", () => { this.changeColor("accent") }); - } - addDefault() { - this.setItem("Default", "palette", "menu-item-default", () => { this.toDefault() }); - } - addRemove() { - this.setItem("Remove", "eraser", "menu-item-remove-highlight", () => { this.removeColor() }); - } - setItem(title: string, icon: string, cls: string, callback: (evt: MouseEvent | KeyboardEvent) => unknown) { - this.addItem(item => { - item.setTitle(title); - item.setIcon(icon); - item.dom.addClass(cls); - item.onClick(evt => { - let index = this.items.findIndex(i => i == item); - this.itemIndexCache.number = index; - callback(evt); - }); - }); - } - showMenu(offset?: number) { - this.checkItemIndexCache(); - this.view.requestMeasure({ - read: (view) => { - let app = view.state.facet(appFacet.reader), - canvasNodeCoords = getActiveCanvasNodeCoords(app), - charCoords = view.coordsForChar((offset ?? this.tagRange.from)); - if (offset !== undefined && !charCoords) { - charCoords = view.coordsForChar(offset - 1); - } - return { charCoords, canvasNodeCoords }; - }, - write: (measure) => { - let { charCoords, canvasNodeCoords } = measure; - if (charCoords) { - let menuCoords = { x: charCoords.left, y: charCoords.bottom }; - if (canvasNodeCoords) { - menuCoords.x += canvasNodeCoords.x; - menuCoords.y += canvasNodeCoords.y; - } - this.showAtPosition(menuCoords); - this.select(this.itemIndexCache.number); - } - } - }); - } - adjustPos(differ: number) { - this.tagRange.to += differ; - this.closeRange.from += differ; - this.closeRange.to += differ; - } - changeColor(color: string) { - let oldTagRange = { from: this.tagRange.from, to: this.tagRange.to }; - let differ = color.length + 2 - (this.tagRange.to - this.tagRange.from); - this.adjustPos(differ); - this.view.dispatch({ - changes: { - from: oldTagRange.from, - to: oldTagRange.to, - insert: `{${color}}` - } - }, { - selection: this.moveCursorAfterTag ? { - anchor: this.tagRange.to, - head: this.tagRange.to - } : undefined, - sequential: true - }); - } - removeColor() { - this.view.dispatch({ - changes: [{ - from: this.openRange.from, - to: this.tagRange.to, - insert: "" - }, { - from: this.closeRange.from, - to: this.closeRange.to, - insert: "" - }] - }); - } - toDefault() { - this.view.dispatch({ - changes: { - from: this.tagRange.from, - to: this.tagRange.to, - insert: "" - } - }); - let tagLen = this.tagLen; - this.adjustPos(-tagLen); - } - onunload() { - (this.view as unknown as null) = null; - super.onunload(); - } - checkItemIndexCache() { - if (this.itemIndexCache.number >= this.items.length) { - this.itemIndexCache.number = this.items.length - 1; - } - } - static create( - view: EditorView, - openRange: PlainRange, - tagRange: PlainRange, - closeRange: PlainRange, - moveCursorAfterTag: boolean, - itemIndexCache: IndexCache = { number: 0 }, - option: { default?: boolean, accent?: boolean, remove?: boolean } = { default: true, accent: true, remove: true } - ) { - let colorMenu = new ColorMenu(view, openRange, tagRange, closeRange, moveCursorAfterTag, itemIndexCache), - colorConfigs = view.state.facet(settingsFacet).colorConfigs; - colorMenu.addColorConfigs(colorConfigs); - if (option?.accent) { colorMenu.addAccent() } - if (option?.default) { colorMenu.addDefault() } - if (option?.remove) { colorMenu.addRemove() } - return colorMenu; - } - static fromToken(view: EditorView, hlToken: Token, moveCursorAfterTag: boolean, itemIndexCache: IndexCache = { number: 0 }, option: { default?: boolean, accent?: boolean, remove?: boolean } = { default: true, accent: true, remove: true }) { - let openRange = { from: hlToken.from, to: hlToken.from + hlToken.openLen }, - tagRange = { from: openRange.to, to: openRange.to + hlToken.tagLen }, - closeRange = { from: hlToken.to - hlToken.closeLen, to: hlToken.to }, - colorConfigs = view.state.facet(settingsFacet).colorConfigs, - colorMenu = new ColorMenu(view, openRange, tagRange, closeRange, moveCursorAfterTag, itemIndexCache); - colorMenu.addColorConfigs(colorConfigs); - if (option?.accent) { colorMenu.addAccent() } - if (option?.default) { colorMenu.addDefault() } - if (option?.remove) { colorMenu.addRemove() } - return colorMenu; - } -} \ No newline at end of file diff --git a/src/editor-mode/ui-components/TagMenu.ts b/src/editor-mode/ui-components/TagMenu.ts new file mode 100644 index 0000000..84bcd80 --- /dev/null +++ b/src/editor-mode/ui-components/TagMenu.ts @@ -0,0 +1,185 @@ +import { Direction, EditorView } from "@codemirror/view"; +import { IconName, Menu } from "obsidian"; +import { IndexCache, ITagMenu, PluginSettings, TagConfig } from "src/types"; +import { appFacet, settingsFacet } from "src/editor-mode/facets"; +import { getActiveCanvasNodeCoords } from "src/editor-mode/editor-utils"; +import { editorPlugin } from "src/editor-mode/extensions"; +import { Format } from "src/enums"; +import { Formatter } from "src/editor-mode/formatting"; +import { supportTag } from "src/format-configs/utils"; + +export class TagMenu extends Menu { + itemIndexCache: IndexCache = { number: 0 }; + view: EditorView; + type: Format; + formatter: Formatter; + settings: PluginSettings; + CLS_PREFIX = "ems-menu-item"; + ICON: string; + constructor(view: EditorView, type: Format) { + super(); + this.constructMenu(view, type); + } + constructMenu(view: EditorView, type: Format) { + this.view = view; + this.type = type; + this.formatter = view.plugin(editorPlugin)!.formatter; + this.settings = view.state.facet(settingsFacet); + this.bindIndexCache(); + this.setClassPrefix(); + this.setIcon(); + this.addConfigs(this.getConfigs()); + this.addDefaultItem(); + this.addRemoveItem(); + this.dom.addClass("ems-menu", "ems-tag-menu"); + if (type == Format.HIGHLIGHT) { + this.dom.addClass("ems-color-menu"); + } + } + setClassPrefix() { + if (this.type == Format.HIGHLIGHT) { + this.CLS_PREFIX += " ems-highlight-"; + } else if (this.type == Format.CUSTOM_SPAN) { + this.CLS_PREFIX += " ems-span-"; + } else if (this.type == Format.FENCED_DIV) { + this.CLS_PREFIX += " ems-div-"; + } + } + setIcon() { + if (this.type == Format.HIGHLIGHT) { + this.ICON = "palette"; + } else { + this.ICON = "shapes"; + } + } + bindIndexCache() { + let { indexCache } = this.view.plugin(editorPlugin)!; + switch (this.type) { + case Format.HIGHLIGHT: + this.itemIndexCache = indexCache.colorMenuItem; + break; + case Format.CUSTOM_SPAN: + this.itemIndexCache = indexCache.spanTagMenuItem; + break; + case Format.FENCED_DIV: + this.itemIndexCache = indexCache.divTagMenuItem; + break; + default: + this.itemIndexCache = { number: 0 }; + } + } + getConfigs() { + let configs: TagConfig[] = []; + if (this.type == Format.HIGHLIGHT) { + configs = this.settings.colorConfigs; + } else if (this.type == Format.CUSTOM_SPAN) { + configs = this.settings.predefinedSpanTag; + } else if (this.type == Format.FENCED_DIV) { + configs = this.settings.predefinedDivTag; + } + return configs; + } + addConfigs(configs: TagConfig[]) { + configs.forEach(({ name, tag, showInMenu }) => { + if (!showInMenu) { return } + this.addConfigItem(name, this.ICON, this.CLS_PREFIX + tag, () => { this.format(tag) }); + }); + if (this.type == Format.HIGHLIGHT && this.settings.showAccentColor) { + this.addConfigItem("Accent", this.ICON, this.CLS_PREFIX + "accent", () => { this.format("accent") }); + } + } + addConfigItem(name: string, icon: IconName, cls: string, callback: (evt: MouseEvent | KeyboardEvent) => unknown) { + this.addItem(item => { + item.setTitle(name); + item.setIcon(icon); + item.dom.addClass(...cls.split(" ")); + item.onClick(evt => { + let index = this.items.findIndex(i => i == item); + this.itemIndexCache.number = index; + callback(evt); + }); + }); + } + addDefaultItem() { + if ( + this.type == Format.HIGHLIGHT && this.settings.showDefaultColor || + this.type == Format.CUSTOM_SPAN && this.settings.showDefaultSpanTag + ) { + this.addConfigItem("Default", this.ICON, this.CLS_PREFIX + "default", () => { this.format("") }); + } + } + addRemoveItem() { + if ( + this.type == Format.HIGHLIGHT && this.settings.showRemoveColor || + this.type == Format.CUSTOM_SPAN && this.settings.showRemoveSpanTag + ) { + this.addConfigItem("Remove", "eraser", "ems-menu-item ems-remove", () => { this.removeFormatting() }); + } + } + format(tag: string) { + this.formatter.startFormat(this.type, tag); + } + removeFormatting() { + this.formatter.startFormat(this.type, undefined, true); + } + showMenu() { + this.checkItemIndexCache(); + this.view.requestMeasure({ + read: (view) => { + let app = view.state.facet(appFacet.reader), + canvasNodeCoords = getActiveCanvasNodeCoords(app), + charOffset = view.state.selection.ranges.at(-1)!.to, + charCoords = view.coordsForChar(charOffset); + if (!charCoords) { + let contentDOMCoords = view.contentDOM.getBoundingClientRect(), + firstBlock = view.lineBlockAt(charOffset), + isRTL = view.textDirection == Direction.RTL; + charCoords = { + top: contentDOMCoords.top, + bottom: contentDOMCoords.top + firstBlock.height, + left: isRTL ? contentDOMCoords.right : contentDOMCoords.left, + right: isRTL ? contentDOMCoords.left : contentDOMCoords.right + }; + } + return { charCoords, canvasNodeCoords }; + }, + write: (measure) => { + let { charCoords, canvasNodeCoords } = measure; + if (charCoords) { + let menuCoords = { x: charCoords.left, y: charCoords.bottom }; + if (canvasNodeCoords) { + menuCoords.x += canvasNodeCoords.x; + menuCoords.y += canvasNodeCoords.y; + } + this.showAtPosition(menuCoords); + this.select(this.itemIndexCache.number); + } + } + }); + } + checkItemIndexCache() { + if (this.itemIndexCache.number >= this.items.length) { + this.itemIndexCache.number = this.items.length - 1; + } + } + static bindMenu(other: Menu, ...args: ConstructorParameters) { + let methodNames = ["addConfigItem", "addConfigs", "bindIndexCache", "checkItemIndexCache", "constructMenu", "format", "removeFormatting", "showMenu", "setClassPrefix", "setIcon", "addDefaultItem", "addRemoveItem", "getConfigs"] as const; + for (let methodName of methodNames) { + Object.defineProperty(other, methodName, { + value: this.prototype[methodName].bind(other) + }); + } + Object.defineProperty(other, "CLS_PREFIX", { + value: "ems-menu-item", + configurable: true, + writable: true + }); + (other as ITagMenu).constructMenu(...args); + return other as ITagMenu; + } + static create(...args: ConstructorParameters) { + let [view, type] = args; + if (!supportTag(type)) { throw new TypeError("This format type doesn't support custom tag.") } + return new this(view, type); + } +} \ No newline at end of file