Compare commits
61 Commits
feat/extra
...
renovate/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42f3968cc0 | ||
|
|
7f2cc885fe | ||
|
|
19a365a370 | ||
|
|
9a50da328e | ||
|
|
181e36a7c1 | ||
|
|
178508d245 | ||
|
|
d132d084cf | ||
|
|
494b55d685 | ||
|
|
51513d3779 | ||
|
|
458398f2ca | ||
|
|
7a6cc4f51e | ||
|
|
f4ccce7de5 | ||
|
|
f8b5417d6c | ||
|
|
87ab41c80c | ||
|
|
d2391f94c0 | ||
|
|
050ddb8c55 | ||
|
|
bc23e0984a | ||
|
|
07de353207 | ||
|
|
c02491d2e6 | ||
|
|
a6ede8f905 | ||
|
|
22941a9ce0 | ||
|
|
633a09d414 | ||
|
|
29f0881c5a | ||
|
|
60debca37b | ||
|
|
30ea81d0fb | ||
|
|
b1d92c4fe6 | ||
|
|
70f46de2d8 | ||
|
|
f1b2d0b870 | ||
|
|
8a385972fc | ||
|
|
28dd85c1d1 | ||
|
|
827c8e0e72 | ||
|
|
162c076a14 | ||
|
|
9386465de7 | ||
|
|
acca22f3a1 | ||
|
|
f8d84814e0 | ||
|
|
c46cf41842 | ||
|
|
64ab1c4116 | ||
|
|
a6de1041c7 | ||
|
|
c8d34e65ea | ||
|
|
51db729546 | ||
|
|
d2052ad236 | ||
|
|
9c4301467f | ||
|
|
e7355dc0e4 | ||
|
|
4110fec94f | ||
|
|
d5e601eae9 | ||
|
|
4f044c4a57 | ||
|
|
5821c350e1 | ||
|
|
edba8188fe | ||
|
|
1471a72633 | ||
|
|
56834cb88a | ||
|
|
a0f16f9184 | ||
|
|
de80eb4806 | ||
|
|
48a4b81fbe | ||
|
|
e225794f72 | ||
|
|
4eef30f8b5 | ||
|
|
569b09609d | ||
|
|
39838c25c2 | ||
|
|
49e90c08a9 | ||
|
|
e777b06fb8 | ||
|
|
497ec2ac74 | ||
|
|
c5d282d203 |
2
.gitignore
vendored
@@ -51,4 +51,4 @@ upload
|
||||
# docs
|
||||
site/
|
||||
apps/*/coverage
|
||||
scripts/translation/.language*.json
|
||||
scripts/translation/.language*.json
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "17.0.1",
|
||||
"mermaid": "11.12.2",
|
||||
"mind-elixir": "5.4.0",
|
||||
"mind-elixir": "5.5.0",
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.28.1",
|
||||
|
||||
@@ -541,7 +541,6 @@ export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMapp
|
||||
|
||||
export class AppContext extends Component {
|
||||
isMainWindow: boolean;
|
||||
windowId: string;
|
||||
components: Component[];
|
||||
beforeUnloadListeners: (WeakRef<BeforeUploadListener> | (() => boolean))[];
|
||||
tabManager!: TabManager;
|
||||
@@ -550,11 +549,10 @@ export class AppContext extends Component {
|
||||
|
||||
lastSearchString?: string;
|
||||
|
||||
constructor(isMainWindow: boolean, windowId: string) {
|
||||
constructor(isMainWindow: boolean) {
|
||||
super();
|
||||
|
||||
this.isMainWindow = isMainWindow;
|
||||
this.windowId = windowId;
|
||||
// non-widget/layout components needed for the application
|
||||
this.components = [];
|
||||
this.beforeUnloadListeners = [];
|
||||
@@ -684,7 +682,8 @@ export class AppContext extends Component {
|
||||
this.beforeUnloadListeners = this.beforeUnloadListeners.filter(l => l !== listener);
|
||||
}
|
||||
}
|
||||
const appContext = new AppContext(window.glob.isMainWindow, window.glob.windowId);
|
||||
|
||||
const appContext = new AppContext(window.glob.isMainWindow);
|
||||
|
||||
// we should save all outstanding changes before the page/app is closed
|
||||
$(window).on("beforeunload", () => {
|
||||
|
||||
@@ -142,15 +142,14 @@ export default class Entrypoints extends Component {
|
||||
}
|
||||
|
||||
async openInWindowCommand({ notePath, hoistedNoteId, viewScope }: NoteCommandData) {
|
||||
const extraWindowId = utils.randomString(4);
|
||||
const extraWindowHash = linkService.calculateHash({ notePath, hoistedNoteId, viewScope });
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const { ipcRenderer } = utils.dynamicRequire("electron");
|
||||
|
||||
ipcRenderer.send("create-extra-window", { extraWindowId, extraWindowHash });
|
||||
ipcRenderer.send("create-extra-window", { extraWindowHash });
|
||||
} else {
|
||||
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=${extraWindowId}${extraWindowHash}`;
|
||||
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=1${extraWindowHash}`;
|
||||
|
||||
window.open(url, "", "width=1000,height=800");
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ import linkService from "../services/link.js";
|
||||
import type { EventData } from "./app_context.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
|
||||
const MAX_SAVED_WINDOWS = 10;
|
||||
|
||||
interface TabState {
|
||||
contexts: NoteContext[];
|
||||
position: number;
|
||||
@@ -27,13 +25,6 @@ interface NoteContextState {
|
||||
viewScope: Record<string, any>;
|
||||
}
|
||||
|
||||
interface WindowState {
|
||||
windowId: string;
|
||||
createdAt: number;
|
||||
closedAt: number;
|
||||
contexts: NoteContextState[];
|
||||
}
|
||||
|
||||
export default class TabManager extends Component {
|
||||
public children: NoteContext[];
|
||||
public mutex: Mutex;
|
||||
@@ -50,6 +41,9 @@ export default class TabManager extends Component {
|
||||
this.recentlyClosedTabs = [];
|
||||
|
||||
this.tabsUpdate = new SpacedUpdate(async () => {
|
||||
if (!appContext.isMainWindow) {
|
||||
return;
|
||||
}
|
||||
if (options.is("databaseReadonly")) {
|
||||
return;
|
||||
}
|
||||
@@ -58,21 +52,9 @@ export default class TabManager extends Component {
|
||||
.map((nc) => nc.getPojoState())
|
||||
.filter((t) => !!t);
|
||||
|
||||
// Update the current window’s openNoteContexts in options
|
||||
const savedWindows = options.getJson("openNoteContexts") || [];
|
||||
const win = savedWindows.find((w: WindowState) => w.windowId === appContext.windowId);
|
||||
if (win) {
|
||||
win.contexts = openNoteContexts;
|
||||
} else {
|
||||
savedWindows.push({
|
||||
windowId: appContext.windowId,
|
||||
createdAt: Date.now(),
|
||||
closedAt: 0,
|
||||
contexts: openNoteContexts
|
||||
} as WindowState);
|
||||
}
|
||||
|
||||
await options.save("openNoteContexts", JSON.stringify(savedWindows));
|
||||
await server.put("options", {
|
||||
openNoteContexts: JSON.stringify(openNoteContexts)
|
||||
});
|
||||
});
|
||||
|
||||
appContext.addBeforeUnloadListener(this);
|
||||
@@ -87,13 +69,8 @@ export default class TabManager extends Component {
|
||||
}
|
||||
|
||||
async loadTabs() {
|
||||
// Get the current window’s openNoteContexts
|
||||
const savedWindows = options.getJson("openNoteContexts") || [];
|
||||
const currentWin = savedWindows.find(w => w.windowId === appContext.windowId);
|
||||
const openNoteContexts = currentWin ? currentWin.contexts : undefined;
|
||||
|
||||
try {
|
||||
const noteContextsToOpen = openNoteContexts || [];
|
||||
const noteContextsToOpen = (appContext.isMainWindow && options.getJson("openNoteContexts")) || [];
|
||||
|
||||
// preload all notes at once
|
||||
await froca.getNotes([...noteContextsToOpen.flatMap((tab: NoteContextState) =>
|
||||
@@ -142,51 +119,6 @@ export default class TabManager extends Component {
|
||||
}
|
||||
});
|
||||
|
||||
// Save window contents
|
||||
if (currentWin as WindowState) {
|
||||
currentWin.createdAt = Date.now();
|
||||
currentWin.closedAt = 0;
|
||||
currentWin.contexts = filteredNoteContexts;
|
||||
} else {
|
||||
if (savedWindows?.length >= MAX_SAVED_WINDOWS) {
|
||||
// Filter out the oldest entry
|
||||
// 1) Never remove the "main" window
|
||||
// 2) Prefer removing the oldest closed window (closedAt !== 0)
|
||||
// 3) If no closed window exists, remove the window with the oldest created window
|
||||
let oldestClosedIndex = -1;
|
||||
let oldestClosedTime = Infinity;
|
||||
let oldestCreatedIndex = -1;
|
||||
let oldestCreatedTime = Infinity;
|
||||
savedWindows.forEach((w: WindowState, i: number) => {
|
||||
if (w.windowId === "main") return;
|
||||
if (w.closedAt !== 0) {
|
||||
if (w.closedAt < oldestClosedTime) {
|
||||
oldestClosedTime = w.closedAt;
|
||||
oldestClosedIndex = i;
|
||||
}
|
||||
} else {
|
||||
if (w.createdAt < oldestCreatedTime) {
|
||||
oldestCreatedTime = w.createdAt;
|
||||
oldestCreatedIndex = i;
|
||||
}
|
||||
}
|
||||
});
|
||||
const indexToRemove = oldestClosedIndex !== -1 ? oldestClosedIndex : oldestCreatedIndex;
|
||||
if (indexToRemove !== -1) {
|
||||
savedWindows.splice(indexToRemove, 1);
|
||||
}
|
||||
}
|
||||
|
||||
savedWindows.push({
|
||||
windowId: appContext.windowId,
|
||||
createdAt: Date.now(),
|
||||
closedAt: 0,
|
||||
contexts: filteredNoteContexts
|
||||
} as WindowState);
|
||||
}
|
||||
|
||||
await options.save("openNoteContexts", JSON.stringify(savedWindows));
|
||||
|
||||
// if there's a notePath in the URL, make sure it's open and active
|
||||
// (useful, for e.g., opening clipped notes from clipper or opening link in an extra window)
|
||||
if (parsedFromUrl.notePath) {
|
||||
|
||||
@@ -27,6 +27,10 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
|
||||
loadResults.addRevision(ec.entityId, ec.noteId, ec.componentId);
|
||||
} else if (ec.entityName === "options") {
|
||||
const attributeEntity = ec.entity as FAttributeRow;
|
||||
if (attributeEntity.name === "openNoteContexts") {
|
||||
continue; // only noise
|
||||
}
|
||||
|
||||
options.set(attributeEntity.name as OptionNames, attributeEntity.value);
|
||||
loadResults.addOption(attributeEntity.name as OptionNames);
|
||||
} else if (ec.entityName === "attachments") {
|
||||
|
||||
@@ -13,8 +13,7 @@ function injectGlobals() {
|
||||
uncheckedWindow.$ = $;
|
||||
uncheckedWindow.WebSocket = () => {};
|
||||
uncheckedWindow.glob = {
|
||||
isMainWindow: true,
|
||||
windowId: "main"
|
||||
isMainWindow: true
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
1
apps/client/src/types.d.ts
vendored
@@ -36,7 +36,6 @@ interface CustomGlobals {
|
||||
isProtectedSessionAvailable: boolean;
|
||||
isDev: boolean;
|
||||
isMainWindow: boolean;
|
||||
windowId: string;
|
||||
maxEntityChangeIdAtLoad: number;
|
||||
maxEntityChangeSyncIdAtLoad: number;
|
||||
assetPath: string;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Dropdown as BootstrapDropdown } from "bootstrap";
|
||||
import clsx from "clsx";
|
||||
import { type ComponentChildren, RefObject } from "preact";
|
||||
import { createPortal } from "preact/compat";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
import NoteContext from "../../components/note_context";
|
||||
@@ -338,15 +338,19 @@ interface AttributesProps extends StatusBarContext {
|
||||
function AttributesButton({ note, attributesShown, setAttributesShown }: AttributesProps) {
|
||||
const [ count, setCount ] = useState(note.attributes.length);
|
||||
|
||||
const refreshCount = useCallback((note: FNote) => {
|
||||
return note.getAttributes().filter(a => !a.isAutoLink).length;
|
||||
}, []);
|
||||
|
||||
// React to note changes.
|
||||
useEffect(() => {
|
||||
setCount(note.getAttributes().filter(a => !a.isAutoLink).length);
|
||||
}, [ note ]);
|
||||
setCount(refreshCount(note));
|
||||
}, [ note, refreshCount ]);
|
||||
|
||||
// React to changes in count.
|
||||
useTriliumEvent("entitiesReloaded", (({loadResults}) => {
|
||||
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
|
||||
setCount(note.attributes.length);
|
||||
setCount(refreshCount(note));
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function SqlResults() {
|
||||
{t("sql_result.no_rows")}
|
||||
</Alert>
|
||||
) : (
|
||||
<div class="sql-console-result-container">
|
||||
<div className="sql-console-result-container selectable-text">
|
||||
{results?.map(rows => {
|
||||
// inserts, updates
|
||||
if (typeof rows === "object" && !Array.isArray(rows)) {
|
||||
|
||||
|
Before Width: | Height: | Size: 545 B |
|
Before Width: | Height: | Size: 727 B |
|
Before Width: | Height: | Size: 828 B |
|
Before Width: | Height: | Size: 931 B |
|
Before Width: | Height: | Size: 292 B |
|
Before Width: | Height: | Size: 355 B |
|
Before Width: | Height: | Size: 434 B |
|
Before Width: | Height: | Size: 492 B |
@@ -6,7 +6,6 @@ import sqlInit from "@triliumnext/server/src/services/sql_init.js";
|
||||
import windowService from "@triliumnext/server/src/services/window.js";
|
||||
import tray from "@triliumnext/server/src/services/tray.js";
|
||||
import options from "@triliumnext/server/src/services/options.js";
|
||||
|
||||
import electronDebug from "electron-debug";
|
||||
import electronDl from "electron-dl";
|
||||
import { PRODUCT_NAME } from "./app-info";
|
||||
@@ -70,12 +69,10 @@ async function main() {
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
app.on("second-instance", async (event, commandLine) => {
|
||||
app.on("second-instance", (event, commandLine) => {
|
||||
const lastFocusedWindow = windowService.getLastFocusedWindow();
|
||||
if (commandLine.includes("--new-window")) {
|
||||
const randomString = (await import("@triliumnext/server/src/services/utils.js")).randomString;
|
||||
const extraWindowId = randomString(4);
|
||||
windowService.createExtraWindow(extraWindowId, "");
|
||||
windowService.createExtraWindow("");
|
||||
} else if (lastFocusedWindow) {
|
||||
if (lastFocusedWindow.isMinimized()) {
|
||||
lastFocusedWindow.restore();
|
||||
@@ -127,8 +124,7 @@ async function onReady() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await normalizeOpenNoteContexts();
|
||||
|
||||
tray.createTray();
|
||||
} else {
|
||||
await windowService.createSetupWindow();
|
||||
@@ -137,30 +133,6 @@ async function onReady() {
|
||||
await windowService.registerGlobalShortcuts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Some windows may have closed abnormally, leaving closedAt as 0 in openNoteContexts.
|
||||
* This function normalizes those timestamps to the current time for correct sorting/filtering.
|
||||
*/
|
||||
async function normalizeOpenNoteContexts() {
|
||||
const savedWindows = options.getOptionJson("openNoteContexts") || [];
|
||||
const now = Date.now();
|
||||
|
||||
let changed = false;
|
||||
for (const win of savedWindows) {
|
||||
if (win.windowId !== "main" && win.closedAt === 0) {
|
||||
win.closedAt = now;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
const { default: cls } = (await import("@triliumnext/server/src/services/cls.js"));
|
||||
cls.wrap(() => {
|
||||
options.setOption("openNoteContexts", JSON.stringify(savedWindows));
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
function getElectronLocale() {
|
||||
const uiLocale = options.getOptionOrNull("locale");
|
||||
const formattingLocale = options.getOptionOrNull("formattingLocale");
|
||||
|
||||
@@ -42,7 +42,7 @@ test("Highlights list is displayed", async ({ page, context }) => {
|
||||
await app.closeAllTabs();
|
||||
await app.goToNoteInNewTab("Highlights list");
|
||||
|
||||
await expect(app.sidebar).toContainText(/highlights/i);
|
||||
await expect(app.sidebar).toContainText("Highlights List");
|
||||
const rootList = app.sidebar.locator(".highlights-list ol");
|
||||
let index = 0;
|
||||
for (const highlightedEl of ["Bold 1", "Italic 1", "Underline 1", "Colored text 1", "Background text 1", "Bold 2", "Italic 2", "Underline 2", "Colored text 2", "Background text 2"]) {
|
||||
|
||||
@@ -59,7 +59,7 @@ export default class App {
|
||||
|
||||
// Wait for the page to load.
|
||||
if (url === "/") {
|
||||
await expect(this.noteTree).toContainText("Trilium Integration Test");
|
||||
await expect(this.page.locator(".tree")).toContainText("Trilium Integration Test");
|
||||
if (!preserveTabs) {
|
||||
await this.closeAllTabs();
|
||||
}
|
||||
|
||||
@@ -382,8 +382,6 @@
|
||||
"tooltip": "Trilium Notes",
|
||||
"close": "Quit Trilium",
|
||||
"recents": "Recent notes",
|
||||
"recently-closed-windows": "Recently closed windows",
|
||||
"tabs-total": "{{number}} tabs total",
|
||||
"bookmarks": "Bookmarks",
|
||||
"today": "Open today's journal note",
|
||||
"new-note": "New note",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</head>
|
||||
<body
|
||||
id="trilium-app"
|
||||
class="desktop heading-style-<%= headingStyle %> layout-<%= layoutOrientation %> platform-<%= platform %> <%= isElectron ? 'electron' : '' %> <%= hasNativeTitleBar ? 'native-titlebar' : '' %> <%= hasBackgroundEffects ? 'background-effects' : '' %> <%= isMainWindow ? '' : 'extra-window' %>"
|
||||
class="desktop heading-style-<%= headingStyle %> layout-<%= layoutOrientation %> platform-<%= platform %> <%= isElectron ? 'electron' : '' %> <%= hasNativeTitleBar ? 'native-titlebar' : '' %> <%= hasBackgroundEffects ? 'background-effects' : '' %>"
|
||||
lang="<%= currentLocale.id %>" dir="<%= currentLocale.rtl ? 'rtl' : 'ltr' %>"
|
||||
>
|
||||
<noscript><%= t("javascript-required") %></noscript>
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
isDev: <%= isDev %>,
|
||||
appCssNoteIds: <%- JSON.stringify(appCssNoteIds) %>,
|
||||
isMainWindow: <%= isMainWindow %>,
|
||||
windowId: "<%= windowId %>",
|
||||
isProtectedSessionAvailable: <%= isProtectedSessionAvailable %>,
|
||||
triliumVersion: "<%= triliumVersion %>",
|
||||
assetPath: "<%= assetPath %>",
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import cls from "../services/cls.js";
|
||||
import sql from "../services/sql.js";
|
||||
|
||||
export default () => {
|
||||
cls.init(() => {
|
||||
const row = sql.getRow<{ value: string }>(
|
||||
`SELECT value FROM options WHERE name = 'openNoteContexts'`
|
||||
);
|
||||
|
||||
if (!row || !row.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = JSON.parse(row.value);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
// Already in new format (array + windowId), skip
|
||||
if (
|
||||
Array.isArray(parsed) &&
|
||||
parsed.length > 0 &&
|
||||
parsed[0] &&
|
||||
typeof parsed[0] === "object" &&
|
||||
parsed[0].windowId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Old format: just contexts
|
||||
const migrated = [
|
||||
{
|
||||
windowId: "main",
|
||||
createdAt: 0,
|
||||
closedAt: 0,
|
||||
contexts: parsed
|
||||
}
|
||||
];
|
||||
|
||||
sql.execute(
|
||||
`UPDATE options SET value = ? WHERE name = 'openNoteContexts'`,
|
||||
[JSON.stringify(migrated)]
|
||||
);
|
||||
|
||||
});
|
||||
};
|
||||
@@ -6,11 +6,6 @@
|
||||
|
||||
// Migrations should be kept in descending order, so the latest migration is first.
|
||||
const MIGRATIONS: (SqlMigration | JsMigration)[] = [
|
||||
// Migrate openNoteContexts option to the new structured format with window metadata
|
||||
{
|
||||
version: 234,
|
||||
module: async () => import("./0234__migrate_open_note_contexts_format")
|
||||
},
|
||||
// Migrate geo map to collection
|
||||
{
|
||||
version: 233,
|
||||
|
||||
@@ -56,7 +56,6 @@ function index(req: Request, res: Response) {
|
||||
appCssNoteIds: getAppCssNoteIds(),
|
||||
isDev,
|
||||
isMainWindow: view === "mobile" ? true : !req.query.extraWindow,
|
||||
windowId: view !== "mobile" && req.query.extraWindow ? req.query.extraWindow : "main",
|
||||
isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable(),
|
||||
triliumVersion: packageJson.version,
|
||||
assetPath,
|
||||
|
||||
@@ -4,7 +4,7 @@ import packageJson from "../../package.json" with { type: "json" };
|
||||
import dataDir from "./data_dir.js";
|
||||
import { AppInfo } from "@triliumnext/commons";
|
||||
|
||||
const APP_DB_VERSION = 234;
|
||||
const APP_DB_VERSION = 233;
|
||||
const SYNC_VERSION = 36;
|
||||
const CLIPPER_PROTOCOL_VERSION = "1.0";
|
||||
|
||||
|
||||
@@ -72,19 +72,6 @@ function getOptionBool(name: FilterOptionsByType<boolean>): boolean {
|
||||
return val === "true";
|
||||
}
|
||||
|
||||
function getOptionJson(name: OptionNames) {
|
||||
const val = getOptionOrNull(name);
|
||||
|
||||
if (typeof val !== "string") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(val);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function setOption<T extends OptionNames>(name: T, value: string | OptionDefinitions[T]) {
|
||||
const option = becca.getOption(name);
|
||||
|
||||
@@ -150,7 +137,6 @@ export default {
|
||||
getOption,
|
||||
getOptionInt,
|
||||
getOptionBool,
|
||||
getOptionJson,
|
||||
setOption,
|
||||
createOption,
|
||||
getOptions,
|
||||
|
||||
@@ -45,15 +45,8 @@ async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts =
|
||||
"openNoteContexts",
|
||||
JSON.stringify([
|
||||
{
|
||||
windowId: "main",
|
||||
createdAt: 0,
|
||||
closedAt: 0,
|
||||
contexts: [
|
||||
{
|
||||
notePath: "root",
|
||||
active: true
|
||||
}
|
||||
]
|
||||
notePath: "root",
|
||||
active: true
|
||||
}
|
||||
]),
|
||||
false
|
||||
@@ -264,15 +257,8 @@ function initStartupOptions() {
|
||||
"openNoteContexts",
|
||||
JSON.stringify([
|
||||
{
|
||||
windowId: "main",
|
||||
createdAt: 0,
|
||||
closedAt: 0,
|
||||
contexts: [
|
||||
{
|
||||
notePath: process.env.TRILIUM_START_NOTE_ID || "root",
|
||||
active: true
|
||||
}
|
||||
]
|
||||
notePath: process.env.TRILIUM_START_NOTE_ID || "root",
|
||||
active: true
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
@@ -147,15 +147,8 @@ async function createInitialDatabase(skipDemoDb?: boolean) {
|
||||
"openNoteContexts",
|
||||
JSON.stringify([
|
||||
{
|
||||
windowId: "main",
|
||||
createdAt: 0,
|
||||
closedAt: 0,
|
||||
contexts: [
|
||||
{
|
||||
notePath: startNoteId,
|
||||
active: true
|
||||
}
|
||||
]
|
||||
notePath: startNoteId,
|
||||
active: true
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
@@ -196,39 +196,6 @@ function updateTrayMenu() {
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
function buildClosedWindowsMenu() {
|
||||
const savedWindows = optionService.getOptionJson("openNoteContexts") || [];
|
||||
const openedWindowIds = windowService.getAllWindowIds();
|
||||
const closedWindows = savedWindows
|
||||
.filter(win => !openedWindowIds.includes(win.windowId))
|
||||
.sort((a, b) => { return a.closedAt - b.closedAt; }); // sort by time in ascending order
|
||||
const menuItems: Electron.MenuItemConstructorOptions[] = [];
|
||||
|
||||
for (let i = closedWindows.length - 1; i >= 0; i--) {
|
||||
const win = closedWindows[i];
|
||||
const activeCtx = win.contexts.find(c => c.active === true);
|
||||
const activateNotePath = (activeCtx ?? win.contexts[0])?.notePath;
|
||||
const activateNoteId = activateNotePath?.split("/").pop() ?? null;
|
||||
if (!activateNoteId) continue;
|
||||
|
||||
// Get the title of the closed window
|
||||
const winTitle = (() => {
|
||||
const raw = becca_service.getNoteTitle(activateNoteId);
|
||||
const truncated = raw.length > 20 ? `${raw.slice(0, 17)}…` : raw;
|
||||
const tabCount = win.contexts.filter(ctx => ctx.mainNtxId === null).length;
|
||||
return tabCount > 1 ? `${truncated} (${t("tray.tabs-total", { number: tabCount })})` : truncated;
|
||||
})();
|
||||
|
||||
menuItems.push({
|
||||
label: winTitle,
|
||||
type: "normal",
|
||||
click: () => win.windowId !== "main" ? windowService.createExtraWindow(win.windowId, "") : windowService.createMainWindow()
|
||||
});
|
||||
}
|
||||
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
const windowVisibilityMenuItems: Electron.MenuItemConstructorOptions[] = [];
|
||||
|
||||
// Only call getWindowTitle if windowVisibilityMap has more than one window
|
||||
@@ -291,12 +258,6 @@ function updateTrayMenu() {
|
||||
icon: getIconPath("recents"),
|
||||
submenu: buildRecentNotesMenu()
|
||||
},
|
||||
{
|
||||
label: t("tray.recently-closed-windows"),
|
||||
type: "submenu",
|
||||
icon: getIconPath("closed-windows"),
|
||||
submenu: buildClosedWindowsMenu()
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: t("tray.close"),
|
||||
|
||||
@@ -16,45 +16,28 @@ import { formatDownloadTitle, isMac, isWindows } from "./utils.js";
|
||||
// Prevent the window being garbage collected
|
||||
let mainWindow: BrowserWindow | null;
|
||||
let setupWindow: BrowserWindow | null;
|
||||
let allWindows: BrowserWindow[] = []; // // Used to store all windows, sorted by the order of focus.
|
||||
|
||||
interface WindowEntry {
|
||||
window: BrowserWindow;
|
||||
windowId: string; // custom window ID
|
||||
}
|
||||
let allWindowEntries: WindowEntry[] = [];
|
||||
|
||||
function trackWindowFocus(win: BrowserWindow, windowId: string) {
|
||||
function trackWindowFocus(win: BrowserWindow) {
|
||||
// We need to get the last focused window from allWindows. If the last window is closed, we return the previous window.
|
||||
// Therefore, we need to push the window into the allWindows array every time it gets focused.
|
||||
win.on("focus", () => {
|
||||
allWindowEntries = allWindowEntries.filter(w => !w.window.isDestroyed() && w.window !== win);
|
||||
allWindowEntries.push({ window: win, windowId: windowId });
|
||||
|
||||
allWindows = allWindows.filter(w => !w.isDestroyed() && w !== win);
|
||||
allWindows.push(win);
|
||||
if (!optionService.getOptionBool("disableTray")) {
|
||||
electron.ipcMain.emit("reload-tray");
|
||||
}
|
||||
});
|
||||
|
||||
win.on("closed", () => {
|
||||
cls.wrap(() => {
|
||||
const savedWindows = optionService.getOptionJson("openNoteContexts") || [];
|
||||
|
||||
const win = savedWindows.find(w => w.windowId === windowId);
|
||||
if (win) {
|
||||
win.closedAt = Date.now();
|
||||
}
|
||||
|
||||
optionService.setOption("openNoteContexts", JSON.stringify(savedWindows));
|
||||
})();
|
||||
|
||||
allWindowEntries = allWindowEntries.filter(w => !w.window.isDestroyed());
|
||||
allWindows = allWindows.filter(w => !w.isDestroyed());
|
||||
if (!optionService.getOptionBool("disableTray")) {
|
||||
electron.ipcMain.emit("reload-tray");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function createExtraWindow(extraWindowId: string, extraWindowHash: string) {
|
||||
async function createExtraWindow(extraWindowHash: string) {
|
||||
const spellcheckEnabled = optionService.getOptionBool("spellCheckEnabled");
|
||||
|
||||
const { BrowserWindow } = await import("electron");
|
||||
@@ -73,15 +56,15 @@ async function createExtraWindow(extraWindowId: string, extraWindowHash: string)
|
||||
});
|
||||
|
||||
win.setMenuBarVisibility(false);
|
||||
win.loadURL(`http://127.0.0.1:${port}/?extraWindow=${extraWindowId}${extraWindowHash}`);
|
||||
win.loadURL(`http://127.0.0.1:${port}/?extraWindow=1${extraWindowHash}`);
|
||||
|
||||
configureWebContents(win.webContents, spellcheckEnabled);
|
||||
|
||||
trackWindowFocus(win, extraWindowId);
|
||||
trackWindowFocus(win);
|
||||
}
|
||||
|
||||
electron.ipcMain.on("create-extra-window", (event, arg) => {
|
||||
createExtraWindow(arg.extraWindowId, arg.extraWindowHash);
|
||||
createExtraWindow(arg.extraWindowHash);
|
||||
});
|
||||
|
||||
interface PrintOpts {
|
||||
@@ -185,8 +168,8 @@ async function getBrowserWindowForPrinting(e: IpcMainEvent, notePath: string, ac
|
||||
return { browserWindow, printReport };
|
||||
}
|
||||
|
||||
async function createMainWindow(app?: App) {
|
||||
if (app && "setUserTasks" in app) {
|
||||
async function createMainWindow(app: App) {
|
||||
if ("setUserTasks" in app) {
|
||||
app.setUserTasks([
|
||||
{
|
||||
program: process.execPath,
|
||||
@@ -236,7 +219,7 @@ async function createMainWindow(app?: App) {
|
||||
mainWindow.on("closed", () => (mainWindow = null));
|
||||
|
||||
configureWebContents(mainWindow.webContents, spellcheckEnabled);
|
||||
trackWindowFocus(mainWindow, "main");
|
||||
trackWindowFocus(mainWindow);
|
||||
}
|
||||
|
||||
function getWindowExtraOpts() {
|
||||
@@ -398,15 +381,11 @@ function getMainWindow() {
|
||||
}
|
||||
|
||||
function getLastFocusedWindow() {
|
||||
return allWindowEntries.length > 0 ? allWindowEntries[allWindowEntries.length - 1]?.window : null;
|
||||
return allWindows.length > 0 ? allWindows[allWindows.length - 1] : null;
|
||||
}
|
||||
|
||||
function getAllWindows() {
|
||||
return allWindowEntries.map(e => e.window);
|
||||
}
|
||||
|
||||
function getAllWindowIds(): string[] {
|
||||
return allWindowEntries.map(e => e.windowId);
|
||||
return allWindows;
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -417,6 +396,5 @@ export default {
|
||||
registerGlobalShortcuts,
|
||||
getMainWindow,
|
||||
getLastFocusedWindow,
|
||||
getAllWindows,
|
||||
getAllWindowIds
|
||||
getAllWindows
|
||||
};
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
"jiti": "2.6.1",
|
||||
"jsonc-eslint-parser": "2.4.2",
|
||||
"react-refresh": "0.18.0",
|
||||
"rollup-plugin-webpack-stats": "2.1.8",
|
||||
"rollup-plugin-webpack-stats": "2.1.9",
|
||||
"tslib": "2.8.1",
|
||||
"tsx": "4.21.0",
|
||||
"typescript": "~5.9.0",
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@ckeditor/ckeditor5-icons": "47.3.0"
|
||||
"@ckeditor/ckeditor5-icons": "47.3.0",
|
||||
"mathlive": "0.108.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import ckeditor from './../theme/icons/math.svg?raw';
|
||||
import './augmentation.js';
|
||||
import "../theme/mathform.css";
|
||||
import 'mathlive';
|
||||
import 'mathlive/fonts.css';
|
||||
import 'mathlive/static.css';
|
||||
|
||||
export { default as Math } from './math.js';
|
||||
export { default as MathUI } from './mathui.js';
|
||||
|
||||
@@ -55,9 +55,9 @@ export default class MathUI extends Plugin {
|
||||
|
||||
this._balloon.showStack( 'main' );
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.formView?.mathInputView.fieldView.element?.focus();
|
||||
});
|
||||
requestAnimationFrame( () => {
|
||||
this.formView?.mathInputView.focus();
|
||||
} );
|
||||
}
|
||||
|
||||
private _createFormView() {
|
||||
@@ -71,31 +71,37 @@ export default class MathUI extends Plugin {
|
||||
throw new CKEditorError( 'math-command' );
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const mathConfig = editor.config.get( 'math' )!;
|
||||
|
||||
const formView = new MainFormView(
|
||||
editor.locale,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
mathConfig.engine!,
|
||||
mathConfig.lazyLoad,
|
||||
{
|
||||
engine: mathConfig.engine!,
|
||||
lazyLoad: mathConfig.lazyLoad,
|
||||
previewUid: this._previewUid,
|
||||
previewClassName: mathConfig.previewClassName!,
|
||||
katexRenderOptions: mathConfig.katexRenderOptions!
|
||||
},
|
||||
mathConfig.enablePreview,
|
||||
this._previewUid,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
mathConfig.previewClassName!,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
mathConfig.popupClassName!,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
mathConfig.katexRenderOptions!
|
||||
mathConfig.popupClassName!
|
||||
);
|
||||
|
||||
formView.mathInputView.bind( 'value' ).to( mathCommand, 'value' );
|
||||
formView.displayButtonView.bind( 'isOn' ).to( mathCommand, 'display' );
|
||||
|
||||
// Form elements should be read-only when corresponding commands are disabled.
|
||||
formView.mathInputView.bind( 'isReadOnly' ).to( mathCommand, 'isEnabled', value => !value );
|
||||
formView.saveButtonView.bind( 'isEnabled' ).to( mathCommand );
|
||||
formView.displayButtonView.bind( 'isEnabled' ).to( mathCommand );
|
||||
formView.mathInputView.bind( 'isReadOnly' ).to( mathCommand, 'isEnabled', ( value: boolean ) => !value );
|
||||
formView.saveButtonView.bind( 'isEnabled' ).to(
|
||||
mathCommand,
|
||||
'isEnabled',
|
||||
formView.mathInputView,
|
||||
'value',
|
||||
( commandEnabled, equation ) => {
|
||||
const normalizedEquation = ( equation ?? '' ).trim();
|
||||
return commandEnabled && normalizedEquation.length > 0;
|
||||
}
|
||||
);
|
||||
formView.displayButtonView.bind( 'isEnabled' ).to( mathCommand, 'isEnabled' );
|
||||
|
||||
// Listen to submit button click
|
||||
this.listenTo( formView, 'submit', () => {
|
||||
@@ -115,24 +121,12 @@ export default class MathUI extends Plugin {
|
||||
} );
|
||||
|
||||
// Allow pressing Enter to submit changes, and use Shift+Enter to insert a new line
|
||||
formView.keystrokes.set('enter', (data, cancel) => {
|
||||
if (!data.shiftKey) {
|
||||
formView.fire('submit');
|
||||
formView.keystrokes.set( 'enter', ( data, cancel ) => {
|
||||
if ( !data.shiftKey ) {
|
||||
formView.fire( 'submit' );
|
||||
cancel();
|
||||
}
|
||||
});
|
||||
|
||||
// Allow the textarea to be resizable
|
||||
formView.mathInputView.fieldView.once('render', () => {
|
||||
const textarea = formView.mathInputView.fieldView.element;
|
||||
if (!textarea) return;
|
||||
Object.assign(textarea.style, {
|
||||
resize: 'both',
|
||||
height: '100px',
|
||||
width: '400px',
|
||||
minWidth: '100%',
|
||||
});
|
||||
});
|
||||
} );
|
||||
|
||||
return formView;
|
||||
}
|
||||
@@ -162,14 +156,12 @@ export default class MathUI extends Plugin {
|
||||
} );
|
||||
|
||||
if ( this._balloon.visibleView === this.formView ) {
|
||||
this.formView.mathInputView.fieldView.element?.select();
|
||||
this.formView.mathInputView.focus();
|
||||
}
|
||||
|
||||
// Show preview element
|
||||
const previewEl = document.getElementById( this._previewUid );
|
||||
if ( previewEl && this.formView.previewEnabled ) {
|
||||
// Force refresh preview
|
||||
this.formView.mathView?.updateMath();
|
||||
if ( previewEl && this.formView.mathView ) {
|
||||
this.formView.mathView.updateMath();
|
||||
}
|
||||
|
||||
this.formView.equation = mathCommand.value ?? '';
|
||||
@@ -206,8 +198,10 @@ export default class MathUI extends Plugin {
|
||||
|
||||
private _removeFormView() {
|
||||
if ( this._isFormInPanel && this.formView ) {
|
||||
this.formView.saveButtonView.focus();
|
||||
// Hide virtual keyboard before removing the form
|
||||
this.formView.hideKeyboard();
|
||||
|
||||
this.formView.saveButtonView.focus();
|
||||
this._balloon.remove( this.formView );
|
||||
|
||||
// Hide preview element
|
||||
|
||||
@@ -1,91 +1,59 @@
|
||||
import { ButtonView, createLabeledTextarea, FocusCycler, LabelView, LabeledFieldView, submitHandler, SwitchButtonView, View, ViewCollection, type TextareaView, type FocusableView, Locale, FocusTracker, KeystrokeHandler } from 'ckeditor5';
|
||||
import { ButtonView, FocusCycler, FocusTracker, KeystrokeHandler, LabelView, submitHandler, SwitchButtonView, View, ViewCollection, type FocusableView, type Locale } from 'ckeditor5';
|
||||
import IconCheck from "@ckeditor/ckeditor5-icons/theme/icons/check.svg?raw";
|
||||
import IconCancel from "@ckeditor/ckeditor5-icons/theme/icons/cancel.svg?raw";
|
||||
import { extractDelimiters, hasDelimiters } from '../utils.js';
|
||||
import MathView from './mathview.js';
|
||||
import MathView, { type MathViewOptions } from './mathview.js';
|
||||
import MathInputView from './mathinputview.js';
|
||||
import '../../theme/mathform.css';
|
||||
import type { KatexOptions } from '../typings-external.js';
|
||||
|
||||
class MathInputView extends LabeledFieldView<TextareaView> {
|
||||
public value: null | string = null;
|
||||
public isReadOnly = false;
|
||||
|
||||
constructor( locale: Locale ) {
|
||||
super( locale, createLabeledTextarea );
|
||||
}
|
||||
}
|
||||
|
||||
export default class MainFormView extends View {
|
||||
public saveButtonView: ButtonView;
|
||||
public mathInputView: MathInputView;
|
||||
public displayButtonView: SwitchButtonView;
|
||||
public cancelButtonView: ButtonView;
|
||||
public previewEnabled: boolean;
|
||||
public previewLabel?: LabelView;
|
||||
public displayButtonView: SwitchButtonView;
|
||||
|
||||
public mathInputView: MathInputView;
|
||||
public mathView?: MathView;
|
||||
public override locale: Locale = new Locale();
|
||||
public lazyLoad: undefined | ( () => Promise<void> );
|
||||
|
||||
public focusTracker = new FocusTracker();
|
||||
public keystrokes = new KeystrokeHandler();
|
||||
private _focusables = new ViewCollection<FocusableView>();
|
||||
private _focusCycler: FocusCycler;
|
||||
|
||||
constructor(
|
||||
locale: Locale,
|
||||
engine:
|
||||
| 'mathjax'
|
||||
| 'katex'
|
||||
| ( (
|
||||
equation: string,
|
||||
element: HTMLElement,
|
||||
display: boolean,
|
||||
) => void ),
|
||||
lazyLoad: undefined | ( () => Promise<void> ),
|
||||
mathViewOptions: MathViewOptions,
|
||||
previewEnabled = false,
|
||||
previewUid: string,
|
||||
previewClassName: Array<string>,
|
||||
popupClassName: Array<string>,
|
||||
katexRenderOptions: KatexOptions
|
||||
popupClassName: Array<string> = []
|
||||
) {
|
||||
super( locale );
|
||||
|
||||
const t = locale.t;
|
||||
|
||||
// Submit button
|
||||
this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save', null );
|
||||
this.saveButtonView.type = 'submit';
|
||||
// Create views
|
||||
this.mathInputView = new MathInputView( locale );
|
||||
this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save', 'submit' );
|
||||
this.cancelButtonView = this._createButton( t( 'Cancel' ), IconCancel, 'ck-button-cancel' );
|
||||
this.cancelButtonView.delegate( 'execute' ).to( this, 'cancel' );
|
||||
this.displayButtonView = this._createDisplayButton( t );
|
||||
|
||||
// Equation input
|
||||
this.mathInputView = this._createMathInput();
|
||||
// Build children
|
||||
|
||||
// Display button
|
||||
this.displayButtonView = this._createDisplayButton();
|
||||
const children: Array<View> = [
|
||||
this.mathInputView,
|
||||
this.displayButtonView
|
||||
];
|
||||
|
||||
// Cancel button
|
||||
this.cancelButtonView = this._createButton( t( 'Cancel' ), IconCancel, 'ck-button-cancel', 'cancel' );
|
||||
if ( previewEnabled ) {
|
||||
const previewLabel = new LabelView( locale );
|
||||
previewLabel.text = t( 'Equation preview' );
|
||||
|
||||
this.previewEnabled = previewEnabled;
|
||||
|
||||
let children = [];
|
||||
if ( this.previewEnabled ) {
|
||||
// Preview label
|
||||
this.previewLabel = new LabelView( locale );
|
||||
this.previewLabel.text = t( 'Equation preview' );
|
||||
|
||||
// Math element
|
||||
this.mathView = new MathView( engine, lazyLoad, locale, previewUid, previewClassName, katexRenderOptions );
|
||||
this.mathView = new MathView( locale, mathViewOptions );
|
||||
this.mathView.bind( 'display' ).to( this.displayButtonView, 'isOn' );
|
||||
|
||||
children = [
|
||||
this.mathInputView,
|
||||
this.displayButtonView,
|
||||
this.previewLabel,
|
||||
this.mathView
|
||||
];
|
||||
} else {
|
||||
children = [
|
||||
this.mathInputView,
|
||||
this.displayButtonView
|
||||
];
|
||||
children.push( previewLabel, this.mathView );
|
||||
}
|
||||
|
||||
// Add UI elements to template
|
||||
this._setupSync( previewEnabled );
|
||||
|
||||
this.setTemplate( {
|
||||
tag: 'form',
|
||||
attributes: {
|
||||
@@ -107,10 +75,30 @@ export default class MainFormView extends View {
|
||||
},
|
||||
children
|
||||
},
|
||||
this.saveButtonView,
|
||||
this.cancelButtonView
|
||||
{
|
||||
tag: 'div',
|
||||
attributes: {
|
||||
class: [
|
||||
'ck-math-button-row'
|
||||
]
|
||||
},
|
||||
children: [
|
||||
this.saveButtonView,
|
||||
this.cancelButtonView
|
||||
]
|
||||
}
|
||||
]
|
||||
} );
|
||||
|
||||
this._focusCycler = new FocusCycler( {
|
||||
focusables: this._focusables,
|
||||
focusTracker: this.focusTracker,
|
||||
keystrokeHandler: this.keystrokes,
|
||||
actions: {
|
||||
focusPrevious: 'shift + tab',
|
||||
focusNext: 'tab'
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
public override render(): void {
|
||||
@@ -121,103 +109,73 @@ export default class MainFormView extends View {
|
||||
view: this
|
||||
} );
|
||||
|
||||
// Register form elements to focusable elements
|
||||
const childViews = [
|
||||
this.mathInputView,
|
||||
const focusableViews = [
|
||||
this.mathInputView.latexTextAreaView,
|
||||
this.displayButtonView,
|
||||
this.saveButtonView,
|
||||
this.cancelButtonView
|
||||
];
|
||||
|
||||
childViews.forEach( v => {
|
||||
focusableViews.forEach( v => {
|
||||
this._focusables.add( v );
|
||||
if ( v.element ) {
|
||||
this._focusables.add( v );
|
||||
this.focusTracker.add( v.element );
|
||||
}
|
||||
} );
|
||||
|
||||
// Listen to keypresses inside form element
|
||||
this.mathInputView.on( 'mathfieldReady', () => {
|
||||
const mathfieldView = this.mathInputView.mathFieldFocusableView;
|
||||
if ( mathfieldView.element ) {
|
||||
if ( this._focusables.has( mathfieldView ) ) {
|
||||
this._focusables.remove( mathfieldView );
|
||||
}
|
||||
this._focusables.add( mathfieldView, 0 );
|
||||
this.focusTracker.add( mathfieldView.element );
|
||||
}
|
||||
} );
|
||||
|
||||
if ( this.element ) {
|
||||
this.keystrokes.listenTo( this.element );
|
||||
}
|
||||
}
|
||||
|
||||
public get equation(): string {
|
||||
return this.mathInputView.value ?? '';
|
||||
}
|
||||
|
||||
public set equation( equation: string ) {
|
||||
const norm = equation.trim();
|
||||
this.mathInputView.value = norm.length ? norm : null;
|
||||
if ( this.mathView ) {
|
||||
this.mathView.value = norm;
|
||||
}
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this._focusCycler.focusFirst();
|
||||
}
|
||||
|
||||
public get equation(): string {
|
||||
return this.mathInputView.fieldView.element?.value ?? '';
|
||||
}
|
||||
private _setupSync( previewEnabled: boolean ): void {
|
||||
this.mathInputView.on( 'change:value', () => {
|
||||
let eq = ( this.mathInputView.value ?? '' ).trim();
|
||||
|
||||
public set equation( equation: string ) {
|
||||
if ( this.mathInputView.fieldView.element ) {
|
||||
this.mathInputView.fieldView.element.value = equation;
|
||||
}
|
||||
if ( this.previewEnabled && this.mathView ) {
|
||||
this.mathView.value = equation;
|
||||
}
|
||||
}
|
||||
if ( hasDelimiters( eq ) ) {
|
||||
const params = extractDelimiters( eq );
|
||||
eq = params.equation;
|
||||
this.displayButtonView.isOn = params.display;
|
||||
|
||||
public focusTracker: FocusTracker = new FocusTracker();
|
||||
public keystrokes: KeystrokeHandler = new KeystrokeHandler();
|
||||
private _focusables = new ViewCollection<FocusableView>();
|
||||
private _focusCycler: FocusCycler = new FocusCycler( {
|
||||
focusables: this._focusables,
|
||||
focusTracker: this.focusTracker,
|
||||
keystrokeHandler: this.keystrokes,
|
||||
actions: {
|
||||
focusPrevious: 'shift + tab',
|
||||
focusNext: 'tab'
|
||||
}
|
||||
} );
|
||||
|
||||
private _createMathInput() {
|
||||
const t = this.locale.t;
|
||||
|
||||
// Create equation input
|
||||
const mathInput = new MathInputView( this.locale );
|
||||
const fieldView = mathInput.fieldView;
|
||||
mathInput.infoText = t( 'Insert equation in TeX format.' );
|
||||
|
||||
const onInput = () => {
|
||||
if ( fieldView.element != null ) {
|
||||
let equationInput = fieldView.element.value.trim();
|
||||
|
||||
// If input has delimiters
|
||||
if ( hasDelimiters( equationInput ) ) {
|
||||
// Get equation without delimiters
|
||||
const params = extractDelimiters( equationInput );
|
||||
|
||||
// Remove delimiters from input field
|
||||
fieldView.element.value = params.equation;
|
||||
|
||||
equationInput = params.equation;
|
||||
|
||||
// update display button and preview
|
||||
this.displayButtonView.isOn = params.display;
|
||||
if ( this.mathInputView.value !== eq ) {
|
||||
this.mathInputView.value = eq.length ? eq : null;
|
||||
}
|
||||
if ( this.previewEnabled && this.mathView ) {
|
||||
// Update preview view
|
||||
this.mathView.value = equationInput;
|
||||
}
|
||||
|
||||
this.saveButtonView.isEnabled = !!equationInput;
|
||||
}
|
||||
};
|
||||
|
||||
fieldView.on( 'render', onInput );
|
||||
fieldView.on( 'input', onInput );
|
||||
|
||||
return mathInput;
|
||||
if ( previewEnabled && this.mathView && this.mathView.value !== eq ) {
|
||||
this.mathView.value = eq;
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
private _createButton(
|
||||
label: string,
|
||||
icon: string,
|
||||
className: string,
|
||||
eventName: string | null
|
||||
) {
|
||||
private _createButton( label: string, icon: string, className: string, type?: 'submit' | 'button' ): ButtonView {
|
||||
const button = new ButtonView( this.locale );
|
||||
|
||||
button.set( {
|
||||
@@ -232,16 +190,14 @@ export default class MainFormView extends View {
|
||||
}
|
||||
} );
|
||||
|
||||
if ( eventName ) {
|
||||
button.delegate( 'execute' ).to( this, eventName );
|
||||
if ( type ) {
|
||||
button.type = type;
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private _createDisplayButton() {
|
||||
const t = this.locale.t;
|
||||
|
||||
private _createDisplayButton( t: ( str: string ) => string ): SwitchButtonView {
|
||||
const switchButton = new SwitchButtonView( this.locale );
|
||||
|
||||
switchButton.set( {
|
||||
@@ -256,15 +212,13 @@ export default class MainFormView extends View {
|
||||
} );
|
||||
|
||||
switchButton.on( 'execute', () => {
|
||||
// Toggle state
|
||||
switchButton.isOn = !switchButton.isOn;
|
||||
|
||||
if ( this.previewEnabled && this.mathView ) {
|
||||
// Update preview view
|
||||
this.mathView.display = switchButton.isOn;
|
||||
}
|
||||
} );
|
||||
|
||||
return switchButton;
|
||||
}
|
||||
|
||||
public hideKeyboard(): void {
|
||||
this.mathInputView.hideKeyboard();
|
||||
}
|
||||
}
|
||||
|
||||
268
packages/ckeditor5-math/src/ui/mathinputview.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
// Math input widget: wraps a MathLive <math-field> and a LaTeX textarea
|
||||
// and keeps them in sync for the CKEditor 5 math dialog.
|
||||
import { View, type Locale, type FocusableView } from 'ckeditor5';
|
||||
import 'mathlive/fonts.css'; // Auto-bundles offline fonts
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
mathVirtualKeyboard?: {
|
||||
visible: boolean;
|
||||
show: () => void;
|
||||
hide: () => void;
|
||||
addEventListener: ( event: string, cb: () => void ) => void;
|
||||
removeEventListener: ( event: string, cb: () => void ) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface MathFieldElement extends HTMLElement {
|
||||
value: string;
|
||||
readOnly: boolean;
|
||||
mathVirtualKeyboardPolicy: string;
|
||||
inlineShortcuts?: Record<string, string>;
|
||||
setValue?: ( value: string, options?: { silenceNotifications?: boolean } ) => void;
|
||||
}
|
||||
|
||||
// Wrapper for the MathLive element to make it focusable in CKEditor's UI system
|
||||
export class MathFieldFocusableView extends View implements FocusableView {
|
||||
public declare element: HTMLElement | null;
|
||||
private _view: MathInputView;
|
||||
constructor( locale: Locale, view: MathInputView ) {
|
||||
super( locale );
|
||||
this._view = view;
|
||||
}
|
||||
public focus(): void {
|
||||
this._view.mathfield?.focus();
|
||||
}
|
||||
public setElement( el: HTMLElement ): void {
|
||||
this.element = el;
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper for the LaTeX textarea to make it focusable in CKEditor's UI system
|
||||
export class LatexTextAreaView extends View implements FocusableView {
|
||||
declare public element: HTMLTextAreaElement;
|
||||
constructor( locale: Locale ) {
|
||||
super( locale );
|
||||
this.setTemplate( { tag: 'textarea', attributes: {
|
||||
class: [ 'ck', 'ck-textarea', 'ck-latex-textarea' ], spellcheck: 'false', tabindex: 0
|
||||
} } );
|
||||
}
|
||||
public focus(): void {
|
||||
this.element?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Main view class for the math input
|
||||
export default class MathInputView extends View {
|
||||
public declare value: string | null;
|
||||
public declare isReadOnly: boolean;
|
||||
public mathfield: MathFieldElement | null = null;
|
||||
public readonly latexTextAreaView: LatexTextAreaView;
|
||||
public readonly mathFieldFocusableView: MathFieldFocusableView;
|
||||
private _destroyed = false;
|
||||
private _vkGeometryHandler?: () => void;
|
||||
private _updating = false;
|
||||
private static _configured = false;
|
||||
|
||||
constructor( locale: Locale ) {
|
||||
super( locale );
|
||||
this.latexTextAreaView = new LatexTextAreaView( locale );
|
||||
this.mathFieldFocusableView = new MathFieldFocusableView( locale, this );
|
||||
this.set( 'value', null );
|
||||
this.set( 'isReadOnly', false );
|
||||
this.setTemplate( {
|
||||
tag: 'div', attributes: { class: [ 'ck', 'ck-math-input' ] },
|
||||
children: [
|
||||
{ tag: 'div', attributes: { class: [ 'ck-mathlive-container' ] } },
|
||||
{ tag: 'label', attributes: { class: [ 'ck-latex-label' ] }, children: [ locale.t( 'LaTeX' ) ] },
|
||||
{ tag: 'div', attributes: { class: [ 'ck-latex-wrapper' ] }, children: [ this.latexTextAreaView ] }
|
||||
]
|
||||
} );
|
||||
}
|
||||
|
||||
public override render(): void {
|
||||
super.render();
|
||||
const textarea = this.latexTextAreaView.element;
|
||||
|
||||
// Sync changes from the LaTeX textarea to the mathfield and model
|
||||
this.listenTo( textarea, 'input', () => {
|
||||
if ( this._updating ) {
|
||||
return;
|
||||
}
|
||||
this._updating = true;
|
||||
const val = textarea.value;
|
||||
this.value = val || null;
|
||||
if ( this.mathfield ) {
|
||||
if ( val === '' ) {
|
||||
this.mathfield.remove();
|
||||
this.mathfield = null;
|
||||
this._initMathField( false );
|
||||
} else if ( this.mathfield.value.trim() !== val.trim() ) {
|
||||
this._setMathfieldValue( val );
|
||||
}
|
||||
}
|
||||
this._updating = false;
|
||||
} );
|
||||
|
||||
// Sync changes from the model (this.value) to the UI elements
|
||||
this.on( 'change:value', ( _e, _n, val ) => {
|
||||
if ( this._updating ) {
|
||||
return;
|
||||
}
|
||||
this._updating = true;
|
||||
const newVal = val ?? '';
|
||||
if ( textarea.value !== newVal ) {
|
||||
textarea.value = newVal;
|
||||
}
|
||||
if ( this.mathfield ) {
|
||||
if ( this.mathfield.value.trim() !== newVal.trim() ) {
|
||||
this._setMathfieldValue( newVal );
|
||||
}
|
||||
} else if ( newVal !== '' ) {
|
||||
this._initMathField( false );
|
||||
}
|
||||
this._updating = false;
|
||||
} );
|
||||
|
||||
// Handle read-only state changes
|
||||
this.on( 'change:isReadOnly', ( _e, _n, val ) => {
|
||||
textarea.readOnly = val;
|
||||
if ( this.mathfield ) {
|
||||
this.mathfield.readOnly = val;
|
||||
}
|
||||
} );
|
||||
|
||||
// Handle virtual keyboard geometry changes
|
||||
const vk = window.mathVirtualKeyboard;
|
||||
if ( vk && !this._vkGeometryHandler ) {
|
||||
this._vkGeometryHandler = () => {
|
||||
if ( vk.visible && this.mathfield ) {
|
||||
this.mathfield.focus();
|
||||
}
|
||||
};
|
||||
vk.addEventListener( 'geometrychange', this._vkGeometryHandler );
|
||||
}
|
||||
|
||||
const initial = this.value ?? '';
|
||||
if ( textarea.value !== initial ) {
|
||||
textarea.value = initial;
|
||||
}
|
||||
this._loadMathLive();
|
||||
}
|
||||
|
||||
// Loads the MathLive library dynamically
|
||||
private async _loadMathLive(): Promise<void> {
|
||||
try {
|
||||
await import( 'mathlive' );
|
||||
await customElements.whenDefined( 'math-field' );
|
||||
if ( this._destroyed ) {
|
||||
return;
|
||||
}
|
||||
if ( !MathInputView._configured ) {
|
||||
const MathfieldClass = customElements.get( 'math-field' ) as any;
|
||||
if ( MathfieldClass ) {
|
||||
MathfieldClass.soundsDirectory = null;
|
||||
MathfieldClass.plonkSound = null;
|
||||
MathInputView._configured = true;
|
||||
}
|
||||
}
|
||||
if ( this.element && !this._destroyed ) {
|
||||
this._initMathField( true );
|
||||
}
|
||||
} catch {
|
||||
const c = this.element?.querySelector( '.ck-mathlive-container' );
|
||||
if ( c ) {
|
||||
c.textContent = 'Math editor unavailable';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initializes the <math-field> element
|
||||
private _initMathField( shouldFocus: boolean ): void {
|
||||
const container = this.element?.querySelector( '.ck-mathlive-container' );
|
||||
if ( !container ) {
|
||||
return;
|
||||
}
|
||||
if ( this.mathfield ) {
|
||||
this._setMathfieldValue( this.value ?? '' );
|
||||
return;
|
||||
}
|
||||
const mf = document.createElement( 'math-field' ) as MathFieldElement;
|
||||
mf.mathVirtualKeyboardPolicy = 'auto';
|
||||
mf.setAttribute( 'tabindex', '0' );
|
||||
mf.value = this.value ?? '';
|
||||
mf.readOnly = this.isReadOnly;
|
||||
container.appendChild( mf );
|
||||
// Set shortcuts after mounting (accessing inlineShortcuts requires mounted element)
|
||||
try {
|
||||
if ( mf.inlineShortcuts ) {
|
||||
mf.inlineShortcuts = { ...mf.inlineShortcuts, dx: 'dx', dy: 'dy', dt: 'dt' };
|
||||
}
|
||||
} catch {
|
||||
// Inline shortcut configuration is optional; ignore failures to avoid breaking the math field.
|
||||
}
|
||||
mf.addEventListener( 'keydown', ev => {
|
||||
if ( ev.key === 'Tab' ) {
|
||||
if ( ev.shiftKey ) {
|
||||
ev.preventDefault();
|
||||
} else {
|
||||
ev.preventDefault();
|
||||
ev.stopImmediatePropagation();
|
||||
this.latexTextAreaView.focus();
|
||||
}
|
||||
}
|
||||
}, { capture: true } );
|
||||
mf.addEventListener( 'input', () => {
|
||||
if ( this._updating ) {
|
||||
return;
|
||||
}
|
||||
this._updating = true;
|
||||
const textarea = this.latexTextAreaView.element;
|
||||
if ( textarea.value.trim() !== mf.value.trim() ) {
|
||||
textarea.value = mf.value;
|
||||
}
|
||||
this.value = mf.value || null;
|
||||
this._updating = false;
|
||||
} );
|
||||
this.mathfield = mf;
|
||||
this.mathFieldFocusableView.setElement( mf );
|
||||
this.fire( 'mathfieldReady' );
|
||||
if ( shouldFocus ) {
|
||||
requestAnimationFrame( () => mf.focus() );
|
||||
}
|
||||
}
|
||||
|
||||
// Updates the mathfield value without triggering loops
|
||||
private _setMathfieldValue( value: string ): void {
|
||||
if ( !this.mathfield ) {
|
||||
return;
|
||||
}
|
||||
if ( this.mathfield.setValue ) {
|
||||
this.mathfield.setValue( value, { silenceNotifications: true } );
|
||||
} else {
|
||||
this.mathfield.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
public hideKeyboard(): void {
|
||||
window.mathVirtualKeyboard?.hide();
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this.mathfield?.focus();
|
||||
}
|
||||
|
||||
public override destroy(): void {
|
||||
this._destroyed = true;
|
||||
const vk = window.mathVirtualKeyboard;
|
||||
if ( vk && this._vkGeometryHandler ) {
|
||||
vk.removeEventListener( 'geometrychange', this._vkGeometryHandler );
|
||||
this._vkGeometryHandler = undefined;
|
||||
}
|
||||
this.hideKeyboard();
|
||||
this.mathfield?.remove();
|
||||
this.mathfield = null;
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
@@ -2,44 +2,44 @@ import { View, type Locale } from 'ckeditor5';
|
||||
import type { KatexOptions } from '../typings-external.js';
|
||||
import { renderEquation } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Configuration options for the MathView.
|
||||
*/
|
||||
export interface MathViewOptions {
|
||||
engine: 'mathjax' | 'katex' | ( ( equation: string, element: HTMLElement, display: boolean ) => void );
|
||||
lazyLoad: undefined | ( () => Promise<void> );
|
||||
previewUid: string;
|
||||
previewClassName: Array<string>;
|
||||
katexRenderOptions: KatexOptions;
|
||||
}
|
||||
|
||||
export default class MathView extends View {
|
||||
/**
|
||||
* The LaTeX equation value to render.
|
||||
* @observable
|
||||
*/
|
||||
public declare value: string;
|
||||
|
||||
/**
|
||||
* Whether to render in display mode (centered) or inline.
|
||||
* @observable
|
||||
*/
|
||||
public declare display: boolean;
|
||||
public previewUid: string;
|
||||
public previewClassName: Array<string>;
|
||||
public katexRenderOptions: KatexOptions;
|
||||
public engine:
|
||||
| 'mathjax'
|
||||
| 'katex'
|
||||
| ( ( equation: string, element: HTMLElement, display: boolean ) => void );
|
||||
public lazyLoad: undefined | ( () => Promise<void> );
|
||||
|
||||
constructor(
|
||||
engine:
|
||||
| 'mathjax'
|
||||
| 'katex'
|
||||
| ( (
|
||||
equation: string,
|
||||
element: HTMLElement,
|
||||
display: boolean,
|
||||
) => void ),
|
||||
lazyLoad: undefined | ( () => Promise<void> ),
|
||||
locale: Locale,
|
||||
previewUid: string,
|
||||
previewClassName: Array<string>,
|
||||
katexRenderOptions: KatexOptions
|
||||
) {
|
||||
/**
|
||||
* Configuration options passed during initialization.
|
||||
*/
|
||||
private options: MathViewOptions;
|
||||
|
||||
constructor( locale: Locale, options: MathViewOptions ) {
|
||||
super( locale );
|
||||
|
||||
this.engine = engine;
|
||||
this.lazyLoad = lazyLoad;
|
||||
this.previewUid = previewUid;
|
||||
this.katexRenderOptions = katexRenderOptions;
|
||||
this.previewClassName = previewClassName;
|
||||
this.options = options;
|
||||
|
||||
this.set( 'value', '' );
|
||||
this.set( 'display', false );
|
||||
|
||||
// Update rendering when state changes.
|
||||
// Checking isRendered prevents errors during initialization.
|
||||
this.on( 'change', () => {
|
||||
if ( this.isRendered ) {
|
||||
this.updateMath();
|
||||
@@ -55,19 +55,39 @@ export default class MathView extends View {
|
||||
}
|
||||
|
||||
public updateMath(): void {
|
||||
if ( this.element ) {
|
||||
void renderEquation(
|
||||
this.value,
|
||||
this.element,
|
||||
this.engine,
|
||||
this.lazyLoad,
|
||||
this.display,
|
||||
true,
|
||||
this.previewUid,
|
||||
this.previewClassName,
|
||||
this.katexRenderOptions
|
||||
);
|
||||
if ( !this.element ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle empty equations
|
||||
if ( !this.value || !this.value.trim() ) {
|
||||
this.element.textContent = '';
|
||||
this.element.classList.remove( 'ck-math-render-error' );
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear previous render
|
||||
this.element.textContent = '';
|
||||
this.element.classList.remove( 'ck-math-render-error' );
|
||||
|
||||
renderEquation(
|
||||
this.value,
|
||||
this.element,
|
||||
this.options.engine,
|
||||
this.options.lazyLoad,
|
||||
this.display,
|
||||
true, // isPreview
|
||||
this.options.previewUid,
|
||||
this.options.previewClassName,
|
||||
this.options.katexRenderOptions
|
||||
).catch( error => {
|
||||
console.error( 'Math rendering failed:', error );
|
||||
|
||||
if ( this.element ) {
|
||||
this.element.textContent = 'Error rendering equation';
|
||||
this.element.classList.add( 'ck-math-render-error' );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
public override render(): void {
|
||||
|
||||
@@ -3,6 +3,20 @@ import Math from '../src/math';
|
||||
import AutoformatMath from '../src/autoformatmath';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
// Suppress MathLive errors during async cleanup in tests
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('unhandledrejection', event => {
|
||||
if (event.reason?.message?.includes('options') || event.reason?.message?.includes('mathlive')) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
window.addEventListener('error', event => {
|
||||
if (event.message?.includes('options') || event.message?.includes('mathlive')) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe( 'CKEditor5 Math DLL', () => {
|
||||
it( 'exports Math', () => {
|
||||
expect( MathDll ).to.equal( Math );
|
||||
|
||||
@@ -2,6 +2,20 @@ import { ClassicEditor, type EditorConfig } from 'ckeditor5';
|
||||
import MathUI from '../src/mathui';
|
||||
import { describe, beforeEach, it, afterEach, expect } from "vitest";
|
||||
|
||||
// Suppress MathLive errors during async cleanup
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('unhandledrejection', event => {
|
||||
if (event.reason?.message?.includes('options') || event.reason?.message?.includes('mathlive')) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
window.addEventListener('error', event => {
|
||||
if (event.message?.includes('options') || event.message?.includes('mathlive')) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
describe( 'Lazy load', () => {
|
||||
let editorElement: HTMLDivElement;
|
||||
let editor: ClassicEditor;
|
||||
@@ -24,11 +38,14 @@ describe( 'Lazy load', () => {
|
||||
beforeEach( () => {
|
||||
editorElement = document.createElement( 'div' );
|
||||
document.body.appendChild( editorElement );
|
||||
|
||||
lazyLoadInvoked = false;
|
||||
} );
|
||||
|
||||
afterEach( () => {
|
||||
afterEach( async () => {
|
||||
if ( mathUIFeature?.formView ) {
|
||||
mathUIFeature._hideUI();
|
||||
}
|
||||
await new Promise( resolve => setTimeout( resolve, 50 ) );
|
||||
editorElement.remove();
|
||||
return editor.destroy();
|
||||
} );
|
||||
@@ -37,6 +54,7 @@ describe( 'Lazy load', () => {
|
||||
await buildEditor( {
|
||||
math: {
|
||||
engine: 'katex',
|
||||
enablePreview: true,
|
||||
lazyLoad: async () => {
|
||||
lazyLoadInvoked = true;
|
||||
}
|
||||
@@ -44,6 +62,15 @@ describe( 'Lazy load', () => {
|
||||
} );
|
||||
|
||||
mathUIFeature._showUI();
|
||||
|
||||
// Trigger render with a non-empty value to bypass empty check optimization
|
||||
if ( mathUIFeature.formView ) {
|
||||
mathUIFeature.formView.equation = 'x^2';
|
||||
}
|
||||
|
||||
// Wait for async rendering and lazy loading
|
||||
await new Promise( resolve => setTimeout( resolve, 100 ) );
|
||||
|
||||
expect( lazyLoadInvoked ).to.be.true;
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -410,7 +410,7 @@ describe( 'MathUI', () => {
|
||||
it( 'should bind mainFormView.mathInputView#value to math command value', () => {
|
||||
const command = editor.commands.get( 'math' );
|
||||
|
||||
expect( formView!.mathInputView.value ).to.null;
|
||||
expect( formView!.mathInputView.value ).to.be.null;
|
||||
|
||||
command!.value = 'x^2';
|
||||
expect( formView!.mathInputView.value ).to.equal( 'x^2' );
|
||||
@@ -419,10 +419,18 @@ describe( 'MathUI', () => {
|
||||
it( 'should execute math command on mainFormView#submit event', () => {
|
||||
const executeSpy = vi.spyOn( editor, 'execute' );
|
||||
|
||||
formView!.mathInputView.fieldView.element!.value = 'x^2';
|
||||
formView!.mathInputView.value = 'x^2';
|
||||
formView!.fire( 'submit' );
|
||||
|
||||
expect(executeSpy.mock.lastCall?.slice(0, 2)).toMatchObject(['math', 'x^2']);
|
||||
expect( executeSpy.mock.lastCall?.slice( 0, 2 ) ).toMatchObject( [ 'math', 'x^2' ] );
|
||||
} );
|
||||
|
||||
it( 'should update equation value when mathInputView changes', () => {
|
||||
formView!.mathInputView.value = 'x^2';
|
||||
expect( formView!.equation ).to.equal( 'x^2' );
|
||||
|
||||
formView!.mathInputView.value = '\\frac{1}{2}';
|
||||
expect( formView!.equation ).to.equal( '\\frac{1}{2}' );
|
||||
} );
|
||||
|
||||
it( 'should hide the balloon on mainFormView#cancel if math command does not have a value', () => {
|
||||
|
||||
@@ -1,35 +1,220 @@
|
||||
/**
|
||||
* Math Equation Editor Dialog Styles - Compact & Readable
|
||||
*/
|
||||
|
||||
/* === Z-INDEX: MathLive UI above CKEditor === */
|
||||
.ML__keyboard, .ML__popover, .ML__menu, .ML__suggestions, .ML__autocomplete,
|
||||
.ML__tooltip, .ML__sr-only, [data-ml-root], #mathlive-suggestion-popover,
|
||||
.mathlive-suggestions-popover, [data-ml-tooltip], .ML__base {
|
||||
z-index: calc(var(--ck-z-panel) + 1000) !important;
|
||||
}
|
||||
.ML__tooltip, [role="tooltip"], .ML__popover[role="tooltip"], .popover, [data-ml-tooltip] {
|
||||
z-index: calc(var(--ck-z-panel) + 2000) !important;
|
||||
position: fixed !important;
|
||||
}
|
||||
.ck.ck-balloon-panel, .ck.ck-balloon-panel .ck-balloon-panel__content {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* === MAIN DIALOG === */
|
||||
.ck.ck-math-form {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
padding: var(--ck-spacing-standard);
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
flex-wrap: wrap;
|
||||
|
||||
& .ck-math-view {
|
||||
flex-basis: 100%;
|
||||
|
||||
& .ck-labeled-view {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
& .ck-label {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
& .ck-button {
|
||||
flex-basis: 50%;
|
||||
}
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--ck-spacing-standard);
|
||||
box-sizing: border-box;
|
||||
max-width: 80vw;
|
||||
max-height: 80vh;
|
||||
overflow: visible;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.ck-math-tex.ck-placeholder::before {
|
||||
display: none !important;
|
||||
/* Scrollable content - vertical scroll, horizontal visible for tooltips */
|
||||
.ck-math-view {
|
||||
overflow-y: auto;
|
||||
overflow-x: visible;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
gap: var(--ck-spacing-standard);
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ck.ck-toolbar-container {
|
||||
z-index: calc(var(--ck-z-panel) + 2);
|
||||
/* === MATH INPUT === */
|
||||
.ck.ck-math-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ck-spacing-standard);
|
||||
width: fit-content;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* === MATHLIVE EDITOR === */
|
||||
.ck.ck-math-input .ck-mathlive-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 50px;
|
||||
padding: var(--ck-spacing-small);
|
||||
border: 1px solid var(--ck-color-input-border);
|
||||
border-radius: var(--ck-border-radius);
|
||||
background: var(--ck-color-input-background) !important;
|
||||
transition: border-color 120ms ease;
|
||||
overflow: visible !important;
|
||||
clip-path: none !important;
|
||||
}
|
||||
.ck.ck-math-input .ck-mathlive-container:focus-within {
|
||||
border-color: var(--ck-color-focus-border);
|
||||
}
|
||||
|
||||
/* Position keyboard & menu buttons */
|
||||
.ck-mathlive-container math-field::part(virtual-keyboard-toggle),
|
||||
.ck-mathlive-container math-field::part(menu-toggle) {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
}
|
||||
.ck-mathlive-container math-field::part(virtual-keyboard-toggle) { right: 40px; }
|
||||
.ck-mathlive-container math-field::part(menu-toggle) {
|
||||
right: 8px;
|
||||
display: flex !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
/* Math field element */
|
||||
.ck.ck-math-form math-field {
|
||||
display: block !important;
|
||||
width: 100%;
|
||||
font-size: 1.5em;
|
||||
background: transparent !important;
|
||||
color: var(--ck-color-input-text);
|
||||
border: none !important;
|
||||
padding: 0;
|
||||
outline: none !important;
|
||||
--selection-background-color: rgba(33, 150, 243, 0.2);
|
||||
--selection-color: inherit;
|
||||
--contains-highlight-background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* === LATEX TEXTAREA === */
|
||||
.ck.ck-math-input .ck-latex-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: fit-content;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
padding: var(--ck-spacing-small);
|
||||
border: 1px solid var(--ck-color-input-border);
|
||||
border-radius: var(--ck-border-radius);
|
||||
background: var(--ck-color-input-background) !important;
|
||||
transition: border-color 120ms ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ck.ck-math-input .ck-latex-wrapper:focus-within {
|
||||
border-color: var(--ck-color-focus-border);
|
||||
}
|
||||
.ck.ck-math-input .ck-latex-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--ck-color-text);
|
||||
opacity: 0.8;
|
||||
margin: 0 0 var(--ck-spacing-small) 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ck.ck-math-input .ck-latex-textarea {
|
||||
width: fit-content;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 60px;
|
||||
max-height: calc(80vh - 300px);
|
||||
resize: both;
|
||||
overflow: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.95em;
|
||||
background: transparent !important;
|
||||
color: var(--ck-color-input-text);
|
||||
border: none !important;
|
||||
padding: 0;
|
||||
outline: none !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* === DISPLAY TOGGLE === */
|
||||
.ck-button-display-toggle {
|
||||
align-self: flex-start;
|
||||
padding: var(--ck-spacing-small) var(--ck-spacing-standard);
|
||||
background: var(--ck-color-input-background);
|
||||
color: var(--ck-color-text);
|
||||
border: 1px solid var(--ck-color-input-border);
|
||||
border-radius: var(--ck-border-radius);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.ck-button-display-toggle:hover { background: var(--ck-color-focus-border); }
|
||||
|
||||
/* === PREVIEW === */
|
||||
.ck-math-preview,
|
||||
.ck.ck-math-preview {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
max-height: none !important;
|
||||
height: auto !important;
|
||||
padding: var(--ck-spacing-small);
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
display: block;
|
||||
text-align: left;
|
||||
overflow-x: auto !important;
|
||||
overflow-y: visible !important;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Center equation when in display mode */
|
||||
.ck-math-preview[data-display="true"],
|
||||
.ck.ck-math-preview[data-display="true"] {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ck-math-preview.ck-error, .ck-math-render-error {
|
||||
border-color: var(--ck-color-error-text);
|
||||
background: var(--ck-color-base-background);
|
||||
color: var(--ck-color-error-text);
|
||||
}
|
||||
|
||||
/* === BUTTONS === */
|
||||
.ck-math-button-row {
|
||||
display: flex;
|
||||
gap: var(--ck-spacing-standard);
|
||||
justify-content: flex-end;
|
||||
margin-top: var(--ck-spacing-standard);
|
||||
}
|
||||
.ck-button-save, .ck-button-cancel {
|
||||
padding: var(--ck-spacing-small) var(--ck-spacing-standard);
|
||||
border: 1px solid var(--ck-color-input-border);
|
||||
border-radius: var(--ck-border-radius);
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
.ck-button-save {
|
||||
background: var(--ck-color-focus-border);
|
||||
color: white;
|
||||
}
|
||||
.ck-button-cancel {
|
||||
background: var(--ck-color-input-background);
|
||||
color: var(--ck-color-text);
|
||||
}
|
||||
.ck-button-save:hover { opacity: 0.9; }
|
||||
.ck-button-cancel:hover { background: var(--ck-color-base-background); }
|
||||
|
||||
/* === OVERFLOW FIX: Allow tooltips to escape === */
|
||||
.ck.ck-balloon-panel,
|
||||
.ck.ck-balloon-panel .ck-balloon-panel__content,
|
||||
.ck.ck-math-form,
|
||||
.ck-math-view,
|
||||
.ck.ck-math-input,
|
||||
.ck.ck-math-input .ck-mathlive-container {
|
||||
overflow: visible !important;
|
||||
clip-path: none !important;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ export default defineConfig( {
|
||||
include: [
|
||||
'tests/**/*.[jt]s'
|
||||
],
|
||||
exclude: [
|
||||
'tests/setup.ts'
|
||||
],
|
||||
globals: true,
|
||||
watch: false,
|
||||
coverage: {
|
||||
|
||||
@@ -50,6 +50,11 @@ const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, LocaleMapping | null> = {
|
||||
coreTranslation: () => import("ckeditor5/translations/ja.js"),
|
||||
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/ja.js"),
|
||||
},
|
||||
pl: {
|
||||
languageCode: "pl",
|
||||
coreTranslation: () => import("ckeditor5/translations/pl.js"),
|
||||
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/pl.js"),
|
||||
},
|
||||
pt: {
|
||||
languageCode: "pt",
|
||||
coreTranslation: () => import("ckeditor5/translations/pt.js"),
|
||||
|
||||
68
pnpm-lock.yaml
generated
@@ -104,8 +104,8 @@ importers:
|
||||
specifier: 0.18.0
|
||||
version: 0.18.0
|
||||
rollup-plugin-webpack-stats:
|
||||
specifier: 2.1.8
|
||||
version: 2.1.8(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
|
||||
specifier: 2.1.9
|
||||
version: 2.1.9(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
|
||||
tslib:
|
||||
specifier: 2.8.1
|
||||
version: 2.8.1
|
||||
@@ -186,7 +186,7 @@ importers:
|
||||
version: 0.2.0(mermaid@11.12.2)
|
||||
'@mind-elixir/node-menu':
|
||||
specifier: 5.0.1
|
||||
version: 5.0.1(mind-elixir@5.4.0)
|
||||
version: 5.0.1(mind-elixir@5.5.0)
|
||||
'@popperjs/core':
|
||||
specifier: 2.11.8
|
||||
version: 2.11.8
|
||||
@@ -278,8 +278,8 @@ importers:
|
||||
specifier: 11.12.2
|
||||
version: 11.12.2
|
||||
mind-elixir:
|
||||
specifier: 5.4.0
|
||||
version: 5.4.0
|
||||
specifier: 5.5.0
|
||||
version: 5.5.0
|
||||
normalize.css:
|
||||
specifier: 8.0.1
|
||||
version: 8.0.1
|
||||
@@ -1073,6 +1073,9 @@ importers:
|
||||
'@ckeditor/ckeditor5-icons':
|
||||
specifier: 47.3.0
|
||||
version: 47.3.0
|
||||
mathlive:
|
||||
specifier: 0.108.2
|
||||
version: 0.108.2
|
||||
devDependencies:
|
||||
'@ckeditor/ckeditor5-dev-build-tools':
|
||||
specifier: 54.2.3
|
||||
@@ -2129,6 +2132,10 @@ packages:
|
||||
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
|
||||
engines: {node: '>=0.1.90'}
|
||||
|
||||
'@cortex-js/compute-engine@0.30.2':
|
||||
resolution: {integrity: sha512-Zx+iisk9WWdbxjm8EYsneIBszvjfUs7BHNwf1jBtSINIgfWGpHrTTq9vW0J59iGCFt6bOFxbmWyxNMRSmksHMA==}
|
||||
engines: {node: '>=21.7.3', npm: '>=10.5.0'}
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -7076,6 +7083,10 @@ packages:
|
||||
compare-versions@6.1.1:
|
||||
resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
|
||||
|
||||
complex-esm@2.1.1-esm1:
|
||||
resolution: {integrity: sha512-IShBEWHILB9s7MnfyevqNGxV0A1cfcSnewL/4uPFiSxkcQL4Mm3FxJ0pXMtCXuWLjYz3lRRyk6OfkeDZcjD6nw==}
|
||||
engines: {node: '>=16.14.2', npm: '>=8.5.0'}
|
||||
|
||||
component-emitter@1.3.1:
|
||||
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
|
||||
|
||||
@@ -10269,6 +10280,9 @@ packages:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
mathlive@0.108.2:
|
||||
resolution: {integrity: sha512-GIZkfprGTxrbHckOvwo92ZmOOxdD018BHDzlrEwYUU+pzR5KabhqI1s43lxe/vqXdF5RLiQKgDcuk5jxEjhkYg==}
|
||||
|
||||
mathml-tag-names@2.1.3:
|
||||
resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
|
||||
|
||||
@@ -10508,8 +10522,8 @@ packages:
|
||||
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
mind-elixir@5.4.0:
|
||||
resolution: {integrity: sha512-yxXajDWoSF6id8b2LKxlhXidxH/v6mx4JV+isrtsZ62RGCMsRbjUMFO9xOfTVH8vyxWhsbCkiAP6/i5hqbyk6w==}
|
||||
mind-elixir@5.5.0:
|
||||
resolution: {integrity: sha512-a/bOTp3wJrK/vTm2/Vn5+9kYL0fNqxWvm8SsVojJO/tltLPPU8yMPzFCZHzGRz1Aoj6bpLxN+ExfIbc28nrNxQ==}
|
||||
|
||||
mini-css-extract-plugin@2.9.4:
|
||||
resolution: {integrity: sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==}
|
||||
@@ -12389,8 +12403,8 @@ packages:
|
||||
resolution: {integrity: sha512-EsoOi8moHN6CAYyTZipxDDVTJn0j2nBCWor4wRU45RQ8ER2qREDykXLr3Ulz6hBh6oBKCFTQIjo21i0FXNo/IA==}
|
||||
hasBin: true
|
||||
|
||||
rollup-plugin-stats@1.5.3:
|
||||
resolution: {integrity: sha512-0IYVGhsFTjcddpqcElzU7Mi4vmDLihCCTH5QgCCgWpNY1VKMXVoEpxmCmGjivtJKLzI6t5QIicsPBC93UWWN2g==}
|
||||
rollup-plugin-stats@1.5.4:
|
||||
resolution: {integrity: sha512-b1hYagYLTyr8mCVUb7e1x9fjxOXFyeWmV9hIr7vYqq/agN+WDaGNzz+KmM3GAx0KGGI2qllOL+zAUi/l39s/Sg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
rolldown: ^1.0.0-beta.0
|
||||
@@ -12416,8 +12430,8 @@ packages:
|
||||
peerDependencies:
|
||||
rollup: ^3.0.0||^4.0.0
|
||||
|
||||
rollup-plugin-webpack-stats@2.1.8:
|
||||
resolution: {integrity: sha512-agc1OE+QwG3sGeTSdruh16DkxPb6QkgR7I3gntPDFHMXsK1bR2ADHUVod1eoE+epAOqiv3idx/hcSqZAI3a1yg==}
|
||||
rollup-plugin-webpack-stats@2.1.9:
|
||||
resolution: {integrity: sha512-ft1vdp3xPjE+zw8A22yCToo5cpymoWCjNDefWNO1awywsDrSDoRJhkoZTENkhJwmfh6oe5ztpGu7PfnJOMXc2g==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
rolldown: ^1.0.0-beta.0
|
||||
@@ -15361,8 +15375,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-core': 47.3.0
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-code-block@47.3.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)':
|
||||
dependencies:
|
||||
@@ -16093,8 +16105,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-ui': 47.3.0
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-restricted-editing@47.3.0':
|
||||
dependencies:
|
||||
@@ -16292,6 +16302,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-icons': 47.3.0
|
||||
'@ckeditor/ckeditor5-ui': 47.3.0
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-upload@47.3.0':
|
||||
dependencies:
|
||||
@@ -16499,6 +16511,11 @@ snapshots:
|
||||
|
||||
'@colors/colors@1.5.0': {}
|
||||
|
||||
'@cortex-js/compute-engine@0.30.2':
|
||||
dependencies:
|
||||
complex-esm: 2.1.1-esm1
|
||||
decimal.js: 10.6.0
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
@@ -18315,9 +18332,9 @@ snapshots:
|
||||
|
||||
'@microsoft/tsdoc@0.15.1': {}
|
||||
|
||||
'@mind-elixir/node-menu@5.0.1(mind-elixir@5.4.0)':
|
||||
'@mind-elixir/node-menu@5.0.1(mind-elixir@5.5.0)':
|
||||
dependencies:
|
||||
mind-elixir: 5.4.0
|
||||
mind-elixir: 5.5.0
|
||||
|
||||
'@mixmark-io/domino@2.2.0': {}
|
||||
|
||||
@@ -22305,6 +22322,8 @@ snapshots:
|
||||
|
||||
compare-versions@6.1.1: {}
|
||||
|
||||
complex-esm@2.1.1-esm1: {}
|
||||
|
||||
component-emitter@1.3.1: {}
|
||||
|
||||
compress-commons@6.0.2:
|
||||
@@ -22991,8 +23010,7 @@ snapshots:
|
||||
|
||||
decimal.js@10.5.0: {}
|
||||
|
||||
decimal.js@10.6.0:
|
||||
optional: true
|
||||
decimal.js@10.6.0: {}
|
||||
|
||||
decko@1.2.0: {}
|
||||
|
||||
@@ -26338,6 +26356,10 @@ snapshots:
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
mathlive@0.108.2:
|
||||
dependencies:
|
||||
'@cortex-js/compute-engine': 0.30.2
|
||||
|
||||
mathml-tag-names@2.1.3: {}
|
||||
|
||||
mdast-util-find-and-replace@3.0.2:
|
||||
@@ -26765,7 +26787,7 @@ snapshots:
|
||||
|
||||
mimic-response@3.1.0: {}
|
||||
|
||||
mind-elixir@5.4.0: {}
|
||||
mind-elixir@5.5.0: {}
|
||||
|
||||
mini-css-extract-plugin@2.9.4(webpack@5.101.3(@swc/core@1.11.29(@swc/helpers@0.5.17))(esbuild@0.27.2)):
|
||||
dependencies:
|
||||
@@ -28808,7 +28830,7 @@ snapshots:
|
||||
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.29
|
||||
optional: true
|
||||
|
||||
rollup-plugin-stats@1.5.3(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)):
|
||||
rollup-plugin-stats@1.5.4(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)):
|
||||
optionalDependencies:
|
||||
rolldown: 1.0.0-beta.29
|
||||
rollup: 4.52.0
|
||||
@@ -28841,9 +28863,9 @@ snapshots:
|
||||
'@rollup/pluginutils': 5.1.4(rollup@4.52.0)
|
||||
rollup: 4.52.0
|
||||
|
||||
rollup-plugin-webpack-stats@2.1.8(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)):
|
||||
rollup-plugin-webpack-stats@2.1.9(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)):
|
||||
dependencies:
|
||||
rollup-plugin-stats: 1.5.3(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
|
||||
rollup-plugin-stats: 1.5.4(rolldown@1.0.0-beta.29)(rollup@4.52.0)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1))
|
||||
optionalDependencies:
|
||||
rolldown: 1.0.0-beta.29
|
||||
rollup: 4.52.0
|
||||
|
||||