mirror of
https://github.com/kotaindah55/extended-markdown-syntax.git
synced 2026-07-22 05:38:06 +00:00
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.
This commit is contained in:
parent
cf701cad4d
commit
f54b8b831b
2 changed files with 185 additions and 171 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
185
src/editor-mode/ui-components/TagMenu.ts
Normal file
185
src/editor-mode/ui-components/TagMenu.ts
Normal file
|
|
@ -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<typeof TagMenu>) {
|
||||
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<typeof TagMenu>) {
|
||||
let [view, type] = args;
|
||||
if (!supportTag(type)) { throw new TypeError("This format type doesn't support custom tag.") }
|
||||
return new this(view, type);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue