Merge remote-tracking branch 'origin/develop' into feature/rtl

; Conflicts:
;	src/public/app/widgets/floating_buttons/help_button.ts
This commit is contained in:
Elian Doran
2025-03-05 21:57:53 +02:00
141 changed files with 2212 additions and 966 deletions

View File

@@ -3,3 +3,4 @@
*.yml *.yml
libraries/* libraries/*
docs/* docs/*
src/public/app/doc_notes/**/*

View File

@@ -66,8 +66,6 @@ chmod 755 $PKG_DIR/trilium.sh
cp bin/tpl/anonymize-database.sql $PKG_DIR/ cp bin/tpl/anonymize-database.sql $PKG_DIR/
cp -r translations $PKG_DIR/ cp -r translations $PKG_DIR/
cp -r dump-db $PKG_DIR/
rm -rf $PKG_DIR/dump-db/node_modules
VERSION=`jq -r ".version" package.json` VERSION=`jq -r ".version" package.json`

View File

@@ -1,6 +1,6 @@
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { dirname, join } from "path"; import { dirname, join } from "path";
import swaggerJsdoc from 'swagger-jsdoc'; import swaggerJsdoc from "swagger-jsdoc";
import fs from "fs"; import fs from "fs";
/* /*
@@ -11,28 +11,30 @@ import fs from "fs";
*/ */
const options = { const options = {
definition: { definition: {
openapi: '3.1.1', openapi: "3.1.1",
info: { info: {
title: 'Trilium Notes - Sync server API', title: "Trilium Notes - Sync server API",
version: '0.96.6', version: "0.96.6",
description: "This is the internal sync server API used by Trilium Notes / TriliumNext Notes.\n\n_If you're looking for the officially supported External Trilium API, see [here](https://triliumnext.github.io/Docs/Wiki/etapi.html)._\n\nThis page does not yet list all routes. For a full list, see the [route controller](https://github.com/TriliumNext/Notes/blob/v0.91.6/src/routes/routes.ts).", description:
contact: { "This is the internal sync server API used by Trilium Notes / TriliumNext Notes.\n\n_If you're looking for the officially supported External Trilium API, see [here](https://triliumnext.github.io/Docs/Wiki/etapi.html)._\n\nThis page does not yet list all routes. For a full list, see the [route controller](https://github.com/TriliumNext/Notes/blob/v0.91.6/src/routes/routes.ts).",
name: "TriliumNext issue tracker", contact: {
url: "https://github.com/TriliumNext/Notes/issues", name: "TriliumNext issue tracker",
}, url: "https://github.com/TriliumNext/Notes/issues"
license: { },
name: "GNU Free Documentation License 1.3 (or later)", license: {
url: "https://www.gnu.org/licenses/fdl-1.3", name: "GNU Free Documentation License 1.3 (or later)",
}, url: "https://www.gnu.org/licenses/fdl-1.3"
}
}
}, },
}, apis: [
apis: [ // Put individual files here to have them ordered first.
// Put individual files here to have them ordered first. "./src/routes/api/setup.ts",
'./src/routes/api/setup.ts', // all other files
// all other files "./src/routes/api/*.ts",
'./src/routes/api/*.ts', './bin/generate-openapi.js' "./bin/generate-openapi.js"
], ]
}; };
const openapiSpecification = swaggerJsdoc(options); const openapiSpecification = swaggerJsdoc(options);

View File

@@ -11,16 +11,14 @@ test("Displays translation on desktop", async ({ page, context }) => {
const app = new App(page, context); const app = new App(page, context);
await app.goto(); await app.goto();
await expect(page.locator("#left-pane .quick-search input")) await expect(page.locator("#left-pane .quick-search input")).toHaveAttribute("placeholder", "Quick search");
.toHaveAttribute("placeholder", "Quick search");
}); });
test("Displays translation on mobile", async ({ page, context }) => { test("Displays translation on mobile", async ({ page, context }) => {
const app = new App(page, context); const app = new App(page, context);
await app.goto({ isMobile: true }); await app.goto({ isMobile: true });
await expect(page.locator("#mobile-sidebar-wrapper .quick-search input")) await expect(page.locator("#mobile-sidebar-wrapper .quick-search input")).toHaveAttribute("placeholder", "Quick search");
.toHaveAttribute("placeholder", "Quick search");
}); });
test("Displays translations in Settings", async ({ page, context }) => { test("Displays translations in Settings", async ({ page, context }) => {

View File

@@ -19,13 +19,13 @@ test("Can drag tabs around", async ({ page, context }) => {
let tab = app.getTab(0); let tab = app.getTab(0);
// Drag the first tab at the end // Drag the first tab at the end
await tab.dragTo(app.getTab(2), { targetPosition: { x: 50, y: 0 }}); await tab.dragTo(app.getTab(2), { targetPosition: { x: 50, y: 0 } });
tab = app.getTab(2); tab = app.getTab(2);
await expect(tab).toContainText(NOTE_TITLE); await expect(tab).toContainText(NOTE_TITLE);
// Drag the tab to the left // Drag the tab to the left
await tab.dragTo(app.getTab(0), { targetPosition: { x: 50, y: 0 }}); await tab.dragTo(app.getTab(0), { targetPosition: { x: 50, y: 0 } });
await expect(app.getTab(0)).toContainText(NOTE_TITLE); await expect(app.getTab(0)).toContainText(NOTE_TITLE);
}); });

View File

@@ -39,7 +39,9 @@ test("Displays lint errors for backend script", async ({ page, context }) => {
}); });
async function expectTooltip(page: Page, tooltip: string) { async function expectTooltip(page: Page, tooltip: string) {
await expect(page.locator(".CodeMirror-lint-tooltip:visible", { await expect(
"hasText": tooltip page.locator(".CodeMirror-lint-tooltip:visible", {
})).toBeVisible(); hasText: tooltip
})
).toBeVisible();
} }

View File

@@ -3,7 +3,8 @@ import App from "../support/app";
test("renders ELK flowchart", async ({ page, context }) => { test("renders ELK flowchart", async ({ page, context }) => {
await testAriaSnapshot({ await testAriaSnapshot({
page, context, page,
context,
noteTitle: "Flowchart ELK on", noteTitle: "Flowchart ELK on",
snapshot: ` snapshot: `
- document: - document:
@@ -22,12 +23,13 @@ test("renders ELK flowchart", async ({ page, context }) => {
- paragraph: Guarantee - paragraph: Guarantee
- text: Interfaces for B - text: Interfaces for B
` `
}) });
}); });
test("renders standard flowchart", async ({ page, context }) => { test("renders standard flowchart", async ({ page, context }) => {
await testAriaSnapshot({ await testAriaSnapshot({
page, context, page,
context,
noteTitle: "Flowchart ELK off", noteTitle: "Flowchart ELK off",
snapshot: ` snapshot: `
- document: - document:
@@ -46,7 +48,7 @@ test("renders standard flowchart", async ({ page, context }) => {
- paragraph: C - paragraph: C
- text: Interfaces for B - text: Interfaces for B
` `
}) });
}); });
interface AriaTestOpts { interface AriaTestOpts {

View File

@@ -44,8 +44,8 @@ test("Highlights list is displayed", async ({ page, context }) => {
await expect(app.sidebar).toContainText("Highlights List"); await expect(app.sidebar).toContainText("Highlights List");
const rootList = app.sidebar.locator(".highlights-list ol"); const rootList = app.sidebar.locator(".highlights-list ol");
let index=0; 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" ]) { 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"]) {
await expect(rootList.locator("li").nth(index++)).toContainText(highlightedEl); await expect(rootList.locator("li").nth(index++)).toContainText(highlightedEl);
} }
}); });
@@ -54,7 +54,7 @@ test("Displays math popup", async ({ page, context }) => {
const app = new App(page, context); const app = new App(page, context);
await app.goto(); await app.goto();
await app.goToNoteInNewTab("Empty text"); await app.goToNoteInNewTab("Empty text");
const noteContent = app.currentNoteSplit.locator(".note-detail-editable-text-editor") const noteContent = app.currentNoteSplit.locator(".note-detail-editable-text-editor");
await noteContent.fill("Hello world"); await noteContent.fill("Hello world");
await noteContent.press("ControlOrMeta+M"); await noteContent.press("ControlOrMeta+M");

View File

@@ -25,7 +25,7 @@ export default class App {
this.tabBar = page.locator(".tab-row-widget-container"); this.tabBar = page.locator(".tab-row-widget-container");
this.noteTree = page.locator(".tree-wrapper"); this.noteTree = page.locator(".tree-wrapper");
this.launcherBar = page.locator("#launcher-container"); this.launcherBar = page.locator("#launcher-container");
this.currentNoteSplit = page.locator(".note-split:not(.hidden-ext)") this.currentNoteSplit = page.locator(".note-split:not(.hidden-ext)");
this.sidebar = page.locator("#right-pane"); this.sidebar = page.locator("#right-pane");
} }
@@ -46,8 +46,7 @@ export default class App {
// Wait for the page to load. // Wait for the page to load.
if (url === "/") { if (url === "/") {
await expect(this.page.locator(".tree")) await expect(this.page.locator(".tree")).toContainText("Trilium Integration Test");
.toContainText("Trilium Integration Test");
await this.closeAllTabs(); await this.closeAllTabs();
} }
} }
@@ -109,11 +108,12 @@ export default class App {
}); });
expect(csrfToken).toBeTruthy(); expect(csrfToken).toBeTruthy();
await expect(await this.page.request.put(`${BASE_URL}/api/options/${key}/${value}`, { await expect(
headers: { await this.page.request.put(`${BASE_URL}/api/options/${key}/${value}`, {
"x-csrf-token": csrfToken headers: {
} "x-csrf-token": csrfToken
})).toBeOK(); }
})
).toBeOK();
} }
} }

43
eslint.config.js Normal file
View File

@@ -0,0 +1,43 @@
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.recommended,
// consider using rules below, once we have a full TS codebase and can be more strict
// tseslint.configs.strictTypeChecked,
// tseslint.configs.stylisticTypeChecked,
// tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname
}
}
},
{
rules: {
// add rule overrides here
"no-undef": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
}
]
}
},
{
ignores: [
"build/*",
"dist/*",
"docs/*",
"libraries/*",
"src/public/app-dist/*",
"src/public/app/doc_notes/*"
]
}
);

901
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -57,6 +57,8 @@
"dev:watch-dist": "tsx ./bin/watch-dist.ts", "dev:watch-dist": "tsx ./bin/watch-dist.ts",
"dev:prettier-check": "prettier . --check", "dev:prettier-check": "prettier . --check",
"dev:prettier-fix": "prettier . --write", "dev:prettier-fix": "prettier . --write",
"dev:linter-check": "eslint .",
"dev:linter-fix": "eslint . --fix",
"chore:update-build-info": "tsx bin/update-build-info.ts", "chore:update-build-info": "tsx bin/update-build-info.ts",
"chore:ci-update-nightly-version": "tsx ./bin/update-nightly-version.ts", "chore:ci-update-nightly-version": "tsx ./bin/update-nightly-version.ts",
"chore:generate-document": "cross-env nodemon ./bin/generate_document.ts 1000", "chore:generate-document": "cross-env nodemon ./bin/generate_document.ts 1000",
@@ -103,7 +105,6 @@
"express-session": "1.18.1", "express-session": "1.18.1",
"force-graph": "1.49.2", "force-graph": "1.49.2",
"fs-extra": "11.3.0", "fs-extra": "11.3.0",
"happy-dom": "17.1.8",
"helmet": "8.0.0", "helmet": "8.0.0",
"html": "1.0.0", "html": "1.0.0",
"html2plaintext": "2.1.4", "html2plaintext": "2.1.4",
@@ -123,7 +124,6 @@
"jsdom": "26.0.0", "jsdom": "26.0.0",
"jsplumb": "2.15.6", "jsplumb": "2.15.6",
"katex": "0.16.21", "katex": "0.16.21",
"knockout": "3.5.1",
"leaflet": "1.9.4", "leaflet": "1.9.4",
"leaflet-gpx": "2.1.2", "leaflet-gpx": "2.1.2",
"mark.js": "8.11.1", "mark.js": "8.11.1",
@@ -150,7 +150,6 @@
"striptags": "3.2.0", "striptags": "3.2.0",
"swagger-ui-express": "5.0.1", "swagger-ui-express": "5.0.1",
"tmp": "0.2.3", "tmp": "0.2.3",
"ts-loader": "9.5.2",
"turndown": "7.2.0", "turndown": "7.2.0",
"unescape": "1.0.1", "unescape": "1.0.1",
"vanilla-js-wheel-zoom": "9.0.4", "vanilla-js-wheel-zoom": "9.0.4",
@@ -168,6 +167,7 @@
"@electron-forge/maker-zip": "7.7.0", "@electron-forge/maker-zip": "7.7.0",
"@electron-forge/plugin-auto-unpack-natives": "7.7.0", "@electron-forge/plugin-auto-unpack-natives": "7.7.0",
"@electron/rebuild": "3.7.1", "@electron/rebuild": "3.7.1",
"@eslint/js": "9.21.0",
"@playwright/test": "1.50.1", "@playwright/test": "1.50.1",
"@popperjs/core": "2.11.8", "@popperjs/core": "2.11.8",
"@types/archiver": "6.0.3", "@types/archiver": "6.0.3",
@@ -216,14 +216,17 @@
"cross-env": "7.0.3", "cross-env": "7.0.3",
"css-loader": "7.1.2", "css-loader": "7.1.2",
"electron": "34.3.0", "electron": "34.3.0",
"eslint": "9.21.0",
"esm": "3.2.25", "esm": "3.2.25",
"happy-dom": "17.2.2",
"i18next-http-backend": "3.0.2", "i18next-http-backend": "3.0.2",
"jsdoc": "4.0.4", "jsdoc": "4.0.4",
"knockout": "3.5.1",
"lorem-ipsum": "2.0.8", "lorem-ipsum": "2.0.8",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"nodemon": "3.1.9", "nodemon": "3.1.9",
"postcss-loader": "8.1.1", "postcss-loader": "8.1.1",
"prettier": "3.5.2", "prettier": "3.5.3",
"rcedit": "4.0.1", "rcedit": "4.0.1",
"rimraf": "6.0.1", "rimraf": "6.0.1",
"sass": "1.85.1", "sass": "1.85.1",
@@ -231,10 +234,12 @@
"split.js": "1.6.5", "split.js": "1.6.5",
"supertest": "7.0.0", "supertest": "7.0.0",
"swagger-jsdoc": "6.2.8", "swagger-jsdoc": "6.2.8",
"ts-loader": "9.5.2",
"tslib": "2.8.1", "tslib": "2.8.1",
"tsx": "4.19.3", "tsx": "4.19.3",
"typedoc": "0.27.9", "typedoc": "0.27.9",
"typescript": "5.8.2", "typescript": "5.8.2",
"typescript-eslint": "8.26.0",
"vitest": "3.0.7", "vitest": "3.0.7",
"webpack": "5.98.0", "webpack": "5.98.0",
"webpack-cli": "6.0.1", "webpack-cli": "6.0.1",

View File

@@ -1,6 +1,6 @@
import { defineConfig, devices } from '@playwright/test'; import { defineConfig, devices } from "@playwright/test";
const SERVER_URL = 'http://127.0.0.1:8082'; const SERVER_URL = "http://127.0.0.1:8082";
/** /**
* Read environment variables from file. * Read environment variables from file.
@@ -14,68 +14,70 @@ const SERVER_URL = 'http://127.0.0.1:8082';
* See https://playwright.dev/docs/test-configuration. * See https://playwright.dev/docs/test-configuration.
*/ */
export default defineConfig({ export default defineConfig({
testDir: './e2e', testDir: "./e2e",
/* Run tests in files in parallel */ /* Run tests in files in parallel */
fullyParallel: true, fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */ /* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
/* Retry on CI only */ /* Retry on CI only */
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */ /* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html', reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
/* Base URL to use in actions like `await page.goto('/')`. */ /* Base URL to use in actions like `await page.goto('/')`. */
baseURL: SERVER_URL, baseURL: SERVER_URL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry', trace: "on-first-retry"
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
}, },
// { /* Configure projects for major browsers */
// name: 'firefox', projects: [
// use: { ...devices['Desktop Firefox'] }, {
// }, name: "chromium",
use: { ...devices["Desktop Chrome"] }
}
// { // {
// name: 'webkit', // name: 'firefox',
// use: { ...devices['Desktop Safari'] }, // use: { ...devices['Desktop Firefox'] },
// }, // },
/* Test against mobile viewports. */ // {
// { // name: 'webkit',
// name: 'Mobile Chrome', // use: { ...devices['Desktop Safari'] },
// use: { ...devices['Pixel 5'] }, // },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */ /* Test against mobile viewports. */
// { // {
// name: 'Microsoft Edge', // name: 'Mobile Chrome',
// use: { ...devices['Desktop Edge'], channel: 'msedge' }, // use: { ...devices['Pixel 5'] },
// }, // },
// { // {
// name: 'Google Chrome', // name: 'Mobile Safari',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' }, // use: { ...devices['iPhone 12'] },
// }, // },
],
/* Run your local dev server before starting the tests */ /* Test against branded browsers. */
webServer: !process.env.TRILIUM_DOCKER ? { // {
command: 'npm run test:integration-mem-db-dev', // name: 'Microsoft Edge',
url: SERVER_URL, // use: { ...devices['Desktop Edge'], channel: 'msedge' },
reuseExistingServer: !process.env.CI, // },
} : undefined, // {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: !process.env.TRILIUM_DOCKER
? {
command: "npm run test:integration-mem-db-dev",
url: SERVER_URL,
reuseExistingServer: !process.env.CI
}
: undefined
}); });

View File

@@ -14,7 +14,7 @@ export default class BTask extends AbstractBeccaEntity<BOption> {
} }
static get hashedProperties() { static get hashedProperties() {
return [ "taskId", "parentNoteId", "title", "dueDate", "isDone", "isDeleted" ]; return ["taskId", "parentNoteId", "title", "dueDate", "isDone", "isDeleted"];
} }
taskId?: string; taskId?: string;

View File

