Compare commits
34 Commits
lightweigh
...
feat/extra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80404b83b0 | ||
|
|
c612bdbfc1 | ||
|
|
3a9e686533 | ||
|
|
9e8d89a170 | ||
|
|
31c70938d6 | ||
|
|
07f3c48d0b | ||
|
|
2821b6da9d | ||
|
|
daba7c398d | ||
|
|
de1ef5b98b | ||
|
|
1bb206d978 | ||
|
|
2fd5ddab86 | ||
|
|
27dc662636 | ||
|
|
52691b5c8c | ||
|
|
8087ed5688 | ||
|
|
79e2c97882 | ||
|
|
1078107776 | ||
|
|
9c9e123e3d | ||
|
|
a8c2947062 | ||
|
|
366166a561 | ||
|
|
4d2b02eddb | ||
|
|
07871853a5 | ||
|
|
254145f0e5 | ||
|
|
c28f11336e | ||
|
|
2e30683b7b | ||
|
|
0af7b8b145 | ||
|
|
5d39b84886 | ||
|
|
537c4051cc | ||
|
|
d0a22bc517 | ||
|
|
19a75acf3f | ||
|
|
3f0abce874 | ||
|
|
36dd29f919 | ||
|
|
d7838f0b67 | ||
|
|
3353d4f436 | ||
|
|
7740154bdc |
2
.gitignore
vendored
@@ -51,4 +51,4 @@ upload
|
||||
# docs
|
||||
site/
|
||||
apps/*/coverage
|
||||
scripts/translation/.language*.json
|
||||
scripts/translation/.language*.json
|
||||
@@ -541,6 +541,7 @@ 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;
|
||||
@@ -549,10 +550,11 @@ export class AppContext extends Component {
|
||||
|
||||
lastSearchString?: string;
|
||||
|
||||
constructor(isMainWindow: boolean) {
|
||||
constructor(isMainWindow: boolean, windowId: string) {
|
||||
super();
|
||||
|
||||
this.isMainWindow = isMainWindow;
|
||||
this.windowId = windowId;
|
||||
// non-widget/layout components needed for the application
|
||||
this.components = [];
|
||||
this.beforeUnloadListeners = [];
|
||||
@@ -682,8 +684,7 @@ export class AppContext extends Component {
|
||||
this.beforeUnloadListeners = this.beforeUnloadListeners.filter(l => l !== listener);
|
||||
}
|
||||
}
|
||||
|
||||
const appContext = new AppContext(window.glob.isMainWindow);
|
||||
const appContext = new AppContext(window.glob.isMainWindow, window.glob.windowId);
|
||||
|
||||
// we should save all outstanding changes before the page/app is closed
|
||||
$(window).on("beforeunload", () => {
|
||||
|
||||
@@ -142,14 +142,15 @@ 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", { extraWindowHash });
|
||||
ipcRenderer.send("create-extra-window", { extraWindowId, extraWindowHash });
|
||||
} else {
|
||||
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=1${extraWindowHash}`;
|
||||
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=${extraWindowId}${extraWindowHash}`;
|
||||
|
||||
window.open(url, "", "width=1000,height=800");
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ 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;
|
||||
@@ -25,6 +27,13 @@ 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;
|
||||
@@ -41,9 +50,6 @@ export default class TabManager extends Component {
|
||||
this.recentlyClosedTabs = [];
|
||||
|
||||
this.tabsUpdate = new SpacedUpdate(async () => {
|
||||
if (!appContext.isMainWindow) {
|
||||
return;
|
||||
}
|
||||
if (options.is("databaseReadonly")) {
|
||||
return;
|
||||
}
|
||||
@@ -52,9 +58,21 @@ export default class TabManager extends Component {
|
||||
.map((nc) => nc.getPojoState())
|
||||
.filter((t) => !!t);
|
||||
|
||||
await server.put("options", {
|
||||
openNoteContexts: JSON.stringify(openNoteContexts)
|
||||
});
|
||||
// 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));
|
||||
});
|
||||
|
||||
appContext.addBeforeUnloadListener(this);
|
||||
@@ -69,8 +87,13 @@ 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 = (appContext.isMainWindow && options.getJson("openNoteContexts")) || [];
|
||||
const noteContextsToOpen = openNoteContexts || [];
|
||||
|
||||
// preload all notes at once
|
||||
await froca.getNotes([...noteContextsToOpen.flatMap((tab: NoteContextState) =>
|
||||
@@ -119,6 +142,51 @@ 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) {
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
|
||||
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
|
||||
<title>Trilium Notes</title>
|
||||
</head>
|
||||
|
||||
<body id="trilium-app">
|
||||
<noscript>Trilium requires JavaScript to be enabled.</noscript>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container" style="display: none"></div>
|
||||
|
||||
<!-- Required to match the PWA's top bar color with the theme -->
|
||||
<!-- This works even when the user directly changes --root-background in CSS -->
|
||||
<div id="background-color-tracker" style="position: absolute; visibility: hidden; color: var(--root-background); transition: color 1ms;"></div>
|
||||
|
||||
<script src="./index.ts" type="module"></script>
|
||||
|
||||
<!-- Required for correct loading of scripts in Electron -->
|
||||
<script>
|
||||
if (typeof module === 'object') {window.module = module; module = undefined;}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,110 +0,0 @@
|
||||
async function bootstrap() {
|
||||
showSplash();
|
||||
await setupGlob();
|
||||
await Promise.all([
|
||||
initJQuery(),
|
||||
loadBootstrapCss()
|
||||
]);
|
||||
loadStylesheets();
|
||||
loadIcons();
|
||||
setBodyAttributes();
|
||||
await loadScripts();
|
||||
hideSplash();
|
||||
}
|
||||
|
||||
async function initJQuery() {
|
||||
const $ = (await import("jquery")).default;
|
||||
window.$ = $;
|
||||
window.jQuery = $;
|
||||
}
|
||||
|
||||
async function setupGlob() {
|
||||
const response = await fetch(`/bootstrap${window.location.search}`);
|
||||
const json = await response.json();
|
||||
|
||||
window.global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
|
||||
window.glob = {
|
||||
...json,
|
||||
activeDialog: null
|
||||
};
|
||||
}
|
||||
|
||||
async function loadBootstrapCss() {
|
||||
// We have to selectively import Bootstrap CSS based on text direction.
|
||||
if (glob.isRtl) {
|
||||
await import("bootstrap/dist/css/bootstrap.rtl.min.css");
|
||||
} else {
|
||||
await import("bootstrap/dist/css/bootstrap.min.css");
|
||||
}
|
||||
}
|
||||
|
||||
function loadStylesheets() {
|
||||
const { assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
|
||||
const cssToLoad: string[] = [];
|
||||
cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`);
|
||||
cssToLoad.push(`api/fonts`);
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`);
|
||||
if (themeCssUrl) {
|
||||
cssToLoad.push(themeCssUrl);
|
||||
}
|
||||
if (themeUseNextAsBase === "next") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`);
|
||||
} else if (themeUseNextAsBase === "next-dark") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`);
|
||||
} else if (themeUseNextAsBase === "next-light") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`);
|
||||
}
|
||||
cssToLoad.push(`${assetPath}/stylesheets/style.css`);
|
||||
|
||||
for (const href of cssToLoad) {
|
||||
const linkEl = document.createElement("link");
|
||||
linkEl.href = href;
|
||||
linkEl.rel = "stylesheet";
|
||||
document.head.appendChild(linkEl);
|
||||
}
|
||||
}
|
||||
|
||||
function loadIcons() {
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.innerText = window.glob.iconPackCss;
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
|
||||
function setBodyAttributes() {
|
||||
const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale } = window.glob;
|
||||
const classesToSet = [
|
||||
device,
|
||||
`heading-style-${headingStyle}`,
|
||||
`layout-${layoutOrientation}`,
|
||||
`platform-${platform}`,
|
||||
isElectron && "electron",
|
||||
hasNativeTitleBar && "native-titlebar",
|
||||
hasBackgroundEffects && "background-effects"
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
for (const classToSet of classesToSet) {
|
||||
document.body.classList.add(classToSet);
|
||||
}
|
||||
|
||||
document.body.lang = currentLocale.id;
|
||||
document.body.dir = currentLocale.rtl ? "rtl" : "ltr";
|
||||
}
|
||||
|
||||
async function loadScripts() {
|
||||
if (glob.device === "mobile") {
|
||||
await import("./mobile.js");
|
||||
} else {
|
||||
await import("./desktop.js");
|
||||
}
|
||||
}
|
||||
|
||||
function showSplash() {
|
||||
// hide body to reduce flickering on the startup. This is done through JS and not CSS to not hide <noscript>
|
||||
document.body.style.display = "none";
|
||||
}
|
||||
|
||||
function hideSplash() {
|
||||
document.body.style.display = "block";
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
|
||||
// @ts-ignore - module = undefined
|
||||
// Required for correct loading of scripts in Electron
|
||||
if (typeof module === 'object') {window.module = module; module = undefined;}
|
||||
|
||||
document.body.style.display = "block";
|
||||
|
||||
15
apps/client/src/runtime.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import $ from "jquery";
|
||||
|
||||
async function loadBootstrap() {
|
||||
if (document.body.dir === "rtl") {
|
||||
await import("bootstrap/dist/css/bootstrap.rtl.min.css");
|
||||
} else {
|
||||
await import("bootstrap/dist/css/bootstrap.min.css");
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).$ = $;
|
||||
(window as any).jQuery = $;
|
||||
await loadBootstrap();
|
||||
|
||||
$("body").show();
|
||||
@@ -27,10 +27,6 @@ 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,7 +13,8 @@ function injectGlobals() {
|
||||
uncheckedWindow.$ = $;
|
||||
uncheckedWindow.WebSocket = () => {};
|
||||
uncheckedWindow.glob = {
|
||||
isMainWindow: true
|
||||
isMainWindow: true,
|
||||
windowId: "main"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "Abruf der Liste von Widgets vom Server ist fehlgeschlagen"
|
||||
},
|
||||
"open-script-note": "Script-Notiz öffnen"
|
||||
}
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Link hinzufügen",
|
||||
@@ -209,8 +208,7 @@
|
||||
"info": {
|
||||
"modalTitle": "Infonachricht",
|
||||
"closeButton": "Schließen",
|
||||
"okButton": "OK",
|
||||
"copy_to_clipboard": "In die Zwischenablage kopieren"
|
||||
"okButton": "OK"
|
||||
},
|
||||
"jump_to_note": {
|
||||
"search_button": "Suche im Volltext",
|
||||
@@ -697,9 +695,7 @@
|
||||
"export_as_image": "Als Bild exportieren",
|
||||
"export_as_image_png": "PNG (Raster)",
|
||||
"export_as_image_svg": "SVG (Vektor)",
|
||||
"note_map": "Notizen Karte",
|
||||
"view_revisions": "Notizrevisionen",
|
||||
"advanced": "Erweitert"
|
||||
"note_map": "Notizen Karte"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "Das Schaltflächen-Widget „{{componentId}}“ hat keinen definierten Klick-Handler"
|
||||
|
||||
@@ -162,8 +162,7 @@
|
||||
"other": "Otro",
|
||||
"quickSearch": "centrarse en la entrada de búsqueda rápida",
|
||||
"inPageSearch": "búsqueda en la página",
|
||||
"title": "Hoja de ayuda",
|
||||
"editShortcuts": "Editar atajos de teclado"
|
||||
"title": "Hoja de ayuda"
|
||||
},
|
||||
"import": {
|
||||
"importIntoNote": "Importar a nota",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Echec du chargement d'un script personnalisé",
|
||||
"message": "Le script n'a pas pu être exécuté à cause de\n\n{{message}}"
|
||||
"message": "Le script de la note avec l'ID \"{{id}}\", intitulé \"{{title}}\" n'a pas pu être exécuté à cause de\n\n{{message}}"
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "Impossible d'obtenir la liste des widgets depuis le serveur"
|
||||
|
||||
@@ -31,17 +31,5 @@
|
||||
},
|
||||
"add_link": {
|
||||
"note": "नोट"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"other": "अन्य"
|
||||
},
|
||||
"clone_to": {
|
||||
"search_for_note_by_its_name": "नोट क नाम से नोट खोजें"
|
||||
},
|
||||
"confirm": {
|
||||
"also_delete_note": "नोट भी डिलीट करें"
|
||||
},
|
||||
"delete_notes": {
|
||||
"delete_notes_preview": "नोट्स प्रिव्यू डिलीट करें"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,7 @@
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Nem sikerült betölteni az egyéni szkriptet",
|
||||
"message": "A skript nem hajtható végre a következő ok miatt:\n\n{{message}}"
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "A Widget-ek letöltése sikertelen volt"
|
||||
},
|
||||
"widget-render-error": {
|
||||
"title": "Nem sikerült renderelni a React widget-et"
|
||||
"message": "A(z) \"{{id}}\" azonosítójú, \"{{title}}\" című jegyzetből származó szkript nem hajtható végre a következő ok miatt:\n\n{{message}}"
|
||||
}
|
||||
},
|
||||
"add_link": {
|
||||
|
||||
@@ -1895,11 +1895,7 @@
|
||||
"create-child-note": "Crea nota figlio",
|
||||
"unhoist": "Sganciare",
|
||||
"toggle-sidebar": "Attiva/disattiva la barra laterale",
|
||||
"dropping-not-allowed": "Non è consentito lasciare appunti in questa posizione.",
|
||||
"clone-indicator-tooltip": "Questa nota ha {{- count}} genitori: {{- parents}}",
|
||||
"clone-indicator-tooltip-single": "Questa nota è stata clonata (1 genitore aggiuntivo: {{- parent}})",
|
||||
"shared-indicator-tooltip": "Questa nota è condivisa pubblicamente",
|
||||
"shared-indicator-tooltip-with-url": "Questa nota è condivisa pubblicamente all'indirizzo: {{- url}}"
|
||||
"dropping-not-allowed": "Non è consentito lasciare appunti in questa posizione."
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Mantieni la finestra in primo piano"
|
||||
@@ -2204,14 +2200,7 @@
|
||||
"execute_sql_description": "Questa nota è una nota SQL. Clicca per eseguire la query SQL.",
|
||||
"shared_copy_to_clipboard": "Copia link negli appunti",
|
||||
"shared_open_in_browser": "Apri il link nel browser",
|
||||
"shared_unshare": "Rimuovi condivisione",
|
||||
"save_status_saved": "Salvato",
|
||||
"save_status_saving": "Salvataggio in corso...",
|
||||
"save_status_unsaved": "Non salvato",
|
||||
"save_status_error": "Salvataggio non riuscito",
|
||||
"save_status_saving_tooltip": "Le modifiche sono state salvate.",
|
||||
"save_status_unsaved_tooltip": "Ci sono modifiche non salvate. Verranno salvate automaticamente tra un attimo.",
|
||||
"save_status_error_tooltip": "Si è verificato un errore durante il salvataggio della nota. Se possibile, prova a copiare il contenuto della nota altrove e a ricaricare l'applicazione."
|
||||
"shared_unshare": "Rimuovi condivisione"
|
||||
},
|
||||
"breadcrumb": {
|
||||
"workspace_badge": "Area di lavoro",
|
||||
@@ -2254,18 +2243,5 @@
|
||||
"empty_button": "Nascondi il pannello",
|
||||
"toggle": "Attiva/disattiva pannello destro",
|
||||
"custom_widget_go_to_source": "Vai al codice sorgente"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_one": "{{count}} allegato",
|
||||
"attachments_many": "{{count}} allegati",
|
||||
"attachments_other": "{{count}} allegati",
|
||||
"layers_one": "{{count}} livello",
|
||||
"layers_many": "{{count}} livelli",
|
||||
"layers_other": "{{count}} livelli",
|
||||
"pages_one": "{{count}} pagina",
|
||||
"pages_many": "{{count}} pagine",
|
||||
"pages_other": "{{count}} pagine",
|
||||
"pages_alt": "Pagina {{pageNumber}}",
|
||||
"pages_loading": "Caricamento in corso..."
|
||||
}
|
||||
}
|
||||
|
||||
14
apps/client/src/types.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import { IconRegistry, Locale } from "@triliumnext/commons";
|
||||
import { IconRegistry } from "@triliumnext/commons";
|
||||
|
||||
import appContext, { AppContext } from "./components/app_context";
|
||||
import type FNote from "./entities/fnote";
|
||||
@@ -36,6 +36,7 @@ interface CustomGlobals {
|
||||
isProtectedSessionAvailable: boolean;
|
||||
isDev: boolean;
|
||||
isMainWindow: boolean;
|
||||
windowId: string;
|
||||
maxEntityChangeIdAtLoad: number;
|
||||
maxEntityChangeSyncIdAtLoad: number;
|
||||
assetPath: string;
|
||||
@@ -47,25 +48,14 @@ interface CustomGlobals {
|
||||
platform?: typeof process.platform;
|
||||
linter: typeof lint;
|
||||
hasNativeTitleBar: boolean;
|
||||
hasBackgroundEffects: boolean;
|
||||
isElectron: boolean;
|
||||
isRtl: boolean;
|
||||
iconRegistry: IconRegistry;
|
||||
themeCssUrl: string;
|
||||
themeUseNextAsBase?: "next" | "next-light" | "next-dark";
|
||||
iconPackCss: string;
|
||||
headingStyle: "plain" | "underline" | "markdown";
|
||||
layoutOrientation: "vertical" | "horizontal";
|
||||
currentLocale: Locale;
|
||||
}
|
||||
|
||||
type RequireMethod = (moduleName: string) => any;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
$: JQueryStatic;
|
||||
jQuery: JQueryStatic;
|
||||
|
||||
logError(message: string);
|
||||
logInfo(message: 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 { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
import NoteContext from "../../components/note_context";
|
||||
@@ -338,19 +338,15 @@ interface AttributesProps extends StatusBarContext {
|
||||
function AttributesButton({ note, attributesShown, setAttributesShown }: AttributesProps) {
|
||||
const [ count, setCount ] = useState(note.attributes.length);
|
||||
|
||||
const getAttributeCount = useCallback((note: FNote) => {
|
||||
return note.getAttributes().filter(a => !a.isAutoLink).length;
|
||||
}, []);
|
||||
|
||||
// React to note changes.
|
||||
useEffect(() => {
|
||||
setCount(getAttributeCount(note));
|
||||
}, [ note, getAttributeCount ]);
|
||||
setCount(note.getAttributes().filter(a => !a.isAutoLink).length);
|
||||
}, [ note ]);
|
||||
|
||||
// React to changes in count.
|
||||
useTriliumEvent("entitiesReloaded", (({loadResults}) => {
|
||||
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
|
||||
setCount(getAttributeCount(note));
|
||||
setCount(note.attributes.length);
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function SqlResults() {
|
||||
{t("sql_result.no_rows")}
|
||||
</Alert>
|
||||
) : (
|
||||
<div className="sql-console-result-container selectable-text">
|
||||
<div class="sql-console-result-container">
|
||||
{results?.map(rows => {
|
||||
// inserts, updates
|
||||
if (typeof rows === "object" && !Array.isArray(rows)) {
|
||||
|
||||
@@ -286,7 +286,7 @@ function useWatchdogCrashHandling() {
|
||||
const currentState = watchdog.state;
|
||||
logInfo(`CKEditor state changed to ${currentState}`);
|
||||
|
||||
if (currentState === "ready" && hasCrashed.current) {
|
||||
if (currentState === "ready") {
|
||||
hasCrashed.current = false;
|
||||
watchdog.editor?.focus();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/// <reference types='vitest' />
|
||||
import preact from "@preact/preset-vite";
|
||||
import { join } from 'path';
|
||||
import webpackStatsPlugin from 'rollup-plugin-webpack-stats';
|
||||
import { defineConfig } from 'vite';
|
||||
import { join, resolve } from 'path';
|
||||
import { defineConfig, type Plugin } from 'vite';
|
||||
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
||||
import webpackStatsPlugin from 'rollup-plugin-webpack-stats';
|
||||
import preact from "@preact/preset-vite";
|
||||
|
||||
const assets = [ "assets", "stylesheets", "fonts", "translations" ];
|
||||
|
||||
@@ -70,15 +70,21 @@ export default defineConfig(() => ({
|
||||
sourcemap: false,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: join(__dirname, "src", "index.html"),
|
||||
desktop: join(__dirname, "src", "desktop.ts"),
|
||||
mobile: join(__dirname, "src", "mobile.ts"),
|
||||
login: join(__dirname, "src", "login.ts"),
|
||||
setup: join(__dirname, "src", "setup.ts"),
|
||||
set_password: join(__dirname, "src", "set_password.ts"),
|
||||
runtime: join(__dirname, "src", "runtime.ts"),
|
||||
print: join(__dirname, "src", "print.tsx")
|
||||
},
|
||||
output: {
|
||||
entryFileNames: "src/[name].js",
|
||||
chunkFileNames: "src/[name].js",
|
||||
assetFileNames: "src/[name].[ext]",
|
||||
manualChunks: {
|
||||
"ckeditor5": [ "@triliumnext/ckeditor5" ]
|
||||
"ckeditor5": [ "@triliumnext/ckeditor5" ],
|
||||
"boxicons": [ "../../node_modules/boxicons/css/boxicons.min.css" ]
|
||||
},
|
||||
},
|
||||
onwarn(warning, rollupWarn) {
|
||||
|
||||
|
After Width: | Height: | Size: 545 B |
|
After Width: | Height: | Size: 727 B |
|
After Width: | Height: | Size: 828 B |
|
After Width: | Height: | Size: 931 B |
BIN
apps/desktop/src/assets/images/tray/closed-windowsTemplate.png
Normal file
|
After Width: | Height: | Size: 292 B |
|
After Width: | Height: | Size: 355 B |
|
After Width: | Height: | Size: 434 B |
|
After Width: | Height: | Size: 492 B |
@@ -6,6 +6,7 @@ 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";
|
||||
@@ -69,10 +70,12 @@ async function main() {
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
app.on("second-instance", (event, commandLine) => {
|
||||
app.on("second-instance", async (event, commandLine) => {
|
||||
const lastFocusedWindow = windowService.getLastFocusedWindow();
|
||||
if (commandLine.includes("--new-window")) {
|
||||
windowService.createExtraWindow("");
|
||||
const randomString = (await import("@triliumnext/server/src/services/utils.js")).randomString;
|
||||
const extraWindowId = randomString(4);
|
||||
windowService.createExtraWindow(extraWindowId, "");
|
||||
} else if (lastFocusedWindow) {
|
||||
if (lastFocusedWindow.isMinimized()) {
|
||||
lastFocusedWindow.restore();
|
||||
@@ -124,7 +127,8 @@ async function onReady() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
await normalizeOpenNoteContexts();
|
||||
tray.createTray();
|
||||
} else {
|
||||
await windowService.createSetupWindow();
|
||||
@@ -133,6 +137,30 @@ 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 List");
|
||||
await expect(app.sidebar).toContainText(/highlights/i);
|
||||
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.page.locator(".tree")).toContainText("Trilium Integration Test");
|
||||
await expect(this.noteTree).toContainText("Trilium Integration Test");
|
||||
if (!preserveTabs) {
|
||||
await this.closeAllTabs();
|
||||
}
|
||||
|
||||
@@ -220,6 +220,7 @@
|
||||
"password-confirmation": "Password confirmation",
|
||||
"button": "Set password"
|
||||
},
|
||||
"javascript-required": "Trilium requires JavaScript to be enabled.",
|
||||
"setup": {
|
||||
"heading": "Trilium Notes setup",
|
||||
"new-document": "I'm a new user, and I want to create a new Trilium document for my notes",
|
||||
@@ -381,6 +382,8 @@
|
||||
"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",
|
||||
|
||||
@@ -10,18 +10,6 @@
|
||||
"creating-and-moving-notes": "नोट्स बनाना और स्थानांतरित करना",
|
||||
"move-note-up": "नोट को ऊपर ले जाएं",
|
||||
"move-note-down": "नोट को नीचे ले जाएं",
|
||||
"note-clipboard": "नोट क्लिपबोर्ड",
|
||||
"duplicate-subtree": "डुप्लिकेट सबट्री",
|
||||
"open-new-tab": "नया टैब खोलें",
|
||||
"second-tab": "लिस्ट में दूसरी टैब एक्टिवेट करें",
|
||||
"third-tab": "लिस्ट में तीसरी टैब एक्टिवेट करें",
|
||||
"fourth-tab": "लिस्ट में चौथी टैब एक्टिवेट करें",
|
||||
"sixth-tab": "लिस्ट में छठी टैब एक्टिवेट करें",
|
||||
"seventh-tab": "लिस्ट में सातवीं टैब एक्टिवेट करें",
|
||||
"eight-tab": "लिस्ट में आठवीं टैब एक्टिवेट करें",
|
||||
"ninth-tab": "लिस्ट में नौवीं टैब एक्टिवेट करें",
|
||||
"last-tab": "लिस्ट में आखिरी टैब एक्टिवेट करें",
|
||||
"show-sql-console": "\"SQL कंसोल\" पेज खोलें",
|
||||
"show-backend-log": "\"बैकेंड लॉग\" पेज खोलें"
|
||||
"note-clipboard": "नोट क्लिपबोर्ड"
|
||||
}
|
||||
}
|
||||
|
||||
60
apps/server/src/assets/views/desktop.ejs
Normal file
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
|
||||
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
|
||||
<title>Trilium Notes</title>
|
||||
<style id="trilium-icon-packs">
|
||||
<%- iconPackCss %>
|
||||
</style>
|
||||
<script src="<%= appPath %>/runtime.js" crossorigin type="module"></script>
|
||||
</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' %>"
|
||||
lang="<%= currentLocale.id %>" dir="<%= currentLocale.rtl ? 'rtl' : 'ltr' %>"
|
||||
>
|
||||
<noscript><%= t("javascript-required") %></noscript>
|
||||
|
||||
<script>
|
||||
// hide body to reduce flickering on the startup. This is done through JS and not CSS to not hide <noscript>
|
||||
document.getElementsByTagName("body")[0].style.display = "none";
|
||||
</script>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container" style="display: none"></div>
|
||||
|
||||
<%- include("./partials/windowGlobal.ejs", locals) %>
|
||||
|
||||
<!-- Required for match the PWA's top bar color with the theme -->
|
||||
<!-- This works even when the user directly changes --root-background in CSS -->
|
||||
<div id="background-color-tracker" style="position: absolute; visibility: hidden; color: var(--root-background); transition: color 1ms;"></div>
|
||||
|
||||
<!-- Required for correct loading of scripts in Electron -->
|
||||
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
|
||||
|
||||
<link href="<%= assetPath %>/stylesheets/ckeditor-theme.css" rel="stylesheet">
|
||||
<link href="api/fonts" rel="stylesheet">
|
||||
<link href="<%= assetPath %>/stylesheets/theme-light.css" rel="stylesheet">
|
||||
|
||||
<% if (themeCssUrl) { %>
|
||||
<link href="<%= themeCssUrl %>" rel="stylesheet">
|
||||
<% } %>
|
||||
|
||||
<% if (themeUseNextAsBase === "next") { %>
|
||||
<link href="<%= assetPath %>/stylesheets/theme-next.css" rel="stylesheet">
|
||||
<% } else if (themeUseNextAsBase === "next-dark") { %>
|
||||
<link href="<%= assetPath %>/stylesheets/theme-next-dark.css" rel="stylesheet">
|
||||
<% } else if (themeUseNextAsBase === "next-light") { %>
|
||||
<link href="<%= assetPath %>/stylesheets/theme-next-light.css" rel="stylesheet">
|
||||
<% } %>
|
||||
|
||||
<link href="<%= assetPath %>/stylesheets/style.css" rel="stylesheet">
|
||||
|
||||
<script src="<%= appPath %>/desktop.js" crossorigin type="module"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -15,6 +15,7 @@
|
||||
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-light.css">
|
||||
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-next.css">
|
||||
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/style.css">
|
||||
<script src="<%= appPath %>/runtime.js" crossorigin type="module"></script>
|
||||
</head>
|
||||
<body lang="<%= currentLocale.id %>" dir="<%= currentLocale.rtl ? 'rtl' : 'ltr' %>">
|
||||
<div class="container login-page">
|
||||
|
||||
137
apps/server/src/assets/views/mobile.ejs
Normal file
@@ -0,0 +1,137 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
|
||||
<meta name="theme-color" content="#fff">
|
||||
<title>Trilium Notes</title>
|
||||
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
|
||||
|
||||
<style>
|
||||
.lds-roller {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
.lds-roller div {
|
||||
animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
transform-origin: 40px 40px;
|
||||
}
|
||||
.lds-roller div:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #000;
|
||||
margin: -4px 0 0 -4px;
|
||||
}
|
||||
.lds-roller div:nth-child(1) {
|
||||
animation-delay: -0.036s;
|
||||
}
|
||||
.lds-roller div:nth-child(1):after {
|
||||
top: 63px;
|
||||
left: 63px;
|
||||
}
|
||||
.lds-roller div:nth-child(2) {
|
||||
animation-delay: -0.072s;
|
||||
}
|
||||
.lds-roller div:nth-child(2):after {
|
||||
top: 68px;
|
||||
left: 56px;
|
||||
}
|
||||
.lds-roller div:nth-child(3) {
|
||||
animation-delay: -0.108s;
|
||||
}
|
||||
.lds-roller div:nth-child(3):after {
|
||||
top: 71px;
|
||||
left: 48px;
|
||||
}
|
||||
.lds-roller div:nth-child(4) {
|
||||
animation-delay: -0.144s;
|
||||
}
|
||||
.lds-roller div:nth-child(4):after {
|
||||
top: 72px;
|
||||
left: 40px;
|
||||
}
|
||||
.lds-roller div:nth-child(5) {
|
||||
animation-delay: -0.18s;
|
||||
}
|
||||
.lds-roller div:nth-child(5):after {
|
||||
top: 71px;
|
||||
left: 32px;
|
||||
}
|
||||
.lds-roller div:nth-child(6) {
|
||||
animation-delay: -0.216s;
|
||||
}
|
||||
.lds-roller div:nth-child(6):after {
|
||||
top: 68px;
|
||||
left: 24px;
|
||||
}
|
||||
.lds-roller div:nth-child(7) {
|
||||
animation-delay: -0.252s;
|
||||
}
|
||||
.lds-roller div:nth-child(7):after {
|
||||
top: 63px;
|
||||
left: 17px;
|
||||
}
|
||||
.lds-roller div:nth-child(8) {
|
||||
animation-delay: -0.288s;
|
||||
}
|
||||
.lds-roller div:nth-child(8):after {
|
||||
top: 56px;
|
||||
left: 12px;
|
||||
}
|
||||
@keyframes lds-roller {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style id="trilium-icon-packs">
|
||||
<%- iconPackCss %>
|
||||
</style>
|
||||
<script src="<%= appPath %>/runtime.js" crossorigin type="module"></script>
|
||||
</head>
|
||||
<body
|
||||
class="mobile heading-style-<%= headingStyle %>"
|
||||
lang="<%= currentLocale.id %>" dir="<%= currentLocale.rtl ? 'rtl' : 'ltr' %>"
|
||||
>
|
||||
<noscript><%= t("javascript-required") %></noscript>
|
||||
|
||||
<div id="context-menu-cover"></div>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container"></div>
|
||||
|
||||
<%- include("./partials/windowGlobal.ejs", locals) %>
|
||||
|
||||
<script src="<%= appPath %>/mobile.js" crossorigin type="module"></script>
|
||||
|
||||
<link href="api/fonts" rel="stylesheet">
|
||||
<link href="<%= assetPath %>/stylesheets/ckeditor-theme.css" rel="stylesheet">
|
||||
<link href="<%= assetPath %>/stylesheets/theme-light.css" rel="stylesheet">
|
||||
<% if (themeCssUrl) { %>
|
||||
<link href="<%= themeCssUrl %>" rel="stylesheet">
|
||||
<% } %>
|
||||
|
||||
<% if (themeUseNextAsBase === "next") { %>
|
||||
<link href="<%= assetPath %>/stylesheets/theme-next.css" rel="stylesheet">
|
||||
<% } else if (themeUseNextAsBase === "next-dark") { %>
|
||||
<link href="<%= assetPath %>/stylesheets/theme-next-dark.css" rel="stylesheet">
|
||||
<% } else if (themeUseNextAsBase === "next-light") { %>
|
||||
<link href="<%= assetPath %>/stylesheets/theme-next-light.css" rel="stylesheet">
|
||||
<% } %>
|
||||
|
||||
<link href="<%= assetPath %>/stylesheets/style.css" rel="stylesheet">
|
||||
|
||||
</body>
|
||||
</html>
|
||||
26
apps/server/src/assets/views/partials/windowGlobal.ejs
Normal file
@@ -0,0 +1,26 @@
|
||||
<script type="text/javascript">
|
||||
global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
|
||||
|
||||
window.glob = {
|
||||
device: "<%= device %>",
|
||||
baseApiUrl: "<%= baseApiUrl %>",
|
||||
activeDialog: null,
|
||||
maxEntityChangeIdAtLoad: <%= maxEntityChangeIdAtLoad %>,
|
||||
maxEntityChangeSyncIdAtLoad: <%= maxEntityChangeSyncIdAtLoad %>,
|
||||
instanceName: '<%= instanceName %>',
|
||||
csrfToken: '<%= csrfToken %>',
|
||||
isDev: <%= isDev %>,
|
||||
appCssNoteIds: <%- JSON.stringify(appCssNoteIds) %>,
|
||||
isMainWindow: <%= isMainWindow %>,
|
||||
windowId: "<%= windowId %>",
|
||||
isProtectedSessionAvailable: <%= isProtectedSessionAvailable %>,
|
||||
triliumVersion: "<%= triliumVersion %>",
|
||||
assetPath: "<%= assetPath %>",
|
||||
appPath: "<%= appPath %>",
|
||||
platform: "<%= platform %>",
|
||||
hasNativeTitleBar: <%= hasNativeTitleBar %>,
|
||||
TRILIUM_SAFE_MODE: <%= !!process.env.TRILIUM_SAFE_MODE %>,
|
||||
isRtl: <%= !!currentLocale.rtl %>,
|
||||
iconRegistry: <%- JSON.stringify(iconRegistry) %>
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,48 @@
|
||||
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,6 +6,11 @@
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import express from "express";
|
||||
import rateLimit from "express-rate-limit";
|
||||
import { existsSync } from "fs";
|
||||
import path from "path";
|
||||
import type serveStatic from "serve-static";
|
||||
|
||||
import { assetUrlFragment } from "../services/asset_path.js";
|
||||
import auth from "../services/auth.js";
|
||||
import { getResourceDir, isDev } from "../services/utils.js";
|
||||
import { doubleCsrfProtection as csrfMiddleware } from "./csrf_protection.js";
|
||||
|
||||
const persistentCacheStatic = (root: string, options?: serveStatic.ServeStaticOptions<express.Response<unknown, Record<string, unknown>>>) => {
|
||||
if (!isDev) {
|
||||
@@ -23,29 +20,19 @@ async function register(app: express.Application) {
|
||||
const srcRoot = path.join(__dirname, "..", "..");
|
||||
const resourceDir = getResourceDir();
|
||||
|
||||
const rootLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100 // limit each IP to 100 requests per windowMs
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
const { createServer: createViteServer } = await import("vite");
|
||||
const clientDir = path.join(srcRoot, "../client");
|
||||
const vite = await createViteServer({
|
||||
server: { middlewareMode: true },
|
||||
appType: "spa",
|
||||
appType: "custom",
|
||||
cacheDir: path.join(srcRoot, "../../.cache/vite"),
|
||||
base: `/${assetUrlFragment}/`,
|
||||
root: clientDir,
|
||||
css: { devSourcemap: true }
|
||||
});
|
||||
app.use(`/${assetUrlFragment}/`, vite.middlewares);
|
||||
app.get(`/`, [ rootLimiter, auth.checkAuth, csrfMiddleware ], (req, res, next) => {
|
||||
req.url = `/${assetUrlFragment}/src/index.html`;
|
||||
vite.middlewares(req, res, next);
|
||||
});
|
||||
app.get(`/index.ts`, (req, res, next) => {
|
||||
req.url = `/${assetUrlFragment}/src/index.ts`;
|
||||
app.use(`/${assetUrlFragment}/`, (req, res, next) => {
|
||||
req.url = `/${assetUrlFragment}${req.url}`;
|
||||
vite.middlewares(req, res, next);
|
||||
});
|
||||
app.use(`/node_modules/@excalidraw/excalidraw/dist/prod`, persistentCacheStatic(path.join(srcRoot, "../../node_modules/@excalidraw/excalidraw/dist/prod")));
|
||||
@@ -55,14 +42,7 @@ async function register(app: express.Application) {
|
||||
throw new Error(`Public directory is missing at: ${path.resolve(publicDir)}`);
|
||||
}
|
||||
|
||||
app.get(`/`, [ rootLimiter, auth.checkAuth, csrfMiddleware ], (_, res) => {
|
||||
// We force the page to not be cached since on mobile the CSRF token can be
|
||||
// broken when closing the browser and coming back in to the page.
|
||||
// The page is restored from cache, but the API call fail.
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.sendFile(path.join(publicDir, "src", "index.html"));
|
||||
});
|
||||
app.use("/assets", persistentCacheStatic(path.join(publicDir, "assets")));
|
||||
app.use(`/${assetUrlFragment}/src`, persistentCacheStatic(path.join(publicDir, "src")));
|
||||
app.use(`/${assetUrlFragment}/stylesheets`, persistentCacheStatic(path.join(publicDir, "stylesheets")));
|
||||
app.use(`/${assetUrlFragment}/fonts`, persistentCacheStatic(path.join(publicDir, "fonts")));
|
||||
app.use(`/${assetUrlFragment}/translations/`, persistentCacheStatic(path.join(publicDir, "translations")));
|
||||
|
||||
@@ -15,10 +15,10 @@ import sql from "../services/sql.js";
|
||||
import { isDev, isElectron, isWindows11 } from "../services/utils.js";
|
||||
import { generateToken as generateCsrfToken } from "./csrf_protection.js";
|
||||
|
||||
|
||||
type View = "desktop" | "mobile" | "print";
|
||||
|
||||
export function bootstrap(req: Request, res: Response) {
|
||||
function index(req: Request, res: Response) {
|
||||
const view = getView(req);
|
||||
const options = optionService.getOptionMap();
|
||||
|
||||
//'overwrite' set to false (default) => the existing token will be re-used and validated
|
||||
@@ -26,14 +26,17 @@ export function bootstrap(req: Request, res: Response) {
|
||||
const csrfToken = generateCsrfToken(req, res, false, false);
|
||||
log.info(`CSRF token generation: ${csrfToken ? "Successful" : "Failed"}`);
|
||||
|
||||
const view = getView(req);
|
||||
// We force the page to not be cached since on mobile the CSRF token can be
|
||||
// broken when closing the browser and coming back in to the page.
|
||||
// The page is restored from cache, but the API call fail.
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
|
||||
const theme = options.theme;
|
||||
const themeNote = attributeService.getNoteWithLabel("appTheme", theme);
|
||||
const nativeTitleBarVisible = options.nativeTitleBarVisible === "true";
|
||||
const iconPacks = getIconPacks();
|
||||
const currentLocale = getCurrentLocale();
|
||||
|
||||
res.send({
|
||||
res.render(view, {
|
||||
device: view,
|
||||
csrfToken,
|
||||
themeCssUrl: getThemeCssUrl(theme, themeNote),
|
||||
@@ -44,27 +47,29 @@ export function bootstrap(req: Request, res: Response) {
|
||||
isElectron,
|
||||
hasNativeTitleBar: isElectron && nativeTitleBarVisible,
|
||||
hasBackgroundEffects: isElectron && isWindows11 && !nativeTitleBarVisible && options.backgroundEffects === "true",
|
||||
mainFontSize: parseInt(options.mainFontSize),
|
||||
treeFontSize: parseInt(options.treeFontSize),
|
||||
detailFontSize: parseInt(options.detailFontSize),
|
||||
maxEntityChangeIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes"),
|
||||
maxEntityChangeSyncIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1"),
|
||||
instanceName: config.General ? config.General.instanceName : null,
|
||||
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,
|
||||
appPath,
|
||||
baseApiUrl: 'api/',
|
||||
currentLocale,
|
||||
isRtl: !!currentLocale.rtl,
|
||||
currentLocale: getCurrentLocale(),
|
||||
iconPackCss: iconPacks
|
||||
.map(p => generateCss(p, p.builtin
|
||||
? `${assetPath}/fonts/${p.fontAttachmentId}.${MIME_TO_EXTENSION_MAPPINGS[p.fontMime]}`
|
||||
: `api/attachments/download/${p.fontAttachmentId}`))
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
iconRegistry: generateIconRegistry(iconPacks),
|
||||
TRILIUM_SAFE_MODE: !!process.env.TRILIUM_SAFE_MODE
|
||||
iconRegistry: generateIconRegistry(iconPacks)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -129,3 +134,7 @@ function getThemeCssUrl(theme: string, themeNote: BNote | null) {
|
||||
function getAppCssNoteIds() {
|
||||
return attributeService.getNotesWithLabel("appCss").map((note) => note.noteId);
|
||||
}
|
||||
|
||||
export default {
|
||||
index
|
||||
};
|
||||
|
||||
@@ -1,73 +1,76 @@
|
||||
import { createPartialContentHandler } from "@triliumnext/express-partial-content";
|
||||
import { isElectron } from "../services/utils.js";
|
||||
import express from "express";
|
||||
|
||||
import auth from "../services/auth.js";
|
||||
import openID from '../services/open_id.js';
|
||||
import totp from './api/totp.js';
|
||||
import recoveryCodes from './api/recovery_codes.js';
|
||||
import { doubleCsrfProtection as csrfMiddleware } from "./csrf_protection.js";
|
||||
import { createPartialContentHandler } from "@triliumnext/express-partial-content";
|
||||
import rateLimit from "express-rate-limit";
|
||||
|
||||
// page routes
|
||||
import setupRoute from "./setup.js";
|
||||
import loginRoute from "./login.js";
|
||||
import indexRoute from "./index.js";
|
||||
|
||||
// API routes
|
||||
import treeApiRoute from "./api/tree.js";
|
||||
import notesApiRoute from "./api/notes.js";
|
||||
import branchesApiRoute from "./api/branches.js";
|
||||
import attachmentsApiRoute from "./api/attachments.js";
|
||||
import autocompleteApiRoute from "./api/autocomplete.js";
|
||||
import cloningApiRoute from "./api/cloning.js";
|
||||
import revisionsApiRoute from "./api/revisions.js";
|
||||
import recentChangesApiRoute from "./api/recent_changes.js";
|
||||
import optionsApiRoute from "./api/options.js";
|
||||
import passwordApiRoute from "./api/password.js";
|
||||
import syncApiRoute from "./api/sync.js";
|
||||
import loginApiRoute from "./api/login.js";
|
||||
import recentNotesRoute from "./api/recent_notes.js";
|
||||
import appInfoRoute from "./api/app_info.js";
|
||||
import exportRoute from "./api/export.js";
|
||||
import importRoute from "./api/import.js";
|
||||
import setupApiRoute from "./api/setup.js";
|
||||
import sqlRoute from "./api/sql.js";
|
||||
import databaseRoute from "./api/database.js";
|
||||
import imageRoute from "./api/image.js";
|
||||
import attributesRoute from "./api/attributes.js";
|
||||
import scriptRoute from "./api/script.js";
|
||||
import senderRoute from "./api/sender.js";
|
||||
import filesRoute from "./api/files.js";
|
||||
import searchRoute from "./api/search.js";
|
||||
import bulkActionRoute from "./api/bulk_action.js";
|
||||
import specialNotesRoute from "./api/special_notes.js";
|
||||
import noteMapRoute from "./api/note_map.js";
|
||||
import clipperRoute from "./api/clipper.js";
|
||||
import similarNotesRoute from "./api/similar_notes.js";
|
||||
import keysRoute from "./api/keys.js";
|
||||
import backendLogRoute from "./api/backend_log.js";
|
||||
import statsRoute from "./api/stats.js";
|
||||
import fontsRoute from "./api/fonts.js";
|
||||
import etapiTokensApiRoutes from "./api/etapi_tokens.js";
|
||||
import relationMapApiRoute from "./api/relation-map.js";
|
||||
import otherRoute from "./api/other.js";
|
||||
import metricsRoute from "./api/metrics.js";
|
||||
import shareRoutes from "../share/routes.js";
|
||||
import ollamaRoute from "./api/ollama.js";
|
||||
import openaiRoute from "./api/openai.js";
|
||||
import anthropicRoute from "./api/anthropic.js";
|
||||
import llmRoute from "./api/llm.js";
|
||||
import systemInfoRoute from "./api/system_info.js";
|
||||
|
||||
import etapiAuthRoutes from "../etapi/auth.js";
|
||||
import etapiAppInfoRoutes from "../etapi/app_info.js";
|
||||
import etapiAttachmentRoutes from "../etapi/attachments.js";
|
||||
import etapiAttributeRoutes from "../etapi/attributes.js";
|
||||
import etapiAuthRoutes from "../etapi/auth.js";
|
||||
import etapiBackupRoute from "../etapi/backup.js";
|
||||
import etapiBranchRoutes from "../etapi/branches.js";
|
||||
import etapiMetricsRoute from "../etapi/metrics.js";
|
||||
import etapiNoteRoutes from "../etapi/notes.js";
|
||||
import etapiSpecRoute from "../etapi/spec.js";
|
||||
import etapiSpecialNoteRoutes from "../etapi/special_notes.js";
|
||||
import auth from "../services/auth.js";
|
||||
import openID from '../services/open_id.js';
|
||||
import { isElectron } from "../services/utils.js";
|
||||
import shareRoutes from "../share/routes.js";
|
||||
import anthropicRoute from "./api/anthropic.js";
|
||||
import appInfoRoute from "./api/app_info.js";
|
||||
import attachmentsApiRoute from "./api/attachments.js";
|
||||
import attributesRoute from "./api/attributes.js";
|
||||
import autocompleteApiRoute from "./api/autocomplete.js";
|
||||
import backendLogRoute from "./api/backend_log.js";
|
||||
import branchesApiRoute from "./api/branches.js";
|
||||
import bulkActionRoute from "./api/bulk_action.js";
|
||||
import clipperRoute from "./api/clipper.js";
|
||||
import cloningApiRoute from "./api/cloning.js";
|
||||
import databaseRoute from "./api/database.js";
|
||||
import etapiTokensApiRoutes from "./api/etapi_tokens.js";
|
||||
import exportRoute from "./api/export.js";
|
||||
import filesRoute from "./api/files.js";
|
||||
import fontsRoute from "./api/fonts.js";
|
||||
import imageRoute from "./api/image.js";
|
||||
import importRoute from "./api/import.js";
|
||||
import keysRoute from "./api/keys.js";
|
||||
import llmRoute from "./api/llm.js";
|
||||
import loginApiRoute from "./api/login.js";
|
||||
import metricsRoute from "./api/metrics.js";
|
||||
import noteMapRoute from "./api/note_map.js";
|
||||
import notesApiRoute from "./api/notes.js";
|
||||
import ollamaRoute from "./api/ollama.js";
|
||||
import openaiRoute from "./api/openai.js";
|
||||
import optionsApiRoute from "./api/options.js";
|
||||
import otherRoute from "./api/other.js";
|
||||
import passwordApiRoute from "./api/password.js";
|
||||
import recentChangesApiRoute from "./api/recent_changes.js";
|
||||
import recentNotesRoute from "./api/recent_notes.js";
|
||||
import recoveryCodes from './api/recovery_codes.js';
|
||||
import relationMapApiRoute from "./api/relation-map.js";
|
||||
import revisionsApiRoute from "./api/revisions.js";
|
||||
import scriptRoute from "./api/script.js";
|
||||
import searchRoute from "./api/search.js";
|
||||
import senderRoute from "./api/sender.js";
|
||||
import setupApiRoute from "./api/setup.js";
|
||||
import similarNotesRoute from "./api/similar_notes.js";
|
||||
import specialNotesRoute from "./api/special_notes.js";
|
||||
import sqlRoute from "./api/sql.js";
|
||||
import statsRoute from "./api/stats.js";
|
||||
import syncApiRoute from "./api/sync.js";
|
||||
import systemInfoRoute from "./api/system_info.js";
|
||||
import totp from './api/totp.js';
|
||||
// API routes
|
||||
import treeApiRoute from "./api/tree.js";
|
||||
import { doubleCsrfProtection as csrfMiddleware } from "./csrf_protection.js";
|
||||
import * as indexRoute from "./index.js";
|
||||
import loginRoute from "./login.js";
|
||||
import etapiSpecRoute from "../etapi/spec.js";
|
||||
import etapiBackupRoute from "../etapi/backup.js";
|
||||
import etapiMetricsRoute from "../etapi/metrics.js";
|
||||
import { apiResultHandler, apiRoute, asyncApiRoute, asyncRoute, route, router, uploadMiddlewareWithErrorHandling } from "./route_api.js";
|
||||
// page routes
|
||||
import setupRoute from "./setup.js";
|
||||
|
||||
const GET = "get",
|
||||
PST = "post",
|
||||
@@ -76,6 +79,7 @@ const GET = "get",
|
||||
DEL = "delete";
|
||||
|
||||
function register(app: express.Application) {
|
||||
route(GET, "/", [auth.checkAuth, csrfMiddleware], indexRoute.index);
|
||||
route(GET, "/login", [auth.checkAppInitialized, auth.checkPasswordSet], loginRoute.loginPage);
|
||||
route(GET, "/set-password", [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPasswordPage);
|
||||
|
||||
@@ -85,7 +89,6 @@ function register(app: express.Application) {
|
||||
skipSuccessfulRequests: true // successful auth to rate-limited ETAPI routes isn't counted. However, successful auth to /login is still counted!
|
||||
});
|
||||
|
||||
route(GET, "/bootstrap", [ auth.checkAuth ], indexRoute.bootstrap);
|
||||
route(PST, "/login", [loginRateLimiter], loginRoute.login);
|
||||
route(PST, "/logout", [csrfMiddleware, auth.checkAuth], loginRoute.logout);
|
||||
route(PST, "/set-password", [auth.checkAppInitialized, auth.checkPasswordNotSet], loginRoute.setPassword);
|
||||
|
||||
@@ -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 = 233;
|
||||
const APP_DB_VERSION = 234;
|
||||
const SYNC_VERSION = 36;
|
||||
const CLIPPER_PROTOCOL_VERSION = "1.0";
|
||||
|
||||
|
||||
@@ -72,6 +72,19 @@ 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);
|
||||
|
||||
@@ -137,6 +150,7 @@ export default {
|
||||
getOption,
|
||||
getOptionInt,
|
||||
getOptionBool,
|
||||
getOptionJson,
|
||||
setOption,
|
||||
createOption,
|
||||
getOptions,
|
||||
|
||||
@@ -45,8 +45,15 @@ async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts =
|
||||
"openNoteContexts",
|
||||
JSON.stringify([
|
||||
{
|
||||
notePath: "root",
|
||||
active: true
|
||||
windowId: "main",
|
||||
createdAt: 0,
|
||||
closedAt: 0,
|
||||
contexts: [
|
||||
{
|
||||
notePath: "root",
|
||||
active: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]),
|
||||
false
|
||||
@@ -257,8 +264,15 @@ function initStartupOptions() {
|
||||
"openNoteContexts",
|
||||
JSON.stringify([
|
||||
{
|
||||
notePath: process.env.TRILIUM_START_NOTE_ID || "root",
|
||||
active: true
|
||||
windowId: "main",
|
||||
createdAt: 0,
|
||||
closedAt: 0,
|
||||
contexts: [
|
||||
{
|
||||
notePath: process.env.TRILIUM_START_NOTE_ID || "root",
|
||||
active: true
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
@@ -147,8 +147,15 @@ async function createInitialDatabase(skipDemoDb?: boolean) {
|
||||
"openNoteContexts",
|
||||
JSON.stringify([
|
||||
{
|
||||
notePath: startNoteId,
|
||||
active: true
|
||||
windowId: "main",
|
||||
createdAt: 0,
|
||||
closedAt: 0,
|
||||
contexts: [
|
||||
{
|
||||
notePath: startNoteId,
|
||||
active: true
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
@@ -196,6 +196,39 @@ 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
|
||||
@@ -258,6 +291,12 @@ 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,28 +16,45 @@ 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.
|
||||
|
||||
function trackWindowFocus(win: BrowserWindow) {
|
||||
interface WindowEntry {
|
||||
window: BrowserWindow;
|
||||
windowId: string; // custom window ID
|
||||
}
|
||||
let allWindowEntries: WindowEntry[] = [];
|
||||
|
||||
function trackWindowFocus(win: BrowserWindow, windowId: string) {
|
||||
// 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", () => {
|
||||
allWindows = allWindows.filter(w => !w.isDestroyed() && w !== win);
|
||||
allWindows.push(win);
|
||||
allWindowEntries = allWindowEntries.filter(w => !w.window.isDestroyed() && w.window !== win);
|
||||
allWindowEntries.push({ window: win, windowId: windowId });
|
||||
|
||||
if (!optionService.getOptionBool("disableTray")) {
|
||||
electron.ipcMain.emit("reload-tray");
|
||||
}
|
||||
});
|
||||
|
||||
win.on("closed", () => {
|
||||
allWindows = allWindows.filter(w => !w.isDestroyed());
|
||||
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());
|
||||
if (!optionService.getOptionBool("disableTray")) {
|
||||
electron.ipcMain.emit("reload-tray");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function createExtraWindow(extraWindowHash: string) {
|
||||
async function createExtraWindow(extraWindowId: string, extraWindowHash: string) {
|
||||
const spellcheckEnabled = optionService.getOptionBool("spellCheckEnabled");
|
||||
|
||||
const { BrowserWindow } = await import("electron");
|
||||
@@ -56,15 +73,15 @@ async function createExtraWindow(extraWindowHash: string) {
|
||||
});
|
||||
|
||||
win.setMenuBarVisibility(false);
|
||||
win.loadURL(`http://127.0.0.1:${port}/?extraWindow=1${extraWindowHash}`);
|
||||
win.loadURL(`http://127.0.0.1:${port}/?extraWindow=${extraWindowId}${extraWindowHash}`);
|
||||
|
||||
configureWebContents(win.webContents, spellcheckEnabled);
|
||||
|
||||
trackWindowFocus(win);
|
||||
trackWindowFocus(win, extraWindowId);
|
||||
}
|
||||
|
||||
electron.ipcMain.on("create-extra-window", (event, arg) => {
|
||||
createExtraWindow(arg.extraWindowHash);
|
||||
createExtraWindow(arg.extraWindowId, arg.extraWindowHash);
|
||||
});
|
||||
|
||||
interface PrintOpts {
|
||||
@@ -168,8 +185,8 @@ async function getBrowserWindowForPrinting(e: IpcMainEvent, notePath: string, ac
|
||||
return { browserWindow, printReport };
|
||||
}
|
||||
|
||||
async function createMainWindow(app: App) {
|
||||
if ("setUserTasks" in app) {
|
||||
async function createMainWindow(app?: App) {
|
||||
if (app && "setUserTasks" in app) {
|
||||
app.setUserTasks([
|
||||
{
|
||||
program: process.execPath,
|
||||
@@ -219,7 +236,7 @@ async function createMainWindow(app: App) {
|
||||
mainWindow.on("closed", () => (mainWindow = null));
|
||||
|
||||
configureWebContents(mainWindow.webContents, spellcheckEnabled);
|
||||
trackWindowFocus(mainWindow);
|
||||
trackWindowFocus(mainWindow, "main");
|
||||
}
|
||||
|
||||
function getWindowExtraOpts() {
|
||||
@@ -381,11 +398,15 @@ function getMainWindow() {
|
||||
}
|
||||
|
||||
function getLastFocusedWindow() {
|
||||
return allWindows.length > 0 ? allWindows[allWindows.length - 1] : null;
|
||||
return allWindowEntries.length > 0 ? allWindowEntries[allWindowEntries.length - 1]?.window : null;
|
||||
}
|
||||
|
||||
function getAllWindows() {
|
||||
return allWindows;
|
||||
return allWindowEntries.map(e => e.window);
|
||||
}
|
||||
|
||||
function getAllWindowIds(): string[] {
|
||||
return allWindowEntries.map(e => e.windowId);
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -396,5 +417,6 @@ export default {
|
||||
registerGlobalShortcuts,
|
||||
getMainWindow,
|
||||
getLastFocusedWindow,
|
||||
getAllWindows
|
||||
getAllWindows,
|
||||
getAllWindowIds
|
||||
};
|
||||
|
||||
@@ -13,18 +13,6 @@
|
||||
"note_structure_description": "नोटों को पदानुक्रमिक रूप से व्यवस्थित किया जा सकता है। फ़ोल्डर्स की कोई आवश्यकता नहीं है, क्योंकि प्रत्येक नोट में उप-नोट हो सकते हैं। एक एकल नोट को पदानुक्रम में कई स्थानों पर जोड़ा जा सकता है।"
|
||||
},
|
||||
"productivity_benefits": {
|
||||
"protected_notes_title": "संरक्षित नोट्स",
|
||||
"web_clipper_title": "वेब क्लिपर"
|
||||
},
|
||||
"note_types": {
|
||||
"canvas_title": "कैनवास",
|
||||
"mindmap_title": "माइंडमैप"
|
||||
},
|
||||
"extensibility_benefits": {
|
||||
"share_title": "वेब पर नोट्स शेयर करें"
|
||||
},
|
||||
"collections": {
|
||||
"calendar_title": "कैलेंडर",
|
||||
"table_title": "टेबल"
|
||||
"protected_notes_title": "संरक्षित नोट्स"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,23 +130,6 @@
|
||||
"mobile_question": "모바일 앱이 있나요?",
|
||||
"mobile_answer": "현재 공식적인 모바일 앱은 없습니다. 하지만, 서버 인스턴스를 가지고 있다면 웹 브라우저를 이용해 접근하거나 PWA로 설치할 수 있습니다. 안드로이드에는 (데스크탑 클라이언트처럼)오프라인에서도 작동하는 TriliumDroid라는 비공식 앱이 있습니다.",
|
||||
"database_question": "어디에 데이터가 저장되나요?",
|
||||
"server_question": "Trilium을 사용하기 위해 서버가 필요한가요?",
|
||||
"title": "자주 묻는 질문",
|
||||
"database_answer": "모든 노트는 애플리케이션 폴더의 SQLite 데이터베이스에 저장됩니다. Trilium이 텍스트 파일 대신 데이터베이스를 사용하는 이유는 성능과 기능 모두 구현하기 훨씬 어렵기 때문입니다(트리 여러 위치에 같은 노트를 두는 Clone과 같은 기능). 애플리케이션 폴더를 찾으려면 About 창으로 가세요.",
|
||||
"server_answer": "아니요, 서버는 웹 브라우저를 통해 접속할 수 있도록 허용하며, 여러 기기를 사용하는 경우 동기화를 관리합니다. 시작하려면 데스크톱 애플리케이션을 다운로드하여 사용하기만 하면 됩니다.",
|
||||
"scaling_question": "이 애플리케이션은 얼마나 많은 노트를 처리할 수 있나요?",
|
||||
"scaling_answer": "사용량에 따라 다르겠지만, 이 애플리케이션은 최소 10만 개의 노트를 문제없이 처리할 수 있습니다. 다만, Trilium은 (NextCloud와 같은) 파일 저장소라기보다는 지식 기반 애플리케이션에 가깝기 때문에, 대용량 파일(파일당 1GB 이상)을 많이 업로드할 경우 동기화 과정이 실패할 수 있다는 점에 유의하십시오.",
|
||||
"network_share_question": "내 데이터베이스를 네트워크 드라이브로 공유할 수 있나요?",
|
||||
"network_share_answer": "아니요, 일반적으로 SQLite 데이터베이스를 네트워크 드라이브로 공유하는 것은 좋지 않습니다. 경우에 따라 작동할 수도 있지만, 네트워크를 통한 파일 잠금이 완벽하지 않아 데이터베이스가 손상될 가능성이 있습니다.",
|
||||
"security_question": "내 데이터는 어떻게 보호되나요?",
|
||||
"security_answer": "기본적으로 노트는 암호화되지 않으며 데이터베이스에서 직접 읽을 수 있습니다. 노트를 암호화 대상으로 표시하면, AES-128-CBC를 사용하여 암호화됩니다."
|
||||
},
|
||||
"final_cta": {
|
||||
"title": "Trilium Notes를 시작할 준비가 되셨나요?",
|
||||
"description": "강력한 기능과 완벽한 개인 정보 보호를 통해 나만의 지식 기반을 구축하세요.",
|
||||
"get_started": "시작하기"
|
||||
},
|
||||
"components": {
|
||||
"link_learn_more": "자세히 알아보기..."
|
||||
"server_question": "Trilium을 사용하기 위해 서버가 필요한가요?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,18 +10,13 @@
|
||||
"title": "Organiser tankene dine. Bygg din personlige kunnskapsbase.",
|
||||
"github": "GitHub",
|
||||
"get_started": "Kom i gang",
|
||||
"dockerhub": "Docker Hub",
|
||||
"screenshot_alt": "Screenshot fra Trilium Notes skrivebordsprogram",
|
||||
"subtitle": "Trilium er en open-source-løsning for å ta notater og organisere en personlig kunnskapsbase. Kan brukes lokalt på arbeidsstasjonen din eller synkroniseres med en selv-hostet løsning for å ha dine notater med deg overalt."
|
||||
"dockerhub": "Docker Hub"
|
||||
},
|
||||
"organization_benefits": {
|
||||
"title": "Organisering",
|
||||
"note_structure_title": "Notatstruktur",
|
||||
"hoisting_title": "Arbeidsflate og fokusering",
|
||||
"attributes_description": "Bruk relasjoner mellom notater eller legg til etiketter for enkel kategorisering. Bruk fremhevede attributter for å legge inn strukturert informasjon som kan brukes i tabeller og tavler.",
|
||||
"note_structure_description": "Notater kan arrangeres herarkisk. Det trengs ikke mapper, siden alle notater kan inneholde undernotater. Ett notat kan legges inn flere steder i herarkiet.",
|
||||
"attributes_title": "Notatetiketter og -relasjoner",
|
||||
"hoisting_description": "Du kan enkelt skille personlige og arbeidsnotater ved å gruppere de under arbeidsrom, som fokuserer notat-treet ditt på kun ønskede notater."
|
||||
"attributes_description": "Bruk relasjoner mellom notater eller legg til etiketter for enkel kategorisering. Bruk fremhevede attributter for å legge inn strukturert informasjon som kan brukes i tabeller og tavler."
|
||||
},
|
||||
"productivity_benefits": {
|
||||
"sync_title": "Synkronisering",
|
||||
@@ -31,12 +26,7 @@
|
||||
"protected_notes_title": "Beskyttede notater",
|
||||
"title": "Produktivitet og sikkerhet",
|
||||
"sync_content": "Bruk en selv-hostet eller cloud-instans for å enkelt synkronisere notater på tvers av enheter, og ha de tilgjengelige fra din mobiltelefon ved hjelp av progressiv web-app.",
|
||||
"jump_to_content": "Hopp raskt til notater eller grensesnittkommandoer over hele hierarkiet ved å søke etter tittel, med \"fuzzy\" matching for å ta hensyn til skrivefeil eller små differanser.",
|
||||
"revisions_content": "Notater lagres periodisk i bakgrunnen og revisjonshistorikk kan brukes for tilbakeblikk eller å omgjøre uønskede endringer. Revisjoner kan også lages manuelt.",
|
||||
"protected_notes_content": "Beskytt sensitiv personlig informasjon ved å kryptere notater og låse de med en passordkryptert sesjon.",
|
||||
"jump_to_title": "Hurtigsøk og kommandoer",
|
||||
"search_content": "Eller søk etter tekst i notatene og finjuster søket ved å filtrere på foreldrenotat eller dybde.",
|
||||
"web_clipper_content": "Hent nettsider (eller screenshots) og legg de direkte i Trilium ved hjelp av web clipper nettleserutvidelse."
|
||||
"jump_to_content": "Hopp raskt til notater eller grensesnittkommandoer over hele hierarkiet ved å søke etter tittel, med \"fuzzy\" matching for å ta hensyn til skrivefeil eller små differanser."
|
||||
},
|
||||
"note_types": {
|
||||
"canvas_title": "Kanvas",
|
||||
@@ -44,26 +34,13 @@
|
||||
"text_title": "Tekstnotat",
|
||||
"code_title": "Kodenotat",
|
||||
"file_title": "Filnotat",
|
||||
"mermaid_title": "Mermaid diagrammer",
|
||||
"title": "Flere måter å presentere informasjonen din",
|
||||
"text_description": "Notatene redigeres med en visuell editor (WYSIWYG), som støtter tabeller, bilder, matematiske uttrykk og kodeblokker med syntaksutheving. Formater tekst hurtig med Markdown-inspirert syntaks eller \"slash-kommandoer\".",
|
||||
"code_description": "Store samlinger med kildekode eller skript bruker en dedikert editor med syntaksfremheving for mange programmeringsspråk og med flere fargetema.",
|
||||
"file_description": "Integrer multimediafiler som PDFer, bilder og video med forhåndsvisning i programmet.",
|
||||
"mermaid_description": "Lag diagrammer som flytskjema, klasse- og sekvensdiagrammer, Ganttdiagrammer og mye mer ved hjelp av Mermaidsyntaks.",
|
||||
"mindmap_description": "Organiser dine tanker visuelt eller gjør en brainstorming.",
|
||||
"others_list": "og andre: <0>notatkart</0>, <1>relasjonskart</1>, <2>lagrede søk</2>, <3>rendret notat</3>, og <4>web view</4>.",
|
||||
"canvas_description": "Arranger figurer, bilder og tekst på et uendelig lerret som bruker samme teknologi som excalidraw.com. Ideelt for diagrammer, skisser og visuell planlegging."
|
||||
"mermaid_title": "Mermaid diagrammer"
|
||||
},
|
||||
"extensibility_benefits": {
|
||||
"import_export_title": "Import/eksport",
|
||||
"scripting_title": "Avansert skripting",
|
||||
"api_title": "REST API",
|
||||
"title": "Deling og utvidbarhet",
|
||||
"share_title": "Del notater på nett",
|
||||
"share_description": "Hvis du har en server, kan den brukes til å dele valgfrie notater med andre.",
|
||||
"scripting_description": "Lag dine egne integrasjoner i Trilium med egendefinerte widgets, eller serversidelogikk.",
|
||||
"import_export_description": "Samhandle med andre programmer ved hjelp av Markdown, ENEX og OML.",
|
||||
"api_description": "Ved hjelp av den innebygde REST-APIen kan du programmatisk samhandle med Trilium."
|
||||
"title": "Deling og utvidbarhet"
|
||||
},
|
||||
"collections": {
|
||||
"title": "Samlinger",
|
||||
@@ -72,11 +49,7 @@
|
||||
"geomap_title": "Geokart",
|
||||
"presentation_title": "Presentasjon",
|
||||
"board_title": "Kanbantavle",
|
||||
"geomap_description": "Planlegg ferien din eller merk deg dine interessepunkter på et geografisk kart ved hjelp av definerbare markører. Vis lagrede GPX-spor for å se reisen din.",
|
||||
"calendar_description": "Organiser dine personlige eller jobb-arrangement ved hjelp av kalender, med støtte for heldags- og flerdagsarrangement. Få rask oversikt over dine arrangementer med ukes- måneds- og årsvisning. Dra og slipp hendelser for enkelt å gjøre endringer.",
|
||||
"table_description": "Vis og rediger informasjon om notater i tabellform, med ulike kolonnetyper som tekst, nummer, avkrysningsbokser, dato og tid, lenker, farger og støtte for relasjoner. Du kan også vise notater i et hierarkisk tre i tabellen.",
|
||||
"board_description": "Organiser oppgaver eller prosjekter i en Kanbantavle hvor du enkelt kan lage nye elementer og kolonner, og endre status på elementer ved å dra de rundt på tavlen.",
|
||||
"presentation_description": "Organiser informasjon i lysbilder og presenter dem i fullskjermmodus med myke overganger. Lysbildene kan også eksporteres til PDF for enkel deling."
|
||||
"geomap_description": "Planlegg ferien din eller merk deg dine interessepunkter på et geografisk kart ved hjelp av definerbare markører. Vis lagrede GPX-spor for å se reisen din."
|
||||
},
|
||||
"header": {
|
||||
"documentation": "Dokumentasjon",
|
||||
@@ -94,19 +67,14 @@
|
||||
"title": "Støtt oss",
|
||||
"financial_donations_title": "Finansiell donasjon",
|
||||
"github_sponsors": "GitHub Sponsors",
|
||||
"financial_donations_description": "Trilium er bygget og vedlikeholdt med <Link>flere hundre timers arbeid</Link>. Ditt bidrag hjelper å holde det åpen kildekode, forbedre funksjonalitet og dekker driftskostnader.",
|
||||
"financial_donations_cta": "Vurder gjerne å støtte hovedutvikleren (<Link>eliandoran</Link>) av programmet via:",
|
||||
"buy_me_a_coffee": "Buy Me A Coffee"
|
||||
"financial_donations_description": "Trilium er bygget og vedlikeholdt med <Link>flere hundre timers arbeid</Link>. Ditt bidrag hjelper å holde det åpen kildekode, forbedre funksjonalitet og dekker driftskostnader."
|
||||
},
|
||||
"download_helper_desktop_windows": {
|
||||
"download_scoop": "Scoop",
|
||||
"title_x64": "Windows 64-bit",
|
||||
"download_zip": "Portable (.zip)",
|
||||
"title_arm64": "Windows på ARM",
|
||||
"download_exe": "Last ned installasjonsprogram (.exe)",
|
||||
"description_x64": "Kompatibel med Intel- eller AMD-enheter som kjører Windows 10 og 11.",
|
||||
"description_arm64": "Kompatibel med ARM-enheter (for eksempel Qualcomm Snapdragon).",
|
||||
"quick_start": "For å installere via Winget:"
|
||||
"download_exe": "Last ned installasjonsprogram (.exe)"
|
||||
},
|
||||
"download_helper_desktop_linux": {
|
||||
"download_deb": ".deb",
|
||||
@@ -116,31 +84,21 @@
|
||||
"download_aur": "AUR",
|
||||
"title_x64": "Linux 64-bit",
|
||||
"download_zip": "Portable (.zip)",
|
||||
"title_arm64": "Linux på ARM",
|
||||
"description_x64": "For de fleste Linux-distribusjoner, kompatibelt med x86_64-arkitektur.",
|
||||
"description_arm64": "For ARM-baserte Linux-distribusjoner, kompatibelt med aarch64-arkitektur.",
|
||||
"quick_start": "Velg egnet pakkeformat avhengig av din distribusjon:"
|
||||
"title_arm64": "Linux på ARM"
|
||||
},
|
||||
"download_helper_server_docker": {
|
||||
"download_ghcr": "ghcr.io",
|
||||
"download_dockerhub": "Docker Hub",
|
||||
"title": "Selv-hostet med Docker",
|
||||
"description": "Installer enkelt på Windows, Linux eller macOS ved bruk av en Docker-container."
|
||||
"title": "Selv-hostet med Docker"
|
||||
},
|
||||
"download_helper_desktop_macos": {
|
||||
"download_homebrew_cask": "Homebrew Cask",
|
||||
"download_zip": "Portable (.zip)",
|
||||
"title_x64": "macOS for Intel",
|
||||
"download_dmg": "Last ned installasjonsprogram (.dmg)",
|
||||
"title_arm64": "macOS for Apple Silicon",
|
||||
"description_x64": "For Intel-baserte Mac-er med macOS Monterey eller nyere.",
|
||||
"description_arm64": "For Apple Silicon Mac-er som de med M1- og M2-chiper.",
|
||||
"quick_start": "For å installere via Homebrew:"
|
||||
"download_dmg": "Last ned installasjonsprogram (.dmg)"
|
||||
},
|
||||
"final_cta": {
|
||||
"get_started": "Kom i gang",
|
||||
"title": "Klar for å begynne med Trilium Notes?",
|
||||
"description": "Skap din personlige kunnskapsbase med kraftig funksjonalitet og fullt personvern."
|
||||
"get_started": "Kom i gang"
|
||||
},
|
||||
"components": {
|
||||
"link_learn_more": "Lær mer..."
|
||||
@@ -150,8 +108,7 @@
|
||||
"platform_small": "for {{platform}}",
|
||||
"linux_small": "for Linux",
|
||||
"platform_big": "v{{version}} for {{platform}}",
|
||||
"linux_big": "v{{version}} for Linux",
|
||||
"more_platforms": "Flere plattformer og serveroppsett"
|
||||
"linux_big": "v{{version}} for Linux"
|
||||
},
|
||||
"footer": {
|
||||
"copyright_and_the": " og ",
|
||||
@@ -161,40 +118,16 @@
|
||||
"download_tar_x64": "x64 (.tar.xz)",
|
||||
"download_tar_arm64": "ARM (.tar.xz)",
|
||||
"download_nixos": "NixOS modul",
|
||||
"title": "Selv-hostet på Linux",
|
||||
"description": "Installer Trilium Notes på din egen server eller VPS, kompatibel med de fleste distribusjoner."
|
||||
"title": "Selv-hostet på Linux"
|
||||
},
|
||||
"download_helper_server_hosted": {
|
||||
"title": "Betalt hosting",
|
||||
"download_triliumcc": "Alternativt sjekk trilium.cc",
|
||||
"description": "Trilium Notes driftet på PikaPods, en betalt tjeneste for enkel tilgang og administrasjon. Ikke direkte tilknyttet Trilium-teamet.",
|
||||
"download_pikapod": "Installer på PikaPods"
|
||||
"download_triliumcc": "Alternativt sjekk trilium.cc"
|
||||
},
|
||||
"faq": {
|
||||
"title": "Ofte stilte spørsmål",
|
||||
"mobile_question": "Finnes det en mobil applikasjon?",
|
||||
"mobile_answer": "Foreløpig er det ikke noe offisiell mobil applikasjon. Men hvis du har en serverinstans kan du koble til denne med en nettleser, og også installere den som en progressiv web-app. For Android finnes det en uoffisiell applikasjon med navn TriliumDroid som også fungerer offline (samme som en skrivebordsklient).",
|
||||
"database_question": "Hvor lagres dataene?",
|
||||
"database_answer": "Alle notater lagres i en SQLite-database i en programmappe. Årsaken til at Trilium bruker database i stedet for rene tekstfiler er både ytelse og at visse funksjoner ellers ville vært vanskelig å implementere, slik som klonede notater (samme notat flere steder). For å finne programmappen, åpne \"om\"-vinduet i programmet.",
|
||||
"server_question": "Trenger jeg en server for å bruke Trilium?",
|
||||
"server_answer": "Nei, serveren tillater tilgang via nettleser og håndterer synkronisering hvis du har flere enheter. For å komme i gang er det nok å laste ned skrivebordsprogrammet og begynne med det.",
|
||||
"scaling_question": "Hvor godt skalerer programmet med store mengder notater?",
|
||||
"scaling_answer": "Avhengig av bruk burde programmet kunne håndtere minst 100.000 notater uten problemer. Merk at synkroniseringen noen ganger kan feile ved opplasting av mange store filer (1GB per fil) siden Trilium er ment for å være en kunnskapsbase mer enn et fillager (som for eksempel NextCloud).",
|
||||
"network_share_question": "Kan jeg dele databasen min over nettverksdeling?",
|
||||
"network_share_answer": "Nei, det er stort sett ikke en god ide å dele en SQLite-database over nettverksdeling. Selv om det kan fungere, er det sjanser for at databasen kan bli ødelagt grunnet problemer med fillåsing over nettverk.",
|
||||
"security_question": "Hvordan er mine data beskyttet?",
|
||||
"security_answer": "Som standard blir ikke notater kryptert og kan leses direkte fra databasen. Når et notat er markert kryptert, blir det kryptert med AES-128-CBC."
|
||||
"title": "Ofte stilte spørsmål"
|
||||
},
|
||||
"404": {
|
||||
"title": "404: Siden ble ikke funnet",
|
||||
"description": "Siden ble ikke funnet. Den kan ha blitt slettet eller adressen er feil."
|
||||
},
|
||||
"contribute": {
|
||||
"title": "Andre måter å bidra",
|
||||
"way_translate": "Oversett programmet til ditt språk via <Link>Weblate</Link>.",
|
||||
"way_community": "Ta del i felleskapet på <Discussions>GitHub Discussions</Discussions> eller på <Matrix>Matrix</Matrix>.",
|
||||
"way_reports": "Meld feil via <Link>GitHub issues</Link>.",
|
||||
"way_document": "Hjelp oss å forbedre dokumentasjonen ved å fortelle om mangler, eller bidra med veiledninger, Ofte Stilte Spørsmål eller tutorials.",
|
||||
"way_market": "Spre ordet: Del Trilium Notes med venner, på blogger eller i sosiale media."
|
||||
"title": "404: Siden ble ikke funnet"
|
||||
}
|
||||
}
|
||||
|
||||
4
docs/README-hi.md
vendored
@@ -107,7 +107,7 @@ Our documentation is available in multiple formats:
|
||||
maps](https://docs.triliumnotes.org/user-guide/note-types/relation-map) and
|
||||
[note/link maps](https://docs.triliumnotes.org/user-guide/note-types/note-map)
|
||||
for visualizing notes and their relations
|
||||
* [Mind Elixir](https://docs.mind-elixir.com/) पर आधारित माइंड मैप्स
|
||||
* Mind maps, based on [Mind Elixir](https://docs.mind-elixir.com/)
|
||||
* [Geo maps](https://docs.triliumnotes.org/user-guide/collections/geomap) with
|
||||
location pins and GPX tracks
|
||||
* [Scripting](https://docs.triliumnotes.org/user-guide/scripts) - see [Advanced
|
||||
@@ -157,7 +157,7 @@ compatible with the latest zadam/trilium version of
|
||||
versions of TriliumNext/Trilium have their sync versions incremented which
|
||||
prevents direct migration.
|
||||
|
||||
## 💬 हमारे साथ चर्चा करें
|
||||
## 💬 Discuss with us
|
||||
|
||||
Feel free to join our official conversations. We would love to hear what
|
||||
features, suggestions, or issues you may have!
|
||||
|
||||
@@ -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.9",
|
||||
"rollup-plugin-webpack-stats": "2.1.8",
|
||||
"tslib": "2.8.1",
|
||||
"tsx": "4.21.0",
|
||||
"typescript": "~5.9.0",
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@ckeditor/ckeditor5-icons": "47.3.0",
|
||||
"mathlive": "0.108.2"
|
||||
"@ckeditor/ckeditor5-icons": "47.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
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.focus();
|
||||
} );
|
||||
requestAnimationFrame(() => {
|
||||
this.formView?.mathInputView.fieldView.element?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
private _createFormView() {
|
||||
@@ -71,37 +71,31 @@ 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,
|
||||
{
|
||||
engine: mathConfig.engine!,
|
||||
lazyLoad: mathConfig.lazyLoad,
|
||||
previewUid: this._previewUid,
|
||||
previewClassName: mathConfig.previewClassName!,
|
||||
katexRenderOptions: mathConfig.katexRenderOptions!
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
mathConfig.engine!,
|
||||
mathConfig.lazyLoad,
|
||||
mathConfig.enablePreview,
|
||||
mathConfig.popupClassName!
|
||||
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!
|
||||
);
|
||||
|
||||
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: 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' );
|
||||
formView.mathInputView.bind( 'isReadOnly' ).to( mathCommand, 'isEnabled', value => !value );
|
||||
formView.saveButtonView.bind( 'isEnabled' ).to( mathCommand );
|
||||
formView.displayButtonView.bind( 'isEnabled' ).to( mathCommand );
|
||||
|
||||
// Listen to submit button click
|
||||
this.listenTo( formView, 'submit', () => {
|
||||
@@ -121,12 +115,24 @@ 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;
|
||||
}
|
||||
@@ -156,12 +162,14 @@ export default class MathUI extends Plugin {
|
||||
} );
|
||||
|
||||
if ( this._balloon.visibleView === this.formView ) {
|
||||
this.formView.mathInputView.focus();
|
||||
this.formView.mathInputView.fieldView.element?.select();
|
||||
}
|
||||
|
||||
// Show preview element
|
||||
const previewEl = document.getElementById( this._previewUid );
|
||||
if ( previewEl && this.formView.mathView ) {
|
||||
this.formView.mathView.updateMath();
|
||||
if ( previewEl && this.formView.previewEnabled ) {
|
||||
// Force refresh preview
|
||||
this.formView.mathView?.updateMath();
|
||||
}
|
||||
|
||||
this.formView.equation = mathCommand.value ?? '';
|
||||
@@ -198,10 +206,8 @@ export default class MathUI extends Plugin {
|
||||
|
||||
private _removeFormView() {
|
||||
if ( this._isFormInPanel && this.formView ) {
|
||||
// Hide virtual keyboard before removing the form
|
||||
this.formView.hideKeyboard();
|
||||
|
||||
this.formView.saveButtonView.focus();
|
||||
|
||||
this._balloon.remove( this.formView );
|
||||
|
||||
// Hide preview element
|
||||
|
||||
@@ -1,59 +1,91 @@
|
||||
import { ButtonView, FocusCycler, FocusTracker, KeystrokeHandler, LabelView, submitHandler, SwitchButtonView, View, ViewCollection, type FocusableView, type Locale } from 'ckeditor5';
|
||||
import { ButtonView, createLabeledTextarea, FocusCycler, LabelView, LabeledFieldView, submitHandler, SwitchButtonView, View, ViewCollection, type TextareaView, type FocusableView, Locale, FocusTracker, KeystrokeHandler } 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, { type MathViewOptions } from './mathview.js';
|
||||
import MathInputView from './mathinputview.js';
|
||||
import MathView from './mathview.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 cancelButtonView: ButtonView;
|
||||
public displayButtonView: SwitchButtonView;
|
||||
|
||||
public mathInputView: MathInputView;
|
||||
public displayButtonView: SwitchButtonView;
|
||||
public cancelButtonView: ButtonView;
|
||||
public previewEnabled: boolean;
|
||||
public previewLabel?: LabelView;
|
||||
public mathView?: MathView;
|
||||
|
||||
public focusTracker = new FocusTracker();
|
||||
public keystrokes = new KeystrokeHandler();
|
||||
private _focusables = new ViewCollection<FocusableView>();
|
||||
private _focusCycler: FocusCycler;
|
||||
public override locale: Locale = new Locale();
|
||||
public lazyLoad: undefined | ( () => Promise<void> );
|
||||
|
||||
constructor(
|
||||
locale: Locale,
|
||||
mathViewOptions: MathViewOptions,
|
||||
engine:
|
||||
| 'mathjax'
|
||||
| 'katex'
|
||||
| ( (
|
||||
equation: string,
|
||||
element: HTMLElement,
|
||||
display: boolean,
|
||||
) => void ),
|
||||
lazyLoad: undefined | ( () => Promise<void> ),
|
||||
previewEnabled = false,
|
||||
popupClassName: Array<string> = []
|
||||
previewUid: string,
|
||||
previewClassName: Array<string>,
|
||||
popupClassName: Array<string>,
|
||||
katexRenderOptions: KatexOptions
|
||||
) {
|
||||
super( locale );
|
||||
|
||||
const t = locale.t;
|
||||
|
||||
// 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 );
|
||||
// Submit button
|
||||
this.saveButtonView = this._createButton( t( 'Save' ), IconCheck, 'ck-button-save', null );
|
||||
this.saveButtonView.type = 'submit';
|
||||
|
||||
// Build children
|
||||
// Equation input
|
||||
this.mathInputView = this._createMathInput();
|
||||
|
||||
const children: Array<View> = [
|
||||
this.mathInputView,
|
||||
this.displayButtonView
|
||||
];
|
||||
// Display button
|
||||
this.displayButtonView = this._createDisplayButton();
|
||||
|
||||
if ( previewEnabled ) {
|
||||
const previewLabel = new LabelView( locale );
|
||||
previewLabel.text = t( 'Equation preview' );
|
||||
// Cancel button
|
||||
this.cancelButtonView = this._createButton( t( 'Cancel' ), IconCancel, 'ck-button-cancel', 'cancel' );
|
||||
|
||||
this.mathView = new MathView( locale, mathViewOptions );
|
||||
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.bind( 'display' ).to( this.displayButtonView, 'isOn' );
|
||||
|
||||
children.push( previewLabel, this.mathView );
|
||||
children = [
|
||||
this.mathInputView,
|
||||
this.displayButtonView,
|
||||
this.previewLabel,
|
||||
this.mathView
|
||||
];
|
||||
} else {
|
||||
children = [
|
||||
this.mathInputView,
|
||||
this.displayButtonView
|
||||
];
|
||||
}
|
||||
|
||||
this._setupSync( previewEnabled );
|
||||
|
||||
// Add UI elements to template
|
||||
this.setTemplate( {
|
||||
tag: 'form',
|
||||
attributes: {
|
||||
@@ -75,30 +107,10 @@ export default class MainFormView extends View {
|
||||
},
|
||||
children
|
||||
},
|
||||
{
|
||||
tag: 'div',
|
||||
attributes: {
|
||||
class: [
|
||||
'ck-math-button-row'
|
||||
]
|
||||
},
|
||||
children: [
|
||||
this.saveButtonView,
|
||||
this.cancelButtonView
|
||||
]
|
||||
}
|
||||
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 {
|
||||
@@ -109,73 +121,103 @@ export default class MainFormView extends View {
|
||||
view: this
|
||||
} );
|
||||
|
||||
const focusableViews = [
|
||||
this.mathInputView.latexTextAreaView,
|
||||
// Register form elements to focusable elements
|
||||
const childViews = [
|
||||
this.mathInputView,
|
||||
this.displayButtonView,
|
||||
this.saveButtonView,
|
||||
this.cancelButtonView
|
||||
];
|
||||
|
||||
focusableViews.forEach( v => {
|
||||
this._focusables.add( v );
|
||||
childViews.forEach( v => {
|
||||
if ( v.element ) {
|
||||
this._focusables.add( v );
|
||||
this.focusTracker.add( v.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 );
|
||||
}
|
||||
} );
|
||||
|
||||
// Listen to keypresses inside form 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();
|
||||
}
|
||||
|
||||
private _setupSync( previewEnabled: boolean ): void {
|
||||
this.mathInputView.on( 'change:value', () => {
|
||||
let eq = ( this.mathInputView.value ?? '' ).trim();
|
||||
|
||||
if ( hasDelimiters( eq ) ) {
|
||||
const params = extractDelimiters( eq );
|
||||
eq = params.equation;
|
||||
this.displayButtonView.isOn = params.display;
|
||||
|
||||
if ( this.mathInputView.value !== eq ) {
|
||||
this.mathInputView.value = eq.length ? eq : null;
|
||||
}
|
||||
}
|
||||
|
||||
if ( previewEnabled && this.mathView && this.mathView.value !== eq ) {
|
||||
this.mathView.value = eq;
|
||||
}
|
||||
} );
|
||||
public get equation(): string {
|
||||
return this.mathInputView.fieldView.element?.value ?? '';
|
||||
}
|
||||
|
||||
private _createButton( label: string, icon: string, className: string, type?: 'submit' | 'button' ): ButtonView {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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.previewEnabled && this.mathView ) {
|
||||
// Update preview view
|
||||
this.mathView.value = equationInput;
|
||||
}
|
||||
|
||||
this.saveButtonView.isEnabled = !!equationInput;
|
||||
}
|
||||
};
|
||||
|
||||
fieldView.on( 'render', onInput );
|
||||
fieldView.on( 'input', onInput );
|
||||
|
||||
return mathInput;
|
||||
}
|
||||
|
||||
private _createButton(
|
||||
label: string,
|
||||
icon: string,
|
||||
className: string,
|
||||
eventName: string | null
|
||||
) {
|
||||
const button = new ButtonView( this.locale );
|
||||
|
||||
button.set( {
|
||||
@@ -190,14 +232,16 @@ export default class MainFormView extends View {
|
||||
}
|
||||
} );
|
||||
|
||||
if ( type ) {
|
||||
button.type = type;
|
||||
if ( eventName ) {
|
||||
button.delegate( 'execute' ).to( this, eventName );
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private _createDisplayButton( t: ( str: string ) => string ): SwitchButtonView {
|
||||
private _createDisplayButton() {
|
||||
const t = this.locale.t;
|
||||
|
||||
const switchButton = new SwitchButtonView( this.locale );
|
||||
|
||||
switchButton.set( {
|
||||
@@ -212,13 +256,15 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
// 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> );
|
||||
|
||||
/**
|
||||
* Configuration options passed during initialization.
|
||||
*/
|
||||
private options: MathViewOptions;
|
||||
|
||||
constructor( locale: Locale, options: MathViewOptions ) {
|
||||
constructor(
|
||||
engine:
|
||||
| 'mathjax'
|
||||
| 'katex'
|
||||
| ( (
|
||||
equation: string,
|
||||
element: HTMLElement,
|
||||
display: boolean,
|
||||
) => void ),
|
||||
lazyLoad: undefined | ( () => Promise<void> ),
|
||||
locale: Locale,
|
||||
previewUid: string,
|
||||
previewClassName: Array<string>,
|
||||
katexRenderOptions: KatexOptions
|
||||
) {
|
||||
super( locale );
|
||||
this.options = options;
|
||||
|
||||
this.engine = engine;
|
||||
this.lazyLoad = lazyLoad;
|
||||
this.previewUid = previewUid;
|
||||
this.katexRenderOptions = katexRenderOptions;
|
||||
this.previewClassName = previewClassName;
|
||||
|
||||
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,39 +55,19 @@ export default class MathView extends View {
|
||||
}
|
||||
|
||||
public updateMath(): void {
|
||||
if ( !this.element ) {
|
||||
return;
|
||||
if ( this.element ) {
|
||||
void renderEquation(
|
||||
this.value,
|
||||
this.element,
|
||||
this.engine,
|
||||
this.lazyLoad,
|
||||
this.display,
|
||||
true,
|
||||
this.previewUid,
|
||||
this.previewClassName,
|
||||
this.katexRenderOptions
|
||||
);
|
||||
}
|
||||
|
||||
// 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,20 +3,6 @@ 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,20 +2,6 @@ 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;
|
||||
@@ -38,14 +24,11 @@ describe( 'Lazy load', () => {
|
||||
beforeEach( () => {
|
||||
editorElement = document.createElement( 'div' );
|
||||
document.body.appendChild( editorElement );
|
||||
|
||||
lazyLoadInvoked = false;
|
||||
} );
|
||||
|
||||
afterEach( async () => {
|
||||
if ( mathUIFeature?.formView ) {
|
||||
mathUIFeature._hideUI();
|
||||
}
|
||||
await new Promise( resolve => setTimeout( resolve, 50 ) );
|
||||
afterEach( () => {
|
||||
editorElement.remove();
|
||||
return editor.destroy();
|
||||
} );
|
||||
@@ -54,7 +37,6 @@ describe( 'Lazy load', () => {
|
||||
await buildEditor( {
|
||||
math: {
|
||||
engine: 'katex',
|
||||
enablePreview: true,
|
||||
lazyLoad: async () => {
|
||||
lazyLoadInvoked = true;
|
||||
}
|
||||
@@ -62,15 +44,6 @@ 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.be.null;
|
||||
expect( formView!.mathInputView.value ).to.null;
|
||||
|
||||
command!.value = 'x^2';
|
||||
expect( formView!.mathInputView.value ).to.equal( 'x^2' );
|
||||
@@ -419,18 +419,10 @@ describe( 'MathUI', () => {
|
||||
it( 'should execute math command on mainFormView#submit event', () => {
|
||||
const executeSpy = vi.spyOn( editor, 'execute' );
|
||||
|
||||
formView!.mathInputView.value = 'x^2';
|
||||
formView!.mathInputView.fieldView.element!.value = 'x^2';
|
||||
formView!.fire( 'submit' );
|
||||
|
||||
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}' );
|
||||
expect(executeSpy.mock.lastCall?.slice(0, 2)).toMatchObject(['math', 'x^2']);
|
||||
} );
|
||||
|
||||
it( 'should hide the balloon on mainFormView#cancel if math command does not have a value', () => {
|
||||
|
||||
@@ -1,220 +1,35 @@
|
||||
/**
|
||||
* 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;
|
||||
flex-direction: column;
|
||||
padding: var(--ck-spacing-standard);
|
||||
box-sizing: border-box;
|
||||
max-width: 80vw;
|
||||
max-height: 80vh;
|
||||
overflow: visible;
|
||||
user-select: text;
|
||||
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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 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-math-tex.ck-placeholder::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* === 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;
|
||||
.ck.ck-toolbar-container {
|
||||
z-index: calc(var(--ck-z-panel) + 2);
|
||||
}
|
||||
|
||||
@@ -22,9 +22,6 @@ export default defineConfig( {
|
||||
include: [
|
||||
'tests/**/*.[jt]s'
|
||||
],
|
||||
exclude: [
|
||||
'tests/setup.ts'
|
||||
],
|
||||
globals: true,
|
||||
watch: false,
|
||||
coverage: {
|
||||
|
||||
@@ -50,11 +50,6 @@ 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"),
|
||||
|
||||
90
pnpm-lock.yaml
generated
@@ -104,8 +104,8 @@ importers:
|
||||
specifier: 0.18.0
|
||||
version: 0.18.0
|
||||
rollup-plugin-webpack-stats:
|
||||
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))
|
||||
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))
|
||||
tslib:
|
||||
specifier: 2.8.1
|
||||
version: 2.8.1
|
||||
@@ -1073,9 +1073,6 @@ 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
|
||||
@@ -2132,10 +2129,6 @@ 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'}
|
||||
@@ -7083,10 +7076,6 @@ 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==}
|
||||
|
||||
@@ -10280,9 +10269,6 @@ 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==}
|
||||
|
||||
@@ -12403,8 +12389,8 @@ packages:
|
||||
resolution: {integrity: sha512-EsoOi8moHN6CAYyTZipxDDVTJn0j2nBCWor4wRU45RQ8ER2qREDykXLr3Ulz6hBh6oBKCFTQIjo21i0FXNo/IA==}
|
||||
hasBin: true
|
||||
|
||||
rollup-plugin-stats@1.5.4:
|
||||
resolution: {integrity: sha512-b1hYagYLTyr8mCVUb7e1x9fjxOXFyeWmV9hIr7vYqq/agN+WDaGNzz+KmM3GAx0KGGI2qllOL+zAUi/l39s/Sg==}
|
||||
rollup-plugin-stats@1.5.3:
|
||||
resolution: {integrity: sha512-0IYVGhsFTjcddpqcElzU7Mi4vmDLihCCTH5QgCCgWpNY1VKMXVoEpxmCmGjivtJKLzI6t5QIicsPBC93UWWN2g==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
rolldown: ^1.0.0-beta.0
|
||||
@@ -12430,8 +12416,8 @@ packages:
|
||||
peerDependencies:
|
||||
rollup: ^3.0.0||^4.0.0
|
||||
|
||||
rollup-plugin-webpack-stats@2.1.9:
|
||||
resolution: {integrity: sha512-ft1vdp3xPjE+zw8A22yCToo5cpymoWCjNDefWNO1awywsDrSDoRJhkoZTENkhJwmfh6oe5ztpGu7PfnJOMXc2g==}
|
||||
rollup-plugin-webpack-stats@2.1.8:
|
||||
resolution: {integrity: sha512-agc1OE+QwG3sGeTSdruh16DkxPb6QkgR7I3gntPDFHMXsK1bR2ADHUVod1eoE+epAOqiv3idx/hcSqZAI3a1yg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
rolldown: ^1.0.0-beta.0
|
||||
@@ -15299,6 +15285,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-ui': 47.3.0
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-block-quote@47.3.0':
|
||||
dependencies:
|
||||
@@ -15309,6 +15297,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-ui': 47.3.0
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-bookmark@47.3.0':
|
||||
dependencies:
|
||||
@@ -15371,6 +15361,8 @@ 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:
|
||||
@@ -15544,8 +15536,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-widget': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
es-toolkit: 1.39.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-easy-image@47.3.0':
|
||||
dependencies:
|
||||
@@ -15583,8 +15573,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
es-toolkit: 1.39.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-editor-inline@47.3.0':
|
||||
dependencies:
|
||||
@@ -15594,6 +15582,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
es-toolkit: 1.39.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-editor-multi-root@47.3.0':
|
||||
dependencies:
|
||||
@@ -15616,6 +15606,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-table': 47.3.0
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-emoji@47.3.0':
|
||||
dependencies:
|
||||
@@ -15641,8 +15633,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-core': 47.3.0
|
||||
'@ckeditor/ckeditor5-engine': 47.3.0
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-essentials@47.3.0':
|
||||
dependencies:
|
||||
@@ -15674,6 +15664,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-ui': 47.3.0
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-export-word@47.3.0':
|
||||
dependencies:
|
||||
@@ -15698,8 +15690,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
es-toolkit: 1.39.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-font@47.3.0':
|
||||
dependencies:
|
||||
@@ -15709,6 +15699,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-ui': 47.3.0
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-footnotes@47.3.0':
|
||||
dependencies:
|
||||
@@ -15739,6 +15731,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-ui': 47.3.0
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-heading@47.3.0':
|
||||
dependencies:
|
||||
@@ -15770,8 +15764,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
'@ckeditor/ckeditor5-widget': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-html-embed@47.3.0':
|
||||
dependencies:
|
||||
@@ -15831,6 +15823,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-ui': 47.3.0
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-indent@47.3.0':
|
||||
dependencies:
|
||||
@@ -15865,8 +15859,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-ui': 47.3.0
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-link@47.3.0':
|
||||
dependencies:
|
||||
@@ -15893,8 +15885,6 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-ui': 47.3.0
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-list@47.3.0':
|
||||
dependencies:
|
||||
@@ -15947,6 +15937,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
'@ckeditor/ckeditor5-widget': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-mention@47.3.0(patch_hash=5981fb59ba35829e4dff1d39cf771000f8a8fdfa7a34b51d8af9549541f2d62d)':
|
||||
dependencies:
|
||||
@@ -15956,6 +15948,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
es-toolkit: 1.39.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-merge-fields@47.3.0':
|
||||
dependencies:
|
||||
@@ -15968,6 +15962,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-widget': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
es-toolkit: 1.39.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-minimap@47.3.0':
|
||||
dependencies:
|
||||
@@ -15976,6 +15972,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-ui': 47.3.0
|
||||
'@ckeditor/ckeditor5-utils': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-operations-compressor@47.3.0':
|
||||
dependencies:
|
||||
@@ -16095,6 +16093,8 @@ 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:
|
||||
@@ -16216,6 +16216,8 @@ snapshots:
|
||||
'@ckeditor/ckeditor5-widget': 47.3.0
|
||||
ckeditor5: 47.3.0
|
||||
es-toolkit: 1.39.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@ckeditor/ckeditor5-template@47.3.0':
|
||||
dependencies:
|
||||
@@ -16497,11 +16499,6 @@ 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
|
||||
@@ -22308,8 +22305,6 @@ snapshots:
|
||||
|
||||
compare-versions@6.1.1: {}
|
||||
|
||||
complex-esm@2.1.1-esm1: {}
|
||||
|
||||
component-emitter@1.3.1: {}
|
||||
|
||||
compress-commons@6.0.2:
|
||||
@@ -22996,7 +22991,8 @@ snapshots:
|
||||
|
||||
decimal.js@10.5.0: {}
|
||||
|
||||
decimal.js@10.6.0: {}
|
||||
decimal.js@10.6.0:
|
||||
optional: true
|
||||
|
||||
decko@1.2.0: {}
|
||||
|
||||
@@ -26342,10 +26338,6 @@ 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:
|
||||
@@ -28816,7 +28808,7 @@ snapshots:
|
||||
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.29
|
||||
optional: true
|
||||
|
||||
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)):
|
||||
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)):
|
||||
optionalDependencies:
|
||||
rolldown: 1.0.0-beta.29
|
||||
rollup: 4.52.0
|
||||
@@ -28849,9 +28841,9 @@ snapshots:
|
||||
'@rollup/pluginutils': 5.1.4(rollup@4.52.0)
|
||||
rollup: 4.52.0
|
||||
|
||||
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)):
|
||||
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)):
|
||||
dependencies:
|
||||
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))
|
||||
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))
|
||||
optionalDependencies:
|
||||
rolldown: 1.0.0-beta.29
|
||||
rollup: 4.52.0
|
||||
|
||||