mirror of
https://github.com/moranrs/table-master.git
synced 2026-07-22 06:53:11 +00:00
510 lines
24 KiB
TypeScript
510 lines
24 KiB
TypeScript
// Floating toolbar for the active table. Implemented as a CM6 ViewPlugin so
|
|
// it lives inside Obsidian's markdown editor and follows scroll / selection /
|
|
// focus changes naturally. Three placement modes are supported (see
|
|
// `FloatingToolbarPosition`):
|
|
// `on-click` (default) — rests at the editor's top-left corner;
|
|
// jumps to click position when clicking inside a table;
|
|
// returns to top-left when clicking outside.
|
|
// `follow-mouse` — follows the mouse pointer inside a table;
|
|
// falls back to top-left otherwise.
|
|
// `top-left` — always pinned to the editor's top-left corner.
|
|
|
|
import { EditorView, ViewPlugin, ViewUpdate, PluginValue } from "@codemirror/view";
|
|
import { App, MarkdownView } from "obsidian";
|
|
import type TableMasterPlugin from "../main";
|
|
import { Icons } from "./icons";
|
|
import { t } from "../i18n";
|
|
import * as actions from "../editor/actions";
|
|
import { getActionContext } from "../editor/contextHelper";
|
|
|
|
/** Resolve the CM6 EditorView that is currently the *active* markdown editor.
|
|
* Returns null when the active workspace leaf isn't a markdown view (file
|
|
* explorer, settings, etc.). Used to gate every toolbar instance on whether
|
|
* it belongs to that view — without this, multiple open / split / hover
|
|
* markdown panes each show their own toolbar simultaneously. */
|
|
function activeMarkdownEditorView(app: App): EditorView | null {
|
|
const md = app.workspace.getActiveViewOfType(MarkdownView);
|
|
// `editor.cm` is the CM6 EditorView in Obsidian's Live Preview build. It's
|
|
// not part of the public types but is the de facto way community plugins
|
|
// bridge from `Editor` to CodeMirror; safe-cast through `unknown`.
|
|
const cm = (md?.editor as unknown as { cm?: EditorView } | undefined)?.cm;
|
|
return cm ?? null;
|
|
}
|
|
|
|
interface ToolbarHost {
|
|
getApp(): App;
|
|
getPlugin(): TableMasterPlugin;
|
|
}
|
|
|
|
interface ToolbarItem {
|
|
icon: string;
|
|
tip: string;
|
|
run: () => void;
|
|
}
|
|
|
|
function appendSvgIcon(parent: HTMLElement, icon: string): void {
|
|
const parsedIcon = new DOMParser().parseFromString(icon, "image/svg+xml");
|
|
const iconEl = parsedIcon.documentElement;
|
|
if (iconEl && iconEl.nodeName.toLowerCase() === "svg") {
|
|
parent.appendChild(iconEl);
|
|
}
|
|
}
|
|
|
|
export function buildFloatingToolbarExt(host: ToolbarHost) {
|
|
return ViewPlugin.fromClass(
|
|
class implements PluginValue {
|
|
view: EditorView;
|
|
dom: HTMLElement;
|
|
visible = false;
|
|
lastFrom = -1;
|
|
|
|
constructor(view: EditorView) {
|
|
this.view = view;
|
|
this.dom = document.createElement("div");
|
|
this.dom.className = "tm-floating-toolbar";
|
|
// Tagged so `TableMasterPlugin.onunload` can sweep up any toolbars
|
|
// whose CM6 destroy() hook didn't fire (Obsidian doesn't always
|
|
// reconfigure already-open editor views on plugin unload).
|
|
this.dom.dataset.tmFloatingToolbar = "1";
|
|
// Hidden until refresh() decides to show it. Toggled via the
|
|
// `is-hidden` CSS class (defined in styles.css) rather than an inline
|
|
// `display` style — obsidianmd lint forbids static style.display
|
|
// assignments because they bypass theme overrides.
|
|
this.dom.classList.add("is-hidden");
|
|
// Stop bubbling to avoid Obsidian re-focusing editor and dropping cursor
|
|
this.dom.addEventListener("mousedown", (e) => e.preventDefault());
|
|
// IMPORTANT: mount on <body>, not view.dom.parentElement. Some
|
|
// Obsidian themes / wrappers put a `transform` (or `filter` /
|
|
// `perspective`) on an ancestor of the editor pane to trigger GPU
|
|
// compositing. Any of those properties anchor `position: fixed` to
|
|
// that ancestor instead of the viewport, which is why the user saw
|
|
// the toolbar systematically offset to the bottom-right. <body> is
|
|
// never inside such a transform, so fixed positioning is reliable.
|
|
document.body.appendChild(this.dom);
|
|
this.render();
|
|
// Subscribe to settings broadcasts so the toolbar reacts to a position
|
|
// change without requiring the user to also click in the editor.
|
|
this.settingsListener = () => this.refresh(this.view);
|
|
document.addEventListener("table-master:settings-changed", this.settingsListener);
|
|
// Track scroll on the editor's scroll container so the toolbar can
|
|
// re-anchor itself if a future placement mode wants viewport-tied
|
|
// coordinates. CM6's `viewportChanged` only fires when the rendered
|
|
// range changes, not on every scroll tick.
|
|
// (Currently a no-op for the active modes, but kept so adding a new
|
|
// mode that needs scroll updates is friction-free.)
|
|
this.scrollListener = () => {
|
|
if (this.raf != null) return;
|
|
this.raf = requestAnimationFrame(() => {
|
|
this.raf = null;
|
|
this.refresh(this.view);
|
|
});
|
|
};
|
|
view.scrollDOM.addEventListener("scroll", this.scrollListener, { passive: true });
|
|
window.addEventListener("scroll", this.scrollListener, { passive: true, capture: true });
|
|
// Install the on-click mousedown handler eagerly. It MUST exist before
|
|
// the user's first click on a table — otherwise that click happens
|
|
// while no listener is attached and the toolbar misses it. The handler
|
|
// self-gates on the current mode, so it's a no-op while the user is
|
|
// in `follow-mouse` / `top-left` mode.
|
|
this.ensureMouseDownListener();
|
|
this.update({ view, docChanged: true, selectionSet: true } as unknown as ViewUpdate);
|
|
}
|
|
|
|
// Most recent mouse coords *in viewport-relative pixels* — fed by a
|
|
// `mousemove` listener and consumed by both the "follow-mouse" and
|
|
// "on-click" position modes. We use `clientX/Y` so the placement code
|
|
// can rely on `position: fixed`, which sidesteps any quirks in the
|
|
// editor's parent chain (custom themes / unusual leaf wrappers etc.
|
|
// — that was the root cause of "top-left mode also doesn't show").
|
|
clientX = 0;
|
|
clientY = 0;
|
|
mouseInTable = false;
|
|
/** True while the on-click toolbar is positioned at the last click
|
|
* point (inside a table). Cleared when the user clicks outside. */
|
|
clickedInTable = false;
|
|
mouseListener: ((e: MouseEvent) => void) | null = null;
|
|
mouseDownListener: ((e: MouseEvent) => void) | null = null;
|
|
settingsListener: (() => void) | null = null;
|
|
scrollListener: (() => void) | null = null;
|
|
raf: number | null = null;
|
|
|
|
update(u: ViewUpdate) {
|
|
const settings = host.getPlugin().settings;
|
|
if (!settings.showFloatingToolbar) {
|
|
this.hide();
|
|
return;
|
|
}
|
|
// `focusChanged` is essential: when the user clicks into another
|
|
// markdown leaf (split / hover / second tab) every ViewPlugin
|
|
// instance receives a focusChanged update. Each refresh() then
|
|
// re-checks whether *its* view is still the active one, so only the
|
|
// currently-active toolbar stays visible — fixing the "two toolbars
|
|
// showing at once" symptom in split / hover-editor layouts.
|
|
if (
|
|
u.docChanged ||
|
|
u.selectionSet ||
|
|
u.viewportChanged ||
|
|
u.geometryChanged ||
|
|
u.focusChanged
|
|
) {
|
|
this.refresh(u.view);
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
this.dom.remove();
|
|
if (this.mouseListener) {
|
|
this.view.dom.removeEventListener("mousemove", this.mouseListener);
|
|
this.mouseListener = null;
|
|
}
|
|
if (this.mouseDownListener) {
|
|
document.removeEventListener("mousedown", this.mouseDownListener, true);
|
|
this.mouseDownListener = null;
|
|
}
|
|
if (this.settingsListener) {
|
|
document.removeEventListener("table-master:settings-changed", this.settingsListener);
|
|
this.settingsListener = null;
|
|
}
|
|
if (this.scrollListener) {
|
|
this.view.scrollDOM.removeEventListener("scroll", this.scrollListener);
|
|
window.removeEventListener("scroll", this.scrollListener, true);
|
|
this.scrollListener = null;
|
|
}
|
|
if (this.raf != null) {
|
|
cancelAnimationFrame(this.raf);
|
|
this.raf = null;
|
|
}
|
|
}
|
|
|
|
/** Shared entry point used by both `update()` and the mouse handler. */
|
|
refresh(view: EditorView) {
|
|
const settings = host.getPlugin().settings;
|
|
if (!settings.showFloatingToolbar) {
|
|
this.removeMouseListener();
|
|
// Note: we do NOT remove the mousedown listener here; it self-gates
|
|
// on `showFloatingToolbar` and must stay attached so re-enabling
|
|
// the toolbar doesn't require re-loading the plugin.
|
|
this.hide();
|
|
return;
|
|
}
|
|
// Only the *active* markdown editor's toolbar should be drawn.
|
|
// Obsidian routinely keeps multiple EditorView instances alive at
|
|
// once — split panes, hover editors, the second pane of a linked
|
|
// pane, the embedded editor inside a popover, etc. Every one of
|
|
// those instances runs this ViewPlugin and mounts its own toolbar
|
|
// DOM on <body>; without this gate they all show up simultaneously
|
|
// (which is exactly the "two toolbars" screenshot the user reported
|
|
// for top-left mode). We deliberately do NOT use `view.hasFocus`:
|
|
// it only flips on keyboard focus, so common cases like "the user
|
|
// just opened the file" or "clicked the toolbar button (which
|
|
// preventDefaults focus shift)" would incorrectly hide us.
|
|
const activeCM = activeMarkdownEditorView(host.getApp());
|
|
if (!activeCM || activeCM !== view) {
|
|
this.hide();
|
|
return;
|
|
}
|
|
// Belt-and-braces: if the active view's DOM somehow has zero size
|
|
// (e.g. mid-layout transition), bail out rather than land the
|
|
// toolbar at (0,0).
|
|
const rect = view.dom.getBoundingClientRect();
|
|
if (rect.width === 0 || rect.height === 0) {
|
|
this.hide();
|
|
return;
|
|
}
|
|
// Normalize the (now-removed) `above-table` value left over in old
|
|
// configs to the new default so the rest of this function only has
|
|
// to worry about the three live modes.
|
|
const rawMode = settings.floatingToolbarPosition as string;
|
|
const mode: "on-click" | "follow-mouse" | "top-left" =
|
|
rawMode === "follow-mouse" || rawMode === "top-left" ? rawMode : "on-click";
|
|
const parent = view.dom.parentElement;
|
|
if (!parent) {
|
|
this.hide();
|
|
return;
|
|
}
|
|
|
|
// Manage the mousemove listener, but NOT the mousedown listener.
|
|
// The mousedown listener is installed once at construction time and
|
|
// self-gates on the current mode — removing it would make on-click
|
|
// mode miss the first click after a mode switch.
|
|
if (mode !== "follow-mouse" && mode !== "top-left") this.removeMouseListener();
|
|
|
|
// Mode `on-click` — toolbar rests at the editor's top-left corner
|
|
// and jumps to the click position when the user clicks inside a
|
|
// table; clicks outside any table snap it back to the corner.
|
|
// The mousedown listener (installed at construction time) handles
|
|
// the jump; refresh() keeps it parked at top-left the rest of the
|
|
// time.
|
|
if (mode === "on-click") {
|
|
this.ensureVisible();
|
|
if (!this.clickedInTable) {
|
|
this.placeTopLeft(view);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Modes `follow-mouse` and `top-left` are *always-visible* anchors
|
|
// so the user can hit table commands even if our table-detection
|
|
// logic gets confused by the active theme. They use `position:
|
|
// fixed` to bypass any parent-positioning quirks.
|
|
this.ensureVisible();
|
|
this.ensureMouseListener();
|
|
if (mode === "top-left") {
|
|
this.placeTopLeft(view);
|
|
return;
|
|
}
|
|
// `follow-mouse`: placement is driven *entirely* by the mousemove
|
|
// handler. refresh() must NOT call placeAtMouse here — otherwise
|
|
// every click / keystroke (which produces a selectionSet update)
|
|
// would re-snap the toolbar to the current pointer, which the user
|
|
// perceives as "a popup at the click position" identical to the
|
|
// on-click mode. We only seed an initial top-left position so the
|
|
// toolbar isn't stranded at (0,0) before the first mousemove.
|
|
if (!this.dom.style.top) this.placeTopLeft(view);
|
|
}
|
|
|
|
/** Make the toolbar measurable & visible without leaving it at a stale position. */
|
|
private ensureVisible() {
|
|
if (this.dom.classList.contains("is-hidden")) {
|
|
this.dom.classList.remove("is-hidden");
|
|
this.visible = true;
|
|
}
|
|
}
|
|
|
|
private ensureMouseListener() {
|
|
if (this.mouseListener) return;
|
|
const fn = (e: MouseEvent) => {
|
|
// Cache pointer coords + table-hover flag immediately so the next
|
|
// animation frame sees the latest values; defer the actual
|
|
// measurement / DOM write to rAF so we don't read layout on every
|
|
// mousemove tick.
|
|
this.clientX = e.clientX;
|
|
this.clientY = e.clientY;
|
|
const target = e.target as HTMLElement | null;
|
|
this.mouseInTable = !!target?.closest?.("table");
|
|
const settings = host.getPlugin().settings;
|
|
if (!settings.showFloatingToolbar) return;
|
|
if (settings.floatingToolbarPosition !== "follow-mouse") return;
|
|
if (this.raf != null) return;
|
|
this.raf = requestAnimationFrame(() => {
|
|
this.raf = null;
|
|
// Place directly here instead of going through refresh(). refresh
|
|
// intentionally never calls placeAtMouse so non-mouse updates
|
|
// (clicks, typing, focus changes) can't snap the toolbar to the
|
|
// pointer; mousemove is the only thing that's allowed to.
|
|
const rect = this.view.dom.getBoundingClientRect();
|
|
if (rect.width === 0 || rect.height === 0) return;
|
|
this.ensureVisible();
|
|
if (this.mouseInTable && !this.dom.classList.contains("is-collapsed")) {
|
|
this.placeAtMouse();
|
|
} else {
|
|
this.placeTopLeft(this.view);
|
|
}
|
|
});
|
|
};
|
|
this.view.dom.addEventListener("mousemove", fn);
|
|
this.mouseListener = fn;
|
|
}
|
|
|
|
private removeMouseListener() {
|
|
if (!this.mouseListener) return;
|
|
this.view.dom.removeEventListener("mousemove", this.mouseListener);
|
|
this.mouseListener = null;
|
|
this.mouseInTable = false;
|
|
}
|
|
|
|
/**
|
|
* Document-level mousedown handler that drives the `on-click` mode.
|
|
* Captured (useCapture=true) so we see the click before CM6 / Obsidian
|
|
* potentially stop propagation. Clicks inside a `<table>` show the
|
|
* toolbar at that point; clicks outside any table (and not on the
|
|
* toolbar itself) hide it.
|
|
*/
|
|
private ensureMouseDownListener() {
|
|
if (this.mouseDownListener) return;
|
|
const fn = (e: MouseEvent) => {
|
|
const settings = host.getPlugin().settings;
|
|
if (!settings.showFloatingToolbar) return;
|
|
if (settings.floatingToolbarPosition !== "on-click") return;
|
|
const target = e.target as HTMLElement | null;
|
|
if (!target) return;
|
|
// Click on the toolbar itself: don't hide, don't reposition; let
|
|
// the button's own click handler run.
|
|
if (this.dom.contains(target)) return;
|
|
// Only act on clicks within our editor view; otherwise stale state
|
|
// from another pane could move our toolbar.
|
|
if (!this.view.dom.contains(target)) return;
|
|
// Don't pop up unless we're the active markdown editor — prevents a
|
|
// background split-pane / hover-editor view from snapping its own
|
|
// toolbar to a click that was intended for the foreground pane.
|
|
const activeCM = activeMarkdownEditorView(host.getApp());
|
|
if (activeCM !== this.view) return;
|
|
if (target.closest("table")) {
|
|
this.clientX = e.clientX;
|
|
this.clientY = e.clientY;
|
|
this.clickedInTable = true;
|
|
this.ensureVisible();
|
|
// When collapsed, keep the toolbar pinned at top-left so the
|
|
// small toggle button doesn't jump around the viewport.
|
|
if (this.dom.classList.contains("is-collapsed")) {
|
|
this.placeTopLeft(this.view);
|
|
} else {
|
|
this.placeAtMouse();
|
|
}
|
|
} else {
|
|
this.clickedInTable = false;
|
|
this.ensureVisible();
|
|
this.placeTopLeft(this.view);
|
|
}
|
|
};
|
|
document.addEventListener("mousedown", fn, true);
|
|
this.mouseDownListener = fn;
|
|
}
|
|
|
|
private removeMouseDownListener() {
|
|
if (!this.mouseDownListener) return;
|
|
document.removeEventListener("mousedown", this.mouseDownListener, true);
|
|
this.mouseDownListener = null;
|
|
}
|
|
|
|
hide() {
|
|
if (!this.visible) return;
|
|
this.dom.classList.add("is-hidden");
|
|
this.visible = false;
|
|
}
|
|
|
|
/**
|
|
* Pin the toolbar to the top-left corner of the editor pane using
|
|
* `position: fixed`. Uses the editor's own bounding rect (in viewport
|
|
* coords) and — because we're mounted on <body> — those coords land
|
|
* exactly where the user expects, with no transformed-ancestor offset.
|
|
*/
|
|
private placeTopLeft(view: EditorView) {
|
|
const rect = view.dom.getBoundingClientRect();
|
|
this.ensureVisible();
|
|
const baseLeft = Math.max(rect.left, 0);
|
|
this.dom.style.top = `${Math.max(rect.top, 0)}px`;
|
|
this.dom.style.left = `${baseLeft}px`;
|
|
}
|
|
|
|
/**
|
|
* Place the toolbar so its top-left corner sits at the latest pointer
|
|
* position. The user explicitly asked for no offset — the toolbar
|
|
* shows up exactly at the click point. Clamp to the viewport so the
|
|
* toolbar is never partially off-screen.
|
|
*/
|
|
private placeAtMouse() {
|
|
// `position: fixed` lives in styles.css; only the dynamic top/left
|
|
// are set inline (see comment in placeTopLeft).
|
|
this.ensureVisible();
|
|
const myWidth = this.dom.offsetWidth || 200;
|
|
const myHeight = this.dom.offsetHeight || 80;
|
|
const maxLeft = window.innerWidth - myWidth - 4;
|
|
const maxTop = window.innerHeight - myHeight - 4;
|
|
const left = Math.min(Math.max(this.clientX, 4), Math.max(maxLeft, 4));
|
|
const top = Math.min(Math.max(this.clientY, 4), Math.max(maxTop, 4));
|
|
this.dom.style.top = `${top}px`;
|
|
this.dom.style.left = `${left}px`;
|
|
}
|
|
|
|
render() {
|
|
this.dom.empty();
|
|
|
|
// Content wrapper holds all button groups.
|
|
const content = this.dom.createDiv({ cls: "tm-toolbar-content" });
|
|
|
|
const groups: ToolbarItem[][] = [
|
|
[
|
|
{ icon: Icons.rowAbove, tip: t("tip.insertRowAbove"), run: () => this.act(actions.insertRowAbove) },
|
|
{ icon: Icons.rowBelow, tip: t("tip.insertRowBelow"), run: () => this.act(actions.insertRowBelow) },
|
|
{ icon: Icons.colLeft, tip: t("tip.insertColLeft"), run: () => this.act(actions.insertColLeft) },
|
|
{ icon: Icons.colRight, tip: t("tip.insertColRight"), run: () => this.act(actions.insertColRight) },
|
|
{ icon: Icons.delRow, tip: t("tip.deleteRow"), run: () => this.act(actions.deleteRow) },
|
|
{ icon: Icons.delCol, tip: t("tip.deleteCol"), run: () => this.act(actions.deleteCol) },
|
|
],
|
|
[
|
|
{ icon: Icons.moveUp, tip: t("tip.moveRowUp"), run: () => this.act(actions.moveRowUp) },
|
|
{ icon: Icons.moveDown, tip: t("tip.moveRowDown"), run: () => this.act(actions.moveRowDown) },
|
|
{ icon: Icons.moveLeft, tip: t("tip.moveColLeft"), run: () => this.act(actions.moveColLeft) },
|
|
{ icon: Icons.moveRight, tip: t("tip.moveColRight"), run: () => this.act(actions.moveColRight) },
|
|
],
|
|
[
|
|
{ icon: Icons.alignLeft, tip: t("tip.alignLeft"), run: () => this.act((c) => actions.alignColumn(c, "left")) },
|
|
{ icon: Icons.alignCenter, tip: t("tip.alignCenter"), run: () => this.act((c) => actions.alignColumn(c, "center")) },
|
|
{ icon: Icons.alignRight, tip: t("tip.alignRight"), run: () => this.act((c) => actions.alignColumn(c, "right")) },
|
|
],
|
|
[
|
|
{ icon: Icons.mergeSelection, tip: t("tip.mergeSelection"), run: () => this.act(actions.mergeSelection) },
|
|
{ icon: Icons.mergeUp, tip: t("tip.mergeUp"), run: () => this.act(actions.mergeUp) },
|
|
{ icon: Icons.mergeDown, tip: t("tip.mergeDown"), run: () => this.act(actions.mergeDown) },
|
|
{ icon: Icons.mergeLeft, tip: t("tip.mergeLeft"), run: () => this.act(actions.mergeLeft) },
|
|
{ icon: Icons.split, tip: t("tip.split"), run: () => this.act(actions.splitCell) },
|
|
],
|
|
[
|
|
{ icon: Icons.grid, tip: t("tip.gridEditor"), run: () => host.getPlugin().openGridEditor() },
|
|
{ icon: Icons.format, tip: t("tip.format"), run: () => this.act(actions.formatTable) },
|
|
{
|
|
icon: Icons.importTable,
|
|
tip: t("tip.importTable"),
|
|
run: () => void host.getPlugin().importTableFromClipboard(),
|
|
},
|
|
],
|
|
];
|
|
|
|
for (const group of groups) {
|
|
const g = content.createDiv({ cls: "tm-group" });
|
|
for (const item of group) {
|
|
const btn = g.createEl("button", { cls: "tm-btn" });
|
|
btn.setAttribute("aria-label", item.tip);
|
|
btn.title = item.tip;
|
|
appendSvgIcon(btn, item.icon);
|
|
btn.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
item.run();
|
|
});
|
|
}
|
|
}
|
|
|
|
const toggleBtn = this.dom.createDiv({ cls: "tm-collapse-toggle" });
|
|
toggleBtn.setAttribute("aria-label", t("tip.collapseToolbar"));
|
|
toggleBtn.title = t("tip.collapseToolbar");
|
|
appendSvgIcon(toggleBtn, Icons.chevronLeft);
|
|
const lockContentSize = () => {
|
|
content.style.width = `${content.offsetWidth}px`;
|
|
content.style.height = `${content.offsetHeight}px`;
|
|
content.dataset.tmWidth = String(content.offsetWidth);
|
|
content.dataset.tmHeight = String(content.offsetHeight);
|
|
};
|
|
const unlockContentSize = () => {
|
|
if (this.dom.classList.contains("is-collapsed")) return;
|
|
content.style.width = "";
|
|
content.style.height = "";
|
|
};
|
|
toggleBtn.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (!this.dom.classList.contains("is-collapsed")) {
|
|
lockContentSize();
|
|
void content.offsetWidth;
|
|
this.dom.classList.add("is-collapsed");
|
|
// Pin to top-left so the collapsed toggle doesn't float mid-table.
|
|
this.placeTopLeft(this.view);
|
|
this.clickedInTable = false;
|
|
} else {
|
|
if (content.dataset.tmWidth) content.style.width = `${content.dataset.tmWidth}px`;
|
|
if (content.dataset.tmHeight) content.style.height = `${content.dataset.tmHeight}px`;
|
|
this.dom.classList.remove("is-collapsed");
|
|
setTimeout(unlockContentSize, 350);
|
|
}
|
|
});
|
|
}
|
|
|
|
act(fn: (ctx: actions.ActionContext) => void) {
|
|
const ctx = getActionContext(host.getApp(), host.getPlugin());
|
|
if (!ctx) return;
|
|
fn(ctx);
|
|
}
|
|
},
|
|
);
|
|
}
|