@@ -24,6 +24,7 @@ import type NoteTreeWidget from "../widgets/note_tree.js";
import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js"; import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js";
import type { ContextMenuEvent } from "../menus/context_menu.js"; import type { ContextMenuEvent } from "../menus/context_menu.js";
import type TypeWidget from "../widgets/type_widgets/type_widget.js"; import type TypeWidget from "../widgets/type_widgets/type_widget.js";
import type EditableTextTypeWidget from "../widgets/type_widgets/editable_text.js";
interface Layout { interface Layout {
getRootWidget: (appContext: AppContext) => RootWidget; getRootWidget: (appContext: AppContext) => RootWidget;
@@ -62,7 +63,7 @@ export interface NoteCommandData extends CommandData {
} }
export interface ExecuteCommandData<T> extends CommandData { export interface ExecuteCommandData<T> extends CommandData {
resolve: (data: T) => void resolve: (data: T) => void;
} }
/** /**
@@ -70,7 +71,7 @@ export interface ExecuteCommandData<T> extends CommandData {
*/ */
export type CommandMappings = { export type CommandMappings = {
"api-log-messages": CommandData; "api-log-messages": CommandData;
focusTree: CommandData, focusTree: CommandData;
focusOnTitle: CommandData; focusOnTitle: CommandData;
focusOnDetail: CommandData; focusOnDetail: CommandData;
focusOnSearchDefinition: Required<CommandData>; focusOnSearchDefinition: Required<CommandData>;
@@ -108,7 +109,7 @@ export type CommandMappings = {
showInfoDialog: ConfirmWithMessageOptions; showInfoDialog: ConfirmWithMessageOptions;
showConfirmDialog: ConfirmWithMessageOptions; showConfirmDialog: ConfirmWithMessageOptions;
showRecentChanges: CommandData & { ancestorNoteId: string }; showRecentChanges: CommandData & { ancestorNoteId: string };
showImportDialog: CommandData & { noteId: string; }; showImportDialog: CommandData & { noteId: string };
openNewNoteSplit: NoteCommandData; openNewNoteSplit: NoteCommandData;
openInWindow: NoteCommandData; openInWindow: NoteCommandData;
openNoteInNewTab: CommandData; openNoteInNewTab: CommandData;
@@ -131,8 +132,10 @@ export type CommandMappings = {
editNoteTitle: ContextMenuCommandData; editNoteTitle: ContextMenuCommandData;
protectSubtree: ContextMenuCommandData; protectSubtree: ContextMenuCommandData;
unprotectSubtree: ContextMenuCommandData; unprotectSubtree: ContextMenuCommandData;
openBulkActionsDialog: ContextMenuCommandData | { openBulkActionsDialog:
selectedOrActiveNoteIds?: string[] | ContextMenuCommandData
| {
selectedOrActiveNoteIds?: string[];
}; };
editBranchPrefix: ContextMenuCommandData; editBranchPrefix: ContextMenuCommandData;
convertNoteToAttachment: ContextMenuCommandData; convertNoteToAttachment: ContextMenuCommandData;
@@ -221,11 +224,11 @@ export type CommandMappings = {
moveTabToNewWindow: CommandData; moveTabToNewWindow: CommandData;
copyTabToNewWindow: CommandData; copyTabToNewWindow: CommandData;
closeActiveTab: CommandData & { closeActiveTab: CommandData & {
$el: JQuery<HTMLElement> $el: JQuery<HTMLElement>;
}, };
setZoomFactorAndSave: { setZoomFactorAndSave: {
zoomFactor: string; zoomFactor: string;
} };
reEvaluateRightPaneVisibility: CommandData; reEvaluateRightPaneVisibility: CommandData;
runActiveNote: CommandData; runActiveNote: CommandData;
@@ -234,18 +237,18 @@ export type CommandMappings = {
}; };
scrollToEnd: CommandData; scrollToEnd: CommandData;
closeThisNoteSplit: CommandData; closeThisNoteSplit: CommandData;
moveThisNoteSplit: CommandData & { isMovingLeft: boolean; }; moveThisNoteSplit: CommandData & { isMovingLeft: boolean };
// Geomap // Geomap
deleteFromMap: { noteId: string }, deleteFromMap: { noteId: string };
openGeoLocation: { noteId: string, event: JQuery.MouseDownEvent } openGeoLocation: { noteId: string; event: JQuery.MouseDownEvent };
toggleZenMode: CommandData; toggleZenMode: CommandData;
updateAttributeList: CommandData & { attributes: Attribute[] }; updateAttributeList: CommandData & { attributes: Attribute[] };
saveAttributes: CommandData; saveAttributes: CommandData;
reloadAttributes: CommandData; reloadAttributes: CommandData;
refreshNoteList: CommandData & { noteId: string; }; refreshNoteList: CommandData & { noteId: string };
refreshResults: {}; refreshResults: {};
refreshSearchDefinition: {}; refreshSearchDefinition: {};
@@ -348,7 +351,7 @@ type EventMappings = {
ntxId: string | null | undefined; // TODO: deduplicate ntxId ntxId: string | null | undefined; // TODO: deduplicate ntxId
}; };
tabReorder: { tabReorder: {
ntxIdsInOrder: string[] ntxIdsInOrder: string[];
}; };
refreshNoteList: { refreshNoteList: {
noteId: string; noteId: string;
@@ -359,6 +362,12 @@ type EventMappings = {
relationMapResetPanZoom: { ntxId: string | null | undefined }; relationMapResetPanZoom: { ntxId: string | null | undefined };
relationMapResetZoomIn: { ntxId: string | null | undefined }; relationMapResetZoomIn: { ntxId: string | null | undefined };
relationMapResetZoomOut: { ntxId: string | null | undefined }; relationMapResetZoomOut: { ntxId: string | null | undefined };
activeNoteChangedEvent: {};
showAddLinkDialog: {
textTypeWidget: EditableTextTypeWidget;
text: string;
};
}; };
export type EventListener<T extends EventNames> = { export type EventListener<T extends EventNames> = {

View File

@@ -18,7 +18,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
children: ChildT[]; children: ChildT[];
initialized: Promise<void> | null; initialized: Promise<void> | null;
parent?: TypedComponent<any>; parent?: TypedComponent<any>;
position!: number; _position!: number;
constructor() { constructor() {
this.componentId = `${this.sanitizedClassName}-${utils.randomString(8)}`; this.componentId = `${this.sanitizedClassName}-${utils.randomString(8)}`;
@@ -31,6 +31,14 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
return this.constructor.name.replace(/[^A-Z0-9]/gi, "_"); return this.constructor.name.replace(/[^A-Z0-9]/gi, "_");
} }
get position() {
return this._position;
}
set position(newPosition: number) {
this._position = newPosition;
}
setParent(parent: TypedComponent<any>) { setParent(parent: TypedComponent<any>) {
this.parent = parent; this.parent = parent;
return this; return this;

View File

@@ -17,7 +17,7 @@ export default class ShortcutComponent extends Component implements EventListene
} }
bindNoteShortcutHandler(labelOrRow: AttributeRow) { bindNoteShortcutHandler(labelOrRow: AttributeRow) {
const handler = () => appContext.tabManager.getActiveContext().setNote(labelOrRow.noteId); const handler = () => appContext.tabManager.getActiveContext()?.setNote(labelOrRow.noteId);
const namespace = labelOrRow.attributeId; const namespace = labelOrRow.attributeId;
if (labelOrRow.isDeleted) { if (labelOrRow.isDeleted) {

View File

@@ -248,7 +248,7 @@ export default class TabManager extends Component {
await noteContext.setEmpty(); await noteContext.setEmpty();
} }
async openEmptyTab(ntxId = null, hoistedNoteId = "root", mainNtxId = null) { async openEmptyTab(ntxId = null, hoistedNoteId = "root", mainNtxId) {
const noteContext = new NoteContext(ntxId, hoistedNoteId, mainNtxId); const noteContext = new NoteContext(ntxId, hoistedNoteId, mainNtxId);
const existingNoteContext = this.children.find((nc) => nc.ntxId === noteContext.ntxId); const existingNoteContext = this.children.find((nc) => nc.ntxId === noteContext.ntxId);

View File

@@ -37,7 +37,25 @@ const NOTE_TYPE_ICONS = {
* end user. Those types should be used only for checking against, they are * end user. Those types should be used only for checking against, they are
* not for direct use. * not for direct use.
*/ */
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "geoMap" | "taskList"; export type NoteType =
| "file"
| "image"
| "search"
| "noteMap"
| "launcher"
| "doc"
| "contentWidget"
| "text"
| "relationMap"
| "render"
| "canvas"
| "mermaid"
| "book"
| "webView"
| "code"
| "mindMap"
| "geoMap"
| "taskList";
export interface NotePathRecord { export interface NotePathRecord {
isArchived: boolean; isArchived: boolean;

View File

@@ -264,7 +264,7 @@ export default class DesktopLayout {
.child(new InfoDialog()) .child(new InfoDialog())
.child(new ConfirmDialog()) .child(new ConfirmDialog())
.child(new PromptDialog()) .child(new PromptDialog())
.child(new CloseZenButton()) .child(new CloseZenButton());
} }
#buildLauncherPane(isHorizontal) { #buildLauncherPane(isHorizontal) {

View File

@@ -4,7 +4,7 @@ import "../stylesheets/bootstrap.scss";
// Required for correct loading of scripts in Electron // Required for correct loading of scripts in Electron
if (typeof module === 'object') {window.module = module; module = undefined;} if (typeof module === 'object') {window.module = module; module = undefined;}
const device = getDeviceType() const device = getDeviceType();
console.log("Setting device cookie to:", device); console.log("Setting device cookie to:", device);
setCookie("trilium-device", device); setCookie("trilium-device", device);
@@ -12,21 +12,21 @@ function setCookie(name: string, value?: string) {
const date = new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1000); const date = new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1000);
const expires = "; expires=" + date.toUTCString(); const expires = "; expires=" + date.toUTCString();
document.cookie = name + "=" + (value || "") + expires + "; path=/"; document.cookie = name + "=" + (value || "") + expires + "; path=/";
} }
function getDeviceType() { function getDeviceType() {
if (window.location.search === '?desktop') return "desktop"; if (window.location.search === "?desktop") return "desktop";
if (window.location.search === '?mobile') return "mobile"; if (window.location.search === "?mobile") return "mobile";
return isMobile() ? "mobile" : "desktop"; return isMobile() ? "mobile" : "desktop";
} }
// https://stackoverflow.com/a/73731646/944162 // https://stackoverflow.com/a/73731646/944162
function isMobile() { function isMobile() {
const mQ = matchMedia?.('(pointer:coarse)'); const mQ = matchMedia?.("(pointer:coarse)");
if (mQ?.media === '(pointer:coarse)') return !!mQ.matches; if (mQ?.media === "(pointer:coarse)") return !!mQ.matches;
if ('orientation' in window) return true; if ("orientation" in window) return true;
const userAgentsRegEx = /\b(Android|iPhone|iPad|iPod|Windows Phone|BlackBerry|webOS|IEMobile)\b/i const userAgentsRegEx = /\b(Android|iPhone|iPad|iPod|Windows Phone|BlackBerry|webOS|IEMobile)\b/i;
return userAgentsRegEx.test(navigator.userAgent) return userAgentsRegEx.test(navigator.userAgent);
} }

View File

@@ -34,8 +34,8 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener<
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers"; const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers";
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers"; const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers";
const isVisibleItem = (parentNoteId === "_lbVisibleLaunchers" || parentNoteId === "_lbMobileVisibleLaunchers"); const isVisibleItem = parentNoteId === "_lbVisibleLaunchers" || parentNoteId === "_lbMobileVisibleLaunchers";
const isAvailableItem = (parentNoteId === "_lbAvailableLaunchers" || parentNoteId === "_lbMobileAvailableLaunchers"); const isAvailableItem = parentNoteId === "_lbAvailableLaunchers" || parentNoteId === "_lbMobileAvailableLaunchers";
const isItem = isVisibleItem || isAvailableItem; const isItem = isVisibleItem || isAvailableItem;
const canBeDeleted = !note?.noteId.startsWith("_"); // fixed notes can't be deleted const canBeDeleted = !note?.noteId.startsWith("_"); // fixed notes can't be deleted
const canBeReset = !canBeDeleted && note?.isLaunchBarConfig(); const canBeReset = !canBeDeleted && note?.isLaunchBarConfig();

View File

@@ -44,7 +44,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
const note = this.node.data.noteId ? await froca.getNote(this.node.data.noteId) : null; const note = this.node.data.noteId ? await froca.getNote(this.node.data.noteId) : null;
const branch = froca.getBranch(this.node.data.branchId); const branch = froca.getBranch(this.node.data.branchId);
const isNotRoot = note?.noteId !== "root"; const isNotRoot = note?.noteId !== "root";
const isHoisted = note?.noteId === appContext.tabManager.getActiveContext().hoistedNoteId; const isHoisted = note?.noteId === appContext.tabManager.getActiveContext()?.hoistedNoteId;
const parentNote = isNotRoot && branch ? await froca.getNote(branch.parentNoteId) : null; const parentNote = isNotRoot && branch ? await froca.getNote(branch.parentNoteId) : null;
// some actions don't support multi-note, so they are disabled when notes are selected, // some actions don't support multi-note, so they are disabled when notes are selected,
@@ -226,8 +226,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
templateNoteId: templateNoteId templateNoteId: templateNoteId
}); });
} else if (command === "openNoteInSplit") { } else if (command === "openNoteInSplit") {
const subContexts = appContext.tabManager.getActiveContext().getSubContexts(); const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
const { ntxId } = subContexts[subContexts.length - 1]; const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath }); this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath });
} else if (command === "convertNoteToAttachment") { } else if (command === "convertNoteToAttachment") {

View File

@@ -1,7 +1,6 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import attributeParser from "./attribute_parser.js"; import attributeParser from "./attribute_parser.js";
describe("Lexing", () => { describe("Lexing", () => {
it("simple label", () => { it("simple label", () => {
expect(attributeParser.lex("#label").map((t: any) => t.text)).toEqual(["#label"]); expect(attributeParser.lex("#label").map((t: any) => t.text)).toEqual(["#label"]);
@@ -41,7 +40,7 @@ describe("Lexing", () => {
}); });
describe.todo("Parser", () => { describe.todo("Parser", () => {
/* #TODO /* #TODO
it("simple label", () => { it("simple label", () => {
const attrs = attributeParser.parse(["#token"].map((t: any) => ({ text: t }))); const attrs = attributeParser.parse(["#token"].map((t: any) => ({ text: t })));

View File

@@ -79,7 +79,7 @@ async function renderAttributes(attributes: FAttribute[], renderIsInheritable: b
return $container; return $container;
} }
const HIDDEN_ATTRIBUTES = [ "originalFileName", "fileSize", "template", "inherit", "cssClass", "iconClass", "pageSize", "viewType", "geolocation", "docName" ]; const HIDDEN_ATTRIBUTES = ["originalFileName", "fileSize", "template", "inherit", "cssClass", "iconClass", "pageSize", "viewType", "geolocation", "docName"];
async function renderNormalAttributes(note: FNote) { async function renderNormalAttributes(note: FNote) {
const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes(); const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes();

View File

@@ -152,10 +152,10 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f
async function activateParentNotePath() { async function activateParentNotePath() {
// this is not perfect, maybe we should find the next/previous sibling, but that's more complex // this is not perfect, maybe we should find the next/previous sibling, but that's more complex
const activeContext = appContext.tabManager.getActiveContext(); const activeContext = appContext.tabManager.getActiveContext();
const parentNotePathArr = activeContext.notePathArray.slice(0, -1); const parentNotePathArr = activeContext?.notePathArray.slice(0, -1);
if (parentNotePathArr.length > 0) { if (parentNotePathArr && parentNotePathArr.length > 0) {
activeContext.setNote(parentNotePathArr.join("/")); activeContext?.setNote(parentNotePathArr.join("/"));
} }
} }

View File

@@ -457,13 +457,13 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
this.BasicWidget = BasicWidget; this.BasicWidget = BasicWidget;
this.activateNote = async (notePath) => { this.activateNote = async (notePath) => {
await appContext.tabManager.getActiveContext().setNote(notePath); await appContext.tabManager.getActiveContext()?.setNote(notePath);
}; };
this.activateNewNote = async (notePath) => { this.activateNewNote = async (notePath) => {
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext().setNote(notePath); await appContext.tabManager.getActiveContext()?.setNote(notePath);
await appContext.triggerEvent("focusAndSelectTitle", {}); await appContext.triggerEvent("focusAndSelectTitle", {});
}; };
@@ -480,8 +480,8 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
this.openSplitWithNote = async (notePath, activate) => { this.openSplitWithNote = async (notePath, activate) => {
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
const subContexts = appContext.tabManager.getActiveContext().getSubContexts(); const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
const { ntxId } = subContexts[subContexts.length - 1]; const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
await appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath }); await appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath });
@@ -591,15 +591,48 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
this.addTextToActiveContextEditor = (text) => appContext.triggerCommand("addTextToActiveEditor", { text }); this.addTextToActiveContextEditor = (text) => appContext.triggerCommand("addTextToActiveEditor", { text });
this.getActiveContextNote = () => appContext.tabManager.getActiveContextNote(); this.getActiveContextNote = (): FNote => {
this.getActiveContext = () => appContext.tabManager.getActiveContext(); const note = appContext.tabManager.getActiveContextNote();
this.getActiveMainContext = () => appContext.tabManager.getActiveMainContext(); if (!note) {
throw new Error("No active context note found");
}
return note;
};
this.getActiveContext = (): NoteContext => {
const context = appContext.tabManager.getActiveContext();
if (!context) {
throw new Error("No active context found");
}
return context;
};
this.getActiveMainContext = (): NoteContext => {
const context = appContext.tabManager.getActiveMainContext();
if (!context) {
throw new Error("No active main context found");
}
return context;
};
this.getNoteContexts = () => appContext.tabManager.getNoteContexts(); this.getNoteContexts = () => appContext.tabManager.getNoteContexts();
this.getMainNoteContexts = () => appContext.tabManager.getMainNoteContexts(); this.getMainNoteContexts = () => appContext.tabManager.getMainNoteContexts();
this.getActiveContextTextEditor = () => appContext.tabManager.getActiveContext()?.getTextEditor(); this.getActiveContextTextEditor = () => {
this.getActiveContextCodeEditor = () => appContext.tabManager.getActiveContext()?.getCodeEditor(); const context = appContext.tabManager.getActiveContext();
if (!context) {
throw new Error("No active context found");
}
return context.getTextEditor();
};
this.getActiveContextCodeEditor = () => {
const context = appContext.tabManager.getActiveContext();
if (!context) {
throw new Error("No active context found");
}
return context.getCodeEditor();
};
this.getActiveNoteDetailWidget = () => new Promise((resolve) => appContext.triggerCommand("executeInActiveNoteDetailWidget", { callback: resolve })); this.getActiveNoteDetailWidget = () => new Promise((resolve) => appContext.triggerCommand("executeInActiveNoteDetailWidget", { callback: resolve }));
this.getActiveContextNotePath = () => appContext.tabManager.getActiveContextNotePath(); this.getActiveContextNotePath = () => appContext.tabManager.getActiveContextNotePath();
@@ -665,5 +698,5 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
} }
export default FrontendScriptApi as any as { export default FrontendScriptApi as any as {
new (startNote: FNote, currentNote: FNote, originEntity: Entity | null, $container: JQuery<HTMLElement> | null): Api; new(startNote: FNote, currentNote: FNote, originEntity: Entity | null, $container: JQuery<HTMLElement> | null): Api;
}; };

View File

@@ -80,7 +80,7 @@ ws.subscribeToMessages(async (message) => {
toastService.showPersistent(toast); toastService.showPersistent(toast);
if (message.result.importedNoteId) { if (message.result.importedNoteId) {
await appContext.tabManager.getActiveContext().setNote(message.result.importedNoteId); await appContext.tabManager.getActiveContext()?.setNote(message.result.importedNoteId);
} }
} }
}); });
@@ -102,7 +102,7 @@ ws.subscribeToMessages(async (message) => {
toastService.showPersistent(toast); toastService.showPersistent(toast);
if (message.result.parentNoteId) { if (message.result.parentNoteId) {
await appContext.tabManager.getActiveContext().setNote(message.result.importedNoteId, { await appContext.tabManager.getActiveContext()?.setNote(message.result.importedNoteId, {
viewScope: { viewScope: {
viewMode: "attachments" viewMode: "attachments"
} }

View File

@@ -99,8 +99,8 @@ const HIGHLIGHT_JS: Library = {
}; };
const LEAFLET: Library = { const LEAFLET: Library = {
css: [ "node_modules/leaflet/dist/leaflet.css" ], css: ["node_modules/leaflet/dist/leaflet.css"]
} };
async function requireLibrary(library: Library) { async function requireLibrary(library: Library) {
if (library.css) { if (library.css) {

View File

@@ -274,8 +274,8 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink); const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink);
const ctrlKey = utils.isCtrlKey(evt); const ctrlKey = utils.isCtrlKey(evt);
const isLeftClick = ("which" in evt && evt.which === 1); const isLeftClick = "which" in evt && evt.which === 1;
const isMiddleClick = ("which" in evt && evt.which === 2); const isMiddleClick = "which" in evt && evt.which === 2;
const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick; const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick;
if (notePath) { if (notePath) {

View File

@@ -322,9 +322,7 @@ function init() {
$.fn.setSelectedNotePath = function (notePath) { $.fn.setSelectedNotePath = function (notePath) {
notePath = notePath || ""; notePath = notePath || "";
$(this).attr(SELECTED_NOTE_PATH_KEY, notePath); $(this).attr(SELECTED_NOTE_PATH_KEY, notePath);
$(this).closest(".input-group").find(".go-to-selected-note-button").toggleClass("disabled", !notePath.trim()).attr("href", `#${notePath}`); // we also set href here so tooltip can be displayed $(this).closest(".input-group").find(".go-to-selected-note-button").toggleClass("disabled", !notePath.trim()).attr("href", `#${notePath}`); // we also set href here so tooltip can be displayed
}; };
@@ -336,11 +334,9 @@ function init() {
} }
}; };
$.fn.setSelectedExternalLink = function (externalLink) { $.fn.setSelectedExternalLink = function (externalLink: string | null) {
if (externalLink) { $(this).attr(SELECTED_EXTERNAL_LINK_KEY, externalLink);
// TODO: This doesn't seem to do anything with the external link, is it normal? $(this).closest(".input-group").find(".go-to-selected-note-button").toggleClass("disabled", true);
$(this).closest(".input-group").find(".go-to-selected-note-button").toggleClass("disabled", true);
}
}; };
$.fn.setNote = async function (noteId) { $.fn.setNote = async function (noteId) {

View File

@@ -86,8 +86,8 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
if (options.activate) { const activeNoteContext = appContext.tabManager.getActiveContext();
const activeNoteContext = appContext.tabManager.getActiveContext(); if (activeNoteContext && options.activate) {
await activeNoteContext.setNote(`${parentNotePath}/${note.noteId}`); await activeNoteContext.setNote(`${parentNotePath}/${note.noteId}`);
if (options.focus === "title") { if (options.focus === "title") {
@@ -152,8 +152,7 @@ async function duplicateSubtree(noteId: string, parentNotePath: string) {
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
const activeNoteContext = appContext.tabManager.getActiveContext(); appContext.tabManager.getActiveContext()?.setNote(`${parentNotePath}/${note.noteId}`);
activeNoteContext.setNote(`${parentNotePath}/${note.noteId}`);
const origNote = await froca.getNote(noteId); const origNote = await froca.getNote(noteId);
toastService.showMessage(t("note_create.duplicated", { title: origNote?.title })); toastService.showMessage(t("note_create.duplicated", { title: origNote?.title }));

View File

@@ -18,7 +18,7 @@ export default class NoteListRenderer {
parentNote, parentNote,
noteIds, noteIds,
showNotePath showNotePath
} };
if (this.viewType === "list" || this.viewType === "grid") { if (this.viewType === "list" || this.viewType === "grid") {
this.viewMode = new ListOrGridView(this.viewType, args); this.viewMode = new ListOrGridView(this.viewType, args);

View File

@@ -147,11 +147,11 @@ async function renderTooltip(note: FNote | null) {
tooltip: true, tooltip: true,
trim: true trim: true
}); });
const isContentEmpty = ($renderedContent[0].innerHTML.length === 0); const isContentEmpty = $renderedContent[0].innerHTML.length === 0;
let content = ""; let content = "";
if (noteTitleWithPathAsSuffix) { if (noteTitleWithPathAsSuffix) {
const classes = [ "note-tooltip-title" ]; const classes = ["note-tooltip-title"];
if (isContentEmpty) { if (isContentEmpty) {
classes.push("note-no-content"); classes.push("note-no-content");
} }

View File

@@ -36,20 +36,20 @@ function parseDate(str: string) {
// Source: https://stackoverflow.com/a/30465299/4898894 // Source: https://stackoverflow.com/a/30465299/4898894
function getMonthsInDateRange(startDate: string, endDate: string) { function getMonthsInDateRange(startDate: string, endDate: string) {
const start = startDate.split('-'); const start = startDate.split("-");
const end = endDate.split('-'); const end = endDate.split("-");
const startYear = parseInt(start[0]); const startYear = parseInt(start[0]);
const endYear = parseInt(end[0]); const endYear = parseInt(end[0]);
const dates = []; const dates = [];
for (let i = startYear; i <= endYear; i++) { for (let i = startYear; i <= endYear; i++) {
const endMonth = i != endYear ? 11 : parseInt(end[1]) - 1; const endMonth = i != endYear ? 11 : parseInt(end[1]) - 1;
const startMon = i === startYear ? parseInt(start[1])-1 : 0; const startMon = i === startYear ? parseInt(start[1]) - 1 : 0;
for(let j = startMon; j <= endMonth; j = j > 12 ? j % 12 || 11 : j+1) { for (let j = startMon; j <= endMonth; j = j > 12 ? j % 12 || 11 : j + 1) {
const month = j+1; const month = j + 1;
const displayMonth = month < 10 ? '0'+month : month; const displayMonth = month < 10 ? "0" + month : month;
dates.push([i, displayMonth].join('-')); dates.push([i, displayMonth].join("-"));
} }
} }
return dates; return dates;
@@ -164,7 +164,7 @@ function escapeHtml(str: string) {
} }
export function escapeQuotes(value: string) { export function escapeQuotes(value: string) {
return value.replaceAll("\"", "&quot;"); return value.replaceAll('"', "&quot;");
} }
function formatSize(size: number) { function formatSize(size: number) {

View File

@@ -11,7 +11,7 @@
interface JQueryStatic { interface JQueryStatic {
ui: JQueryUI.UI; ui: JQueryUI.UI;
}; }
declare namespace JQueryUI { declare namespace JQueryUI {
interface UI { interface UI {
@@ -679,13 +679,13 @@ declare namespace Fancytree {
activate = 1, activate = 1,
expand = 2, expand = 2,
activate_and_expand = 3, activate_and_expand = 3,
activate_dblclick_expands = 4, activate_dblclick_expands = 4
} }
enum FancytreeSelectMode { enum FancytreeSelectMode {
single = 1, single = 1,
multi = 2, multi = 2,
mutlti_hier = 3, mutlti_hier = 3
} }
/** Context object passed to events and hook functions. */ /** Context object passed to events and hook functions. */

View File

@@ -13,7 +13,7 @@ declare module "draggabilly" {
containment: HTMLElement containment: HTMLElement
}); });
element: HTMLElement; element: HTMLElement;
on(event: "pointerDown" | "dragStart" | "dragEnd" | "dragMove", callback: Callback) on(event: "pointerDown" | "dragStart" | "dragEnd" | "dragMove", callback: Callback);
dragEnd(); dragEnd();
isDragging: boolean; isDragging: boolean;
positionDrag: () => void; positionDrag: () => void;
@@ -21,6 +21,6 @@ declare module "draggabilly" {
} }
} }
declare module '@mind-elixir/node-menu' { declare module "@mind-elixir/node-menu" {
export default mindmap; export default mindmap;
} }

View File

@@ -85,7 +85,7 @@ declare global {
getSelectedNotePath(): string | undefined; getSelectedNotePath(): string | undefined;
getSelectedNoteId(): string | null; getSelectedNoteId(): string | null;
setSelectedNotePath(notePath: string | null | undefined); setSelectedNotePath(notePath: string | null | undefined);
getSelectedExternalLink(this: HTMLElement): string | undefined; getSelectedExternalLink(): string | undefined;
setSelectedExternalLink(externalLink: string | null | undefined); setSelectedExternalLink(externalLink: string | null | undefined);
setNote(noteId: string); setNote(noteId: string);
markRegExp(regex: RegExp, opts: { markRegExp(regex: RegExp, opts: {

View File

@@ -41,7 +41,7 @@ export default class BookmarkButtons extends FlexContainer<Component> {
: new OpenNoteButtonWidget(note).class("launcher-button"); : new OpenNoteButtonWidget(note).class("launcher-button");
if (this.settings.titlePlacement) { if (this.settings.titlePlacement) {
if (!('settings' in buttonWidget)) { if (!("settings" in buttonWidget)) {
(buttonWidget as any).settings = {}; (buttonWidget as any).settings = {};
} }

View File

@@ -8,7 +8,7 @@ import appContext from "../../components/app_context.js";
import openService from "../../services/open.js"; import openService from "../../services/open.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import { Dropdown } from "bootstrap"; import { Dropdown } from "bootstrap";
import type attachmentsApiRoute from "../../../../routes/api/attachments.js" import type attachmentsApiRoute from "../../../../routes/api/attachments.js";
import type FAttachment from "../../entities/fattachment.js"; import type FAttachment from "../../entities/fattachment.js";
import type AttachmentDetailWidget from "../attachment_detail.js"; import type AttachmentDetailWidget from "../attachment_detail.js";
@@ -105,8 +105,7 @@ export default class AttachmentActionsWidget extends BasicWidget {
this.$uploadNewRevisionInput = this.$widget.find(".attachment-upload-new-revision-input"); this.$uploadNewRevisionInput = this.$widget.find(".attachment-upload-new-revision-input");
this.$uploadNewRevisionInput.on("change", async () => { this.$uploadNewRevisionInput.on("change", async () => {
const fileToUpload = this.$uploadNewRevisionInput[0].files?.item(0); // copy to allow reset below
const fileToUpload = this.$uploadNewRevisionInput[0].files?.item(0); // copy to allow reset below
this.$uploadNewRevisionInput.val(""); this.$uploadNewRevisionInput.val("");
if (fileToUpload) { if (fileToUpload) {
const result = await server.upload(`attachments/${this.attachmentId}/file`, fileToUpload); const result = await server.upload(`attachments/${this.attachmentId}/file`, fileToUpload);
@@ -131,7 +130,6 @@ export default class AttachmentActionsWidget extends BasicWidget {
const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']"); const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']");
$openAttachmentCustomButton.addClass("disabled").append($('<span class="bx bx-info-circle disabled-tooltip" />').attr("title", t("attachments_actions.open_custom_client_only"))); $openAttachmentCustomButton.addClass("disabled").append($('<span class="bx bx-info-circle disabled-tooltip" />').attr("title", t("attachments_actions.open_custom_client_only")));
} }
} }
async openAttachmentCommand() { async openAttachmentCommand() {
@@ -170,11 +168,10 @@ export default class AttachmentActionsWidget extends BasicWidget {
return; return;
} }
const { note: newNote } = await server.post<ReturnType<typeof attachmentsApiRoute.convertAttachmentToNote>>(`attachments/${this.attachmentId}/convert-to-note`); const { note: newNote } = await server.post<ReturnType<typeof attachmentsApiRoute.convertAttachmentToNote>>(`attachments/${this.attachmentId}/convert-to-note`);
toastService.showMessage(t("attachments_actions.convert_success", { title: this.attachment.title })); toastService.showMessage(t("attachments_actions.convert_success", { title: this.attachment.title }));
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext().setNote(newNote.noteId); await appContext.tabManager.getActiveContext()?.setNote(newNote.noteId);
} }
async renameAttachmentCommand() { async renameAttachmentCommand() {

View File

@@ -43,8 +43,8 @@ const DROPDOWN_TPL = `
data-calendar-input="month"></button> data-calendar-input="month"></button>
<ul class="dropdown-menu" data-calendar-input="month-list"> <ul class="dropdown-menu" data-calendar-input="month-list">
${Object.entries(MONTHS) ${Object.entries(MONTHS)
.map(([i, month]) => `<li><button class="dropdown-item" data-value=${i}>${month}</button></li>`) .map(([i, month]) => `<li><button class="dropdown-item" data-value=${i}>${month}</button></li>`)
.join("")} .join("")}
</ul> </ul>
<button class="calendar-btn tn-tool-button bx bx-chevron-right" data-calendar-toggle="next"></button> <button class="calendar-btn tn-tool-button bx bx-chevron-right" data-calendar-toggle="next"></button>
@@ -149,7 +149,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
const note = await dateNoteService.getDayNote(date); const note = await dateNoteService.getDayNote(date);
if (note) { if (note) {
appContext.tabManager.getActiveContext().setNote(note.noteId); appContext.tabManager.getActiveContext()?.setNote(note.noteId);
this.dropdown?.hide(); this.dropdown?.hide();
} else { } else {
toastService.showError(t("calendar.cannot_find_day_note")); toastService.showError(t("calendar.cannot_find_day_note"));
@@ -189,10 +189,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
async dropdownShown() { async dropdownShown() {
await libraryLoader.requireLibrary(libraryLoader.CALENDAR_WIDGET); await libraryLoader.requireLibrary(libraryLoader.CALENDAR_WIDGET);
this.init(appContext.tabManager.getActiveContextNote()?.getOwnedLabelValue("dateNote") ?? null);
const activeNote = appContext.tabManager.getActiveContextNote();
this.init(activeNote?.getOwnedLabelValue("dateNote"));
} }
init(activeDate: string | null) { init(activeDate: string | null) {

View File

@@ -78,7 +78,7 @@ export default class NoteLauncher extends AbstractLauncher {
} }
getHoistedNoteId() { getHoistedNoteId() {
return this.launcherNote.getRelationValue("hoistedNote") || appContext.tabManager.getActiveContext().hoistedNoteId; return this.launcherNote.getRelationValue("hoistedNote") || appContext.tabManager.getActiveContext()?.hoistedNoteId;
} }
getTitle() { getTitle() {

View File

@@ -10,6 +10,6 @@ export default class TodayLauncher extends NoteLauncher {
} }
getHoistedNoteId() { getHoistedNoteId() {
return appContext.tabManager.getActiveContext().hoistedNoteId; return appContext.tabManager.getActiveContext()?.hoistedNoteId;
} }
} }

View File

@@ -228,7 +228,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
toastService.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title })); toastService.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title }));
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext().setNote(newAttachment.ownerId, { await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, {
viewScope: { viewScope: {
viewMode: "attachments", viewMode: "attachments",
attachmentId: newAttachment.attachmentId attachmentId: newAttachment.attachmentId

View File

@@ -24,8 +24,7 @@ export default class LeftPaneContainer extends FlexContainer<Component> {
if (visible) { if (visible) {
this.triggerEvent("focusTree", {}); this.triggerEvent("focusTree", {});
} else { } else {
const activeNoteContext = appContext.tabManager.getActiveContext(); this.triggerEvent("focusOnDetail", { ntxId: appContext.tabManager.getActiveContext()?.ntxId });
this.triggerEvent("focusOnDetail", { ntxId: activeNoteContext.ntxId });
} }
} }
} }

View File

@@ -139,7 +139,8 @@ export default class RibbonContainer extends NoteContextAwareWidget {
return super.isEnabled() && this.noteContext?.viewScope?.viewMode === "default"; return super.isEnabled() && this.noteContext?.viewScope?.viewMode === "default";
} }
ribbon(widget: NoteContextAwareWidget) { // TODO: Base class ribbon(widget: NoteContextAwareWidget) {
// TODO: Base class
super.child(widget); super.child(widget);
this.ribbonWidgets.push(widget); this.ribbonWidgets.push(widget);
@@ -236,7 +237,12 @@ export default class RibbonContainer extends NoteContextAwareWidget {
const $ribbonTitle = $('<div class="ribbon-tab-title">') const $ribbonTitle = $('<div class="ribbon-tab-title">')
.attr("data-ribbon-component-id", ribbonWidget.componentId) .attr("data-ribbon-component-id", ribbonWidget.componentId)
.attr("data-ribbon-component-name", (ribbonWidget as any).name as string) // TODO: base class for ribbon widgets .attr("data-ribbon-component-name", (ribbonWidget as any).name as string) // TODO: base class for ribbon widgets
.append($('<span class="ribbon-tab-title-icon">').addClass(ret.icon).attr("title", ret.title).attr("data-toggle-command", (ribbonWidget as any).toggleCommand)) // TODO: base class .append(
$('<span class="ribbon-tab-title-icon">')
.addClass(ret.icon)
.attr("title", ret.title)
.attr("data-toggle-command", (ribbonWidget as any).toggleCommand)
) // TODO: base class
.append(" ") .append(" ")
.append($('<span class="ribbon-tab-title-label">').text(ret.title)); .append($('<span class="ribbon-tab-title-label">').text(ret.title));

View File

@@ -1,8 +1,29 @@
import FlexContainer from "./flex_container.js"; import FlexContainer from "./flex_container.js";
import appContext from "../../components/app_context.js"; import appContext from "../../components/app_context.js";
import NoteContext from "../../components/note_context.js";
import type { CommandMappings, EventNames, EventData } from "../../components/app_context.js";
import type BasicWidget from "../basic_widget.js";
export default class SplitNoteContainer extends FlexContainer { interface NoteContextEvent {
constructor(widgetFactory) { noteContext: NoteContext;
}
interface SplitNoteWidget extends BasicWidget {
hasBeenAlreadyShown?: boolean;
ntxId?: string;
}
type WidgetFactory = () => SplitNoteWidget;
interface Widgets {
[key: string]: SplitNoteWidget;
}
export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
private widgetFactory: WidgetFactory;
private widgets: Widgets;
constructor(widgetFactory: WidgetFactory) {
super("row"); super("row");
this.widgetFactory = widgetFactory; this.widgetFactory = widgetFactory;
@@ -13,7 +34,7 @@ export default class SplitNoteContainer extends FlexContainer {
this.collapsible(); this.collapsible();
} }
async newNoteContextCreatedEvent({ noteContext }) { async newNoteContextCreatedEvent({ noteContext }: NoteContextEvent) {
const widget = this.widgetFactory(); const widget = this.widgetFactory();
const $renderedWidget = widget.render(); const $renderedWidget = widget.render();
@@ -23,20 +44,32 @@ export default class SplitNoteContainer extends FlexContainer {
this.$widget.append($renderedWidget); this.$widget.append($renderedWidget);
widget.handleEvent("initialRenderComplete"); widget.handleEvent("initialRenderComplete", {});
widget.toggleExt(false); widget.toggleExt(false);
this.widgets[noteContext.ntxId] = widget; if (noteContext.ntxId) {
this.widgets[noteContext.ntxId] = widget;
}
await widget.handleEvent("setNoteContext", { noteContext }); await widget.handleEvent("setNoteContext", { noteContext });
this.child(widget); this.child(widget);
} }
async openNewNoteSplitEvent({ ntxId, notePath, hoistedNoteId, viewScope }) { async openNewNoteSplitEvent({ ntxId, notePath, hoistedNoteId, viewScope }: {
ntxId: string;
notePath?: string;
hoistedNoteId?: string;
viewScope?: any;
}) {
const mainNtxId = appContext.tabManager.getActiveMainContext().ntxId; const mainNtxId = appContext.tabManager.getActiveMainContext().ntxId;
if (!mainNtxId) {
logError("empty mainNtxId!");
return;
}
if (!ntxId) { if (!ntxId) {
logError("empty ntxId!"); logError("empty ntxId!");
@@ -53,7 +86,7 @@ export default class SplitNoteContainer extends FlexContainer {
// insert the note context after the originating note context // insert the note context after the originating note context
ntxIds.splice(ntxIds.indexOf(ntxId) + 1, 0, noteContext.ntxId); ntxIds.splice(ntxIds.indexOf(ntxId) + 1, 0, noteContext.ntxId);
this.triggerCommand("noteContextReorder", { ntxIdsInOrder: ntxIds }); this.triggerCommand("noteContextReorder" as keyof CommandMappings, { ntxIdsInOrder: ntxIds });
// move the note context rendered widget after the originating widget // move the note context rendered widget after the originating widget
this.$widget.find(`[data-ntx-id="${noteContext.ntxId}"]`).insertAfter(this.$widget.find(`[data-ntx-id="${ntxId}"]`)); this.$widget.find(`[data-ntx-id="${noteContext.ntxId}"]`).insertAfter(this.$widget.find(`[data-ntx-id="${ntxId}"]`));
@@ -67,11 +100,11 @@ export default class SplitNoteContainer extends FlexContainer {
} }
} }
closeThisNoteSplitCommand({ ntxId }) { closeThisNoteSplitCommand({ ntxId }: { ntxId: string }): void {
appContext.tabManager.removeNoteContext(ntxId); appContext.tabManager.removeNoteContext(ntxId);
} }
async moveThisNoteSplitCommand({ ntxId, isMovingLeft }) { async moveThisNoteSplitCommand({ ntxId, isMovingLeft }: { ntxId: string; isMovingLeft: boolean }): Promise<void> {
if (!ntxId) { if (!ntxId) {
logError("empty ntxId!"); logError("empty ntxId!");
return; return;
@@ -96,7 +129,7 @@ export default class SplitNoteContainer extends FlexContainer {
const newNtxIds = [...ntxIds.slice(0, leftIndex), ntxIds[leftIndex + 1], ntxIds[leftIndex], ...ntxIds.slice(leftIndex + 2)]; const newNtxIds = [...ntxIds.slice(0, leftIndex), ntxIds[leftIndex + 1], ntxIds[leftIndex], ...ntxIds.slice(leftIndex + 2)];
const isChangingMainContext = !contexts[leftIndex].mainNtxId; const isChangingMainContext = !contexts[leftIndex].mainNtxId;
this.triggerCommand("noteContextReorder", { this.triggerCommand("noteContextReorder" as keyof CommandMappings, {
ntxIdsInOrder: newNtxIds, ntxIdsInOrder: newNtxIds,
oldMainNtxId: isChangingMainContext ? ntxIds[leftIndex] : null, oldMainNtxId: isChangingMainContext ? ntxIds[leftIndex] : null,
newMainNtxId: isChangingMainContext ? ntxIds[leftIndex + 1] : null newMainNtxId: isChangingMainContext ? ntxIds[leftIndex + 1] : null
@@ -109,16 +142,16 @@ export default class SplitNoteContainer extends FlexContainer {
await appContext.tabManager.activateNoteContext(isMovingLeft ? ntxIds[leftIndex + 1] : ntxIds[leftIndex]); await appContext.tabManager.activateNoteContext(isMovingLeft ? ntxIds[leftIndex + 1] : ntxIds[leftIndex]);
} }
activeContextChangedEvent() { activeContextChangedEvent(): void {
this.refresh(); this.refresh();
} }
noteSwitchedAndActivatedEvent() { noteSwitchedAndActivatedEvent(): void {
this.refresh(); this.refresh();
} }
noteContextRemovedEvent({ ntxIds }) { noteContextRemovedEvent({ ntxIds }: { ntxIds: string[] }): void {
this.children = this.children.filter((c) => !ntxIds.includes(c.ntxId)); this.children = this.children.filter((c) => c.ntxId && !ntxIds.includes(c.ntxId));
for (const ntxId of ntxIds) { for (const ntxId of ntxIds) {
this.$widget.find(`[data-ntx-id="${ntxId}"]`).remove(); this.$widget.find(`[data-ntx-id="${ntxId}"]`).remove();
@@ -127,7 +160,7 @@ export default class SplitNoteContainer extends FlexContainer {
} }
} }
contextsReopenedEvent({ ntxId, afterNtxId }) { contextsReopenedEvent({ ntxId, afterNtxId }: { ntxId?: string; afterNtxId?: string }): void {
if (ntxId === undefined || afterNtxId === undefined) { if (ntxId === undefined || afterNtxId === undefined) {
// no single split reopened // no single split reopened
return; return;
@@ -135,13 +168,11 @@ export default class SplitNoteContainer extends FlexContainer {
this.$widget.find(`[data-ntx-id="${ntxId}"]`).insertAfter(this.$widget.find(`[data-ntx-id="${afterNtxId}"]`)); this.$widget.find(`[data-ntx-id="${ntxId}"]`).insertAfter(this.$widget.find(`[data-ntx-id="${afterNtxId}"]`));
} }
async refresh() { async refresh(): Promise<void> {
this.toggleExt(true); this.toggleExt(true);
} }
toggleInt(show) {} // not needed toggleExt(show: boolean): void {
toggleExt(show) {
const activeMainContext = appContext.tabManager.getActiveMainContext(); const activeMainContext = appContext.tabManager.getActiveMainContext();
const activeNtxId = activeMainContext ? activeMainContext.ntxId : null; const activeNtxId = activeMainContext ? activeMainContext.ntxId : null;
@@ -149,7 +180,7 @@ export default class SplitNoteContainer extends FlexContainer {
const noteContext = appContext.tabManager.getNoteContextById(ntxId); const noteContext = appContext.tabManager.getNoteContextById(ntxId);
const widget = this.widgets[ntxId]; const widget = this.widgets[ntxId];
widget.toggleExt(show && activeNtxId && [noteContext.ntxId, noteContext.mainNtxId].includes(activeNtxId)); widget.toggleExt(show && activeNtxId !== null && [noteContext.ntxId, noteContext.mainNtxId].includes(activeNtxId));
} }
} }
@@ -158,41 +189,50 @@ export default class SplitNoteContainer extends FlexContainer {
* are not executed, we're waiting for the first tab activation, and then we update the tab. After this initial * are not executed, we're waiting for the first tab activation, and then we update the tab. After this initial
* activation, further note switches are always propagated to the tabs. * activation, further note switches are always propagated to the tabs.
*/ */
handleEventInChildren(name, data) { handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<any> | null {
if (["noteSwitched", "noteSwitchedAndActivated"].includes(name)) { if (["noteSwitched", "noteSwitchedAndActivated"].includes(name)) {
// this event is propagated only to the widgets of a particular tab // this event is propagated only to the widgets of a particular tab
const widget = this.widgets[data.noteContext.ntxId]; const noteContext = (data as NoteContextEvent).noteContext;
const widget = noteContext.ntxId ? this.widgets[noteContext.ntxId] : undefined;
if (!widget) { if (!widget) {
return Promise.resolve(); return Promise.resolve();
} }
if (widget.hasBeenAlreadyShown || name === "noteSwitchedAndActivated" || appContext.tabManager.getActiveMainContext() === data.noteContext.getMainContext()) { if (widget.hasBeenAlreadyShown || name === "noteSwitchedAndActivatedEvent" || appContext.tabManager.getActiveMainContext() === noteContext.getMainContext()) {
widget.hasBeenAlreadyShown = true; widget.hasBeenAlreadyShown = true;
return [widget.handleEvent("noteSwitched", data), this.refreshNotShown(data)]; return Promise.all([
widget.handleEvent("noteSwitched", { noteContext, notePath: noteContext.notePath }),
this.refreshNotShown({ noteContext })
]);
} else { } else {
return Promise.resolve(); return Promise.resolve();
} }
} }
if (name === "activeContextChanged") { if (name === "activeContextChanged") {
return this.refreshNotShown(data); return this.refreshNotShown(data as NoteContextEvent);
} else { } else {
return super.handleEventInChildren(name, data); return super.handleEventInChildren(name, data);
} }
} }
refreshNotShown(data) { private refreshNotShown(data: NoteContextEvent): Promise<any> {
const promises = []; const promises: Promise<any>[] = [];
for (const subContext of data.noteContext.getMainContext().getSubContexts()) { for (const subContext of data.noteContext.getMainContext().getSubContexts()) {
if (!subContext.ntxId) {
continue;
}
const widget = this.widgets[subContext.ntxId]; const widget = this.widgets[subContext.ntxId];
if (!widget.hasBeenAlreadyShown) { if (!widget.hasBeenAlreadyShown) {
widget.hasBeenAlreadyShown = true; widget.hasBeenAlreadyShown = true;
promises.push(widget.handleEvent("activeContextChanged", { noteContext: subContext })); const eventPromise = widget.handleEvent("activeContextChanged", { noteContext: subContext });
promises.push(eventPromise || Promise.resolve());
} }
} }

View File

@@ -5,6 +5,15 @@ import openService from "../../services/open.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
interface AppInfo {
appVersion: string;
dbVersion: number;
syncVersion: number;
buildDate: string;
buildRevision: string;
dataDirectory: string;
}
const TPL = ` const TPL = `
<div class="about-dialog modal fade mx-auto" tabindex="-1" role="dialog"> <div class="about-dialog modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
@@ -35,12 +44,10 @@ const TPL = `
<th>${t("about.build_date")}</th> <th>${t("about.build_date")}</th>
<td class="build-date"></td> <td class="build-date"></td>
</tr> </tr>
<tr> <tr>
<th>${t("about.build_revision")}</th> <th>${t("about.build_revision")}</th>
<td><a class="tn-link" href="" class="build-revision external" target="_blank"></a></td> <td><a class="tn-link build-revision external" href="" target="_blank"></a></td>
</tr> </tr>
<tr> <tr>
<th>${t("about.data_directory")}</th> <th>${t("about.data_directory")}</th>
<td class="data-directory"></td> <td class="data-directory"></td>
@@ -59,7 +66,14 @@ const TPL = `
`; `;
export default class AboutDialog extends BasicWidget { export default class AboutDialog extends BasicWidget {
doRender() { private $appVersion!: JQuery<HTMLElement>;
private $dbVersion!: JQuery<HTMLElement>;
private $syncVersion!: JQuery<HTMLElement>;
private $buildDate!: JQuery<HTMLElement>;
private $buildRevision!: JQuery<HTMLElement>;
private $dataDirectory!: JQuery<HTMLElement>;
doRender(): void {
this.$widget = $(TPL); this.$widget = $(TPL);
this.$appVersion = this.$widget.find(".app-version"); this.$appVersion = this.$widget.find(".app-version");
this.$dbVersion = this.$widget.find(".db-version"); this.$dbVersion = this.$widget.find(".db-version");
@@ -70,11 +84,11 @@ export default class AboutDialog extends BasicWidget {
} }
async refresh() { async refresh() {
const appInfo = await server.get("app-info"); const appInfo = await server.get<AppInfo>("app-info");
this.$appVersion.text(appInfo.appVersion); this.$appVersion.text(appInfo.appVersion);
this.$dbVersion.text(appInfo.dbVersion); this.$dbVersion.text(appInfo.dbVersion.toString());
this.$syncVersion.text(appInfo.syncVersion); this.$syncVersion.text(appInfo.syncVersion.toString());
this.$buildDate.text(formatDateTime(appInfo.buildDate)); this.$buildDate.text(formatDateTime(appInfo.buildDate));
this.$buildRevision.text(appInfo.buildRevision); this.$buildRevision.text(appInfo.buildRevision);
this.$buildRevision.attr("href", `https://github.com/TriliumNext/Notes/commit/${appInfo.buildRevision}`); this.$buildRevision.attr("href", `https://github.com/TriliumNext/Notes/commit/${appInfo.buildRevision}`);
@@ -84,9 +98,9 @@ export default class AboutDialog extends BasicWidget {
href: "#", href: "#",
class: "tn-link", class: "tn-link",
text: appInfo.dataDirectory text: appInfo.dataDirectory
}) }).prop("outerHTML")
); );
this.$dataDirectory.find("a").on("click", (event) => { this.$dataDirectory.find("a").on("click", (event: JQuery.ClickEvent) => {
event.preventDefault(); event.preventDefault();
openService.openDirectory(appInfo.dataDirectory); openService.openDirectory(appInfo.dataDirectory);
}); });
@@ -97,7 +111,6 @@ export default class AboutDialog extends BasicWidget {
async openAboutDialogEvent() { async openAboutDialogEvent() {
await this.refresh(); await this.refresh();
utils.openDialog(this.$widget); utils.openDialog(this.$widget);
} }
} }

View File

@@ -3,6 +3,9 @@ import treeService from "../../services/tree.js";
import noteAutocompleteService from "../../services/note_autocomplete.js"; import noteAutocompleteService from "../../services/note_autocomplete.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import type { Suggestion } from "../../services/note_autocomplete.js";
import type { default as TextTypeWidget } from "../type_widgets/editable_text.js";
import type { EventData } from "../../components/app_context.js";
const TPL = ` const TPL = `
<div class="add-link-dialog modal mx-auto" tabindex="-1" role="dialog"> <div class="add-link-dialog modal mx-auto" tabindex="-1" role="dialog">
@@ -56,6 +59,14 @@ const TPL = `
</div>`; </div>`;
export default class AddLinkDialog extends BasicWidget { export default class AddLinkDialog extends BasicWidget {
private $form!: JQuery<HTMLElement>;
private $autoComplete!: JQuery<HTMLElement>;
private $linkTitle!: JQuery<HTMLElement>;
private $addLinkTitleSettings!: JQuery<HTMLElement>;
private $addLinkTitleRadios!: JQuery<HTMLElement>;
private $addLinkTitleFormGroup!: JQuery<HTMLElement>;
private textTypeWidget: TextTypeWidget | null = null;
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.$form = this.$widget.find(".add-link-form"); this.$form = this.$widget.find(".add-link-form");
@@ -65,20 +76,17 @@ export default class AddLinkDialog extends BasicWidget {
this.$addLinkTitleRadios = this.$widget.find(".add-link-title-radios"); this.$addLinkTitleRadios = this.$widget.find(".add-link-title-radios");
this.$addLinkTitleFormGroup = this.$widget.find(".add-link-title-form-group"); this.$addLinkTitleFormGroup = this.$widget.find(".add-link-title-form-group");
/** @var TextTypeWidget */
this.textTypeWidget = null;
this.$form.on("submit", () => { this.$form.on("submit", () => {
if (this.$autoComplete.getSelectedNotePath()) { if (this.$autoComplete.getSelectedNotePath()) {
this.$widget.modal("hide"); this.$widget.modal("hide");
const linkTitle = this.getLinkType() === "reference-link" ? null : this.$linkTitle.val(); const linkTitle = this.getLinkType() === "reference-link" ? null : this.$linkTitle.val();
this.textTypeWidget.addLink(this.$autoComplete.getSelectedNotePath(), linkTitle); this.textTypeWidget?.addLink(this.$autoComplete.getSelectedNotePath()!, linkTitle);
} else if (this.$autoComplete.getSelectedExternalLink()) { } else if (this.$autoComplete.getSelectedExternalLink()) {
this.$widget.modal("hide"); this.$widget.modal("hide");
this.textTypeWidget.addLink(this.$autoComplete.getSelectedExternalLink(), this.$linkTitle.val()); this.textTypeWidget?.addLink(this.$autoComplete.getSelectedExternalLink()!, this.$linkTitle.val(), true);
} else { } else {
logError("No link to add."); logError("No link to add.");
} }
@@ -87,12 +95,12 @@ export default class AddLinkDialog extends BasicWidget {
}); });
} }
async showAddLinkDialogEvent({ textTypeWidget, text = "" }) { async showAddLinkDialogEvent({ textTypeWidget, text = "" }: EventData<"showAddLinkDialog">) {
this.textTypeWidget = textTypeWidget; this.textTypeWidget = textTypeWidget;
this.$addLinkTitleSettings.toggle(!this.textTypeWidget.hasSelection()); this.$addLinkTitleSettings.toggle(!this.textTypeWidget.hasSelection());
this.$addLinkTitleSettings.find("input[type=radio]").on("change", (e) => this.updateTitleSettingsVisibility(e)); this.$addLinkTitleSettings.find("input[type=radio]").on("change", () => this.updateTitleSettingsVisibility());
// with selection hyperlink is implied // with selection hyperlink is implied
if (this.textTypeWidget.hasSelection()) { if (this.textTypeWidget.hasSelection()) {
@@ -108,9 +116,8 @@ export default class AddLinkDialog extends BasicWidget {
this.$autoComplete.val(""); this.$autoComplete.val("");
this.$linkTitle.val(""); this.$linkTitle.val("");
const setDefaultLinkTitle = async (noteId) => { const setDefaultLinkTitle = async (noteId: string) => {
const noteTitle = await treeService.getNoteTitle(noteId); const noteTitle = await treeService.getNoteTitle(noteId);
this.$linkTitle.val(noteTitle); this.$linkTitle.val(noteTitle);
}; };
@@ -119,7 +126,7 @@ export default class AddLinkDialog extends BasicWidget {
allowCreatingNotes: true allowCreatingNotes: true
}); });
this.$autoComplete.on("autocomplete:noteselected", (event, suggestion, dataset) => { this.$autoComplete.on("autocomplete:noteselected", (event: JQuery.Event, suggestion: Suggestion) => {
if (!suggestion.notePath) { if (!suggestion.notePath) {
return false; return false;
} }
@@ -133,7 +140,7 @@ export default class AddLinkDialog extends BasicWidget {
} }
}); });
this.$autoComplete.on("autocomplete:externallinkselected", (event, suggestion, dataset) => { this.$autoComplete.on("autocomplete:externallinkselected", (event: JQuery.Event, suggestion: Suggestion) => {
if (!suggestion.externalLink) { if (!suggestion.externalLink) {
return false; return false;
} }
@@ -143,11 +150,11 @@ export default class AddLinkDialog extends BasicWidget {
this.$linkTitle.val(suggestion.externalLink); this.$linkTitle.val(suggestion.externalLink);
}); });
this.$autoComplete.on("autocomplete:cursorchanged", (event, suggestion, dataset) => { this.$autoComplete.on("autocomplete:cursorchanged", (event: JQuery.Event, suggestion: Suggestion) => {
if (suggestion.externalLink) { if (suggestion.externalLink) {
this.$linkTitle.val(suggestion.externalLink); this.$linkTitle.val(suggestion.externalLink);
} else { } else {
const noteId = treeService.getNoteIdFromUrl(suggestion.notePath); const noteId = treeService.getNoteIdFromUrl(suggestion.notePath!);
if (noteId) { if (noteId) {
setDefaultLinkTitle(noteId); setDefaultLinkTitle(noteId);
@@ -164,7 +171,7 @@ export default class AddLinkDialog extends BasicWidget {
this.$autoComplete.trigger("focus").trigger("select"); // to be able to quickly remove entered text this.$autoComplete.trigger("focus").trigger("select"); // to be able to quickly remove entered text
} }
getLinkType() { private getLinkType() {
if (this.$autoComplete.getSelectedExternalLink()) { if (this.$autoComplete.getSelectedExternalLink()) {
return "external-link"; return "external-link";
} }
@@ -172,7 +179,7 @@ export default class AddLinkDialog extends BasicWidget {
return this.$addLinkTitleSettings.find("input[type=radio]:checked").val(); return this.$addLinkTitleSettings.find("input[type=radio]:checked").val();
} }
updateTitleSettingsVisibility() { private updateTitleSettingsVisibility() {
const linkType = this.getLinkType(); const linkType = this.getLinkType();
this.$addLinkTitleFormGroup.toggle(linkType !== "reference-link"); this.$addLinkTitleFormGroup.toggle(linkType !== "reference-link");

View File

@@ -250,7 +250,7 @@ ws.subscribeToMessages(async (message) => {
message: message, message: message,
icon: "arrow-square-up-right" icon: "arrow-square-up-right"
}; };
}; }
if (message.taskType !== "export") { if (message.taskType !== "export") {
return; return;

View File

@@ -320,7 +320,8 @@ export default class RevisionsDialog extends BasicWidget {
// as a URL to be used in a note. Instead, if they copy and paste it into a note, it will be uploaded as a new note // as a URL to be used in a note. Instead, if they copy and paste it into a note, it will be uploaded as a new note
.attr("src", `data:${fullRevision.mime};base64,${fullRevision.content}`) .attr("src", `data:${fullRevision.mime};base64,${fullRevision.content}`)
.css("max-width", "100%") .css("max-width", "100%")
.css("max-height", "100%").html() .css("max-height", "100%")
.html()
); );
} }
} else if (revisionItem.type === "file") { } else if (revisionItem.type === "file") {

View File

@@ -65,7 +65,7 @@ export default class CodeButtonsWidget extends NoteContextAwareWidget {
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext().setNote(notePath); await appContext.tabManager.getActiveContext()?.setNote(notePath);
toastService.showMessage(t("code_buttons.sql_console_saved_message", { notePath: await treeService.getNotePathTitle(notePath) })); toastService.showMessage(t("code_buttons.sql_console_saved_message", { notePath: await treeService.getNotePathTitle(notePath) }));
}); });

View File

@@ -1,5 +1,5 @@
import { t } from "../../services/i18n.js"; import { t } from "../../services/i18n.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js" import NoteContextAwareWidget from "../note_context_aware_widget.js";
const TPL = `\ const TPL = `\
<div class="geo-map-buttons"> <div class="geo-map-buttons">

View File

@@ -157,7 +157,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
if (backlink.relationName) { if (backlink.relationName) {
$item.append($("<p>").text(`${t("zpetne_odkazy.relation")}: ${backlink.relationName}`)); $item.append($("<p>").text(`${t("zpetne_odkazy.relation")}: ${backlink.relationName}`));
} else { } else {
$item.append(...backlink.excerpts ?? []); $item.append(...(backlink.excerpts ?? []));
} }
this.$items.append($item); this.$items.append($item);

View File

@@ -19,10 +19,10 @@ const TPL = `\
</style> </style>
<div class="geo-map-container"></div> <div class="geo-map-container"></div>
</div>` </div>`;
export type Leaflet = typeof import("leaflet"); export type Leaflet = typeof import("leaflet");
export type InitCallback = ((L: Leaflet) => void); export type InitCallback = (L: Leaflet) => void;
export default class GeoMapWidget extends NoteContextAwareWidget { export default class GeoMapWidget extends NoteContextAwareWidget {
@@ -40,24 +40,23 @@ export default class GeoMapWidget extends NoteContextAwareWidget {
this.$container = this.$widget.find(".geo-map-container"); this.$container = this.$widget.find(".geo-map-container");
library_loader.requireLibrary(library_loader.LEAFLET) library_loader.requireLibrary(library_loader.LEAFLET).then(async () => {
.then(async () => { const L = (await import("leaflet")).default;
const L = (await import("leaflet")).default;
const map = L.map(this.$container[0], { const map = L.map(this.$container[0], {
worldCopyJump: true worldCopyJump: true
});
this.map = map;
if (this.initCallback) {
this.initCallback(L);
}
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
detectRetina: true
}).addTo(map);
}); });
this.map = map;
if (this.initCallback) {
this.initCallback(L);
}
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
detectRetina: true
}).addTo(map);
});
} }
} }

View File

@@ -66,7 +66,7 @@ export default class MermaidWidget extends NoteContextAwareWidget {
mermaid.mermaidAPI.initialize({ mermaid.mermaidAPI.initialize({
startOnLoad: false, startOnLoad: false,
...getMermaidConfig() as any ...(getMermaidConfig() as any)
}); });
this.$display.empty(); this.$display.empty();

View File

@@ -28,8 +28,8 @@ class MobileDetailMenuWidget extends BasicWidget {
x: e.pageX, x: e.pageX,
y: e.pageY, y: e.pageY,
items: [ items: [
{ title: t("mobile_detail_menu.insert_child_note"), command: "insertChildNote", uiIcon: "bx bx-plus", enabled: note.type !== "search" }, { title: t("mobile_detail_menu.insert_child_note"), command: "insertChildNote", uiIcon: "bx bx-plus", enabled: note?.type !== "search" },
{ title: t("mobile_detail_menu.delete_this_note"), command: "delete", uiIcon: "bx bx-trash", enabled: note.noteId !== "root" } { title: t("mobile_detail_menu.delete_this_note"), command: "delete", uiIcon: "bx bx-trash", enabled: note?.noteId !== "root" }
], ],
selectMenuItemHandler: async ({ command }) => { selectMenuItemHandler: async ({ command }) => {
if (command === "insertChildNote") { if (command === "insertChildNote") {

View File

@@ -81,7 +81,16 @@ const typeWidgetClasses = {
* A `NoteType` altered by the note detail widget, taking into consideration whether the note is editable or not and adding special note types such as an empty one, * A `NoteType` altered by the note detail widget, taking into consideration whether the note is editable or not and adding special note types such as an empty one,
* for protected session or attachment information. * for protected session or attachment information.
*/ */
type ExtendedNoteType = Exclude<NoteType, "mermaid" | "launcher" | "text" | "code"> | "empty" | "readOnlyCode" | "readOnlyText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession"; type ExtendedNoteType =
| Exclude<NoteType, "mermaid" | "launcher" | "text" | "code">
| "empty"
| "readOnlyCode"
| "readOnlyText"
| "editableText"
| "editableCode"
| "attachmentDetail"
| "attachmentList"
| "protectedSession";
export default class NoteDetailWidget extends NoteContextAwareWidget { export default class NoteDetailWidget extends NoteContextAwareWidget {
@@ -332,7 +341,9 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
const label = attrs.find( const label = attrs.find(
(attr) => (attr) =>
attr.type === "label" && ["readOnly", "autoReadOnlyDisabled", "cssClass", "displayRelations", "hideRelations"].includes(attr.name ?? "") && attributeService.isAffecting(attr, this.note) attr.type === "label" &&
["readOnly", "autoReadOnlyDisabled", "cssClass", "displayRelations", "hideRelations"].includes(attr.name ?? "") &&
attributeService.isAffecting(attr, this.note)
); );
const relation = attrs.find((attr) => attr.type === "relation" && ["template", "inherit", "renderNote"].includes(attr.name ?? "") && attributeService.isAffecting(attr, this.note)); const relation = attrs.find((attr) => attr.type === "relation" && ["template", "inherit", "renderNote"].includes(attr.name ?? "") && attributeService.isAffecting(attr, this.note));

View File

@@ -139,7 +139,7 @@ interface NotesAndRelationsData {
source: string; source: string;
target: string; target: string;
name: string; name: string;
}[] }[];
} }
// Replace // Replace
@@ -152,7 +152,7 @@ interface ResponseLink {
interface PostNotesMapResponse { interface PostNotesMapResponse {
notes: string[]; notes: string[];
links: ResponseLink[], links: ResponseLink[];
noteIdToDescendantCountMap: Record<string, number>; noteIdToDescendantCountMap: Record<string, number>;
} }
@@ -160,7 +160,7 @@ interface GroupedLink {
id: string; id: string;
sourceNoteId: string; sourceNoteId: string;
targetNoteId: string; targetNoteId: string;
names: string[] names: string[];
} }
interface CssData { interface CssData {
@@ -313,9 +313,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
ctx.fillStyle = color; ctx.fillStyle = color;
ctx.beginPath(); ctx.beginPath();
if (node.x && node.y) { if (node.x && node.y) {
ctx.arc(node.x, node.y, ctx.arc(node.x, node.y, this.noteIdToSizeMap[node.id], 0, 2 * Math.PI, false);
this.noteIdToSizeMap[node.id], 0,
2 * Math.PI, false);
} }
ctx.fill(); ctx.fill();
}) })
@@ -324,7 +322,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
.warmupTicks(30) .warmupTicks(30)
.onNodeClick((node) => { .onNodeClick((node) => {
if (node.id) { if (node.id) {
appContext.tabManager.getActiveContext().setNote((node as Node).id); appContext.tabManager.getActiveContext()?.setNote((node as Node).id);
} }
}) })
.onNodeRightClick((node, e) => { .onNodeRightClick((node, e) => {
@@ -373,7 +371,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
if (mapRootNoteId === "hoisted") { if (mapRootNoteId === "hoisted") {
mapRootNoteId = hoistedNoteService.getHoistedNoteId(); mapRootNoteId = hoistedNoteService.getHoistedNoteId();
} else if (!mapRootNoteId) { } else if (!mapRootNoteId) {
mapRootNoteId = appContext.tabManager.getActiveContext().parentNoteId; mapRootNoteId = appContext.tabManager.getActiveContext()?.parentNoteId;
} }
return mapRootNoteId ?? ""; return mapRootNoteId ?? "";
@@ -467,13 +465,13 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
} }
if (source.x && source.y && target.x && target.y) { if (source.x && source.y && target.x && target.y) {
const x = ((source.x) + (target.x)) / 2; const x = (source.x + target.x) / 2;
const y = ((source.y) + (target.y)) / 2; const y = (source.y + target.y) / 2;
ctx.save(); ctx.save();
ctx.translate(x, y); ctx.translate(x, y);
const deltaY = (source.y) - (target.y); const deltaY = source.y - target.y;
const deltaX = (source.x) - (target.x); const deltaX = source.x - target.x;
let angle = Math.atan2(deltaY, deltaX); let angle = Math.atan2(deltaY, deltaX);
let moveY = 2; let moveY = 2;

View File

@@ -94,7 +94,7 @@ export default class NoteTitleWidget extends NoteContextAwareWidget {
|| utils.isLaunchBarConfig(note.noteId) || utils.isLaunchBarConfig(note.noteId)
|| this.noteContext?.viewScope?.viewMode !== "default"; || this.noteContext?.viewScope?.viewMode !== "default";
this.$noteTitle.val(isReadOnly ? await this.noteContext?.getNavigationTitle() || "" : note.title); this.$noteTitle.val(isReadOnly ? (await this.noteContext?.getNavigationTitle()) || "" : note.title);
this.$noteTitle.prop("readonly", isReadOnly); this.$noteTitle.prop("readonly", isReadOnly);
this.setProtectedStatus(note); this.setProtectedStatus(note);

View File

@@ -159,11 +159,11 @@ interface CreateLauncherResponse {
message: string; message: string;
note: { note: {
noteId: string; noteId: string;
} };
} }
interface ExpandedSubtreeResponse { interface ExpandedSubtreeResponse {
branchIds: string[] branchIds: string[];
} }
interface Node extends Fancytree.NodeData { interface Node extends Fancytree.NodeData {
@@ -180,7 +180,6 @@ interface RefreshContext {
} }
export default class NoteTreeWidget extends NoteContextAwareWidget { export default class NoteTreeWidget extends NoteContextAwareWidget {
private $tree!: JQuery<HTMLElement>; private $tree!: JQuery<HTMLElement>;
private $treeActions!: JQuery<HTMLElement>; private $treeActions!: JQuery<HTMLElement>;
private $treeSettingsButton!: JQuery<HTMLElement>; private $treeSettingsButton!: JQuery<HTMLElement>;
@@ -425,10 +424,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const activeNoteContext = appContext.tabManager.getActiveContext(); const activeNoteContext = appContext.tabManager.getActiveContext();
const opts: SetNoteOpts = {}; const opts: SetNoteOpts = {};
if (activeNoteContext.viewScope?.viewMode === "contextual-help") { if (activeNoteContext?.viewScope?.viewMode === "contextual-help") {
opts.viewScope = activeNoteContext.viewScope; opts.viewScope = activeNoteContext.viewScope;
} }
await activeNoteContext.setNote(notePath, opts); await activeNoteContext?.setNote(notePath, opts);
}, },
expand: (event, data) => this.setExpanded(data.node.data.branchId, true), expand: (event, data) => this.setExpanded(data.node.data.branchId, true),
collapse: (event, data) => this.setExpanded(data.node.data.branchId, false), collapse: (event, data) => this.setExpanded(data.node.data.branchId, false),
@@ -571,10 +570,13 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
clones: { clones: {
highlightActiveClones: true highlightActiveClones: true
}, },
enhanceTitle: async function (event: Event, data: { enhanceTitle: async function (
node: Fancytree.FancytreeNode; event: Event,
noteId: string; data: {
}) { node: Fancytree.FancytreeNode;
noteId: string;
}
) {
const node = data.node; const node = data.node;
if (!node.data.noteId) { if (!node.data.noteId) {
@@ -617,10 +619,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
// TODO: Deduplicate with server's notes.ts#getAndValidateParent // TODO: Deduplicate with server's notes.ts#getAndValidateParent
if (!["search", "launcher"].includes(note.type) if (!["search", "launcher"].includes(note.type)
&& !note.isOptions() && !note.isOptions()
&& !note.isLaunchBarConfig() && !note.isLaunchBarConfig()
&& !note.noteId.startsWith("_help") && !note.noteId.startsWith("_help")
) { ) {
const $createChildNoteButton = $(`<span class="tree-item-button add-note-button bx bx-plus" title="${t("note_tree.create-child-note")}"></span>`).on( const $createChildNoteButton = $(`<span class="tree-item-button add-note-button bx bx-plus" title="${t("note_tree.create-child-note")}"></span>`).on(
"click", "click",
cancelClickPropagation cancelClickPropagation
@@ -1756,6 +1758,6 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
appContext.tabManager.getActiveContext().setNote(resp.note.noteId); appContext.tabManager.getActiveContext()?.setNote(resp.note.noteId);
} }
} }

View File

@@ -9,7 +9,7 @@ export default class ProtectedNoteSwitchWidget extends SwitchWidget {
super.doRender(); super.doRender();
this.switchOnName = t("protect_note.toggle-on"); this.switchOnName = t("protect_note.toggle-on");
this.switchOnTooltip = t("protect_note.toggle-on-hint"); this.switchOnTooltip = t("protect_note.toggle-on-hint");
this.switchOffName = t("protect_note.toggle-off"); this.switchOffName = t("protect_note.toggle-off");
this.switchOffTooltip = t("protect_note.toggle-off-hint"); this.switchOffTooltip = t("protect_note.toggle-off-hint");

View File

@@ -126,7 +126,7 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
return; return;
} }
if (![ "list", "grid", "calendar"].includes(type)) { if (!["list", "grid", "calendar"].includes(type)) {
throw new Error(t("book_properties.invalid_view_type", { type })); throw new Error(t("book_properties.invalid_view_type", { type }));
} }

View File

@@ -122,7 +122,7 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget {
const { top } = this.$widget[0].getBoundingClientRect(); const { top } = this.$widget[0].getBoundingClientRect();
const height = ($(window).height() ?? 0) - top; const height = ($(window).height() ?? 0) - top;
const width = (this.$widget.width() ?? 0); const width = this.$widget.width() ?? 0;
this.$widget.find(".note-map-container") this.$widget.find(".note-map-container")
.height(height) .height(height)

View File

@@ -135,8 +135,7 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
} }
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.getBranchRows().find((branch) => branch.noteId === this.noteId) || if (loadResults.getBranchRows().find((branch) => branch.noteId === this.noteId) || (this.noteId != null && loadResults.isNoteReloaded(this.noteId))) {
(this.noteId != null && loadResults.isNoteReloaded(this.noteId))) {
this.refresh(); this.refresh();
} }
} }

View File

@@ -46,6 +46,9 @@ export default class NotePropertiesWidget extends NoteContextAwareWidget {
async refreshWithNote(note: FNote) { async refreshWithNote(note: FNote) {
const pageUrl = note.getLabelValue("pageUrl"); const pageUrl = note.getLabelValue("pageUrl");
this.$pageUrl.attr("href", pageUrl).attr("title", pageUrl).text(pageUrl ?? ""); this.$pageUrl
.attr("href", pageUrl)
.attr("title", pageUrl)
.text(pageUrl ?? "");
} }
} }

View File

@@ -246,7 +246,7 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext().setNote(notePath); await appContext.tabManager.getActiveContext()?.setNote(notePath);
// Note the {{- notePathTitle}} in json file is not typo, it's unescaping // Note the {{- notePathTitle}} in json file is not typo, it's unescaping
// See https://www.i18next.com/translation-function/interpolation#unescape // See https://www.i18next.com/translation-function/interpolation#unescape
toastService.showMessage(t("search_definition.search_note_saved", { notePathTitle: await treeService.getNotePathTitle(notePath) })); toastService.showMessage(t("search_definition.search_note_saved", { notePathTitle: await treeService.getNotePathTitle(notePath) }));

View File

@@ -40,7 +40,6 @@ interface SimilarNote {
noteId: string; noteId: string;
} }
export default class SimilarNotesWidget extends NoteContextAwareWidget { export default class SimilarNotesWidget extends NoteContextAwareWidget {
private $similarNotesWrapper!: JQuery<HTMLElement>; private $similarNotesWrapper!: JQuery<HTMLElement>;

View File

@@ -185,7 +185,7 @@ export default class SwitchWidget extends NoteContextAwareWidget {
/** Gets or sets whether the switch is enabled. */ /** Gets or sets whether the switch is enabled. */
get canToggle() { get canToggle() {
return (!this.$switchButton.hasClass("disabled")); return !this.$switchButton.hasClass("disabled");
} }
set canToggle(isEnabled) { set canToggle(isEnabled) {

View File

@@ -79,7 +79,7 @@ export default class SyncStatusWidget extends BasicWidget {
settings: { settings: {
// TriliumNextTODO: narrow types and use TitlePlacement Type // TriliumNextTODO: narrow types and use TitlePlacement Type
titlePlacement: string; titlePlacement: string;
} };
constructor() { constructor() {
super(); super();
@@ -106,7 +106,6 @@ export default class SyncStatusWidget extends BasicWidget {
return; return;
} }
Tooltip.getOrCreateInstance(this.$widget.find(`.sync-status-${className}`)[0], { Tooltip.getOrCreateInstance(this.$widget.find(`.sync-status-${className}`)[0], {
html: true, html: true,
placement: this.settings.titlePlacement, placement: this.settings.titlePlacement,

View File

@@ -15,7 +15,7 @@ const TAB_CONTAINER_MIN_WIDTH = 24;
const TAB_CONTAINER_MAX_WIDTH = 240; const TAB_CONTAINER_MAX_WIDTH = 240;
const TAB_CONTAINER_LEFT_PADDING = 5; const TAB_CONTAINER_LEFT_PADDING = 5;
const NEW_TAB_WIDTH = 32; const NEW_TAB_WIDTH = 32;
const MIN_FILLER_WIDTH = (isDesktop ? 50 : 15); const MIN_FILLER_WIDTH = isDesktop ? 50 : 15;
const MARGIN_WIDTH = 5; const MARGIN_WIDTH = 5;
const TAB_SIZE_SMALL = 84; const TAB_SIZE_SMALL = 84;

View File

@@ -17,10 +17,10 @@ export default class TemplateSwitchWidget extends SwitchWidget {
super.doRender(); super.doRender();
this.switchOnName = t("template_switch.template"); this.switchOnName = t("template_switch.template");
this.switchOnTooltip = t("template_switch.toggle-on-hint"); this.switchOnTooltip = t("template_switch.toggle-on-hint");
this.switchOffName = t("template_switch.template"); this.switchOffName = t("template_switch.template");
this.switchOffTooltip = t("template_switch.toggle-off-hint"); this.switchOffTooltip = t("template_switch.toggle-off-hint");
this.$helpButton.attr("data-help-page", "template.html").show(); this.$helpButton.attr("data-help-page", "template.html").show();
} }

View File

@@ -55,8 +55,8 @@ const TPL = `<div class="toc-widget">
</div>`; </div>`;
interface Toc { interface Toc {
$toc: JQuery<HTMLElement>, $toc: JQuery<HTMLElement>;
headingCount: number headingCount: number;
} }
export default class TocWidget extends RightPanelWidget { export default class TocWidget extends RightPanelWidget {
@@ -89,8 +89,8 @@ export default class TocWidget extends RightPanelWidget {
return false; return false;
} }
const isHelpNote = (this.note.type === "doc" && this.note.noteId.startsWith("_help")); const isHelpNote = this.note.type === "doc" && this.note.noteId.startsWith("_help");
const isTextNote = (this.note.type === "text"); const isTextNote = this.note.type === "text";
const isNoteSupported = isTextNote || isHelpNote; const isNoteSupported = isTextNote || isHelpNote;
return isNoteSupported return isNoteSupported
@@ -156,7 +156,7 @@ export default class TocWidget extends RightPanelWidget {
const tocLabelValue = this.tocLabelValue; const tocLabelValue = this.tocLabelValue;
const visible = (tocLabelValue === "" || tocLabelValue === "show") || headingCount >= (options.getInt("minTocHeadings") ?? 0); const visible = tocLabelValue === "" || tocLabelValue === "show" || headingCount >= (options.getInt("minTocHeadings") ?? 0);
this.toggleInt(visible); this.toggleInt(visible);
if (this.noteContext?.viewScope) { if (this.noteContext?.viewScope) {
this.noteContext.viewScope.tocPreviousVisible = visible; this.noteContext.viewScope.tocPreviousVisible = visible;

View File

@@ -63,7 +63,7 @@ export default class AttachmentDetailTypeWidget extends TypeWidget {
this.$linksWrapper.empty().append( this.$linksWrapper.empty().append(
t("attachment_detail.owning_note"), t("attachment_detail.owning_note"),
(await linkService.createLink(this.noteId)), await linkService.createLink(this.noteId),
t("attachment_detail.you_can_also_open"), t("attachment_detail.you_can_also_open"),
await linkService.createLink(this.noteId, { await linkService.createLink(this.noteId, {
title: t("attachment_detail.list_of_all_attachments"), title: t("attachment_detail.list_of_all_attachments"),
@@ -74,7 +74,7 @@ export default class AttachmentDetailTypeWidget extends TypeWidget {
$helpButton $helpButton
); );
const attachment = (this.attachmentId) ? await froca.getAttachment(this.attachmentId, true) : null; const attachment = this.attachmentId ? await froca.getAttachment(this.attachmentId, true) : null;
if (!attachment) { if (!attachment) {
this.$wrapper.html("<strong>" + t("attachment_detail.attachment_deleted") + "</strong>"); this.$wrapper.html("<strong>" + t("attachment_detail.attachment_deleted") + "</strong>");

View File

@@ -66,7 +66,7 @@ export default class AttachmentListTypeWidget extends TypeWidget {
.text(t("attachment_list.upload_attachments")) .text(t("attachment_list.upload_attachments"))
.on("click", () => { .on("click", () => {
if (this.noteId) { if (this.noteId) {
this.triggerCommand("showUploadAttachmentsDialog", { noteId: this.noteId }) this.triggerCommand("showUploadAttachmentsDialog", { noteId: this.noteId });
} }
}), }),
$helpButton $helpButton

View File

@@ -36,9 +36,7 @@ export default class BookTypeWidget extends TypeWidget {
} }
async doRefresh(note: FNote) { async doRefresh(note: FNote) {
this.$helpNoChildren.toggle( this.$helpNoChildren.toggle(!this.note?.hasChildren() && this.note?.getAttributeValue("label", "viewType") !== "calendar");
!this.note?.hasChildren()
&& this.note?.getAttributeValue("label", "viewType") !== "calendar");
} }
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {

View File

@@ -59,9 +59,9 @@ const TPL = `
`; `;
interface CanvasContent { interface CanvasContent {
elements: ExcalidrawElement[], elements: ExcalidrawElement[];
files: BinaryFileData[], files: BinaryFileData[];
appState: Partial<AppState> appState: Partial<AppState>;
} }
interface AttachmentMetadata { interface AttachmentMetadata {
@@ -198,7 +198,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
} }
(window.process.env as any).PREACT = false; (window.process.env as any).PREACT = false;
const excalidraw = (await import("@excalidraw/excalidraw")); const excalidraw = await import("@excalidraw/excalidraw");
this.excalidrawLib = excalidraw; this.excalidrawLib = excalidraw;
const { createRoot } = await import("react-dom/client"); const { createRoot } = await import("react-dom/client");
@@ -476,7 +476,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
createExcalidrawReactApp(react: typeof React, excalidrawComponent: React.MemoExoticComponent<(props: ExcalidrawProps) => JSX.Element>) { createExcalidrawReactApp(react: typeof React, excalidrawComponent: React.MemoExoticComponent<(props: ExcalidrawProps) => JSX.Element>) {
const excalidrawWrapperRef = react.useRef<HTMLElement>(null); const excalidrawWrapperRef = react.useRef<HTMLElement>(null);
this.excalidrawWrapperRef = excalidrawWrapperRef; this.excalidrawWrapperRef = excalidrawWrapperRef;
const [dimensions, setDimensions] = react.useState<{ width?: number, height?: number}>({ const [dimensions, setDimensions] = react.useState<{ width?: number; height?: number }>({
width: undefined, width: undefined,
height: undefined height: undefined
}); });
@@ -541,10 +541,6 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
excalidrawAPI: (api: ExcalidrawImperativeAPI) => { excalidrawAPI: (api: ExcalidrawImperativeAPI) => {
this.excalidrawApi = api; this.excalidrawApi = api;
}, },
onPaste: (data: unknown, event: unknown) => {
console.log("Verbose: excalidraw internal paste. No trilium action implemented.", data, event);
return false;
},
onLibraryChange: () => { onLibraryChange: () => {
this.libraryChanged = true; this.libraryChanged = true;

View File

@@ -7,91 +7,78 @@ export function buildConfig() {
image: { image: {
styles: { styles: {
options: [ options: [
'inline', "inline",
'alignBlockLeft', "alignBlockLeft",
'alignCenter', "alignCenter",
'alignBlockRight', "alignBlockRight",
'alignLeft', "alignLeft",
'alignRight', "alignRight",
'full', // full and side are for BC since the old images have been created with these styles "full", // full and side are for BC since the old images have been created with these styles
'side' "side"
] ]
}, },
resizeOptions: [ resizeOptions: [
{ {
name: 'imageResize:original', name: "imageResize:original",
value: null, value: null,
icon: 'original' icon: "original"
}, },
{ {
name: 'imageResize:25', name: "imageResize:25",
value: '25', value: "25",
icon: 'small' icon: "small"
}, },
{ {
name: 'imageResize:50', name: "imageResize:50",
value: '50', value: "50",
icon: 'medium' icon: "medium"
}, },
{ {
name: 'imageResize:75', name: "imageResize:75",
value: '75', value: "75",
icon: 'medium' icon: "medium"
} }
], ],
toolbar: [ toolbar: [
// Image styles, see https://ckeditor.com/docs/ckeditor5/latest/features/images/images-styles.html#demo. // Image styles, see https://ckeditor.com/docs/ckeditor5/latest/features/images/images-styles.html#demo.
'imageStyle:inline', "imageStyle:inline",
'imageStyle:alignCenter', "imageStyle:alignCenter",
{ {
name: "imageStyle:wrapText", name: "imageStyle:wrapText",
title: "Wrap text", title: "Wrap text",
items: [ items: ["imageStyle:alignLeft", "imageStyle:alignRight"],
'imageStyle:alignLeft', defaultItem: "imageStyle:alignRight"
'imageStyle:alignRight',
],
defaultItem: 'imageStyle:alignRight'
}, },
{ {
name: "imageStyle:block", name: "imageStyle:block",
title: "Block align", title: "Block align",
items: [ items: ["imageStyle:alignBlockLeft", "imageStyle:alignBlockRight"],
'imageStyle:alignBlockLeft', defaultItem: "imageStyle:alignBlockLeft"
'imageStyle:alignBlockRight'
],
defaultItem: "imageStyle:alignBlockLeft",
}, },
'|', "|",
'imageResize:25', "imageResize:25",
'imageResize:50', "imageResize:50",
'imageResize:original', "imageResize:original",
'|', "|",
'toggleImageCaption' "toggleImageCaption"
], ],
upload: { upload: {
types: [ 'jpeg', 'png', 'gif', 'bmp', 'webp', 'tiff', 'svg', 'svg+xml', 'avif' ] types: ["jpeg", "png", "gif", "bmp", "webp", "tiff", "svg", "svg+xml", "avif"]
} }
}, },
heading: { heading: {
options: [ options: [
{ model: 'paragraph' as const, title: 'Paragraph', class: 'ck-heading_paragraph' }, { model: "paragraph" as const, title: "Paragraph", class: "ck-heading_paragraph" },
// // heading1 is not used since that should be a note's title // // heading1 is not used since that should be a note's title
{ model: 'heading2' as const, view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' }, { model: "heading2" as const, view: "h2", title: "Heading 2", class: "ck-heading_heading2" },
{ model: 'heading3' as const, view: 'h3', title: 'Heading 3', class: 'ck-heading_heading3' }, { model: "heading3" as const, view: "h3", title: "Heading 3", class: "ck-heading_heading3" },
{ model: 'heading4' as const, view: 'h4', title: 'Heading 4', class: 'ck-heading_heading4' }, { model: "heading4" as const, view: "h4", title: "Heading 4", class: "ck-heading_heading4" },
{ model: 'heading5' as const, view: 'h5', title: 'Heading 5', class: 'ck-heading_heading5' }, { model: "heading5" as const, view: "h5", title: "Heading 5", class: "ck-heading_heading5" },
{ model: 'heading6' as const, view: 'h6', title: 'Heading 6', class: 'ck-heading_heading6' } { model: "heading6" as const, view: "h6", title: "Heading 6", class: "ck-heading_heading6" }
] ]
}, },
table: { table: {
contentToolbar: [ contentToolbar: ["tableColumn", "tableRow", "mergeTableCells", "tableProperties", "tableCellProperties", "toggleTableCaption"]
'tableColumn',
'tableRow',
'mergeTableCells',
'tableProperties',
'tableCellProperties',
'toggleTableCaption'
]
}, },
list: { list: {
properties: { properties: {
@@ -101,17 +88,17 @@ export function buildConfig() {
} }
}, },
link: { link: {
defaultProtocol: 'https://', defaultProtocol: "https://",
allowedProtocols: ALLOWED_PROTOCOLS allowedProtocols: ALLOWED_PROTOCOLS
}, },
// This value must be kept in sync with the language defined in webpack.config.js. // This value must be kept in sync with the language defined in webpack.config.js.
language: 'en' language: "en"
} };
} }
export function buildToolbarConfig(isClassicToolbar: boolean) { export function buildToolbarConfig(isClassicToolbar: boolean) {
if (isClassicToolbar) { if (isClassicToolbar) {
const multilineToolbar = utils.isDesktop() && options.get("textNoteEditorMultilineToolbar") === "true" const multilineToolbar = utils.isDesktop() && options.get("textNoteEditorMultilineToolbar") === "true";
return buildClassicToolbar(multilineToolbar); return buildClassicToolbar(multilineToolbar);
} else { } else {
return buildFloatingToolbar(); return buildFloatingToolbar();
@@ -123,101 +110,92 @@ function buildClassicToolbar(multilineToolbar: boolean) {
return { return {
toolbar: { toolbar: {
items: [ items: [
'heading', 'fontSize', "heading",
'|', "fontSize",
'bold', 'italic', "|",
"bold",
"italic",
{ {
label: "Text formatting", label: "Text formatting",
icon: "text", icon: "text",
items: [ items: ["underline", "strikethrough", "superscript", "subscript", "code"]
'underline',
'strikethrough',
'superscript',
'subscript',
'code',
],
}, },
'|', "|",
'fontColor', 'fontBackgroundColor', 'removeFormat', "fontColor",
'|', "fontBackgroundColor",
'bulletedList', 'numberedList', 'todoList', "removeFormat",
'|', "|",
'blockQuote', 'insertTable', 'codeBlock', 'footnote', "bulletedList",
"numberedList",
"todoList",
"|",
"blockQuote",
"insertTable",
"codeBlock",
"footnote",
{ {
label: "Insert", label: "Insert",
icon: "plus", icon: "plus",
items: [ items: ["imageUpload", "|", "link", "internallink", "includeNote", "|", "specialCharacters", "math", "mermaid", "horizontalLine", "pageBreak"]
'imageUpload',
'|',
'link',
'internallink',
'includeNote',
'|',
'specialCharacters',
'math',
'mermaid',
'horizontalLine',
'pageBreak'
]
}, },
'|', "|",
'outdent', 'indent', "outdent",
'|', "indent",
'markdownImport', 'cuttonote', 'findAndReplace' "|",
"markdownImport",
"cuttonote",
"findAndReplace"
], ],
shouldNotGroupWhenFull: multilineToolbar shouldNotGroupWhenFull: multilineToolbar
} }
} };
} }
function buildFloatingToolbar() { function buildFloatingToolbar() {
return { return {
toolbar: { toolbar: {
items: [ items: [
'fontSize', "fontSize",
'bold', "bold",
'italic', "italic",
'underline', "underline",
'strikethrough', "strikethrough",
'superscript', "superscript",
'subscript', "subscript",
'fontColor', "fontColor",
'fontBackgroundColor', "fontBackgroundColor",
'code', "code",
'link', "link",
'removeFormat', "removeFormat",
'internallink', "internallink",
'cuttonote' "cuttonote"
] ]
}, },
blockToolbar: [ blockToolbar: [
'heading', "heading",
'|', "|",
'bulletedList', 'numberedList', 'todoList', "bulletedList",
'|', "numberedList",
'blockQuote', 'codeBlock', 'insertTable', "todoList",
'footnote', "|",
{ "blockQuote",
label: "Insert", "codeBlock",
icon: "plus", "insertTable",
items: [ "footnote",
'internallink', {
'includeNote', label: "Insert",
'|', icon: "plus",
'math', items: ["internallink", "includeNote", "|", "math", "mermaid", "horizontalLine", "pageBreak"]
'mermaid', },
'horizontalLine', "|",
'pageBreak' "outdent",
] "indent",
}, "|",
'|', "imageUpload",
'outdent', 'indent', "markdownImport",
'|', "specialCharacters",
'imageUpload', "findAndReplace"
'markdownImport', ]
'specialCharacters',
'findAndReplace'
]
}; };
} }

View File

@@ -15,7 +15,7 @@ import CodeMimeTypesOptions from "./options/code_notes/code_mime_types.js";
import ImageOptions from "./options/images/images.js"; import ImageOptions from "./options/images/images.js";
import SpellcheckOptions from "./options/spellcheck.js"; import SpellcheckOptions from "./options/spellcheck.js";
import PasswordOptions from "./options/password/password.js"; import PasswordOptions from "./options/password/password.js";
import ProtectedSessionTimeoutOptions from "./options/password/protected_session_timeout.js" import ProtectedSessionTimeoutOptions from "./options/password/protected_session_timeout.js";
import EtapiOptions from "./options/etapi.js"; import EtapiOptions from "./options/etapi.js";
import BackupOptions from "./options/backup.js"; import BackupOptions from "./options/backup.js";
import SyncOptions from "./options/sync.js"; import SyncOptions from "./options/sync.js";

View File

@@ -313,7 +313,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.watchdog?.editor.editing.view.focus(); this.watchdog?.editor.editing.view.focus();
} }
show() {} show() { }
getEditor() { getEditor() {
return this.watchdog?.editor; return this.watchdog?.editor;
@@ -360,14 +360,14 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.addTextToEditor(text); this.addTextToEditor(text);
} }
async addLink(notePath, linkTitle) { async addLink(notePath, linkTitle, externalLink = false) {
await this.initialized; await this.initialized;
if (linkTitle) { if (linkTitle) {
if (this.hasSelection()) { if (this.hasSelection()) {
this.watchdog.editor.execute("link", `#${notePath}`); this.watchdog.editor.execute("link", externalLink ? `${notePath}` : `#${notePath}`);
} else { } else {
await this.addLinkToEditor(`#${notePath}`, linkTitle); await this.addLinkToEditor(externalLink ? `${notePath}` : `#${notePath}`, linkTitle);
} }
} else { } else {
this.watchdog.editor.execute("referenceLink", { href: "#" + notePath }); this.watchdog.editor.execute("referenceLink", { href: "#" + notePath });

View File

@@ -1,7 +1,7 @@
import { GPX, Marker, type LatLng, type LeafletMouseEvent } from "leaflet"; import { GPX, Marker, type LatLng, type LeafletMouseEvent } from "leaflet";
import type FNote from "../../entities/fnote.js"; import type FNote from "../../entities/fnote.js";
import GeoMapWidget, { type InitCallback, type Leaflet } from "../geo_map.js"; import GeoMapWidget, { type InitCallback, type Leaflet } from "../geo_map.js";
import TypeWidget from "./type_widget.js" import TypeWidget from "./type_widget.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import toastService from "../../services/toast.js"; import toastService from "../../services/toast.js";
import dialogService from "../../services/dialog.js"; import dialogService from "../../services/dialog.js";
@@ -75,21 +75,21 @@ const TPL = `\
const LOCATION_ATTRIBUTE = "geolocation"; const LOCATION_ATTRIBUTE = "geolocation";
const CHILD_NOTE_ICON = "bx bx-pin"; const CHILD_NOTE_ICON = "bx bx-pin";
const DEFAULT_COORDINATES: [ number, number ] = [ 3.878638227135724, 446.6630455551659 ]; const DEFAULT_COORDINATES: [number, number] = [3.878638227135724, 446.6630455551659];
const DEFAULT_ZOOM = 2; const DEFAULT_ZOOM = 2;
interface MapData { interface MapData {
view?: { view?: {
center?: LatLng | [ number, number ]; center?: LatLng | [number, number];
zoom?: number; zoom?: number;
} };
} }
// TODO: Deduplicate // TODO: Deduplicate
interface CreateChildResponse { interface CreateChildResponse {
note: { note: {
noteId: string; noteId: string;
} };
} }
enum State { enum State {
@@ -220,7 +220,7 @@ export default class GeoMapTypeWidget extends TypeWidget {
return; return;
} }
const [ lat, lng ] = latLng.split(",", 2).map((el) => parseFloat(el)); const [lat, lng] = latLng.split(",", 2).map((el) => parseFloat(el));
const L = this.L; const L = this.L;
const icon = this.#buildIcon(note.getIcon(), note.getColorClass(), note.title); const icon = this.#buildIcon(note.getIcon(), note.getColorClass(), note.title);
@@ -228,10 +228,10 @@ export default class GeoMapTypeWidget extends TypeWidget {
icon, icon,
draggable: true, draggable: true,
autoPan: true, autoPan: true,
autoPanSpeed: 5, autoPanSpeed: 5
}) })
.addTo(map) .addTo(map)
.on("moveend", e => { .on("moveend", (e) => {
this.moveMarker(note.noteId, (e.target as Marker).getLatLng()); this.moveMarker(note.noteId, (e.target as Marker).getLatLng());
}); });
marker.on("mousedown", ({ originalEvent }) => { marker.on("mousedown", ({ originalEvent }) => {
@@ -264,9 +264,9 @@ export default class GeoMapTypeWidget extends TypeWidget {
<img class="icon-shadow" src="${asset_path}/node_modules/leaflet/dist/images/marker-shadow.png" /> <img class="icon-shadow" src="${asset_path}/node_modules/leaflet/dist/images/marker-shadow.png" />
<span class="bx ${bxIconClass} ${colorClass}"></span> <span class="bx ${bxIconClass} ${colorClass}"></span>
<span class="title-label">${title}</span>`, <span class="title-label">${title}</span>`,
iconSize: [ 25, 41 ], iconSize: [25, 41],
iconAnchor: [ 12, 41 ] iconAnchor: [12, 41]
}) });
} }
#changeState(newState: State) { #changeState(newState: State) {
@@ -296,7 +296,7 @@ export default class GeoMapTypeWidget extends TypeWidget {
} }
async moveMarker(noteId: string, latLng: LatLng | null) { async moveMarker(noteId: string, latLng: LatLng | null) {
const value = (latLng ? [latLng.lat, latLng.lng].join(",") : ""); const value = latLng ? [latLng.lat, latLng.lng].join(",") : "";
await attributes.setLabel(noteId, LOCATION_ATTRIBUTE, value); await attributes.setLabel(noteId, LOCATION_ATTRIBUTE, value);
} }
@@ -361,7 +361,7 @@ export default class GeoMapTypeWidget extends TypeWidget {
// If any of note has its location attribute changed. // If any of note has its location attribute changed.
// TODO: Should probably filter by parent here as well. // TODO: Should probably filter by parent here as well.
const attributeRows = loadResults.getAttributeRows(); const attributeRows = loadResults.getAttributeRows();
if (attributeRows.find((at) => [ LOCATION_ATTRIBUTE, "color" ].includes(at.name ?? ""))) { if (attributeRows.find((at) => [LOCATION_ATTRIBUTE, "color"].includes(at.name ?? ""))) {
this.#reloadMarkers(); this.#reloadMarkers();
} }
} }

View File

@@ -259,7 +259,7 @@ export default class MindMapWidget extends TypeWidget {
return await this.mind.exportSvg().text(); return await this.mind.exportSvg().text();
} }
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded"> ) { async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (this.noteId && loadResults.isNoteReloaded(this.noteId)) { if (this.noteId && loadResults.isNoteReloaded(this.noteId)) {
this.refresh(); this.refresh();
} }

View File

@@ -60,7 +60,7 @@ interface Theme {
val: string; val: string;
} }
type Response = Record<string, Theme[]> type Response = Record<string, Theme[]>;
/** /**
* Contains appearance settings for code blocks within text notes, such as the theme for the syntax highlighter. * Contains appearance settings for code blocks within text notes, such as the theme for the syntax highlighter.

View File

@@ -32,7 +32,7 @@ const TPL = `
</label> </label>
</div> </div>
<button class="btn btn-micro restart-app-button">${t("electron_integration.restart-app-button")}</button> <button class="btn btn-secondary btn-micro restart-app-button">${t("electron_integration.restart-app-button")}</button>
</div> </div>
`; `;

View File

@@ -94,10 +94,12 @@ export default class ThemeOptions extends OptionsWidget {
this.$themeSelect.empty(); this.$themeSelect.empty();
for (const theme of themes) { for (const theme of themes) {
this.$themeSelect.append($("<option>") this.$themeSelect.append(
.attr("value", theme.val) $("<option>")
.attr("data-note-id", theme.noteId || "") .attr("value", theme.val)
.text(theme.title)); .attr("data-note-id", theme.noteId || "")
.text(theme.title)
);
} }
this.$themeSelect.val(options.theme); this.$themeSelect.val(options.theme);

View File

@@ -97,9 +97,8 @@ export default class CodeMimeTypesOptions extends OptionsWidget {
const checkbox = $(`<label class="tn-checkbox">`) const checkbox = $(`<label class="tn-checkbox">`)
.append($('<input type="checkbox" class="form-check-input">').attr("id", id).attr("data-mime-type", mimeType.mime).prop("checked", mimeType.enabled)) .append($('<input type="checkbox" class="form-check-input">').attr("id", id).attr("data-mime-type", mimeType.mime).prop("checked", mimeType.enabled))
.on("change", () => this.save()) .on("change", () => this.save())
.append(mimeType.title) .append(mimeType.title);
return $("<li>") return $("<li>").append(checkbox);
.append(checkbox);
} }
} }

View File

@@ -102,13 +102,20 @@ export const DEFAULT_ALLOWED_TAGS = [
]; ];
const TPL = ` const TPL = `
<div class="options-section"> <div class="html-import-tags-settings options-section">
<style>
.html-import-tags-settings .allowed-html-tags {
height: 150px;
margin-bottom: 12px;
font-family: monospace;
}
</style>
<h4>${t("import.html_import_tags.title")}</h4> <h4>${t("import.html_import_tags.title")}</h4>
<p>${t("import.html_import_tags.description")}</p> <p>${t("import.html_import_tags.description")}</p>
<textarea class="allowed-html-tags form-control" style="height: 150px; font-family: monospace;" <textarea class="allowed-html-tags form-control" spellcheck="false"
placeholder="${t("import.html_import_tags.placeholder")}"></textarea> placeholder="${t("import.html_import_tags.placeholder")}"></textarea>
<div> <div>
<button class="btn btn-sm btn-secondary reset-to-default"> <button class="btn btn-sm btn-secondary reset-to-default">

View File

@@ -31,7 +31,7 @@ export default class NoteErasureTimeoutOptions extends TimeSelector {
const $timeSelector = this.$widget; const $timeSelector = this.$widget;
// inject TimeSelector widget template // inject TimeSelector widget template
this.$widget = $(TPL); this.$widget = $(TPL);
this.$widget.find("#time-selector-placeholder").replaceWith($timeSelector) this.$widget.find("#time-selector-placeholder").replaceWith($timeSelector);
this.$eraseDeletedNotesButton = this.$widget.find("#erase-deleted-notes-now-button"); this.$eraseDeletedNotesButton = this.$widget.find("#erase-deleted-notes-now-button");

View File

@@ -26,6 +26,6 @@ export default class RevisionsSnapshotIntervalOptions extends TimeSelector {
const $timeSelector = this.$widget; const $timeSelector = this.$widget;
// inject TimeSelector widget template // inject TimeSelector widget template
this.$widget = $(TPL); this.$widget = $(TPL);
this.$widget.find("#time-selector-placeholder").replaceWith($timeSelector) this.$widget.find("#time-selector-placeholder").replaceWith($timeSelector);
} }
} }

View File

@@ -29,8 +29,8 @@ export default class ShareSettingsOptions extends OptionsWidget {
this.$widget = $(TPL); this.$widget = $(TPL);
this.contentSized(); this.contentSized();
this.$shareRootCheck = this.$widget.find('.share-root-check'); this.$shareRootCheck = this.$widget.find(".share-root-check");
this.$shareRootStatus = this.$widget.find('.share-root-status'); this.$shareRootStatus = this.$widget.find(".share-root-status");
// Add change handlers for both checkboxes // Add change handlers for both checkboxes
this.$widget.find('input[type="checkbox"]').on("change", (e: JQuery.ChangeEvent) => { this.$widget.find('input[type="checkbox"]').on("change", (e: JQuery.ChangeEvent) => {
@@ -38,7 +38,7 @@ export default class ShareSettingsOptions extends OptionsWidget {
// Show/hide share root status section based on redirectBareDomain checkbox // Show/hide share root status section based on redirectBareDomain checkbox
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
if (target.name === 'redirectBareDomain') { if (target.name === "redirectBareDomain") {
this.$shareRootCheck.toggle(target.checked); this.$shareRootCheck.toggle(target.checked);
if (target.checked) { if (target.checked) {
this.checkShareRoot(); this.checkShareRoot();
@@ -47,7 +47,7 @@ export default class ShareSettingsOptions extends OptionsWidget {
}); });
// Add click handler for check share root button // Add click handler for check share root button
this.$widget.find('.check-share-root').on("click", () => this.checkShareRoot()); this.$widget.find(".check-share-root").on("click", () => this.checkShareRoot());
} }
async optionsLoaded(options: OptionMap) { async optionsLoaded(options: OptionMap) {
@@ -62,28 +62,26 @@ export default class ShareSettingsOptions extends OptionsWidget {
} }
async checkShareRoot() { async checkShareRoot() {
const $button = this.$widget.find('.check-share-root'); const $button = this.$widget.find(".check-share-root");
$button.prop('disabled', true); $button.prop("disabled", true);
try { try {
const shareRootNotes = await searchService.searchForNotes("#shareRoot"); const shareRootNotes = await searchService.searchForNotes("#shareRoot");
const sharedShareRootNote = shareRootNotes.find(note => note.isShared()); const sharedShareRootNote = shareRootNotes.find((note) => note.isShared());
if (sharedShareRootNote) { if (sharedShareRootNote) {
this.$shareRootStatus this.$shareRootStatus
.removeClass('text-danger') .removeClass("text-danger")
.addClass('text-success') .addClass("text-success")
.text(t("share.share_root_found", {noteTitle: sharedShareRootNote.title})); .text(t("share.share_root_found", { noteTitle: sharedShareRootNote.title }));
} else { } else {
this.$shareRootStatus this.$shareRootStatus
.removeClass('text-success') .removeClass("text-success")
.addClass('text-danger') .addClass("text-danger")
.text(shareRootNotes.length > 0 .text(shareRootNotes.length > 0 ? t("share.share_root_not_shared", { noteTitle: shareRootNotes[0].title }) : t("share.share_root_not_found"));
? t("share.share_root_not_shared", {noteTitle: shareRootNotes[0].title})
: t("share.share_root_not_found"));
} }
} finally { } finally {
$button.prop('disabled', false); $button.prop("disabled", false);
} }
} }

View File

@@ -25,6 +25,6 @@ export default class ProtectedSessionTimeoutOptions extends TimeSelector {
const $timeSelector = this.$widget; const $timeSelector = this.$widget;
// inject TimeSelector widget template // inject TimeSelector widget template
this.$widget = $(TPL); this.$widget = $(TPL);
this.$widget.find("#time-selector-placeholder").replaceWith($timeSelector) this.$widget.find("#time-selector-placeholder").replaceWith($timeSelector);
} }
} }

View File

@@ -54,7 +54,7 @@ export default class TimeSelector extends OptionsWidget {
this.optionValueId = options.optionValueId; this.optionValueId = options.optionValueId;
this.optionTimeScaleId = options.optionTimeScaleId; this.optionTimeScaleId = options.optionTimeScaleId;
this.includedTimeScales = options.includedTimeScales || new Set(["seconds", "minutes", "hours", "days"]); this.includedTimeScales = options.includedTimeScales || new Set(["seconds", "minutes", "hours", "days"]);
this.minimumSeconds = options.minimumSeconds || 0 this.minimumSeconds = options.minimumSeconds || 0;
} }
doRender() { doRender() {
@@ -131,10 +131,10 @@ export default class TimeSelector extends OptionsWidget {
private setInternalTimeInSeconds(time: number) { private setInternalTimeInSeconds(time: number) {
if (time < this.minimumSeconds) { if (time < this.minimumSeconds) {
toastService.showError(t("time_selector.minimum_input", {minimumSeconds: this.minimumSeconds})); toastService.showError(t("time_selector.minimum_input", { minimumSeconds: this.minimumSeconds }));
return this.internalTimeInSeconds = this.minimumSeconds; return (this.internalTimeInSeconds = this.minimumSeconds);
} }
return this.internalTimeInSeconds = time; return (this.internalTimeInSeconds = time);
} }
} }

View File

@@ -87,7 +87,7 @@ const TPL = `
`; `;
function buildTasks(tasks: FTask[]) { function buildTasks(tasks: FTask[]) {
let html = ''; let html = "";
const now = dayjs(); const now = dayjs();
const dateFormat = "DD-MM-YYYY"; const dateFormat = "DD-MM-YYYY";
@@ -137,7 +137,9 @@ export default class TaskListWidget extends TypeWidget {
private $taskContainer!: JQuery<HTMLElement>; private $taskContainer!: JQuery<HTMLElement>;
private $addNewTask!: JQuery<HTMLElement>; private $addNewTask!: JQuery<HTMLElement>;
static getType() { return "taskList" } static getType() {
return "taskList";
}
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
@@ -231,19 +233,18 @@ export default class TaskListWidget extends TypeWidget {
return []; return [];
} }
return (await froca.getTasks(this.noteId)) return (await froca.getTasks(this.noteId)).toSorted((a, b) => {
.toSorted((a, b) => { // Sort by due date, closest date first.
// Sort by due date, closest date first. if (!a.dueDate) {
if (!a.dueDate) { return 1;
return 1; }
}
if (!b.dueDate) { if (!b.dueDate) {
return -1; return -1;
} }
return a.dueDate.localeCompare(b.dueDate, "en"); return a.dueDate.localeCompare(b.dueDate, "en");
}); });
} }
async doRefresh(note: FNote) { async doRefresh(note: FNote) {

View File

@@ -68,7 +68,7 @@ const TPL = `
interface CreateChildResponse { interface CreateChildResponse {
note: { note: {
noteId: string; noteId: string;
} };
} }
export default class CalendarView extends ViewMode { export default class CalendarView extends ViewMode {
@@ -126,7 +126,7 @@ export default class CalendarView extends ViewMode {
weekNumbers: this.parentNote.hasAttribute("label", "calendar:weekNumbers"), weekNumbers: this.parentNote.hasAttribute("label", "calendar:weekNumbers"),
locale: await CalendarView.#getLocale(), locale: await CalendarView.#getLocale(),
height: "100%", height: "100%",
eventContent: (e => { eventContent: (e) => {
let html = ""; let html = "";
const { iconClass, promotedAttributes } = e.event.extendedProps; const { iconClass, promotedAttributes } = e.event.extendedProps;
@@ -138,7 +138,7 @@ export default class CalendarView extends ViewMode {
// Promoted attributes // Promoted attributes
if (promotedAttributes) { if (promotedAttributes) {
for (const [ name, value ] of Object.entries(promotedAttributes)) { for (const [name, value] of Object.entries(promotedAttributes)) {
html += `\ html += `\
<div class="promoted-attribute"> <div class="promoted-attribute">
<span class="promoted-attribute-name">${name}</span>: <span class="promoted-attribute-value">${value}</span> <span class="promoted-attribute-name">${name}</span>: <span class="promoted-attribute-value">${value}</span>
@@ -147,7 +147,7 @@ export default class CalendarView extends ViewMode {
} }
return { html }; return { html };
}), },
dateClick: async (e) => { dateClick: async (e) => {
if (!this.isCalendarRoot) { if (!this.isCalendarRoot) {
return; return;
@@ -155,7 +155,7 @@ export default class CalendarView extends ViewMode {
const note = await date_notes.getDayNote(e.dateStr); const note = await date_notes.getDayNote(e.dateStr);
if (note) { if (note) {
appContext.tabManager.getActiveContext().setNote(note.noteId); appContext.tabManager.getActiveContext()?.setNote(note.noteId);
} }
} }
}); });
@@ -260,7 +260,7 @@ export default class CalendarView extends ViewMode {
// TODO: Deduplicate get type. // TODO: Deduplicate get type.
const dateNotesForMonth = await server.get<Record<string, string>>(`special-notes/notes-for-month/${month}?calendarRoot=${this.parentNote.noteId}`); const dateNotesForMonth = await server.get<Record<string, string>>(`special-notes/notes-for-month/${month}?calendarRoot=${this.parentNote.noteId}`);
const dateNoteIds = Object.values(dateNotesForMonth); const dateNoteIds = Object.values(dateNotesForMonth);
allDateNoteIds = [ ...allDateNoteIds, ...dateNoteIds ]; allDateNoteIds = [...allDateNoteIds, ...dateNoteIds];
} }
// Request all the date notes. // Request all the date notes.
@@ -379,7 +379,7 @@ export default class CalendarView extends ViewMode {
const result: Record<string, string> = {}; const result: Record<string, string> = {};
for (const promotedAttribute of filteredPromotedAttributes) { for (const promotedAttribute of filteredPromotedAttributes) {
const [ type, name ] = promotedAttribute.name.split(":", 2); const [type, name] = promotedAttribute.name.split(":", 2);
const definition = promotedAttribute.getDefinition(); const definition = promotedAttribute.getDefinition();
if (definition.multiplicity !== "single") { if (definition.multiplicity !== "single") {
@@ -411,7 +411,7 @@ export default class CalendarView extends ViewMode {
if (customTitleValue.startsWith("#")) { if (customTitleValue.startsWith("#")) {
const labelValue = note.getAttributeValue("label", attributeName); const labelValue = note.getAttributeValue("label", attributeName);
if (labelValue) { if (labelValue) {
return [ labelValue ]; return [labelValue];
} }
} else if (allowRelations && customTitleValue.startsWith("~")) { } else if (allowRelations && customTitleValue.startsWith("~")) {
const relations = note.getRelations(attributeName); const relations = note.getRelations(attributeName);
@@ -432,7 +432,7 @@ export default class CalendarView extends ViewMode {
} }
} }
return [ note.title ]; return [note.title];
} }
static #formatDateToLocalISO(date: Date | null | undefined) { static #formatDateToLocalISO(date: Date | null | undefined) {
@@ -442,7 +442,7 @@ export default class CalendarView extends ViewMode {
const offset = date.getTimezoneOffset(); const offset = date.getTimezoneOffset();
const localDate = new Date(date.getTime() - offset * 60 * 1000); const localDate = new Date(date.getTime() - offset * 60 * 1000);
return localDate.toISOString().split('T')[0]; return localDate.toISOString().split("T")[0];
} }
static #offsetDate(date: Date | string | null | undefined, offset: number) { static #offsetDate(date: Date | string | null | undefined, offset: number) {

View File

@@ -75,7 +75,6 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.calendar-dropdown-widget .calendar-week span { .calendar-dropdown-widget .calendar-week span {
flex-direction: column; flex-direction: column;
flex: 0 0 14.28%; flex: 0 0 14.28%;

View File

@@ -165,6 +165,11 @@ span[style] {
overflow: unset !important; overflow: unset !important;
} }
/* TODO: This will break once we translate the language */
.ck-content pre[data-language="Auto-detected"]:after {
display: none !important;
}
/* /*
* Code note specific fixes. * Code note specific fixes.
*/ */
@@ -202,9 +207,8 @@ span[style] {
@supports selector(.todo-list__label__description:has(*)) and (height: 1lh) { @supports selector(.todo-list__label__description:has(*)) and (height: 1lh) {
.note-detail-printable .todo-list__label__description { .note-detail-printable .todo-list__label__description {
/* The percentage of the line height that the check box occupies */ /* The percentage of the line height that the check box occupies */
--box-ratio: .75; --box-ratio: 0.75;
/* The size of the gap between the check box and the caption */ /* The size of the gap between the check box and the caption */
--box-text-gap: 0.25em; --box-text-gap: 0.25em;
@@ -301,9 +305,16 @@ blockquote {
pre > code { pre > code {
widows: 6; widows: 6;
orphans: 6; orphans: 6;
overflow: auto;
white-space: pre-wrap !important;
} }
h1, h2, h3, h4, h5, h6 { h1,
h2,
h3,
h4,
h5,
h6 {
page-break-after: avoid; page-break-after: avoid;
break-after: avoid; break-after: avoid;
} }

View File

@@ -32,7 +32,7 @@
--cmd-button-icon-color: white; --cmd-button-icon-color: white;
--cmd-button-keyboard-shortcut-background: #0000004d; --cmd-button-keyboard-shortcut-background: #0000004d;
--cmd-button-keyboard-shortcut-color: white; --cmd-button-keyboard-shortcut-color: white;
--cmd-button-disabled-opacity: .5; --cmd-button-disabled-opacity: 0.5;
--icon-button-color: currentColor; --icon-button-color: currentColor;
--icon-button-hover-background: var(--hover-item-background-color); --icon-button-hover-background: var(--hover-item-background-color);
@@ -41,7 +41,7 @@
--muted-text-color: #bbb; --muted-text-color: #bbb;
--input-background-color: #ffffff12; --input-background-color: #ffffff12;
--input-text-color: #ffffffc7; --input-text-color: #ffffffc7;
--input-placeholder-color: #b7b7b782; --input-placeholder-color: #b7b7b782;
--input-selection-background: gray; --input-selection-background: gray;
--input-selection-text-color: white; --input-selection-text-color: white;

View File

@@ -32,7 +32,7 @@
--cmd-button-icon-color: black; --cmd-button-icon-color: black;
--cmd-button-keyboard-shortcut-background: #00000017; --cmd-button-keyboard-shortcut-background: #00000017;
--cmd-button-keyboard-shortcut-color: black; --cmd-button-keyboard-shortcut-color: black;
--cmd-button-disabled-opacity: .5; --cmd-button-disabled-opacity: 0.5;
--icon-button-color: currentColor; --icon-button-color: currentColor;
--icon-button-hover-background: var(--hover-item-background-color); --icon-button-hover-background: var(--hover-item-background-color);

View File

@@ -61,7 +61,7 @@
--help-backdrop-blur: 10px; --help-backdrop-blur: 10px;
--icon-button-size: 32px; --icon-button-size: 32px;
--icon-button-icon-ratio: .65; --icon-button-icon-ratio: 0.65;
/* Theme capabilities */ /* Theme capabilities */
--tab-note-icons: true; --tab-note-icons: true;

View File

@@ -30,11 +30,11 @@ button.btn.btn-primary:active,
button.btn.btn-secondary:active, button.btn.btn-secondary:active,
button.btn.btn-sm:not(.select-button):active, button.btn.btn-sm:not(.select-button):active,
button.btn.btn-success:active { button.btn.btn-success:active {
opacity: .85; opacity: 0.85;
box-shadow: unset; box-shadow: unset;
background: var(--cmd-button-background-color) !important; background: var(--cmd-button-background-color) !important;
color: var(--cmd-button-text-color) !important; color: var(--cmd-button-text-color) !important;
transform: scale(.95); transform: scale(0.95);
} }
button.btn.btn-primary:disabled, button.btn.btn-primary:disabled,
@@ -57,7 +57,7 @@ button.btn.btn-secondary span.bx,
button.btn.btn-sm span.bx, button.btn.btn-sm span.bx,
button.btn.btn-success span.bx { button.btn.btn-success span.bx {
color: var(--cmd-button-icon-color); color: var(--cmd-button-icon-color);
padding-right: .35em; padding-right: 0.35em;
font-size: 1.2em; font-size: 1.2em;
} }
@@ -66,12 +66,12 @@ button.btn.btn-primary kbd,
button.btn.btn-secondary kbd, button.btn.btn-secondary kbd,
button.btn.btn-sm kbd, button.btn.btn-sm kbd,
button.btn.btn-success kbd { button.btn.btn-success kbd {
margin-left: .5em; margin-left: 0.5em;
background: var(--cmd-button-keyboard-shortcut-background); background: var(--cmd-button-keyboard-shortcut-background);
color: var(--cmd-button-keyboard-shortcut-color); color: var(--cmd-button-keyboard-shortcut-color);
font-size: .6em; font-size: 0.6em;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: .5pt; letter-spacing: 0.5pt;
} }
/* /*
@@ -101,7 +101,7 @@ button.btn.btn-success kbd {
--icon-button-hover-background: var(--tab-close-button-hover-background); --icon-button-hover-background: var(--tab-close-button-hover-background);
--icon-button-hover-color: var(--tab-close-button-hover-color); --icon-button-hover-color: var(--tab-close-button-hover-color);
--icon-button-size: 24px; --icon-button-size: 24px;
--icon-button-icon-ratio: .8; --icon-button-icon-ratio: 0.8;
border-radius: 50%; border-radius: 50%;
} }
@@ -124,7 +124,7 @@ button.btn.btn-success kbd {
:root .icon-action:not(.global-menu-button):active::before, :root .icon-action:not(.global-menu-button):active::before,
:root .tn-tool-button:active::before { :root .tn-tool-button:active::before {
transform: scale(.85); transform: scale(0.85);
} }
:root .icon-action:not(.global-menu-button):focus-visible, :root .icon-action:not(.global-menu-button):focus-visible,
@@ -137,7 +137,7 @@ button.btn.btn-success kbd {
*/ */
input:disabled { input:disabled {
opacity: .33; opacity: 0.33;
} }
/* Text boxes */ /* Text boxes */
@@ -190,8 +190,9 @@ textarea:focus,
outline-offset: 0; outline-offset: 0;
background: var(--input-focus-background); background: var(--input-focus-background);
color: var(--input-focus-color); color: var(--input-focus-color);
transition: outline-color 50ms linear, transition:
outline-offset 200ms ease-out; outline-color 50ms linear,
outline-offset 200ms ease-out;
} }
input::placeholder, input::placeholder,
@@ -229,12 +230,12 @@ input::selection,
outline: 3px solid var(--input-focus-outline-color); outline: 3px solid var(--input-focus-outline-color);
outline-offset: 0; outline-offset: 0;
background: var(--input-focus-background); background: var(--input-focus-background);
transition: outline-color 50ms linear, transition:
outline-offset 200ms ease-out; outline-color 50ms linear,
outline-offset 200ms ease-out;
} }
.input-group input .input-group input .input-group input:hover,
.input-group input:hover,
.input-group input:focus, .input-group input:focus,
.input-group .form-control, .input-group .form-control,
.input-group .form-control:hover, .input-group .form-control:hover,
@@ -277,7 +278,7 @@ input::selection,
} }
.input-group a.disabled { .input-group a.disabled {
opacity: .5; opacity: 0.5;
/* Workaround to set the "background" property. */ /* Workaround to set the "background" property. */
--button-disabled-background-color: transparent; --button-disabled-background-color: transparent;
--button-disabled-text-color: var(--input-action-button-color); --button-disabled-text-color: var(--input-action-button-color);
@@ -319,8 +320,7 @@ select.form-control,
outline: 3px solid transparent; outline: 3px solid transparent;
outline-offset: 6px; outline-offset: 6px;
padding-right: calc(15px + 1.5rem); padding-right: calc(15px + 1.5rem);
background: var(--input-background-color) background: var(--input-background-color) var(--dropdown-arrow);
var(--dropdown-arrow);
color: var(--input-text-color); color: var(--input-text-color);
border: unset; border: unset;
border-radius: 0.375rem; border-radius: 0.375rem;
@@ -330,8 +330,7 @@ select:hover,
select.form-select:hover, select.form-select:hover,
select.form-control:hover, select.form-control:hover,
.select-button.dropdown-toggle.btn:hover { .select-button.dropdown-toggle.btn:hover {
background: var(--input-hover-background) background: var(--input-hover-background) var(--dropdown-arrow);
var(--dropdown-arrow);
color: var(--input-hover-color); color: var(--input-hover-color);
} }
@@ -347,11 +346,11 @@ select.form-control:focus,
box-shadow: unset; box-shadow: unset;
outline: 3px solid var(--input-focus-outline-color); outline: 3px solid var(--input-focus-outline-color);
outline-offset: 0; outline-offset: 0;
background: var(--select-focus-background) background: var(--select-focus-background) var(--dropdown-arrow);
var(--dropdown-arrow);
color: var(--select-focus-text-color); color: var(--select-focus-text-color);
transition: outline-color 50ms linear, transition:
outline-offset 200ms ease-out; outline-color 50ms linear,
outline-offset 200ms ease-out;
} }
option { option {
@@ -360,7 +359,7 @@ option {
optgroup { optgroup {
color: var(--select-group-heading-text-color); color: var(--select-group-heading-text-color);
font-size: .75em; font-size: 0.75em;
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
line-height: 40px; line-height: 40px;
@@ -374,9 +373,9 @@ optgroup {
* </label> * </label>
*/ */
.tn-file-input { .tn-file-input {
position: relative; position: relative;
padding: .375rem 2.25rem .375rem .75rem; padding: 0.375rem 2.25rem 0.375rem 0.75rem;
} }
.tn-file-input input[type="file"] { .tn-file-input input[type="file"] {
@@ -416,19 +415,17 @@ optgroup {
/* Check boxes and radio buttons */ /* Check boxes and radio buttons */
@supports selector(label:has(*)) { @supports selector(label:has(*)) {
/* Check box & radio button commons */ /* Check box & radio button commons */
/* The parent label */ /* The parent label */
label.tn-radio, label.tn-radio,
label.tn-checkbox { label.tn-checkbox {
--box-size: 1em; --box-size: 1em;
--box-label-gap: .5em; --box-label-gap: 0.5em;
position: relative; position: relative;
padding-left: calc(var(--box-size) + var(--box-label-gap)) !important; padding-left: calc(var(--box-size) + var(--box-label-gap)) !important;
user-select: none; user-select: none;
} }
/* The original input */ /* The original input */
@@ -455,8 +452,8 @@ optgroup {
height: var(--box-size); height: var(--box-size);
} }
label.tn-radio:has(>input[type="radio"]:focus-visible)::before, label.tn-radio:has(> input[type="radio"]:focus-visible)::before,
label.tn-checkbox:has(>input[type="checkbox"]:focus-visible)::before { label.tn-checkbox:has(> input[type="checkbox"]:focus-visible)::before {
outline: 2px solid var(--input-focus-outline-color); outline: 2px solid var(--input-focus-outline-color);
} }
@@ -468,14 +465,15 @@ optgroup {
background: var(--radio-checkbox-background); background: var(--radio-checkbox-background);
} }
label.tn-checkbox:has(>input[type="checkbox"]:not(:disabled)):hover:before { label.tn-checkbox:has(> input[type="checkbox"]:not(:disabled)):hover:before {
background: var(--radio-checkbox-hover-background); background: var(--radio-checkbox-hover-background);
} }
@keyframes checkbox-checked { @keyframes checkbox-checked {
from { from {
transform: scale(2); transform: scale(2);
} to { }
to {
transform: scale(1); transform: scale(1);
} }
} }
@@ -484,15 +482,16 @@ optgroup {
label.tn-checkbox::after { label.tn-checkbox::after {
mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3e%3ctitle%3echeck-bold%3c/title%3e%3cpath d='M9%2c20.42L2.79%2c14.21L5.62%2c11.38L9%2c14.77L18.88%2c4.88L21.71%2c7.71L9%2c20.42Z' /%3e%3c/svg%3e"); mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3e%3ctitle%3echeck-bold%3c/title%3e%3cpath d='M9%2c20.42L2.79%2c14.21L5.62%2c11.38L9%2c14.77L18.88%2c4.88L21.71%2c7.71L9%2c20.42Z' /%3e%3c/svg%3e");
mask-position: center center; mask-position: center center;
mask-size: .95em; mask-size: 0.95em;
background-color: var(--radio-checkbox-indicator-color); background-color: var(--radio-checkbox-indicator-color);
transform: scale(0); transform: scale(0);
opacity: 0; opacity: 0;
transition: transform 300ms ease-out, transition:
opacity 300ms linear; transform 300ms ease-out,
opacity 300ms linear;
} }
label.tn-checkbox:has(>input[type="checkbox"]:checked)::after { label.tn-checkbox:has(> input[type="checkbox"]:checked)::after {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
transition: opacity 100ms ease-in-out; transition: opacity 100ms ease-in-out;
@@ -511,15 +510,16 @@ optgroup {
background: var(--radio-checkbox-background); background: var(--radio-checkbox-background);
} }
label.tn-radio:has(>input[type="radio"]:not(:disabled)):hover::before { label.tn-radio:has(> input[type="radio"]:not(:disabled)):hover::before {
background: var(--radio-checkbox-hover-background); background: var(--radio-checkbox-hover-background);
} }
@keyframes radio-checked { @keyframes radio-checked {
from { from {
transform: scale(1.5); transform: scale(1.5);
} to { }
transform: scale(.5); to {
transform: scale(0.5);
} }
} }
@@ -528,12 +528,13 @@ optgroup {
background: var(--radio-checkbox-indicator-color); background: var(--radio-checkbox-indicator-color);
transform: scale(0); transform: scale(0);
opacity: 0; opacity: 0;
transition: opacity 300ms linear, transition:
transform 300ms ease-in; opacity 300ms linear,
transform 300ms ease-in;
} }
label.tn-radio:has(>input[type="radio"]:checked)::after { label.tn-radio:has(> input[type="radio"]:checked)::after {
transform: scale(.5); transform: scale(0.5);
opacity: 1; opacity: 1;
transition: opacity 150ms linear; transition: opacity 150ms linear;
animation: radio-checked 200ms ease-out; animation: radio-checked 200ms ease-out;
@@ -545,9 +546,8 @@ optgroup {
label.tn-radio:has(> input[type="radio"]:disabled)::after, label.tn-radio:has(> input[type="radio"]:disabled)::after,
label.tn-checkbox:has(> input[type="checkbox"]:disabled)::before, label.tn-checkbox:has(> input[type="checkbox"]:disabled)::before,
label.tn-checkbox:has(> input[type="checkbox"]:disabled)::after { label.tn-checkbox:has(> input[type="checkbox"]:disabled)::after {
opacity: .5; opacity: 0.5;
} }
} }
/* Switches */ /* Switches */
@@ -580,9 +580,10 @@ body a.tn-link:visited,
font-weight: normal; font-weight: normal;
text-decoration: underline; text-decoration: underline;
transition: background-color 200ms ease-out, transition:
box-shadow 200ms ease-out, background-color 200ms ease-out,
color 300ms ease-out; box-shadow 200ms ease-out,
color 300ms ease-out;
} }
body a.tn-link:focus-visible, body a.tn-link:focus-visible,
@@ -597,9 +598,10 @@ body a.tn-link:hover,
--background: var(--link-hover-background); --background: var(--link-hover-background);
color: var(--link-hover-color); color: var(--link-hover-color);
transition: background-color 100ms ease-in, transition:
box-shadow 100ms ease-in, background-color 100ms ease-in,
color 150ms ease-in; box-shadow 100ms ease-in,
color 150ms ease-in;
} }
a.tn-link.external:not(.no-arrow)::after, a.tn-link.external:not(.no-arrow)::after,
@@ -607,16 +609,18 @@ a.tn-link[href^="http://"]:not(.no-arrow)::after,
a.tn-link[href^="https://"]:not(.no-arrow)::after, a.tn-link[href^="https://"]:not(.no-arrow)::after,
.use-tn-links a.external:not(.no-arrow)::after, .use-tn-links a.external:not(.no-arrow)::after,
.use-tn-links a[href^="http://"]:not(.no-arrow)::after, .use-tn-links a[href^="http://"]:not(.no-arrow)::after,
.use-tn-links a[href^="https://"]:not(.no-arrow)::after { .use-tn-links a[href^="https://"]:not(.no-arrow)::after
{
display: inline-block; display: inline-block;
opacity: .5; opacity: 0.5;
} }
@keyframes link-arrow-blink { @keyframes link-arrow-blink {
from { from {
opacity: 1; opacity: 1;
} to { }
opacity: .5; to {
opacity: 0.5;
} }
} }
@@ -625,7 +629,8 @@ a.tn-link:hover[href^="http://"]:not(.no-arrow)::after,
a.tn-link:hover[href^="https://"]:not(.no-arrow)::after, a.tn-link:hover[href^="https://"]:not(.no-arrow)::after,
.use-tn-links a:hover.external:not(.no-arrow)::after, .use-tn-links a:hover.external:not(.no-arrow)::after,
.use-tn-links a:hover[href^="http://"]:not(.no-arrow)::after, .use-tn-links a:hover[href^="http://"]:not(.no-arrow)::after,
.use-tn-links a:hover[href^="https://"]:not(.no-arrow)::after { .use-tn-links a:hover[href^="https://"]:not(.no-arrow)::after
{
animation: link-arrow-blink 500ms linear alternate infinite; animation: link-arrow-blink 500ms linear alternate infinite;
} }

Some files were not shown because too many files have changed in this diff Show More