mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-04 05:28:59 +01:00 
			
		
		
		
	Merge branch 'develop' into porting_js
This commit is contained in:
		
						commit
						8e66dc300f
					
				@ -3,3 +3,4 @@
 | 
			
		||||
*.yml
 | 
			
		||||
libraries/*
 | 
			
		||||
docs/*
 | 
			
		||||
src/public/app/doc_notes/**/*
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { fileURLToPath } from "url";
 | 
			
		||||
import { dirname, join } from "path";
 | 
			
		||||
import swaggerJsdoc from 'swagger-jsdoc';
 | 
			
		||||
import swaggerJsdoc from "swagger-jsdoc";
 | 
			
		||||
import fs from "fs";
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
@ -11,28 +11,30 @@ import fs from "fs";
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const options = {
 | 
			
		||||
  definition: {
 | 
			
		||||
    openapi: '3.1.1',
 | 
			
		||||
    info: {
 | 
			
		||||
      title: 'Trilium Notes - Sync server API',
 | 
			
		||||
      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).",
 | 
			
		||||
      contact: {
 | 
			
		||||
        name: "TriliumNext issue tracker",
 | 
			
		||||
        url: "https://github.com/TriliumNext/Notes/issues",
 | 
			
		||||
      },
 | 
			
		||||
      license: {
 | 
			
		||||
        name: "GNU Free Documentation License 1.3 (or later)",
 | 
			
		||||
        url: "https://www.gnu.org/licenses/fdl-1.3",
 | 
			
		||||
      },
 | 
			
		||||
    definition: {
 | 
			
		||||
        openapi: "3.1.1",
 | 
			
		||||
        info: {
 | 
			
		||||
            title: "Trilium Notes - Sync server API",
 | 
			
		||||
            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).",
 | 
			
		||||
            contact: {
 | 
			
		||||
                name: "TriliumNext issue tracker",
 | 
			
		||||
                url: "https://github.com/TriliumNext/Notes/issues"
 | 
			
		||||
            },
 | 
			
		||||
            license: {
 | 
			
		||||
                name: "GNU Free Documentation License 1.3 (or later)",
 | 
			
		||||
                url: "https://www.gnu.org/licenses/fdl-1.3"
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  apis: [
 | 
			
		||||
    // Put individual files here to have them ordered first.
 | 
			
		||||
    './src/routes/api/setup.ts',
 | 
			
		||||
    // all other files
 | 
			
		||||
    './src/routes/api/*.ts', './bin/generate-openapi.js'
 | 
			
		||||
  ],
 | 
			
		||||
    apis: [
 | 
			
		||||
        // Put individual files here to have them ordered first.
 | 
			
		||||
        "./src/routes/api/setup.ts",
 | 
			
		||||
        // all other files
 | 
			
		||||
        "./src/routes/api/*.ts",
 | 
			
		||||
        "./bin/generate-openapi.js"
 | 
			
		||||
    ]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const openapiSpecification = swaggerJsdoc(options);
 | 
			
		||||
 | 
			
		||||
@ -11,16 +11,14 @@ test("Displays translation on desktop", async ({ page, context }) => {
 | 
			
		||||
    const app = new App(page, context);
 | 
			
		||||
    await app.goto();
 | 
			
		||||
 | 
			
		||||
    await expect(page.locator("#left-pane .quick-search input"))
 | 
			
		||||
        .toHaveAttribute("placeholder", "Quick search");
 | 
			
		||||
    await expect(page.locator("#left-pane .quick-search input")).toHaveAttribute("placeholder", "Quick search");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("Displays translation on mobile", async ({ page, context }) => {
 | 
			
		||||
    const app = new App(page, context);
 | 
			
		||||
    await app.goto({ isMobile: true });
 | 
			
		||||
 | 
			
		||||
    await expect(page.locator("#mobile-sidebar-wrapper .quick-search input"))
 | 
			
		||||
        .toHaveAttribute("placeholder", "Quick search");
 | 
			
		||||
    await expect(page.locator("#mobile-sidebar-wrapper .quick-search input")).toHaveAttribute("placeholder", "Quick search");
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("Displays translations in Settings", async ({ page, context }) => {
 | 
			
		||||
 | 
			
		||||
@ -19,13 +19,13 @@ test("Can drag tabs around", async ({ page, context }) => {
 | 
			
		||||
    let tab = app.getTab(0);
 | 
			
		||||
 | 
			
		||||
    // 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);
 | 
			
		||||
    await expect(tab).toContainText(NOTE_TITLE);
 | 
			
		||||
 | 
			
		||||
    // 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);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,9 @@ test("Displays lint errors for backend script", async ({ page, context }) => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function expectTooltip(page: Page, tooltip: string) {
 | 
			
		||||
    await expect(page.locator(".CodeMirror-lint-tooltip:visible", {
 | 
			
		||||
        "hasText": tooltip
 | 
			
		||||
    })).toBeVisible();
 | 
			
		||||
    await expect(
 | 
			
		||||
        page.locator(".CodeMirror-lint-tooltip:visible", {
 | 
			
		||||
            hasText: tooltip
 | 
			
		||||
        })
 | 
			
		||||
    ).toBeVisible();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,8 @@ import App from "../support/app";
 | 
			
		||||
 | 
			
		||||
test("renders ELK flowchart", async ({ page, context }) => {
 | 
			
		||||
    await testAriaSnapshot({
 | 
			
		||||
        page, context,
 | 
			
		||||
        page,
 | 
			
		||||
        context,
 | 
			
		||||
        noteTitle: "Flowchart ELK on",
 | 
			
		||||
        snapshot: `
 | 
			
		||||
            - document:
 | 
			
		||||
@ -22,12 +23,13 @@ test("renders ELK flowchart", async ({ page, context }) => {
 | 
			
		||||
                - paragraph: Guarantee
 | 
			
		||||
                - text: Interfaces for B
 | 
			
		||||
        `
 | 
			
		||||
    })
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("renders standard flowchart", async ({ page, context }) => {
 | 
			
		||||
    await testAriaSnapshot({
 | 
			
		||||
        page, context,
 | 
			
		||||
        page,
 | 
			
		||||
        context,
 | 
			
		||||
        noteTitle: "Flowchart ELK off",
 | 
			
		||||
        snapshot: `
 | 
			
		||||
            - document:
 | 
			
		||||
@ -46,7 +48,7 @@ test("renders standard flowchart", async ({ page, context }) => {
 | 
			
		||||
                - paragraph: C
 | 
			
		||||
                - text: Interfaces for B
 | 
			
		||||
        `
 | 
			
		||||
    })
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
interface AriaTestOpts {
 | 
			
		||||
 | 
			
		||||
@ -44,8 +44,8 @@ test("Highlights list is displayed", async ({ page, context }) => {
 | 
			
		||||
 | 
			
		||||
    await expect(app.sidebar).toContainText("Highlights List");
 | 
			
		||||
    const rootList = app.sidebar.locator(".highlights-list ol");
 | 
			
		||||
    let index=0;
 | 
			
		||||
    for (const highlightedEl of [ "Bold 1", "Italic 1", "Underline 1", "Colored text 1", "Background text 1", "Bold 2", "Italic 2", "Underline 2", "Colored text 2", "Background text 2" ]) {
 | 
			
		||||
    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"]) {
 | 
			
		||||
        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);
 | 
			
		||||
    await app.goto();
 | 
			
		||||
    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.press("ControlOrMeta+M");
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,7 @@ export default class App {
 | 
			
		||||
        this.tabBar = page.locator(".tab-row-widget-container");
 | 
			
		||||
        this.noteTree = page.locator(".tree-wrapper");
 | 
			
		||||
        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");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -46,8 +46,7 @@ export default class App {
 | 
			
		||||
 | 
			
		||||
        // Wait for the page to load.
 | 
			
		||||
        if (url === "/") {
 | 
			
		||||
            await expect(this.page.locator(".tree"))
 | 
			
		||||
                .toContainText("Trilium Integration Test");
 | 
			
		||||
            await expect(this.page.locator(".tree")).toContainText("Trilium Integration Test");
 | 
			
		||||
            await this.closeAllTabs();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -109,11 +108,12 @@ export default class App {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        expect(csrfToken).toBeTruthy();
 | 
			
		||||
        await expect(await this.page.request.put(`${BASE_URL}/api/options/${key}/${value}`, {
 | 
			
		||||
            headers: {
 | 
			
		||||
                "x-csrf-token": csrfToken
 | 
			
		||||
            }
 | 
			
		||||
        })).toBeOK();
 | 
			
		||||
        await expect(
 | 
			
		||||
            await this.page.request.put(`${BASE_URL}/api/options/${key}/${value}`, {
 | 
			
		||||
                headers: {
 | 
			
		||||
                    "x-csrf-token": csrfToken
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        ).toBeOK();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -172,7 +172,7 @@
 | 
			
		||||
        "mini-css-extract-plugin": "2.9.2",
 | 
			
		||||
        "nodemon": "3.1.9",
 | 
			
		||||
        "postcss-loader": "8.1.1",
 | 
			
		||||
        "prettier": "3.5.2",
 | 
			
		||||
        "prettier": "3.5.3",
 | 
			
		||||
        "rcedit": "4.0.1",
 | 
			
		||||
        "rimraf": "6.0.1",
 | 
			
		||||
        "sass": "1.85.1",
 | 
			
		||||
@ -14776,9 +14776,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/prettier": {
 | 
			
		||||
      "version": "3.5.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz",
 | 
			
		||||
      "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==",
 | 
			
		||||
      "version": "3.5.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
 | 
			
		||||
      "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "bin": {
 | 
			
		||||
 | 
			
		||||
@ -223,7 +223,7 @@
 | 
			
		||||
    "mini-css-extract-plugin": "2.9.2",
 | 
			
		||||
    "nodemon": "3.1.9",
 | 
			
		||||
    "postcss-loader": "8.1.1",
 | 
			
		||||
    "prettier": "3.5.2",
 | 
			
		||||
    "prettier": "3.5.3",
 | 
			
		||||
    "rcedit": "4.0.1",
 | 
			
		||||
    "rimraf": "6.0.1",
 | 
			
		||||
    "sass": "1.85.1",
 | 
			
		||||
 | 
			
		||||
@ -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.
 | 
			
		||||
@ -14,68 +14,70 @@ const SERVER_URL = 'http://127.0.0.1:8082';
 | 
			
		||||
 * See https://playwright.dev/docs/test-configuration.
 | 
			
		||||
 */
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  testDir: './e2e',
 | 
			
		||||
  /* Run tests in files in parallel */
 | 
			
		||||
  fullyParallel: true,
 | 
			
		||||
  /* Fail the build on CI if you accidentally left test.only in the source code. */
 | 
			
		||||
  forbidOnly: !!process.env.CI,
 | 
			
		||||
  /* Retry on CI only */
 | 
			
		||||
  retries: process.env.CI ? 2 : 0,
 | 
			
		||||
  /* Opt out of parallel tests on CI. */
 | 
			
		||||
  workers: process.env.CI ? 1 : undefined,
 | 
			
		||||
  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
 | 
			
		||||
  reporter: 'html',
 | 
			
		||||
  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
 | 
			
		||||
  use: {
 | 
			
		||||
    /* Base URL to use in actions like `await page.goto('/')`. */
 | 
			
		||||
    baseURL: SERVER_URL,
 | 
			
		||||
    testDir: "./e2e",
 | 
			
		||||
    /* Run tests in files in parallel */
 | 
			
		||||
    fullyParallel: true,
 | 
			
		||||
    /* Fail the build on CI if you accidentally left test.only in the source code. */
 | 
			
		||||
    forbidOnly: !!process.env.CI,
 | 
			
		||||
    /* Retry on CI only */
 | 
			
		||||
    retries: process.env.CI ? 2 : 0,
 | 
			
		||||
    /* Opt out of parallel tests on CI. */
 | 
			
		||||
    workers: process.env.CI ? 1 : undefined,
 | 
			
		||||
    /* Reporter to use. See https://playwright.dev/docs/test-reporters */
 | 
			
		||||
    reporter: "html",
 | 
			
		||||
    /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
 | 
			
		||||
    use: {
 | 
			
		||||
        /* Base URL to use in actions like `await page.goto('/')`. */
 | 
			
		||||
        baseURL: SERVER_URL,
 | 
			
		||||
 | 
			
		||||
    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
 | 
			
		||||
    trace: 'on-first-retry',
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /* Configure projects for major browsers */
 | 
			
		||||
  projects: [
 | 
			
		||||
    {
 | 
			
		||||
      name: 'chromium',
 | 
			
		||||
      use: { ...devices['Desktop Chrome'] },
 | 
			
		||||
        /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
 | 
			
		||||
        trace: "on-first-retry"
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // {
 | 
			
		||||
    //   name: 'firefox',
 | 
			
		||||
    //   use: { ...devices['Desktop Firefox'] },
 | 
			
		||||
    // },
 | 
			
		||||
    /* Configure projects for major browsers */
 | 
			
		||||
    projects: [
 | 
			
		||||
        {
 | 
			
		||||
            name: "chromium",
 | 
			
		||||
            use: { ...devices["Desktop Chrome"] }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    // {
 | 
			
		||||
    //   name: 'webkit',
 | 
			
		||||
    //   use: { ...devices['Desktop Safari'] },
 | 
			
		||||
    // },
 | 
			
		||||
        // {
 | 
			
		||||
        //   name: 'firefox',
 | 
			
		||||
        //   use: { ...devices['Desktop Firefox'] },
 | 
			
		||||
        // },
 | 
			
		||||
 | 
			
		||||
    /* Test against mobile viewports. */
 | 
			
		||||
    // {
 | 
			
		||||
    //   name: 'Mobile Chrome',
 | 
			
		||||
    //   use: { ...devices['Pixel 5'] },
 | 
			
		||||
    // },
 | 
			
		||||
    // {
 | 
			
		||||
    //   name: 'Mobile Safari',
 | 
			
		||||
    //   use: { ...devices['iPhone 12'] },
 | 
			
		||||
    // },
 | 
			
		||||
        // {
 | 
			
		||||
        //   name: 'webkit',
 | 
			
		||||
        //   use: { ...devices['Desktop Safari'] },
 | 
			
		||||
        // },
 | 
			
		||||
 | 
			
		||||
    /* Test against branded browsers. */
 | 
			
		||||
    // {
 | 
			
		||||
    //   name: 'Microsoft Edge',
 | 
			
		||||
    //   use: { ...devices['Desktop Edge'], channel: 'msedge' },
 | 
			
		||||
    // },
 | 
			
		||||
    // {
 | 
			
		||||
    //   name: 'Google Chrome',
 | 
			
		||||
    //   use: { ...devices['Desktop Chrome'], channel: 'chrome' },
 | 
			
		||||
    // },
 | 
			
		||||
  ],
 | 
			
		||||
        /* Test against mobile viewports. */
 | 
			
		||||
        // {
 | 
			
		||||
        //   name: 'Mobile Chrome',
 | 
			
		||||
        //   use: { ...devices['Pixel 5'] },
 | 
			
		||||
        // },
 | 
			
		||||
        // {
 | 
			
		||||
        //   name: 'Mobile Safari',
 | 
			
		||||
        //   use: { ...devices['iPhone 12'] },
 | 
			
		||||
        // },
 | 
			
		||||
 | 
			
		||||
  /* 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,
 | 
			
		||||
        /* Test against branded browsers. */
 | 
			
		||||
        // {
 | 
			
		||||
        //   name: 'Microsoft Edge',
 | 
			
		||||
        //   use: { ...devices['Desktop Edge'], channel: 'msedge' },
 | 
			
		||||
        // },
 | 
			
		||||
        // {
 | 
			
		||||
        //   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
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@ export default class BTask extends AbstractBeccaEntity<BOption> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static get hashedProperties() {
 | 
			
		||||
        return [ "taskId", "parentNoteId", "title", "dueDate", "isDone", "isDeleted" ];
 | 
			
		||||
        return ["taskId", "parentNoteId", "title", "dueDate", "isDone", "isDeleted"];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    taskId?: string;
 | 
			
		||||
 | 
			
		||||
@ -61,7 +61,7 @@ export interface NoteCommandData extends CommandData {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ExecuteCommandData<T> extends CommandData {
 | 
			
		||||
    resolve: (data: T) => void
 | 
			
		||||
    resolve: (data: T) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -69,7 +69,7 @@ export interface ExecuteCommandData<T> extends CommandData {
 | 
			
		||||
 */
 | 
			
		||||
export type CommandMappings = {
 | 
			
		||||
    "api-log-messages": CommandData;
 | 
			
		||||
    focusTree: CommandData,
 | 
			
		||||
    focusTree: CommandData;
 | 
			
		||||
    focusOnTitle: CommandData;
 | 
			
		||||
    focusOnDetail: CommandData;
 | 
			
		||||
    focusOnSearchDefinition: Required<CommandData>;
 | 
			
		||||
@ -107,7 +107,7 @@ export type CommandMappings = {
 | 
			
		||||
    showInfoDialog: ConfirmWithMessageOptions;
 | 
			
		||||
    showConfirmDialog: ConfirmWithMessageOptions;
 | 
			
		||||
    showRecentChanges: CommandData & { ancestorNoteId: string };
 | 
			
		||||
    showImportDialog: CommandData & { noteId: string; };
 | 
			
		||||
    showImportDialog: CommandData & { noteId: string };
 | 
			
		||||
    openNewNoteSplit: NoteCommandData;
 | 
			
		||||
    openInWindow: NoteCommandData;
 | 
			
		||||
    openNoteInNewTab: CommandData;
 | 
			
		||||
@ -130,9 +130,11 @@ export type CommandMappings = {
 | 
			
		||||
    editNoteTitle: ContextMenuCommandData;
 | 
			
		||||
    protectSubtree: ContextMenuCommandData;
 | 
			
		||||
    unprotectSubtree: ContextMenuCommandData;
 | 
			
		||||
    openBulkActionsDialog: ContextMenuCommandData | {
 | 
			
		||||
        selectedOrActiveNoteIds?: string[]
 | 
			
		||||
    };
 | 
			
		||||
    openBulkActionsDialog:
 | 
			
		||||
        | ContextMenuCommandData
 | 
			
		||||
        | {
 | 
			
		||||
              selectedOrActiveNoteIds?: string[];
 | 
			
		||||
          };
 | 
			
		||||
    editBranchPrefix: ContextMenuCommandData;
 | 
			
		||||
    convertNoteToAttachment: ContextMenuCommandData;
 | 
			
		||||
    duplicateSubtree: ContextMenuCommandData;
 | 
			
		||||
@ -220,11 +222,11 @@ export type CommandMappings = {
 | 
			
		||||
    moveTabToNewWindow: CommandData;
 | 
			
		||||
    copyTabToNewWindow: CommandData;
 | 
			
		||||
    closeActiveTab: CommandData & {
 | 
			
		||||
        $el: JQuery<HTMLElement>
 | 
			
		||||
    },
 | 
			
		||||
        $el: JQuery<HTMLElement>;
 | 
			
		||||
    };
 | 
			
		||||
    setZoomFactorAndSave: {
 | 
			
		||||
        zoomFactor: string;
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    reEvaluateRightPaneVisibility: CommandData;
 | 
			
		||||
    runActiveNote: CommandData;
 | 
			
		||||
@ -233,18 +235,18 @@ export type CommandMappings = {
 | 
			
		||||
    };
 | 
			
		||||
    scrollToEnd: CommandData;
 | 
			
		||||
    closeThisNoteSplit: CommandData;
 | 
			
		||||
    moveThisNoteSplit: CommandData & { isMovingLeft: boolean; };
 | 
			
		||||
    moveThisNoteSplit: CommandData & { isMovingLeft: boolean };
 | 
			
		||||
 | 
			
		||||
    // Geomap
 | 
			
		||||
    deleteFromMap: { noteId: string },
 | 
			
		||||
    openGeoLocation: { noteId: string, event: JQuery.MouseDownEvent }
 | 
			
		||||
    deleteFromMap: { noteId: string };
 | 
			
		||||
    openGeoLocation: { noteId: string; event: JQuery.MouseDownEvent };
 | 
			
		||||
 | 
			
		||||
    toggleZenMode: CommandData;
 | 
			
		||||
 | 
			
		||||
    updateAttributeList: CommandData & { attributes: Attribute[] };
 | 
			
		||||
    saveAttributes: CommandData;
 | 
			
		||||
    reloadAttributes: CommandData;
 | 
			
		||||
    refreshNoteList: CommandData & { noteId: string; };
 | 
			
		||||
    refreshNoteList: CommandData & { noteId: string };
 | 
			
		||||
 | 
			
		||||
    refreshResults: {};
 | 
			
		||||
    refreshSearchDefinition: {};
 | 
			
		||||
@ -347,7 +349,7 @@ type EventMappings = {
 | 
			
		||||
        ntxId: string | null | undefined; // TODO: deduplicate ntxId
 | 
			
		||||
    };
 | 
			
		||||
    tabReorder: {
 | 
			
		||||
        ntxIdsInOrder: string[]
 | 
			
		||||
        ntxIdsInOrder: string[];
 | 
			
		||||
    };
 | 
			
		||||
    refreshNoteList: {
 | 
			
		||||
        noteId: string;
 | 
			
		||||
 | 
			
		||||
@ -37,7 +37,25 @@ const NOTE_TYPE_ICONS = {
 | 
			
		||||
 * end user. Those types should be used only for checking against, they are
 | 
			
		||||
 * 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 {
 | 
			
		||||
    isArchived: boolean;
 | 
			
		||||
 | 
			
		||||
@ -264,7 +264,7 @@ export default class DesktopLayout {
 | 
			
		||||
            .child(new InfoDialog())
 | 
			
		||||
            .child(new ConfirmDialog())
 | 
			
		||||
            .child(new PromptDialog())
 | 
			
		||||
            .child(new CloseZenButton())
 | 
			
		||||
            .child(new CloseZenButton());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #buildLauncherPane(isHorizontal) {
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ import "../stylesheets/bootstrap.scss";
 | 
			
		||||
// Required for correct loading of scripts in Electron
 | 
			
		||||
if (typeof module === 'object') {window.module = module; module = undefined;}
 | 
			
		||||
 | 
			
		||||
const device = getDeviceType()
 | 
			
		||||
const device = getDeviceType();
 | 
			
		||||
console.log("Setting device cookie to:", 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 expires = "; expires=" + date.toUTCString();
 | 
			
		||||
 | 
			
		||||
    document.cookie = name + "=" + (value || "")  + expires + "; path=/";
 | 
			
		||||
    document.cookie = name + "=" + (value || "") + expires + "; path=/";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getDeviceType() {
 | 
			
		||||
    if (window.location.search === '?desktop') return "desktop";
 | 
			
		||||
    if (window.location.search === '?mobile') return "mobile";
 | 
			
		||||
    if (window.location.search === "?desktop") return "desktop";
 | 
			
		||||
    if (window.location.search === "?mobile") return "mobile";
 | 
			
		||||
    return isMobile() ? "mobile" : "desktop";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://stackoverflow.com/a/73731646/944162
 | 
			
		||||
function isMobile() {
 | 
			
		||||
    const mQ = matchMedia?.('(pointer:coarse)');
 | 
			
		||||
    if (mQ?.media === '(pointer:coarse)') return !!mQ.matches;
 | 
			
		||||
    const mQ = matchMedia?.("(pointer:coarse)");
 | 
			
		||||
    if (mQ?.media === "(pointer:coarse)") return !!mQ.matches;
 | 
			
		||||
 | 
			
		||||
    if ('orientation' in window) return true;
 | 
			
		||||
    const userAgentsRegEx = /\b(Android|iPhone|iPad|iPod|Windows Phone|BlackBerry|webOS|IEMobile)\b/i
 | 
			
		||||
    return userAgentsRegEx.test(navigator.userAgent)
 | 
			
		||||
    if ("orientation" in window) return true;
 | 
			
		||||
    const userAgentsRegEx = /\b(Android|iPhone|iPad|iPod|Windows Phone|BlackBerry|webOS|IEMobile)\b/i;
 | 
			
		||||
    return userAgentsRegEx.test(navigator.userAgent);
 | 
			
		||||
}
 | 
			
		||||
@ -34,8 +34,8 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener<
 | 
			
		||||
 | 
			
		||||
        const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers";
 | 
			
		||||
        const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers";
 | 
			
		||||
        const isVisibleItem = (parentNoteId === "_lbVisibleLaunchers" || parentNoteId === "_lbMobileVisibleLaunchers");
 | 
			
		||||
        const isAvailableItem = (parentNoteId === "_lbAvailableLaunchers" || parentNoteId === "_lbMobileAvailableLaunchers");
 | 
			
		||||
        const isVisibleItem = parentNoteId === "_lbVisibleLaunchers" || parentNoteId === "_lbMobileVisibleLaunchers";
 | 
			
		||||
        const isAvailableItem = parentNoteId === "_lbAvailableLaunchers" || parentNoteId === "_lbMobileAvailableLaunchers";
 | 
			
		||||
        const isItem = isVisibleItem || isAvailableItem;
 | 
			
		||||
        const canBeDeleted = !note?.noteId.startsWith("_"); // fixed notes can't be deleted
 | 
			
		||||
        const canBeReset = !canBeDeleted && note?.isLaunchBarConfig();
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
import { describe, it, expect } from "vitest";
 | 
			
		||||
import attributeParser from "./attribute_parser.js";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
describe("Lexing", () => {
 | 
			
		||||
    it("simple label", () => {
 | 
			
		||||
        expect(attributeParser.lex("#label").map((t: any) => t.text)).toEqual(["#label"]);
 | 
			
		||||
@ -41,7 +40,7 @@ describe("Lexing", () => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe.todo("Parser", () => {
 | 
			
		||||
  /* #TODO
 | 
			
		||||
    /* #TODO
 | 
			
		||||
    it("simple label", () => {
 | 
			
		||||
 | 
			
		||||
        const attrs = attributeParser.parse(["#token"].map((t: any) => ({ text: t })));
 | 
			
		||||
 | 
			
		||||
@ -79,7 +79,7 @@ async function renderAttributes(attributes: FAttribute[], renderIsInheritable: b
 | 
			
		||||
    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) {
 | 
			
		||||
    const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes();
 | 
			
		||||
 | 
			
		||||
@ -99,8 +99,8 @@ const HIGHLIGHT_JS: Library = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const LEAFLET: Library = {
 | 
			
		||||
    css: [ "node_modules/leaflet/dist/leaflet.css" ],
 | 
			
		||||
}
 | 
			
		||||
    css: ["node_modules/leaflet/dist/leaflet.css"]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
async function requireLibrary(library: Library) {
 | 
			
		||||
    if (library.css) {
 | 
			
		||||
 | 
			
		||||
@ -274,8 +274,8 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
 | 
			
		||||
    const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink);
 | 
			
		||||
 | 
			
		||||
    const ctrlKey = utils.isCtrlKey(evt);
 | 
			
		||||
    const isLeftClick = ("which" in evt && evt.which === 1);
 | 
			
		||||
    const isMiddleClick = ("which" in evt && evt.which === 2);
 | 
			
		||||
    const isLeftClick = "which" in evt && evt.which === 1;
 | 
			
		||||
    const isMiddleClick = "which" in evt && evt.which === 2;
 | 
			
		||||
    const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick;
 | 
			
		||||
 | 
			
		||||
    if (notePath) {
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@ export default class NoteListRenderer {
 | 
			
		||||
            parentNote,
 | 
			
		||||
            noteIds,
 | 
			
		||||
            showNotePath
 | 
			
		||||
        }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (this.viewType === "list" || this.viewType === "grid") {
 | 
			
		||||
            this.viewMode = new ListOrGridView(this.viewType, args);
 | 
			
		||||
 | 
			
		||||
@ -147,11 +147,11 @@ async function renderTooltip(note: FNote | null) {
 | 
			
		||||
        tooltip: true,
 | 
			
		||||
        trim: true
 | 
			
		||||
    });
 | 
			
		||||
    const isContentEmpty = ($renderedContent[0].innerHTML.length === 0);
 | 
			
		||||
    const isContentEmpty = $renderedContent[0].innerHTML.length === 0;
 | 
			
		||||
 | 
			
		||||
    let content = "";
 | 
			
		||||
    if (noteTitleWithPathAsSuffix) {
 | 
			
		||||
        const classes = [ "note-tooltip-title" ];
 | 
			
		||||
        const classes = ["note-tooltip-title"];
 | 
			
		||||
        if (isContentEmpty) {
 | 
			
		||||
            classes.push("note-no-content");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -33,20 +33,20 @@ function parseDate(str: string) {
 | 
			
		||||
 | 
			
		||||
// Source: https://stackoverflow.com/a/30465299/4898894
 | 
			
		||||
function getMonthsInDateRange(startDate: string, endDate: string) {
 | 
			
		||||
    const start = startDate.split('-');
 | 
			
		||||
    const end = endDate.split('-');
 | 
			
		||||
    const start = startDate.split("-");
 | 
			
		||||
    const end = endDate.split("-");
 | 
			
		||||
    const startYear = parseInt(start[0]);
 | 
			
		||||
    const endYear = parseInt(end[0]);
 | 
			
		||||
    const dates = [];
 | 
			
		||||
 | 
			
		||||
    for (let i = startYear; i <= endYear; i++) {
 | 
			
		||||
        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) {
 | 
			
		||||
            const month = j+1;
 | 
			
		||||
            const displayMonth = month < 10 ? '0'+month : month;
 | 
			
		||||
            dates.push([i, displayMonth].join('-'));
 | 
			
		||||
        for (let j = startMon; j <= endMonth; j = j > 12 ? j % 12 || 11 : j + 1) {
 | 
			
		||||
            const month = j + 1;
 | 
			
		||||
            const displayMonth = month < 10 ? "0" + month : month;
 | 
			
		||||
            dates.push([i, displayMonth].join("-"));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return dates;
 | 
			
		||||
@ -161,7 +161,7 @@ function escapeHtml(str: string) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function escapeQuotes(value: string) {
 | 
			
		||||
    return value.replaceAll("\"", """);
 | 
			
		||||
    return value.replaceAll('"', """);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function formatSize(size: number) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										6
									
								
								src/public/app/types-fancytree.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								src/public/app/types-fancytree.d.ts
									
									
									
									
										vendored
									
									
								
							@ -11,7 +11,7 @@
 | 
			
		||||
 | 
			
		||||
interface JQueryStatic {
 | 
			
		||||
    ui: JQueryUI.UI;
 | 
			
		||||
};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare namespace JQueryUI {
 | 
			
		||||
    interface UI {
 | 
			
		||||
@ -679,13 +679,13 @@ declare namespace Fancytree {
 | 
			
		||||
        activate = 1,
 | 
			
		||||
        expand = 2,
 | 
			
		||||
        activate_and_expand = 3,
 | 
			
		||||
        activate_dblclick_expands = 4,
 | 
			
		||||
        activate_dblclick_expands = 4
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    enum FancytreeSelectMode {
 | 
			
		||||
        single = 1,
 | 
			
		||||
        multi = 2,
 | 
			
		||||
        mutlti_hier = 3,
 | 
			
		||||
        mutlti_hier = 3
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Context object passed to events and hook functions. */
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								src/public/app/types-lib.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								src/public/app/types-lib.d.ts
									
									
									
									
										vendored
									
									
								
							@ -13,7 +13,7 @@ declare module "draggabilly" {
 | 
			
		||||
            containment: HTMLElement
 | 
			
		||||
        });
 | 
			
		||||
        element: HTMLElement;
 | 
			
		||||
        on(event: "pointerDown" | "dragStart" | "dragEnd" | "dragMove", callback: Callback)
 | 
			
		||||
        on(event: "pointerDown" | "dragStart" | "dragEnd" | "dragMove", callback: Callback);
 | 
			
		||||
        dragEnd();
 | 
			
		||||
        isDragging: boolean;
 | 
			
		||||
        positionDrag: () => void;
 | 
			
		||||
@ -21,6 +21,6 @@ declare module "draggabilly" {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare module '@mind-elixir/node-menu' {
 | 
			
		||||
declare module "@mind-elixir/node-menu" {
 | 
			
		||||
    export default mindmap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -41,7 +41,7 @@ export default class BookmarkButtons extends FlexContainer<Component> {
 | 
			
		||||
                : new OpenNoteButtonWidget(note).class("launcher-button");
 | 
			
		||||
 | 
			
		||||
            if (this.settings.titlePlacement) {
 | 
			
		||||
                if (!('settings' in buttonWidget)) {
 | 
			
		||||
                if (!("settings" in buttonWidget)) {
 | 
			
		||||
                    (buttonWidget as any).settings = {};
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@ import appContext from "../../components/app_context.js";
 | 
			
		||||
import openService from "../../services/open.js";
 | 
			
		||||
import utils from "../../services/utils.js";
 | 
			
		||||
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 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.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("");
 | 
			
		||||
            if (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']");
 | 
			
		||||
            $openAttachmentCustomButton.addClass("disabled").append($('<span class="bx bx-info-circle disabled-tooltip" />').attr("title", t("attachments_actions.open_custom_client_only")));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async openAttachmentCommand() {
 | 
			
		||||
@ -170,7 +168,6 @@ export default class AttachmentActionsWidget extends BasicWidget {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        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 }));
 | 
			
		||||
        await ws.waitForMaxKnownEntityChangeId();
 | 
			
		||||
 | 
			
		||||
@ -139,7 +139,8 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
			
		||||
        return super.isEnabled() && this.noteContext?.viewScope?.viewMode === "default";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ribbon(widget: NoteContextAwareWidget) { // TODO: Base class
 | 
			
		||||
    ribbon(widget: NoteContextAwareWidget) {
 | 
			
		||||
        // TODO: Base class
 | 
			
		||||
        super.child(widget);
 | 
			
		||||
 | 
			
		||||
        this.ribbonWidgets.push(widget);
 | 
			
		||||
@ -236,7 +237,12 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
			
		||||
            const $ribbonTitle = $('<div class="ribbon-tab-title">')
 | 
			
		||||
                .attr("data-ribbon-component-id", ribbonWidget.componentId)
 | 
			
		||||
                .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($('<span class="ribbon-tab-title-label">').text(ret.title));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -250,7 +250,7 @@ ws.subscribeToMessages(async (message) => {
 | 
			
		||||
            message: message,
 | 
			
		||||
            icon: "arrow-square-up-right"
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (message.taskType !== "export") {
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
                        .attr("src", `data:${fullRevision.mime};base64,${fullRevision.content}`)
 | 
			
		||||
                        .css("max-width", "100%")
 | 
			
		||||
                        .css("max-height", "100%").html()
 | 
			
		||||
                        .css("max-height", "100%")
 | 
			
		||||
                        .html()
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        } else if (revisionItem.type === "file") {
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { t } from "../../services/i18n.js";
 | 
			
		||||
import NoteContextAwareWidget from "../note_context_aware_widget.js"
 | 
			
		||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
 | 
			
		||||
 | 
			
		||||
const TPL = `\
 | 
			
		||||
<div class="geo-map-buttons">
 | 
			
		||||
 | 
			
		||||
@ -51,7 +51,7 @@ export default class ContextualHelpButton extends NoteContextAwareWidget {
 | 
			
		||||
        if (this.note && this.note.type !== "book" && byNoteType[this.note.type]) {
 | 
			
		||||
            this.helpNoteIdToOpen = byNoteType[this.note.type];
 | 
			
		||||
        } else if (this.note && this.note.type === "book") {
 | 
			
		||||
            this.helpNoteIdToOpen = byBookType[this.note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""]
 | 
			
		||||
            this.helpNoteIdToOpen = byBookType[(this.note.getAttributeValue("label", "viewType") as ViewTypeOptions) ?? ""];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return !!this.helpNoteIdToOpen;
 | 
			
		||||
@ -64,7 +64,7 @@ export default class ContextualHelpButton extends NoteContextAwareWidget {
 | 
			
		||||
            const targetNote = `_help_${this.helpNoteIdToOpen}`;
 | 
			
		||||
            const helpSubcontext = subContexts.find((s) => s.viewScope?.viewMode === "contextual-help");
 | 
			
		||||
            const viewScope: ViewScope = {
 | 
			
		||||
                viewMode: "contextual-help",
 | 
			
		||||
                viewMode: "contextual-help"
 | 
			
		||||
            };
 | 
			
		||||
            if (!helpSubcontext) {
 | 
			
		||||
                // The help is not already open, open a new split with it.
 | 
			
		||||
@ -74,7 +74,7 @@ export default class ContextualHelpButton extends NoteContextAwareWidget {
 | 
			
		||||
                    notePath: targetNote,
 | 
			
		||||
                    hoistedNoteId: "_help",
 | 
			
		||||
                    viewScope
 | 
			
		||||
                })
 | 
			
		||||
                });
 | 
			
		||||
            } else {
 | 
			
		||||
                // There is already a help window open, make sure it opens on the right note.
 | 
			
		||||
                helpSubcontext.setNote(targetNote, { viewScope });
 | 
			
		||||
 | 
			
		||||
@ -157,7 +157,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
 | 
			
		||||
            if (backlink.relationName) {
 | 
			
		||||
                $item.append($("<p>").text(`${t("zpetne_odkazy.relation")}: ${backlink.relationName}`));
 | 
			
		||||
            } else {
 | 
			
		||||
                $item.append(...backlink.excerpts ?? []);
 | 
			
		||||
                $item.append(...(backlink.excerpts ?? []));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.$items.append($item);
 | 
			
		||||
 | 
			
		||||
@ -19,10 +19,10 @@ const TPL = `\
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <div class="geo-map-container"></div>
 | 
			
		||||
</div>`
 | 
			
		||||
</div>`;
 | 
			
		||||
 | 
			
		||||
export type Leaflet = typeof import("leaflet");
 | 
			
		||||
export type InitCallback = ((L: Leaflet) => void);
 | 
			
		||||
export type InitCallback = (L: Leaflet) => void;
 | 
			
		||||
 | 
			
		||||
export default class GeoMapWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
@ -40,24 +40,23 @@ export default class GeoMapWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
        this.$container = this.$widget.find(".geo-map-container");
 | 
			
		||||
 | 
			
		||||
        library_loader.requireLibrary(library_loader.LEAFLET)
 | 
			
		||||
            .then(async () => {
 | 
			
		||||
                const L = (await import("leaflet")).default;
 | 
			
		||||
        library_loader.requireLibrary(library_loader.LEAFLET).then(async () => {
 | 
			
		||||
            const L = (await import("leaflet")).default;
 | 
			
		||||
 | 
			
		||||
                const map = L.map(this.$container[0], {
 | 
			
		||||
                    worldCopyJump: true
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                this.map = map;
 | 
			
		||||
                if (this.initCallback) {
 | 
			
		||||
                    this.initCallback(L);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
 | 
			
		||||
                    attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
 | 
			
		||||
                    detectRetina: true
 | 
			
		||||
                }).addTo(map);
 | 
			
		||||
            const map = L.map(this.$container[0], {
 | 
			
		||||
                worldCopyJump: true
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            this.map = map;
 | 
			
		||||
            if (this.initCallback) {
 | 
			
		||||
                this.initCallback(L);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
 | 
			
		||||
                attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
 | 
			
		||||
                detectRetina: true
 | 
			
		||||
            }).addTo(map);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -66,7 +66,7 @@ export default class MermaidWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
        mermaid.mermaidAPI.initialize({
 | 
			
		||||
            startOnLoad: false,
 | 
			
		||||
            ...getMermaidConfig() as any
 | 
			
		||||
            ...(getMermaidConfig() as any)
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.$display.empty();
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 * 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 {
 | 
			
		||||
 | 
			
		||||
@ -329,7 +338,9 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
            const label = attrs.find(
 | 
			
		||||
                (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));
 | 
			
		||||
 | 
			
		||||
@ -139,7 +139,7 @@ interface NotesAndRelationsData {
 | 
			
		||||
        source: string;
 | 
			
		||||
        target: string;
 | 
			
		||||
        name: string;
 | 
			
		||||
    }[]
 | 
			
		||||
    }[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Replace
 | 
			
		||||
@ -152,7 +152,7 @@ interface ResponseLink {
 | 
			
		||||
 | 
			
		||||
interface PostNotesMapResponse {
 | 
			
		||||
    notes: string[];
 | 
			
		||||
    links: ResponseLink[],
 | 
			
		||||
    links: ResponseLink[];
 | 
			
		||||
    noteIdToDescendantCountMap: Record<string, number>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -160,7 +160,7 @@ interface GroupedLink {
 | 
			
		||||
    id: string;
 | 
			
		||||
    sourceNoteId: string;
 | 
			
		||||
    targetNoteId: string;
 | 
			
		||||
    names: string[]
 | 
			
		||||
    names: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface CssData {
 | 
			
		||||
@ -313,9 +313,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
 | 
			
		||||
                ctx.fillStyle = color;
 | 
			
		||||
                ctx.beginPath();
 | 
			
		||||
                if (node.x && node.y) {
 | 
			
		||||
                    ctx.arc(node.x, node.y,
 | 
			
		||||
                        this.noteIdToSizeMap[node.id], 0,
 | 
			
		||||
                        2 * Math.PI, false);
 | 
			
		||||
                    ctx.arc(node.x, node.y, this.noteIdToSizeMap[node.id], 0, 2 * Math.PI, false);
 | 
			
		||||
                }
 | 
			
		||||
                ctx.fill();
 | 
			
		||||
            })
 | 
			
		||||
@ -467,13 +465,13 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (source.x && source.y && target.x && target.y) {
 | 
			
		||||
            const x = ((source.x) + (target.x)) / 2;
 | 
			
		||||
            const y = ((source.y) + (target.y)) / 2;
 | 
			
		||||
            const x = (source.x + target.x) / 2;
 | 
			
		||||
            const y = (source.y + target.y) / 2;
 | 
			
		||||
            ctx.save();
 | 
			
		||||
            ctx.translate(x, y);
 | 
			
		||||
 | 
			
		||||
            const deltaY = (source.y) - (target.y);
 | 
			
		||||
            const deltaX = (source.x) - (target.x);
 | 
			
		||||
            const deltaY = source.y - target.y;
 | 
			
		||||
            const deltaX = source.x - target.x;
 | 
			
		||||
 | 
			
		||||
            let angle = Math.atan2(deltaY, deltaX);
 | 
			
		||||
            let moveY = 2;
 | 
			
		||||
 | 
			
		||||
@ -91,7 +91,7 @@ export default class NoteTitleWidget extends NoteContextAwareWidget {
 | 
			
		||||
    async refreshWithNote(note: FNote) {
 | 
			
		||||
        const isReadOnly = (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) || utils.isLaunchBarConfig(note.noteId) || 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.setProtectedStatus(note);
 | 
			
		||||
 | 
			
		||||
@ -159,11 +159,11 @@ interface CreateLauncherResponse {
 | 
			
		||||
    message: string;
 | 
			
		||||
    note: {
 | 
			
		||||
        noteId: string;
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface ExpandedSubtreeResponse {
 | 
			
		||||
    branchIds: string[]
 | 
			
		||||
    branchIds: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Node extends Fancytree.NodeData {
 | 
			
		||||
@ -180,7 +180,6 @@ interface RefreshContext {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default class NoteTreeWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
    private $tree!: JQuery<HTMLElement>;
 | 
			
		||||
    private $treeActions!: JQuery<HTMLElement>;
 | 
			
		||||
    private $treeSettingsButton!: JQuery<HTMLElement>;
 | 
			
		||||
@ -571,10 +570,13 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
 | 
			
		||||
            clones: {
 | 
			
		||||
                highlightActiveClones: true
 | 
			
		||||
            },
 | 
			
		||||
            enhanceTitle: async function (event: Event, data: {
 | 
			
		||||
                node: Fancytree.FancytreeNode;
 | 
			
		||||
                noteId: string;
 | 
			
		||||
            }) {
 | 
			
		||||
            enhanceTitle: async function (
 | 
			
		||||
                event: Event,
 | 
			
		||||
                data: {
 | 
			
		||||
                    node: Fancytree.FancytreeNode;
 | 
			
		||||
                    noteId: string;
 | 
			
		||||
                }
 | 
			
		||||
            ) {
 | 
			
		||||
                const node = data.node;
 | 
			
		||||
 | 
			
		||||
                if (!node.data.noteId) {
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ export default class ProtectedNoteSwitchWidget extends SwitchWidget {
 | 
			
		||||
        super.doRender();
 | 
			
		||||
 | 
			
		||||
        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.switchOffTooltip = t("protect_note.toggle-off-hint");
 | 
			
		||||
 | 
			
		||||
@ -126,7 +126,7 @@ export default class BookPropertiesWidget extends NoteContextAwareWidget {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (![ "list", "grid", "calendar"].includes(type)) {
 | 
			
		||||
        if (!["list", "grid", "calendar"].includes(type)) {
 | 
			
		||||
            throw new Error(t("book_properties.invalid_view_type", { type }));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -122,7 +122,7 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget {
 | 
			
		||||
        const { top } = this.$widget[0].getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
        const height = ($(window).height() ?? 0) - top;
 | 
			
		||||
        const width = (this.$widget.width() ?? 0);
 | 
			
		||||
        const width = this.$widget.width() ?? 0;
 | 
			
		||||
 | 
			
		||||
        this.$widget.find(".note-map-container")
 | 
			
		||||
            .height(height)
 | 
			
		||||
 | 
			
		||||
@ -135,8 +135,7 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
 | 
			
		||||
        if (loadResults.getBranchRows().find((branch) => branch.noteId === this.noteId) ||
 | 
			
		||||
            (this.noteId != null && loadResults.isNoteReloaded(this.noteId))) {
 | 
			
		||||
        if (loadResults.getBranchRows().find((branch) => branch.noteId === this.noteId) || (this.noteId != null && loadResults.isNoteReloaded(this.noteId))) {
 | 
			
		||||
            this.refresh();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -46,6 +46,9 @@ export default class NotePropertiesWidget extends NoteContextAwareWidget {
 | 
			
		||||
    async refreshWithNote(note: FNote) {
 | 
			
		||||
        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 ?? "");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,6 @@ interface SimilarNote {
 | 
			
		||||
    noteId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default class SimilarNotesWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
    private $similarNotesWrapper!: JQuery<HTMLElement>;
 | 
			
		||||
 | 
			
		||||
@ -185,7 +185,7 @@ export default class SwitchWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
    /** Gets or sets whether the switch is enabled. */
 | 
			
		||||
    get canToggle() {
 | 
			
		||||
        return (!this.$switchButton.hasClass("disabled"));
 | 
			
		||||
        return !this.$switchButton.hasClass("disabled");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    set canToggle(isEnabled) {
 | 
			
		||||
 | 
			
		||||
@ -79,7 +79,7 @@ export default class SyncStatusWidget extends BasicWidget {
 | 
			
		||||
    settings: {
 | 
			
		||||
        // TriliumNextTODO: narrow types and use TitlePlacement Type
 | 
			
		||||
        titlePlacement: string;
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
@ -106,7 +106,6 @@ export default class SyncStatusWidget extends BasicWidget {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        Tooltip.getOrCreateInstance(this.$widget.find(`.sync-status-${className}`)[0], {
 | 
			
		||||
            html: true,
 | 
			
		||||
            placement: this.settings.titlePlacement,
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,7 @@ const TAB_CONTAINER_MIN_WIDTH = 24;
 | 
			
		||||
const TAB_CONTAINER_MAX_WIDTH = 240;
 | 
			
		||||
const TAB_CONTAINER_LEFT_PADDING = 5;
 | 
			
		||||
const NEW_TAB_WIDTH = 32;
 | 
			
		||||
const MIN_FILLER_WIDTH = (isDesktop ? 50 : 15);
 | 
			
		||||
const MIN_FILLER_WIDTH = isDesktop ? 50 : 15;
 | 
			
		||||
const MARGIN_WIDTH = 5;
 | 
			
		||||
 | 
			
		||||
const TAB_SIZE_SMALL = 84;
 | 
			
		||||
 | 
			
		||||
@ -17,10 +17,10 @@ export default class TemplateSwitchWidget extends SwitchWidget {
 | 
			
		||||
        super.doRender();
 | 
			
		||||
 | 
			
		||||
        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.switchOffTooltip =  t("template_switch.toggle-off-hint");
 | 
			
		||||
        this.switchOffTooltip = t("template_switch.toggle-off-hint");
 | 
			
		||||
 | 
			
		||||
        this.$helpButton.attr("data-help-page", "template.html").show();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -55,8 +55,8 @@ const TPL = `<div class="toc-widget">
 | 
			
		||||
</div>`;
 | 
			
		||||
 | 
			
		||||
interface Toc {
 | 
			
		||||
    $toc: JQuery<HTMLElement>,
 | 
			
		||||
    headingCount: number
 | 
			
		||||
    $toc: JQuery<HTMLElement>;
 | 
			
		||||
    headingCount: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default class TocWidget extends RightPanelWidget {
 | 
			
		||||
@ -89,8 +89,8 @@ export default class TocWidget extends RightPanelWidget {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const isHelpNote = (this.note.type === "doc" && this.note.noteId.startsWith("_help"));
 | 
			
		||||
        const isTextNote = (this.note.type === "text");
 | 
			
		||||
        const isHelpNote = this.note.type === "doc" && this.note.noteId.startsWith("_help");
 | 
			
		||||
        const isTextNote = this.note.type === "text";
 | 
			
		||||
        const isNoteSupported = isTextNote || isHelpNote;
 | 
			
		||||
 | 
			
		||||
        return isNoteSupported
 | 
			
		||||
@ -156,7 +156,7 @@ export default class TocWidget extends RightPanelWidget {
 | 
			
		||||
 | 
			
		||||
        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);
 | 
			
		||||
        if (this.noteContext?.viewScope) {
 | 
			
		||||
            this.noteContext.viewScope.tocPreviousVisible = visible;
 | 
			
		||||
 | 
			
		||||
@ -63,7 +63,7 @@ export default class AttachmentDetailTypeWidget extends TypeWidget {
 | 
			
		||||
 | 
			
		||||
        this.$linksWrapper.empty().append(
 | 
			
		||||
            t("attachment_detail.owning_note"),
 | 
			
		||||
            (await linkService.createLink(this.noteId)),
 | 
			
		||||
            await linkService.createLink(this.noteId),
 | 
			
		||||
            t("attachment_detail.you_can_also_open"),
 | 
			
		||||
            await linkService.createLink(this.noteId, {
 | 
			
		||||
                title: t("attachment_detail.list_of_all_attachments"),
 | 
			
		||||
@ -74,7 +74,7 @@ export default class AttachmentDetailTypeWidget extends TypeWidget {
 | 
			
		||||
            $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) {
 | 
			
		||||
            this.$wrapper.html("<strong>" + t("attachment_detail.attachment_deleted") + "</strong>");
 | 
			
		||||
 | 
			
		||||
@ -66,7 +66,7 @@ export default class AttachmentListTypeWidget extends TypeWidget {
 | 
			
		||||
                    .text(t("attachment_list.upload_attachments"))
 | 
			
		||||
                    .on("click", () => {
 | 
			
		||||
                        if (this.noteId) {
 | 
			
		||||
                            this.triggerCommand("showUploadAttachmentsDialog", { noteId: this.noteId })
 | 
			
		||||
                            this.triggerCommand("showUploadAttachmentsDialog", { noteId: this.noteId });
 | 
			
		||||
                        }
 | 
			
		||||
                    }),
 | 
			
		||||
                $helpButton
 | 
			
		||||
 | 
			
		||||
@ -36,9 +36,7 @@ export default class BookTypeWidget extends TypeWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async doRefresh(note: FNote) {
 | 
			
		||||
        this.$helpNoChildren.toggle(
 | 
			
		||||
            !this.note?.hasChildren()
 | 
			
		||||
            && this.note?.getAttributeValue("label", "viewType") !== "calendar");
 | 
			
		||||
        this.$helpNoChildren.toggle(!this.note?.hasChildren() && this.note?.getAttributeValue("label", "viewType") !== "calendar");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
 | 
			
		||||
 | 
			
		||||
@ -59,9 +59,9 @@ const TPL = `
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
interface CanvasContent {
 | 
			
		||||
    elements: ExcalidrawElement[],
 | 
			
		||||
    files: BinaryFileData[],
 | 
			
		||||
    appState: Partial<AppState>
 | 
			
		||||
    elements: ExcalidrawElement[];
 | 
			
		||||
    files: BinaryFileData[];
 | 
			
		||||
    appState: Partial<AppState>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface AttachmentMetadata {
 | 
			
		||||
@ -198,7 +198,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
 | 
			
		||||
        }
 | 
			
		||||
        (window.process.env as any).PREACT = false;
 | 
			
		||||
 | 
			
		||||
        const excalidraw = (await import("@excalidraw/excalidraw"));
 | 
			
		||||
        const excalidraw = await import("@excalidraw/excalidraw");
 | 
			
		||||
        this.excalidrawLib = excalidraw;
 | 
			
		||||
 | 
			
		||||
        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>) {
 | 
			
		||||
        const excalidrawWrapperRef = react.useRef<HTMLElement>(null);
 | 
			
		||||
        this.excalidrawWrapperRef = excalidrawWrapperRef;
 | 
			
		||||
        const [dimensions, setDimensions] = react.useState<{ width?: number, height?: number}>({
 | 
			
		||||
        const [dimensions, setDimensions] = react.useState<{ width?: number; height?: number }>({
 | 
			
		||||
            width: undefined,
 | 
			
		||||
            height: undefined
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@ -7,91 +7,78 @@ export function buildConfig() {
 | 
			
		||||
        image: {
 | 
			
		||||
            styles: {
 | 
			
		||||
                options: [
 | 
			
		||||
                    'inline',
 | 
			
		||||
                    'alignBlockLeft',
 | 
			
		||||
                    'alignCenter',
 | 
			
		||||
                    'alignBlockRight',
 | 
			
		||||
                    'alignLeft',
 | 
			
		||||
                    'alignRight',
 | 
			
		||||
                    'full', // full and side are for BC since the old images have been created with these styles
 | 
			
		||||
                    'side'
 | 
			
		||||
                    "inline",
 | 
			
		||||
                    "alignBlockLeft",
 | 
			
		||||
                    "alignCenter",
 | 
			
		||||
                    "alignBlockRight",
 | 
			
		||||
                    "alignLeft",
 | 
			
		||||
                    "alignRight",
 | 
			
		||||
                    "full", // full and side are for BC since the old images have been created with these styles
 | 
			
		||||
                    "side"
 | 
			
		||||
                ]
 | 
			
		||||
            },
 | 
			
		||||
            resizeOptions: [
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'imageResize:original',
 | 
			
		||||
                    name: "imageResize:original",
 | 
			
		||||
                    value: null,
 | 
			
		||||
                    icon: 'original'
 | 
			
		||||
                    icon: "original"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'imageResize:25',
 | 
			
		||||
                    value: '25',
 | 
			
		||||
                    icon: 'small'
 | 
			
		||||
                    name: "imageResize:25",
 | 
			
		||||
                    value: "25",
 | 
			
		||||
                    icon: "small"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'imageResize:50',
 | 
			
		||||
                    value: '50',
 | 
			
		||||
                    icon: 'medium'
 | 
			
		||||
                    name: "imageResize:50",
 | 
			
		||||
                    value: "50",
 | 
			
		||||
                    icon: "medium"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: 'imageResize:75',
 | 
			
		||||
                    value: '75',
 | 
			
		||||
                    icon: 'medium'
 | 
			
		||||
                    name: "imageResize:75",
 | 
			
		||||
                    value: "75",
 | 
			
		||||
                    icon: "medium"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            toolbar: [
 | 
			
		||||
                // Image styles, see https://ckeditor.com/docs/ckeditor5/latest/features/images/images-styles.html#demo.
 | 
			
		||||
                'imageStyle:inline',
 | 
			
		||||
                'imageStyle:alignCenter',
 | 
			
		||||
                "imageStyle:inline",
 | 
			
		||||
                "imageStyle:alignCenter",
 | 
			
		||||
                {
 | 
			
		||||
                    name: "imageStyle:wrapText",
 | 
			
		||||
                    title: "Wrap text",
 | 
			
		||||
                    items: [
 | 
			
		||||
                        'imageStyle:alignLeft',
 | 
			
		||||
                        'imageStyle:alignRight',
 | 
			
		||||
                    ],
 | 
			
		||||
                    defaultItem: 'imageStyle:alignRight'
 | 
			
		||||
                    items: ["imageStyle:alignLeft", "imageStyle:alignRight"],
 | 
			
		||||
                    defaultItem: "imageStyle:alignRight"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: "imageStyle:block",
 | 
			
		||||
                    title: "Block align",
 | 
			
		||||
                    items: [
 | 
			
		||||
                        'imageStyle:alignBlockLeft',
 | 
			
		||||
                        'imageStyle:alignBlockRight'
 | 
			
		||||
                    ],
 | 
			
		||||
                    defaultItem: "imageStyle:alignBlockLeft",
 | 
			
		||||
                    items: ["imageStyle:alignBlockLeft", "imageStyle:alignBlockRight"],
 | 
			
		||||
                    defaultItem: "imageStyle:alignBlockLeft"
 | 
			
		||||
                },
 | 
			
		||||
                '|',
 | 
			
		||||
                'imageResize:25',
 | 
			
		||||
                'imageResize:50',
 | 
			
		||||
                'imageResize:original',
 | 
			
		||||
                '|',
 | 
			
		||||
                'toggleImageCaption'
 | 
			
		||||
                "|",
 | 
			
		||||
                "imageResize:25",
 | 
			
		||||
                "imageResize:50",
 | 
			
		||||
                "imageResize:original",
 | 
			
		||||
                "|",
 | 
			
		||||
                "toggleImageCaption"
 | 
			
		||||
            ],
 | 
			
		||||
            upload: {
 | 
			
		||||
                types: [ 'jpeg', 'png', 'gif', 'bmp', 'webp', 'tiff', 'svg', 'svg+xml', 'avif' ]
 | 
			
		||||
                types: ["jpeg", "png", "gif", "bmp", "webp", "tiff", "svg", "svg+xml", "avif"]
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        heading: {
 | 
			
		||||
            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
 | 
			
		||||
                { 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: '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: 'heading6' as const, view: 'h6', title: 'Heading 6', class: 'ck-heading_heading6' }
 | 
			
		||||
                { 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: "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: "heading6" as const, view: "h6", title: "Heading 6", class: "ck-heading_heading6" }
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        table: {
 | 
			
		||||
            contentToolbar: [
 | 
			
		||||
                'tableColumn',
 | 
			
		||||
                'tableRow',
 | 
			
		||||
                'mergeTableCells',
 | 
			
		||||
                'tableProperties',
 | 
			
		||||
                'tableCellProperties',
 | 
			
		||||
                'toggleTableCaption'
 | 
			
		||||
            ]
 | 
			
		||||
            contentToolbar: ["tableColumn", "tableRow", "mergeTableCells", "tableProperties", "tableCellProperties", "toggleTableCaption"]
 | 
			
		||||
        },
 | 
			
		||||
        list: {
 | 
			
		||||
            properties: {
 | 
			
		||||
@ -101,17 +88,17 @@ export function buildConfig() {
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        link: {
 | 
			
		||||
            defaultProtocol: 'https://',
 | 
			
		||||
            defaultProtocol: "https://",
 | 
			
		||||
            allowedProtocols: ALLOWED_PROTOCOLS
 | 
			
		||||
        },
 | 
			
		||||
        // This value must be kept in sync with the language defined in webpack.config.js.
 | 
			
		||||
        language: 'en'
 | 
			
		||||
    }
 | 
			
		||||
        language: "en"
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function buildToolbarConfig(isClassicToolbar: boolean) {
 | 
			
		||||
    if (isClassicToolbar) {
 | 
			
		||||
        const multilineToolbar = utils.isDesktop() && options.get("textNoteEditorMultilineToolbar") === "true"
 | 
			
		||||
        const multilineToolbar = utils.isDesktop() && options.get("textNoteEditorMultilineToolbar") === "true";
 | 
			
		||||
        return buildClassicToolbar(multilineToolbar);
 | 
			
		||||
    } else {
 | 
			
		||||
        return buildFloatingToolbar();
 | 
			
		||||
@ -123,101 +110,92 @@ function buildClassicToolbar(multilineToolbar: boolean) {
 | 
			
		||||
    return {
 | 
			
		||||
        toolbar: {
 | 
			
		||||
            items: [
 | 
			
		||||
                'heading', 'fontSize',
 | 
			
		||||
                '|',
 | 
			
		||||
                'bold', 'italic',
 | 
			
		||||
                "heading",
 | 
			
		||||
                "fontSize",
 | 
			
		||||
                "|",
 | 
			
		||||
                "bold",
 | 
			
		||||
                "italic",
 | 
			
		||||
                {
 | 
			
		||||
                    label: "Text formatting",
 | 
			
		||||
                    icon: "text",
 | 
			
		||||
                    items: [
 | 
			
		||||
                        'underline',
 | 
			
		||||
                        'strikethrough',
 | 
			
		||||
                        'superscript',
 | 
			
		||||
                        'subscript',
 | 
			
		||||
                        'code',
 | 
			
		||||
                    ],
 | 
			
		||||
                    items: ["underline", "strikethrough", "superscript", "subscript", "code"]
 | 
			
		||||
                },
 | 
			
		||||
                '|',
 | 
			
		||||
                'fontColor', 'fontBackgroundColor', 'removeFormat',
 | 
			
		||||
                '|',
 | 
			
		||||
                'bulletedList', 'numberedList', 'todoList',
 | 
			
		||||
                '|',
 | 
			
		||||
                'blockQuote', 'insertTable', 'codeBlock', 'footnote',
 | 
			
		||||
                "|",
 | 
			
		||||
                "fontColor",
 | 
			
		||||
                "fontBackgroundColor",
 | 
			
		||||
                "removeFormat",
 | 
			
		||||
                "|",
 | 
			
		||||
                "bulletedList",
 | 
			
		||||
                "numberedList",
 | 
			
		||||
                "todoList",
 | 
			
		||||
                "|",
 | 
			
		||||
                "blockQuote",
 | 
			
		||||
                "insertTable",
 | 
			
		||||
                "codeBlock",
 | 
			
		||||
                "footnote",
 | 
			
		||||
                {
 | 
			
		||||
                    label: "Insert",
 | 
			
		||||
                    icon: "plus",
 | 
			
		||||
                    items: [
 | 
			
		||||
                        'imageUpload',
 | 
			
		||||
                        '|',
 | 
			
		||||
                        'link',
 | 
			
		||||
                        'internallink',
 | 
			
		||||
                        'includeNote',
 | 
			
		||||
                        '|',
 | 
			
		||||
                        'specialCharacters',
 | 
			
		||||
                        'math',
 | 
			
		||||
                        'mermaid',
 | 
			
		||||
                        'horizontalLine',
 | 
			
		||||
                        'pageBreak'
 | 
			
		||||
                    ]
 | 
			
		||||
                    items: ["imageUpload", "|", "link", "internallink", "includeNote", "|", "specialCharacters", "math", "mermaid", "horizontalLine", "pageBreak"]
 | 
			
		||||
                },
 | 
			
		||||
                '|',
 | 
			
		||||
                'outdent', 'indent',
 | 
			
		||||
                '|',
 | 
			
		||||
                'markdownImport', 'cuttonote', 'findAndReplace'
 | 
			
		||||
                "|",
 | 
			
		||||
                "outdent",
 | 
			
		||||
                "indent",
 | 
			
		||||
                "|",
 | 
			
		||||
                "markdownImport",
 | 
			
		||||
                "cuttonote",
 | 
			
		||||
                "findAndReplace"
 | 
			
		||||
            ],
 | 
			
		||||
            shouldNotGroupWhenFull: multilineToolbar
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildFloatingToolbar() {
 | 
			
		||||
    return {
 | 
			
		||||
        toolbar: {
 | 
			
		||||
			items: [
 | 
			
		||||
				'fontSize',
 | 
			
		||||
				'bold',
 | 
			
		||||
				'italic',
 | 
			
		||||
				'underline',
 | 
			
		||||
				'strikethrough',
 | 
			
		||||
				'superscript',
 | 
			
		||||
				'subscript',
 | 
			
		||||
				'fontColor',
 | 
			
		||||
				'fontBackgroundColor',
 | 
			
		||||
				'code',
 | 
			
		||||
				'link',
 | 
			
		||||
				'removeFormat',
 | 
			
		||||
				'internallink',
 | 
			
		||||
				'cuttonote'
 | 
			
		||||
			]
 | 
			
		||||
		},
 | 
			
		||||
            items: [
 | 
			
		||||
                "fontSize",
 | 
			
		||||
                "bold",
 | 
			
		||||
                "italic",
 | 
			
		||||
                "underline",
 | 
			
		||||
                "strikethrough",
 | 
			
		||||
                "superscript",
 | 
			
		||||
                "subscript",
 | 
			
		||||
                "fontColor",
 | 
			
		||||
                "fontBackgroundColor",
 | 
			
		||||
                "code",
 | 
			
		||||
                "link",
 | 
			
		||||
                "removeFormat",
 | 
			
		||||
                "internallink",
 | 
			
		||||
                "cuttonote"
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
		blockToolbar: [
 | 
			
		||||
			'heading',
 | 
			
		||||
			'|',
 | 
			
		||||
			'bulletedList', 'numberedList', 'todoList',
 | 
			
		||||
			'|',
 | 
			
		||||
			'blockQuote', 'codeBlock', 'insertTable',
 | 
			
		||||
			'footnote',
 | 
			
		||||
			{
 | 
			
		||||
				label: "Insert",
 | 
			
		||||
				icon: "plus",
 | 
			
		||||
				items: [
 | 
			
		||||
					'internallink',
 | 
			
		||||
					'includeNote',
 | 
			
		||||
					'|',
 | 
			
		||||
					'math',
 | 
			
		||||
					'mermaid',
 | 
			
		||||
					'horizontalLine',
 | 
			
		||||
					'pageBreak'
 | 
			
		||||
				]
 | 
			
		||||
			},
 | 
			
		||||
			'|',
 | 
			
		||||
			'outdent', 'indent',
 | 
			
		||||
			'|',
 | 
			
		||||
			'imageUpload',
 | 
			
		||||
			'markdownImport',
 | 
			
		||||
			'specialCharacters',
 | 
			
		||||
			'findAndReplace'
 | 
			
		||||
		]
 | 
			
		||||
        blockToolbar: [
 | 
			
		||||
            "heading",
 | 
			
		||||
            "|",
 | 
			
		||||
            "bulletedList",
 | 
			
		||||
            "numberedList",
 | 
			
		||||
            "todoList",
 | 
			
		||||
            "|",
 | 
			
		||||
            "blockQuote",
 | 
			
		||||
            "codeBlock",
 | 
			
		||||
            "insertTable",
 | 
			
		||||
            "footnote",
 | 
			
		||||
            {
 | 
			
		||||
                label: "Insert",
 | 
			
		||||
                icon: "plus",
 | 
			
		||||
                items: ["internallink", "includeNote", "|", "math", "mermaid", "horizontalLine", "pageBreak"]
 | 
			
		||||
            },
 | 
			
		||||
            "|",
 | 
			
		||||
            "outdent",
 | 
			
		||||
            "indent",
 | 
			
		||||
            "|",
 | 
			
		||||
            "imageUpload",
 | 
			
		||||
            "markdownImport",
 | 
			
		||||
            "specialCharacters",
 | 
			
		||||
            "findAndReplace"
 | 
			
		||||
        ]
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,7 @@ import CodeMimeTypesOptions from "./options/code_notes/code_mime_types.js";
 | 
			
		||||
import ImageOptions from "./options/images/images.js";
 | 
			
		||||
import SpellcheckOptions from "./options/spellcheck.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 BackupOptions from "./options/backup.js";
 | 
			
		||||
import SyncOptions from "./options/sync.js";
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { GPX, Marker, type LatLng, type LeafletMouseEvent } from "leaflet";
 | 
			
		||||
import type FNote from "../../entities/fnote.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 toastService from "../../services/toast.js";
 | 
			
		||||
import dialogService from "../../services/dialog.js";
 | 
			
		||||
@ -75,21 +75,21 @@ const TPL = `\
 | 
			
		||||
 | 
			
		||||
const LOCATION_ATTRIBUTE = "geolocation";
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
interface MapData {
 | 
			
		||||
    view?: {
 | 
			
		||||
        center?: LatLng | [ number, number ];
 | 
			
		||||
        center?: LatLng | [number, number];
 | 
			
		||||
        zoom?: number;
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: Deduplicate
 | 
			
		||||
interface CreateChildResponse {
 | 
			
		||||
    note: {
 | 
			
		||||
        noteId: string;
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum State {
 | 
			
		||||
@ -220,7 +220,7 @@ export default class GeoMapTypeWidget extends TypeWidget {
 | 
			
		||||
            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 icon = this.#buildIcon(note.getIcon(), note.getColorClass(), note.title);
 | 
			
		||||
 | 
			
		||||
@ -228,10 +228,10 @@ export default class GeoMapTypeWidget extends TypeWidget {
 | 
			
		||||
            icon,
 | 
			
		||||
            draggable: true,
 | 
			
		||||
            autoPan: true,
 | 
			
		||||
            autoPanSpeed: 5,
 | 
			
		||||
            autoPanSpeed: 5
 | 
			
		||||
        })
 | 
			
		||||
            .addTo(map)
 | 
			
		||||
            .on("moveend", e => {
 | 
			
		||||
            .on("moveend", (e) => {
 | 
			
		||||
                this.moveMarker(note.noteId, (e.target as Marker).getLatLng());
 | 
			
		||||
            });
 | 
			
		||||
        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" />
 | 
			
		||||
                <span class="bx ${bxIconClass} ${colorClass}"></span>
 | 
			
		||||
                <span class="title-label">${title}</span>`,
 | 
			
		||||
            iconSize: [ 25, 41 ],
 | 
			
		||||
            iconAnchor: [ 12, 41 ]
 | 
			
		||||
        })
 | 
			
		||||
            iconSize: [25, 41],
 | 
			
		||||
            iconAnchor: [12, 41]
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #changeState(newState: State) {
 | 
			
		||||
@ -296,7 +296,7 @@ export default class GeoMapTypeWidget extends TypeWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -361,7 +361,7 @@ export default class GeoMapTypeWidget extends TypeWidget {
 | 
			
		||||
        // If any of note has its location attribute changed.
 | 
			
		||||
        // TODO: Should probably filter by parent here as well.
 | 
			
		||||
        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();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -259,7 +259,7 @@ export default class MindMapWidget extends TypeWidget {
 | 
			
		||||
        return await this.mind.exportSvg().text();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded"> ) {
 | 
			
		||||
    async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
 | 
			
		||||
        if (this.noteId && loadResults.isNoteReloaded(this.noteId)) {
 | 
			
		||||
            this.refresh();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -60,7 +60,7 @@ interface Theme {
 | 
			
		||||
    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.
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,7 @@ const TPL = `
 | 
			
		||||
        </label>
 | 
			
		||||
    </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>
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -94,10 +94,12 @@ export default class ThemeOptions extends OptionsWidget {
 | 
			
		||||
        this.$themeSelect.empty();
 | 
			
		||||
 | 
			
		||||
        for (const theme of themes) {
 | 
			
		||||
            this.$themeSelect.append($("<option>")
 | 
			
		||||
                .attr("value", theme.val)
 | 
			
		||||
                .attr("data-note-id", theme.noteId || "")
 | 
			
		||||
                .text(theme.title));
 | 
			
		||||
            this.$themeSelect.append(
 | 
			
		||||
                $("<option>")
 | 
			
		||||
                    .attr("value", theme.val)
 | 
			
		||||
                    .attr("data-note-id", theme.noteId || "")
 | 
			
		||||
                    .text(theme.title)
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.$themeSelect.val(options.theme);
 | 
			
		||||
 | 
			
		||||
@ -97,9 +97,8 @@ export default class CodeMimeTypesOptions extends OptionsWidget {
 | 
			
		||||
        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))
 | 
			
		||||
            .on("change", () => this.save())
 | 
			
		||||
            .append(mimeType.title)
 | 
			
		||||
            .append(mimeType.title);
 | 
			
		||||
 | 
			
		||||
        return $("<li>")
 | 
			
		||||
            .append(checkbox);
 | 
			
		||||
        return $("<li>").append(checkbox);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -31,7 +31,7 @@ export default class NoteErasureTimeoutOptions extends TimeSelector {
 | 
			
		||||
        const $timeSelector = this.$widget;
 | 
			
		||||
        // inject TimeSelector widget template
 | 
			
		||||
        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");
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,6 @@ export default class RevisionsSnapshotIntervalOptions extends TimeSelector {
 | 
			
		||||
        const $timeSelector = this.$widget;
 | 
			
		||||
        // inject TimeSelector widget template
 | 
			
		||||
        this.$widget = $(TPL);
 | 
			
		||||
        this.$widget.find("#time-selector-placeholder").replaceWith($timeSelector)
 | 
			
		||||
        this.$widget.find("#time-selector-placeholder").replaceWith($timeSelector);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -29,8 +29,8 @@ export default class ShareSettingsOptions extends OptionsWidget {
 | 
			
		||||
        this.$widget = $(TPL);
 | 
			
		||||
        this.contentSized();
 | 
			
		||||
 | 
			
		||||
        this.$shareRootCheck = this.$widget.find('.share-root-check');
 | 
			
		||||
        this.$shareRootStatus = this.$widget.find('.share-root-status');
 | 
			
		||||
        this.$shareRootCheck = this.$widget.find(".share-root-check");
 | 
			
		||||
        this.$shareRootStatus = this.$widget.find(".share-root-status");
 | 
			
		||||
 | 
			
		||||
        // Add change handlers for both checkboxes
 | 
			
		||||
        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
 | 
			
		||||
            const target = e.target as HTMLInputElement;
 | 
			
		||||
            if (target.name === 'redirectBareDomain') {
 | 
			
		||||
            if (target.name === "redirectBareDomain") {
 | 
			
		||||
                this.$shareRootCheck.toggle(target.checked);
 | 
			
		||||
                if (target.checked) {
 | 
			
		||||
                    this.checkShareRoot();
 | 
			
		||||
@ -47,7 +47,7 @@ export default class ShareSettingsOptions extends OptionsWidget {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // 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) {
 | 
			
		||||
@ -62,28 +62,26 @@ export default class ShareSettingsOptions extends OptionsWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async checkShareRoot() {
 | 
			
		||||
        const $button = this.$widget.find('.check-share-root');
 | 
			
		||||
        $button.prop('disabled', true);
 | 
			
		||||
        const $button = this.$widget.find(".check-share-root");
 | 
			
		||||
        $button.prop("disabled", true);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const shareRootNotes = await searchService.searchForNotes("#shareRoot");
 | 
			
		||||
            const sharedShareRootNote = shareRootNotes.find(note => note.isShared());
 | 
			
		||||
            const sharedShareRootNote = shareRootNotes.find((note) => note.isShared());
 | 
			
		||||
 | 
			
		||||
            if (sharedShareRootNote) {
 | 
			
		||||
                this.$shareRootStatus
 | 
			
		||||
                    .removeClass('text-danger')
 | 
			
		||||
                    .addClass('text-success')
 | 
			
		||||
                    .text(t("share.share_root_found", {noteTitle: sharedShareRootNote.title}));
 | 
			
		||||
                    .removeClass("text-danger")
 | 
			
		||||
                    .addClass("text-success")
 | 
			
		||||
                    .text(t("share.share_root_found", { noteTitle: sharedShareRootNote.title }));
 | 
			
		||||
            } else {
 | 
			
		||||
                this.$shareRootStatus
 | 
			
		||||
                    .removeClass('text-success')
 | 
			
		||||
                    .addClass('text-danger')
 | 
			
		||||
                    .text(shareRootNotes.length > 0
 | 
			
		||||
                        ? t("share.share_root_not_shared", {noteTitle: shareRootNotes[0].title})
 | 
			
		||||
                        : t("share.share_root_not_found"));
 | 
			
		||||
                    .removeClass("text-success")
 | 
			
		||||
                    .addClass("text-danger")
 | 
			
		||||
                    .text(shareRootNotes.length > 0 ? t("share.share_root_not_shared", { noteTitle: shareRootNotes[0].title }) : t("share.share_root_not_found"));
 | 
			
		||||
            }
 | 
			
		||||
        } finally {
 | 
			
		||||
            $button.prop('disabled', false);
 | 
			
		||||
            $button.prop("disabled", false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,6 @@ export default class ProtectedSessionTimeoutOptions extends TimeSelector {
 | 
			
		||||
        const $timeSelector = this.$widget;
 | 
			
		||||
        // inject TimeSelector widget template
 | 
			
		||||
        this.$widget = $(TPL);
 | 
			
		||||
        this.$widget.find("#time-selector-placeholder").replaceWith($timeSelector)
 | 
			
		||||
        this.$widget.find("#time-selector-placeholder").replaceWith($timeSelector);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -54,7 +54,7 @@ export default class TimeSelector extends OptionsWidget {
 | 
			
		||||
        this.optionValueId = options.optionValueId;
 | 
			
		||||
        this.optionTimeScaleId = options.optionTimeScaleId;
 | 
			
		||||
        this.includedTimeScales = options.includedTimeScales || new Set(["seconds", "minutes", "hours", "days"]);
 | 
			
		||||
        this.minimumSeconds = options.minimumSeconds || 0
 | 
			
		||||
        this.minimumSeconds = options.minimumSeconds || 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    doRender() {
 | 
			
		||||
@ -131,10 +131,10 @@ export default class TimeSelector extends OptionsWidget {
 | 
			
		||||
 | 
			
		||||
    private setInternalTimeInSeconds(time: number) {
 | 
			
		||||
        if (time < this.minimumSeconds) {
 | 
			
		||||
            toastService.showError(t("time_selector.minimum_input", {minimumSeconds: this.minimumSeconds}));
 | 
			
		||||
            return this.internalTimeInSeconds = this.minimumSeconds;
 | 
			
		||||
            toastService.showError(t("time_selector.minimum_input", { minimumSeconds: this.minimumSeconds }));
 | 
			
		||||
            return (this.internalTimeInSeconds = this.minimumSeconds);
 | 
			
		||||
        }
 | 
			
		||||
        return this.internalTimeInSeconds = time;
 | 
			
		||||
        return (this.internalTimeInSeconds = time);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -87,7 +87,7 @@ const TPL = `
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
function buildTasks(tasks: FTask[]) {
 | 
			
		||||
    let html = '';
 | 
			
		||||
    let html = "";
 | 
			
		||||
 | 
			
		||||
    const now = dayjs();
 | 
			
		||||
    const dateFormat = "DD-MM-YYYY";
 | 
			
		||||
@ -137,7 +137,9 @@ export default class TaskListWidget extends TypeWidget {
 | 
			
		||||
    private $taskContainer!: JQuery<HTMLElement>;
 | 
			
		||||
    private $addNewTask!: JQuery<HTMLElement>;
 | 
			
		||||
 | 
			
		||||
    static getType() { return "taskList" }
 | 
			
		||||
    static getType() {
 | 
			
		||||
        return "taskList";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    doRender() {
 | 
			
		||||
        this.$widget = $(TPL);
 | 
			
		||||
@ -231,19 +233,18 @@ export default class TaskListWidget extends TypeWidget {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (await froca.getTasks(this.noteId))
 | 
			
		||||
            .toSorted((a, b) => {
 | 
			
		||||
                // Sort by due date, closest date first.
 | 
			
		||||
                if (!a.dueDate) {
 | 
			
		||||
                    return 1;
 | 
			
		||||
                }
 | 
			
		||||
        return (await froca.getTasks(this.noteId)).toSorted((a, b) => {
 | 
			
		||||
            // Sort by due date, closest date first.
 | 
			
		||||
            if (!a.dueDate) {
 | 
			
		||||
                return 1;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
                if (!b.dueDate) {
 | 
			
		||||
                    return -1;
 | 
			
		||||
                }
 | 
			
		||||
            if (!b.dueDate) {
 | 
			
		||||
                return -1;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
                return a.dueDate.localeCompare(b.dueDate, "en");
 | 
			
		||||
            });
 | 
			
		||||
            return a.dueDate.localeCompare(b.dueDate, "en");
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async doRefresh(note: FNote) {
 | 
			
		||||
 | 
			
		||||
@ -68,7 +68,7 @@ const TPL = `
 | 
			
		||||
interface CreateChildResponse {
 | 
			
		||||
    note: {
 | 
			
		||||
        noteId: string;
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default class CalendarView extends ViewMode {
 | 
			
		||||
@ -126,7 +126,7 @@ export default class CalendarView extends ViewMode {
 | 
			
		||||
            weekNumbers: this.parentNote.hasAttribute("label", "calendar:weekNumbers"),
 | 
			
		||||
            locale: await CalendarView.#getLocale(),
 | 
			
		||||
            height: "100%",
 | 
			
		||||
            eventContent: (e => {
 | 
			
		||||
            eventContent: (e) => {
 | 
			
		||||
                let html = "";
 | 
			
		||||
                const { iconClass, promotedAttributes } = e.event.extendedProps;
 | 
			
		||||
 | 
			
		||||
@ -138,7 +138,7 @@ export default class CalendarView extends ViewMode {
 | 
			
		||||
 | 
			
		||||
                // Promoted attributes
 | 
			
		||||
                if (promotedAttributes) {
 | 
			
		||||
                    for (const [ name, value ] of Object.entries(promotedAttributes)) {
 | 
			
		||||
                    for (const [name, value] of Object.entries(promotedAttributes)) {
 | 
			
		||||
                        html += `\
 | 
			
		||||
                        <div class="promoted-attribute">
 | 
			
		||||
                            <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 };
 | 
			
		||||
            }),
 | 
			
		||||
            },
 | 
			
		||||
            dateClick: async (e) => {
 | 
			
		||||
                if (!this.isCalendarRoot) {
 | 
			
		||||
                    return;
 | 
			
		||||
@ -260,7 +260,7 @@ export default class CalendarView extends ViewMode {
 | 
			
		||||
            // TODO: Deduplicate get type.
 | 
			
		||||
            const dateNotesForMonth = await server.get<Record<string, string>>(`special-notes/notes-for-month/${month}?calendarRoot=${this.parentNote.noteId}`);
 | 
			
		||||
            const dateNoteIds = Object.values(dateNotesForMonth);
 | 
			
		||||
            allDateNoteIds = [ ...allDateNoteIds, ...dateNoteIds ];
 | 
			
		||||
            allDateNoteIds = [...allDateNoteIds, ...dateNoteIds];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Request all the date notes.
 | 
			
		||||
@ -379,7 +379,7 @@ export default class CalendarView extends ViewMode {
 | 
			
		||||
        const result: Record<string, string> = {};
 | 
			
		||||
 | 
			
		||||
        for (const promotedAttribute of filteredPromotedAttributes) {
 | 
			
		||||
            const [ type, name ] = promotedAttribute.name.split(":", 2);
 | 
			
		||||
            const [type, name] = promotedAttribute.name.split(":", 2);
 | 
			
		||||
            const definition = promotedAttribute.getDefinition();
 | 
			
		||||
 | 
			
		||||
            if (definition.multiplicity !== "single") {
 | 
			
		||||
@ -411,7 +411,7 @@ export default class CalendarView extends ViewMode {
 | 
			
		||||
            if (customTitleValue.startsWith("#")) {
 | 
			
		||||
                const labelValue = note.getAttributeValue("label", attributeName);
 | 
			
		||||
                if (labelValue) {
 | 
			
		||||
                    return [ labelValue ];
 | 
			
		||||
                    return [labelValue];
 | 
			
		||||
                }
 | 
			
		||||
            } else if (allowRelations && customTitleValue.startsWith("~")) {
 | 
			
		||||
                const relations = note.getRelations(attributeName);
 | 
			
		||||
@ -432,7 +432,7 @@ export default class CalendarView extends ViewMode {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return [ note.title ];
 | 
			
		||||
        return [note.title];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static async #setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) {
 | 
			
		||||
@ -456,7 +456,7 @@ export default class CalendarView extends ViewMode {
 | 
			
		||||
 | 
			
		||||
        const offset = date.getTimezoneOffset();
 | 
			
		||||
        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) {
 | 
			
		||||
 | 
			
		||||
@ -75,7 +75,6 @@
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.calendar-dropdown-widget .calendar-week span {
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    flex: 0 0 14.28%;
 | 
			
		||||
 | 
			
		||||
@ -202,9 +202,8 @@ span[style] {
 | 
			
		||||
 | 
			
		||||
@supports selector(.todo-list__label__description:has(*)) and (height: 1lh) {
 | 
			
		||||
    .note-detail-printable .todo-list__label__description {
 | 
			
		||||
 | 
			
		||||
        /* 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 */
 | 
			
		||||
        --box-text-gap: 0.25em;
 | 
			
		||||
 | 
			
		||||
@ -303,7 +302,12 @@ pre > code {
 | 
			
		||||
    orphans: 6;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1, h2, h3, h4, h5, h6 {
 | 
			
		||||
h1,
 | 
			
		||||
h2,
 | 
			
		||||
h3,
 | 
			
		||||
h4,
 | 
			
		||||
h5,
 | 
			
		||||
h6 {
 | 
			
		||||
    page-break-after: avoid;
 | 
			
		||||
    break-after: avoid;
 | 
			
		||||
}
 | 
			
		||||
@ -32,7 +32,7 @@
 | 
			
		||||
    --cmd-button-icon-color: white;
 | 
			
		||||
    --cmd-button-keyboard-shortcut-background: #0000004d;
 | 
			
		||||
    --cmd-button-keyboard-shortcut-color: white;
 | 
			
		||||
    --cmd-button-disabled-opacity: .5;
 | 
			
		||||
    --cmd-button-disabled-opacity: 0.5;
 | 
			
		||||
 | 
			
		||||
    --icon-button-color: currentColor;
 | 
			
		||||
    --icon-button-hover-background: var(--hover-item-background-color);
 | 
			
		||||
@ -41,7 +41,7 @@
 | 
			
		||||
    --muted-text-color: #bbb;
 | 
			
		||||
 | 
			
		||||
    --input-background-color: #ffffff12;
 | 
			
		||||
    --input-text-color:  #ffffffc7;
 | 
			
		||||
    --input-text-color: #ffffffc7;
 | 
			
		||||
    --input-placeholder-color: #b7b7b782;
 | 
			
		||||
    --input-selection-background: gray;
 | 
			
		||||
    --input-selection-text-color: white;
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,7 @@
 | 
			
		||||
    --cmd-button-icon-color: black;
 | 
			
		||||
    --cmd-button-keyboard-shortcut-background: #00000017;
 | 
			
		||||
    --cmd-button-keyboard-shortcut-color: black;
 | 
			
		||||
    --cmd-button-disabled-opacity: .5;
 | 
			
		||||
    --cmd-button-disabled-opacity: 0.5;
 | 
			
		||||
 | 
			
		||||
    --icon-button-color: currentColor;
 | 
			
		||||
    --icon-button-hover-background: var(--hover-item-background-color);
 | 
			
		||||
 | 
			
		||||
@ -61,7 +61,7 @@
 | 
			
		||||
    --help-backdrop-blur: 10px;
 | 
			
		||||
 | 
			
		||||
    --icon-button-size: 32px;
 | 
			
		||||
    --icon-button-icon-ratio: .65;
 | 
			
		||||
    --icon-button-icon-ratio: 0.65;
 | 
			
		||||
 | 
			
		||||
    /* Theme capabilities */
 | 
			
		||||
    --tab-note-icons: true;
 | 
			
		||||
 | 
			
		||||
@ -30,11 +30,11 @@ button.btn.btn-primary:active,
 | 
			
		||||
button.btn.btn-secondary:active,
 | 
			
		||||
button.btn.btn-sm:not(.select-button):active,
 | 
			
		||||
button.btn.btn-success:active {
 | 
			
		||||
    opacity: .85;
 | 
			
		||||
    opacity: 0.85;
 | 
			
		||||
    box-shadow: unset;
 | 
			
		||||
    background: var(--cmd-button-background-color) !important;
 | 
			
		||||
    color: var(--cmd-button-text-color) !important;
 | 
			
		||||
    transform: scale(.95);
 | 
			
		||||
    transform: scale(0.95);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button.btn.btn-primary:disabled,
 | 
			
		||||
@ -57,7 +57,7 @@ button.btn.btn-secondary span.bx,
 | 
			
		||||
button.btn.btn-sm span.bx,
 | 
			
		||||
button.btn.btn-success span.bx {
 | 
			
		||||
    color: var(--cmd-button-icon-color);
 | 
			
		||||
    padding-right: .35em;
 | 
			
		||||
    padding-right: 0.35em;
 | 
			
		||||
    font-size: 1.2em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -66,12 +66,12 @@ button.btn.btn-primary kbd,
 | 
			
		||||
button.btn.btn-secondary kbd,
 | 
			
		||||
button.btn.btn-sm kbd,
 | 
			
		||||
button.btn.btn-success kbd {
 | 
			
		||||
    margin-left: .5em;
 | 
			
		||||
    margin-left: 0.5em;
 | 
			
		||||
    background: var(--cmd-button-keyboard-shortcut-background);
 | 
			
		||||
    color: var(--cmd-button-keyboard-shortcut-color);
 | 
			
		||||
    font-size: .6em;
 | 
			
		||||
    font-size: 0.6em;
 | 
			
		||||
    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-color: var(--tab-close-button-hover-color);
 | 
			
		||||
    --icon-button-size: 24px;
 | 
			
		||||
    --icon-button-icon-ratio: .8;
 | 
			
		||||
    --icon-button-icon-ratio: 0.8;
 | 
			
		||||
 | 
			
		||||
    border-radius: 50%;
 | 
			
		||||
}
 | 
			
		||||
@ -124,7 +124,7 @@ button.btn.btn-success kbd {
 | 
			
		||||
 | 
			
		||||
:root .icon-action:not(.global-menu-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,
 | 
			
		||||
@ -137,7 +137,7 @@ button.btn.btn-success kbd {
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
input:disabled {
 | 
			
		||||
    opacity: .33;
 | 
			
		||||
    opacity: 0.33;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Text boxes */
 | 
			
		||||
@ -190,8 +190,9 @@ textarea:focus,
 | 
			
		||||
    outline-offset: 0;
 | 
			
		||||
    background: var(--input-focus-background);
 | 
			
		||||
    color: var(--input-focus-color);
 | 
			
		||||
    transition: outline-color 50ms linear,
 | 
			
		||||
                outline-offset 200ms ease-out;
 | 
			
		||||
    transition:
 | 
			
		||||
        outline-color 50ms linear,
 | 
			
		||||
        outline-offset 200ms ease-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input::placeholder,
 | 
			
		||||
@ -229,12 +230,12 @@ input::selection,
 | 
			
		||||
    outline: 3px solid var(--input-focus-outline-color);
 | 
			
		||||
    outline-offset: 0;
 | 
			
		||||
    background: var(--input-focus-background);
 | 
			
		||||
    transition: outline-color 50ms linear,
 | 
			
		||||
                outline-offset 200ms ease-out;
 | 
			
		||||
    transition:
 | 
			
		||||
        outline-color 50ms linear,
 | 
			
		||||
        outline-offset 200ms ease-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.input-group input
 | 
			
		||||
.input-group input:hover,
 | 
			
		||||
.input-group input .input-group input:hover,
 | 
			
		||||
.input-group input:focus,
 | 
			
		||||
.input-group .form-control,
 | 
			
		||||
.input-group .form-control:hover,
 | 
			
		||||
@ -277,7 +278,7 @@ input::selection,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.input-group a.disabled {
 | 
			
		||||
    opacity: .5;
 | 
			
		||||
    opacity: 0.5;
 | 
			
		||||
    /* Workaround to set the "background" property. */
 | 
			
		||||
    --button-disabled-background-color: transparent;
 | 
			
		||||
    --button-disabled-text-color: var(--input-action-button-color);
 | 
			
		||||
@ -319,8 +320,7 @@ select.form-control,
 | 
			
		||||
    outline: 3px solid transparent;
 | 
			
		||||
    outline-offset: 6px;
 | 
			
		||||
    padding-right: calc(15px + 1.5rem);
 | 
			
		||||
    background: var(--input-background-color)
 | 
			
		||||
                var(--dropdown-arrow);
 | 
			
		||||
    background: var(--input-background-color) var(--dropdown-arrow);
 | 
			
		||||
    color: var(--input-text-color);
 | 
			
		||||
    border: unset;
 | 
			
		||||
    border-radius: 0.375rem;
 | 
			
		||||
@ -330,8 +330,7 @@ select:hover,
 | 
			
		||||
select.form-select:hover,
 | 
			
		||||
select.form-control:hover,
 | 
			
		||||
.select-button.dropdown-toggle.btn:hover {
 | 
			
		||||
    background: var(--input-hover-background)
 | 
			
		||||
                var(--dropdown-arrow);
 | 
			
		||||
    background: var(--input-hover-background) var(--dropdown-arrow);
 | 
			
		||||
    color: var(--input-hover-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -347,11 +346,11 @@ select.form-control:focus,
 | 
			
		||||
    box-shadow: unset;
 | 
			
		||||
    outline: 3px solid var(--input-focus-outline-color);
 | 
			
		||||
    outline-offset: 0;
 | 
			
		||||
    background: var(--select-focus-background)
 | 
			
		||||
                var(--dropdown-arrow);
 | 
			
		||||
    background: var(--select-focus-background) var(--dropdown-arrow);
 | 
			
		||||
    color: var(--select-focus-text-color);
 | 
			
		||||
    transition: outline-color 50ms linear,
 | 
			
		||||
                outline-offset 200ms ease-out;
 | 
			
		||||
    transition:
 | 
			
		||||
        outline-color 50ms linear,
 | 
			
		||||
        outline-offset 200ms ease-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
option {
 | 
			
		||||
@ -360,7 +359,7 @@ option {
 | 
			
		||||
 | 
			
		||||
optgroup {
 | 
			
		||||
    color: var(--select-group-heading-text-color);
 | 
			
		||||
    font-size: .75em;
 | 
			
		||||
    font-size: 0.75em;
 | 
			
		||||
    font-weight: normal;
 | 
			
		||||
    font-style: normal;
 | 
			
		||||
    line-height: 40px;
 | 
			
		||||
@ -374,9 +373,9 @@ optgroup {
 | 
			
		||||
 * </label>
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
 .tn-file-input {
 | 
			
		||||
.tn-file-input {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    padding: .375rem 2.25rem .375rem .75rem;
 | 
			
		||||
    padding: 0.375rem 2.25rem 0.375rem 0.75rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tn-file-input input[type="file"] {
 | 
			
		||||
@ -416,19 +415,17 @@ optgroup {
 | 
			
		||||
/* Check boxes and radio buttons */
 | 
			
		||||
 | 
			
		||||
@supports selector(label:has(*)) {
 | 
			
		||||
 | 
			
		||||
    /* Check box & radio button commons */
 | 
			
		||||
 | 
			
		||||
    /* The parent label */
 | 
			
		||||
    label.tn-radio,
 | 
			
		||||
    label.tn-checkbox {
 | 
			
		||||
        --box-size: 1em;
 | 
			
		||||
        --box-label-gap: .5em;
 | 
			
		||||
        --box-label-gap: 0.5em;
 | 
			
		||||
 | 
			
		||||
        position: relative;
 | 
			
		||||
        padding-left: calc(var(--box-size) + var(--box-label-gap)) !important;
 | 
			
		||||
        user-select: none;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* The original input */
 | 
			
		||||
@ -455,8 +452,8 @@ optgroup {
 | 
			
		||||
        height: var(--box-size);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    label.tn-radio:has(>input[type="radio"]:focus-visible)::before,
 | 
			
		||||
    label.tn-checkbox:has(>input[type="checkbox"]:focus-visible)::before {
 | 
			
		||||
    label.tn-radio:has(> input[type="radio"]:focus-visible)::before,
 | 
			
		||||
    label.tn-checkbox:has(> input[type="checkbox"]:focus-visible)::before {
 | 
			
		||||
        outline: 2px solid var(--input-focus-outline-color);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -468,14 +465,15 @@ optgroup {
 | 
			
		||||
        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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @keyframes checkbox-checked {
 | 
			
		||||
        from {
 | 
			
		||||
            transform: scale(2);
 | 
			
		||||
        } to {
 | 
			
		||||
        }
 | 
			
		||||
        to {
 | 
			
		||||
            transform: scale(1);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -484,15 +482,16 @@ optgroup {
 | 
			
		||||
    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-position: center center;
 | 
			
		||||
        mask-size: .95em;
 | 
			
		||||
        mask-size: 0.95em;
 | 
			
		||||
        background-color: var(--radio-checkbox-indicator-color);
 | 
			
		||||
        transform: scale(0);
 | 
			
		||||
        opacity: 0;
 | 
			
		||||
        transition: transform 300ms ease-out,
 | 
			
		||||
                    opacity 300ms linear;
 | 
			
		||||
        transition:
 | 
			
		||||
            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;
 | 
			
		||||
        transform: scale(1);
 | 
			
		||||
        transition: opacity 100ms ease-in-out;
 | 
			
		||||
@ -511,15 +510,16 @@ optgroup {
 | 
			
		||||
        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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @keyframes radio-checked {
 | 
			
		||||
        from {
 | 
			
		||||
            transform: scale(1.5);
 | 
			
		||||
        } to {
 | 
			
		||||
            transform: scale(.5);
 | 
			
		||||
        }
 | 
			
		||||
        to {
 | 
			
		||||
            transform: scale(0.5);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -528,12 +528,13 @@ optgroup {
 | 
			
		||||
        background: var(--radio-checkbox-indicator-color);
 | 
			
		||||
        transform: scale(0);
 | 
			
		||||
        opacity: 0;
 | 
			
		||||
        transition: opacity 300ms linear,
 | 
			
		||||
                    transform 300ms ease-in;
 | 
			
		||||
        transition:
 | 
			
		||||
            opacity 300ms linear,
 | 
			
		||||
            transform 300ms ease-in;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    label.tn-radio:has(>input[type="radio"]:checked)::after {
 | 
			
		||||
        transform: scale(.5);
 | 
			
		||||
    label.tn-radio:has(> input[type="radio"]:checked)::after {
 | 
			
		||||
        transform: scale(0.5);
 | 
			
		||||
        opacity: 1;
 | 
			
		||||
        transition: opacity 150ms linear;
 | 
			
		||||
        animation: radio-checked 200ms ease-out;
 | 
			
		||||
@ -545,9 +546,8 @@ optgroup {
 | 
			
		||||
    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)::after {
 | 
			
		||||
        opacity: .5;
 | 
			
		||||
        opacity: 0.5;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Switches */
 | 
			
		||||
@ -579,9 +579,10 @@ body a.tn-link:visited,
 | 
			
		||||
    font-weight: normal;
 | 
			
		||||
    text-decoration: underline;
 | 
			
		||||
 | 
			
		||||
    transition: background-color 200ms ease-out,
 | 
			
		||||
                box-shadow 200ms ease-out,
 | 
			
		||||
                color 300ms ease-out;
 | 
			
		||||
    transition:
 | 
			
		||||
        background-color 200ms ease-out,
 | 
			
		||||
        box-shadow 200ms ease-out,
 | 
			
		||||
        color 300ms ease-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body a.tn-link:focus-visible,
 | 
			
		||||
@ -596,9 +597,10 @@ body a.tn-link:hover,
 | 
			
		||||
    --background: var(--link-hover-background);
 | 
			
		||||
    color: var(--link-hover-color);
 | 
			
		||||
 | 
			
		||||
    transition: background-color 100ms ease-in,
 | 
			
		||||
                box-shadow 100ms ease-in,
 | 
			
		||||
                color 150ms ease-in;
 | 
			
		||||
    transition:
 | 
			
		||||
        background-color 100ms ease-in,
 | 
			
		||||
        box-shadow 100ms ease-in,
 | 
			
		||||
        color 150ms ease-in;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a.tn-link.external:not(.no-arrow)::after,
 | 
			
		||||
@ -606,16 +608,18 @@ a.tn-link[href^="http://"]: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[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;
 | 
			
		||||
    opacity: .5;
 | 
			
		||||
    opacity: 0.5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes link-arrow-blink {
 | 
			
		||||
    from {
 | 
			
		||||
        opacity: 1;
 | 
			
		||||
    } to {
 | 
			
		||||
        opacity: .5;
 | 
			
		||||
    }
 | 
			
		||||
    to {
 | 
			
		||||
        opacity: 0.5;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -624,7 +628,8 @@ a.tn-link:hover[href^="http://"]: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[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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -146,13 +146,13 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
 | 
			
		||||
    padding-top: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
div.find-replace-widget div.find-widget-found-wrapper {
 | 
			
		||||
div.find-replace-widget div.find-widget-found-wrapper > span {
 | 
			
		||||
    min-width: 50px;
 | 
			
		||||
    font-style: normal;
 | 
			
		||||
    font-weight: normal;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 /* The up / down buttons of the "Find in text" input */
 | 
			
		||||
/* The up / down buttons of the "Find in text" input */
 | 
			
		||||
.find-replace-widget .input-group button {
 | 
			
		||||
    font-size: 1.3em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@ div.note-type-dropdown .check {
 | 
			
		||||
    margin-right: 6px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 /* Editability dropdown */
 | 
			
		||||
/* Editability dropdown */
 | 
			
		||||
 | 
			
		||||
div.editability-dropdown a.dropdown-item {
 | 
			
		||||
    padding: 4px 16px 4px 0;
 | 
			
		||||
@ -29,8 +29,8 @@ div.editability-dropdown a.dropdown-item {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.editability-dropdown .dropdown-item .description {
 | 
			
		||||
    opacity: .75;
 | 
			
		||||
    font-size: .85em;
 | 
			
		||||
    opacity: 0.75;
 | 
			
		||||
    font-size: 0.85em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
@ -38,8 +38,8 @@ div.editability-dropdown a.dropdown-item {
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
.attribute-list .add-new-attribute-button,
 | 
			
		||||
.attribute-list .save-attributes-button  {
 | 
			
		||||
    bottom: .3em;
 | 
			
		||||
.attribute-list .save-attributes-button {
 | 
			
		||||
    bottom: 0.3em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.attribute-list .save-attributes-button {
 | 
			
		||||
@ -70,12 +70,23 @@ div.editability-dropdown a.dropdown-item {
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
.note-info-widget-table th {
 | 
			
		||||
    opacity: .65;
 | 
			
		||||
    opacity: 0.65;
 | 
			
		||||
    font-weight: 500;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:root .note-info-widget-table button.calculate-button {
 | 
			
		||||
    min-width: 0;
 | 
			
		||||
    padding: 4px 10px !important;
 | 
			
		||||
    font-size: .8em;
 | 
			
		||||
    font-size: 0.8em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Attribute detail dialog
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/* Labels */
 | 
			
		||||
.attr-edit-table th {
 | 
			
		||||
    padding-right: 12px;
 | 
			
		||||
    font-weight: normal;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
@ -77,7 +77,7 @@ body.background-effects.platform-win32.layout-vertical #vertical-main-container
 | 
			
		||||
/* #endregion */
 | 
			
		||||
 | 
			
		||||
/* Matches when the left pane is collapsed */
 | 
			
		||||
:has(#left-pane.hidden-int) {
 | 
			
		||||
:has(.layout-vertical #left-pane.hidden-int) {
 | 
			
		||||
    --center-pane-border-radius: 0;
 | 
			
		||||
    --tab-first-item-horiz-offset: 5px;
 | 
			
		||||
}
 | 
			
		||||
@ -266,7 +266,9 @@ div.quick-search {
 | 
			
		||||
    padding: var(--padding-top) var(--padding-right) var(--padding-bottom) var(--padding-left);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
div.quick-search, div.quick-search:hover, div.quick-search:focus-within {
 | 
			
		||||
div.quick-search,
 | 
			
		||||
div.quick-search:hover,
 | 
			
		||||
div.quick-search:focus-within {
 | 
			
		||||
    /* Prevent changes to background and outline when the state of the input group changes */
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    outline: none;
 | 
			
		||||
@ -1086,7 +1088,7 @@ html body .dropdown-item[disabled] {
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    padding: 1em 8px 14px 8px;
 | 
			
		||||
    text-transform: uppercase;
 | 
			
		||||
    font-size: .8em;
 | 
			
		||||
    font-size: 0.8em;
 | 
			
		||||
    letter-spacing: 1pt;
 | 
			
		||||
    color: var(--menu-item-group-header-color) !important;
 | 
			
		||||
}
 | 
			
		||||
@ -1826,13 +1828,13 @@ body.background-effects.zen #root-widget {
 | 
			
		||||
    --root-background: transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* Alert bar */
 | 
			
		||||
 | 
			
		||||
@keyframes alert-show {
 | 
			
		||||
    from {
 | 
			
		||||
        opacity: 0;
 | 
			
		||||
    } to {
 | 
			
		||||
    }
 | 
			
		||||
    to {
 | 
			
		||||
        opacity: 1;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1844,7 +1846,7 @@ body.background-effects.zen #root-widget {
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
    padding: 8px 16px;
 | 
			
		||||
    background: var(--alert-bar-background) !important;
 | 
			
		||||
    font-size: .9em;
 | 
			
		||||
    font-size: 0.9em;
 | 
			
		||||
    font-weight: normal;
 | 
			
		||||
    animation: alert-show 300ms ease-in;
 | 
			
		||||
    border-bottom: 2px solid #0000001c !important;
 | 
			
		||||
@ -1866,7 +1868,7 @@ div.promoted-attributes-container {
 | 
			
		||||
 | 
			
		||||
div.promoted-attributes-container,
 | 
			
		||||
div.promoted-attributes-container input {
 | 
			
		||||
    font-size: .9rem;
 | 
			
		||||
    font-size: 0.9rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* A promoted attribute card */
 | 
			
		||||
@ -1905,7 +1907,7 @@ div.promoted-attribute-cell > * {
 | 
			
		||||
div.promoted-attribute-cell > label {
 | 
			
		||||
    font-weight: normal;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
    opacity: .75;
 | 
			
		||||
    opacity: 0.75;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
div.promoted-attribute-cell:not(:has(input[type="checkbox"])) > label::after {
 | 
			
		||||
 | 
			
		||||
@ -61,7 +61,7 @@
 | 
			
		||||
  },
 | 
			
		||||
  "clone_to": {
 | 
			
		||||
    "clone_notes_to": "Notizen klonen nach...",
 | 
			
		||||
    "close":"Schließen",
 | 
			
		||||
    "close": "Schließen",
 | 
			
		||||
    "help_on_links": "Hilfe zu Links",
 | 
			
		||||
    "notes_to_clone": "Notizen zum Klonen",
 | 
			
		||||
    "target_parent_note": "Ziel-Übergeordnetenotiz",
 | 
			
		||||
@ -74,7 +74,7 @@
 | 
			
		||||
  },
 | 
			
		||||
  "confirm": {
 | 
			
		||||
    "confirmation": "Bestätigung",
 | 
			
		||||
    "close":"Schließen",
 | 
			
		||||
    "close": "Schließen",
 | 
			
		||||
    "cancel": "Abbrechen",
 | 
			
		||||
    "ok": "OK",
 | 
			
		||||
    "are_you_sure_remove_note": "Bist du sicher, dass du \"{{title}}\" von der Beziehungskarte entfernen möchten? ",
 | 
			
		||||
@ -93,7 +93,6 @@
 | 
			
		||||
    "cancel": "Abbrechen",
 | 
			
		||||
    "ok": "OK",
 | 
			
		||||
    "deleted_relation_text": "Notiz {{- note}} (soll gelöscht werden) wird von Beziehung {{- relation}} ausgehend von {{- source}} referenziert."
 | 
			
		||||
 | 
			
		||||
  },
 | 
			
		||||
  "export": {
 | 
			
		||||
    "export_note_title": "Notiz exportieren",
 | 
			
		||||
 | 
			
		||||
@ -43,9 +43,9 @@ function getDayNotesForMonth(req: Request) {
 | 
			
		||||
            AND attr.value LIKE '${month}%'`;
 | 
			
		||||
 | 
			
		||||
    if (calendarRoot) {
 | 
			
		||||
        const rows = sql.getRows<{ date: string, noteId: string }>(query);
 | 
			
		||||
        const rows = sql.getRows<{ date: string; noteId: string }>(query);
 | 
			
		||||
        const result: Record<string, string> = {};
 | 
			
		||||
        for (const {date, noteId} of rows) {
 | 
			
		||||
        for (const { date, noteId } of rows) {
 | 
			
		||||
            const note = becca.getNote(noteId);
 | 
			
		||||
            if (note?.hasAncestor(String(calendarRoot))) {
 | 
			
		||||
                result[date] = noteId;
 | 
			
		||||
 | 
			
		||||
@ -7,9 +7,7 @@ import yaml from "js-yaml";
 | 
			
		||||
import type { JsonObject } from "swagger-ui-express";
 | 
			
		||||
 | 
			
		||||
const __dirname = dirname(fileURLToPath(import.meta.url));
 | 
			
		||||
const etapiDocument = yaml.load(
 | 
			
		||||
    await readFile(join(__dirname, "../etapi/etapi.openapi.yaml"), "utf8")
 | 
			
		||||
) as JsonObject;
 | 
			
		||||
const etapiDocument = yaml.load(await readFile(join(__dirname, "../etapi/etapi.openapi.yaml"), "utf8")) as JsonObject;
 | 
			
		||||
const apiDocument = JSON.parse(await readFile(join(__dirname, "api", "openapi.json"), "utf-8"));
 | 
			
		||||
 | 
			
		||||
function register(app: Application) {
 | 
			
		||||
 | 
			
		||||
@ -35,7 +35,7 @@ export interface TriliumConfig {
 | 
			
		||||
    Session: {
 | 
			
		||||
        cookiePath: string;
 | 
			
		||||
        cookieMaxAge: number;
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
    Sync: {
 | 
			
		||||
        syncServerHost: string;
 | 
			
		||||
        syncServerTimeout: string;
 | 
			
		||||
 | 
			
		||||
@ -408,8 +408,7 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: HiddenSubtree
 | 
			
		||||
                value: attr.value,
 | 
			
		||||
                isInheritable: false
 | 
			
		||||
            }).save();
 | 
			
		||||
        } else if (attr.name === "docName"
 | 
			
		||||
                || (existingAttribute.noteId.startsWith("_help") && attr.name === "iconClass")) {
 | 
			
		||||
        } else if (attr.name === "docName" || (existingAttribute.noteId.startsWith("_help") && attr.name === "iconClass")) {
 | 
			
		||||
            if (existingAttribute.value !== attr.value) {
 | 
			
		||||
                existingAttribute.value = attr.value ?? "";
 | 
			
		||||
                console.log("Updating attribute ", attrId);
 | 
			
		||||
 | 
			
		||||
@ -78,7 +78,7 @@ export default function buildLaunchBarConfig() {
 | 
			
		||||
        { id: "_lbProtectedSession", title: t("hidden-subtree.protected-session-title"), type: "launcher", builtinWidget: "protectedSession", icon: "bx bx bx-shield-quarter" },
 | 
			
		||||
        { id: "_lbSyncStatus", title: t("hidden-subtree.sync-status-title"), type: "launcher", builtinWidget: "syncStatus", icon: "bx bx-wifi" },
 | 
			
		||||
        { id: "_lbSettings", title: t("hidden-subtree.settings-title"), type: "launcher", command: "showOptions", icon: "bx bx-cog" }
 | 
			
		||||
    ]
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    const mobileAvailableLaunchers: HiddenSubtreeItem[] = [
 | 
			
		||||
        { id: "_lbMobileNewNote", ...sharedLaunchers.newNote },
 | 
			
		||||
@ -98,5 +98,5 @@ export default function buildLaunchBarConfig() {
 | 
			
		||||
        desktopVisibleLaunchers,
 | 
			
		||||
        mobileAvailableLaunchers,
 | 
			
		||||
        mobileVisibleLaunchers
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@ describe("sanitize", () => {
 | 
			
		||||
                    </tbody>
 | 
			
		||||
                </table>
 | 
			
		||||
            </figure>`;
 | 
			
		||||
    const clean = trimIndentation`\
 | 
			
		||||
        const clean = trimIndentation`\
 | 
			
		||||
            <p>
 | 
			
		||||
                <span style="color:hsl(0, 0%, 90%)">
 | 
			
		||||
                    Hi
 | 
			
		||||
@ -48,6 +48,6 @@ describe("sanitize", () => {
 | 
			
		||||
                    </tbody>
 | 
			
		||||
                </table>
 | 
			
		||||
            </figure>`;
 | 
			
		||||
        expect(html_sanitizer.sanitize(dirty)) .toBe(clean);
 | 
			
		||||
        expect(html_sanitizer.sanitize(dirty)).toBe(clean);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -153,20 +153,22 @@ function sanitize(dirtyHtml: string) {
 | 
			
		||||
        },
 | 
			
		||||
        allowedStyles: {
 | 
			
		||||
            "*": {
 | 
			
		||||
                "color": colorRegex,
 | 
			
		||||
                color: colorRegex,
 | 
			
		||||
                "background-color": colorRegex
 | 
			
		||||
            },
 | 
			
		||||
            "figure": {
 | 
			
		||||
                "float": [ /^\s*(left|right|none)\s*$/ ],
 | 
			
		||||
                "width": sizeRegex,
 | 
			
		||||
                "height": sizeRegex
 | 
			
		||||
            figure: {
 | 
			
		||||
                float: [/^\s*(left|right|none)\s*$/],
 | 
			
		||||
                width: sizeRegex,
 | 
			
		||||
                height: sizeRegex
 | 
			
		||||
            },
 | 
			
		||||
            "table": {
 | 
			
		||||
            table: {
 | 
			
		||||
                "border-color": colorRegex,
 | 
			
		||||
                "border-style": [ /^\s*(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset)\s*$/ ]
 | 
			
		||||
                "border-style": [/^\s*(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset)\s*$/]
 | 
			
		||||
            },
 | 
			
		||||
            "td": {
 | 
			
		||||
                "border": [ /^\s*\d+(?:px|em|%)\s*(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset)\s*(#(0x)?[0-9a-fA-F]+|rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)|hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\))\s*$/ ]
 | 
			
		||||
            td: {
 | 
			
		||||
                border: [
 | 
			
		||||
                    /^\s*\d+(?:px|em|%)\s*(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset)\s*(#(0x)?[0-9a-fA-F]+|rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)|hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\))\s*$/
 | 
			
		||||
                ]
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        allowedSchemes: ALLOWED_PROTOCOLS,
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@
 | 
			
		||||
import { parse, Renderer, type Tokens } from "marked";
 | 
			
		||||
 | 
			
		||||
const renderer = new Renderer({ async: false });
 | 
			
		||||
renderer.code = ({text, lang, escaped}: Tokens.Code) => {
 | 
			
		||||
renderer.code = ({ text, lang, escaped }: Tokens.Code) => {
 | 
			
		||||
    if (!text) {
 | 
			
		||||
        return "";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -20,18 +20,22 @@ async function testImport(fileName: string, mimetype: string) {
 | 
			
		||||
        codeImportedAsCode: true
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return new Promise<{ buffer: Buffer, importedNote: BNote }>((resolve, reject) => {
 | 
			
		||||
    return new Promise<{ buffer: Buffer; importedNote: BNote }>((resolve, reject) => {
 | 
			
		||||
        cls.init(async () => {
 | 
			
		||||
            const rootNote = becca.getNote("root");
 | 
			
		||||
            if (!rootNote) {
 | 
			
		||||
                reject("Missing root note.");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const importedNote = single.importSingleFile(taskContext, {
 | 
			
		||||
                originalname: fileName,
 | 
			
		||||
                mimetype,
 | 
			
		||||
                buffer: buffer
 | 
			
		||||
            }, rootNote as BNote);
 | 
			
		||||
            const importedNote = single.importSingleFile(
 | 
			
		||||
                taskContext,
 | 
			
		||||
                {
 | 
			
		||||
                    originalname: fileName,
 | 
			
		||||
                    mimetype,
 | 
			
		||||
                    buffer: buffer
 | 
			
		||||
                },
 | 
			
		||||
                rootNote as BNote
 | 
			
		||||
            );
 | 
			
		||||
            resolve({
 | 
			
		||||
                buffer,
 | 
			
		||||
                importedNote
 | 
			
		||||
@ -85,4 +89,4 @@ describe("processNoteContent", () => {
 | 
			
		||||
        expect(importedNote.mime).toBe("text/html");
 | 
			
		||||
        expect(importedNote.getContent().toString()).toBe("<h2>Hello world</h2>\n<p>Plain text goes here.</p>\n");
 | 
			
		||||
    });
 | 
			
		||||
})
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -61,4 +61,4 @@ describe("processNoteContent", () => {
 | 
			
		||||
        const htmlNote = rootNote.children.find((ch) => ch.title === "IREN Reports Q2 FY25 Results");
 | 
			
		||||
        expect(htmlNote?.getContent().toString().substring(0, 4)).toEqual("<div");
 | 
			
		||||
    });
 | 
			
		||||
})
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -3,39 +3,34 @@ import { parseNoteMeta } from "./in_app_help.js";
 | 
			
		||||
import type NoteMeta from "./meta/note_meta.js";
 | 
			
		||||
 | 
			
		||||
describe("In-app help", () => {
 | 
			
		||||
 | 
			
		||||
    it("preserves custom folder icon", () => {
 | 
			
		||||
        const meta: NoteMeta = {
 | 
			
		||||
            "isClone": false,
 | 
			
		||||
            "noteId": "yoAe4jV2yzbd",
 | 
			
		||||
            "notePath": [
 | 
			
		||||
                "OkOZllzB3fqN",
 | 
			
		||||
                "yoAe4jV2yzbd"
 | 
			
		||||
            ],
 | 
			
		||||
            "title": "Features",
 | 
			
		||||
            "notePosition": 40,
 | 
			
		||||
            "prefix": null,
 | 
			
		||||
            "isExpanded": false,
 | 
			
		||||
            "type": "text",
 | 
			
		||||
            "mime": "text/html",
 | 
			
		||||
            "attributes": [
 | 
			
		||||
            isClone: false,
 | 
			
		||||
            noteId: "yoAe4jV2yzbd",
 | 
			
		||||
            notePath: ["OkOZllzB3fqN", "yoAe4jV2yzbd"],
 | 
			
		||||
            title: "Features",
 | 
			
		||||
            notePosition: 40,
 | 
			
		||||
            prefix: null,
 | 
			
		||||
            isExpanded: false,
 | 
			
		||||
            type: "text",
 | 
			
		||||
            mime: "text/html",
 | 
			
		||||
            attributes: [
 | 
			
		||||
                {
 | 
			
		||||
                    "type": "label",
 | 
			
		||||
                    "name": "iconClass",
 | 
			
		||||
                    "value": "bx bx-star",
 | 
			
		||||
                    "isInheritable": false,
 | 
			
		||||
                    "position": 10
 | 
			
		||||
                    type: "label",
 | 
			
		||||
                    name: "iconClass",
 | 
			
		||||
                    value: "bx bx-star",
 | 
			
		||||
                    isInheritable: false,
 | 
			
		||||
                    position: 10
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "format": "html",
 | 
			
		||||
            "attachments": [],
 | 
			
		||||
            "dirFileName": "Features",
 | 
			
		||||
            "children": []
 | 
			
		||||
            format: "html",
 | 
			
		||||
            attachments: [],
 | 
			
		||||
            dirFileName: "Features",
 | 
			
		||||
            children: []
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const item = parseNoteMeta(meta, "/");
 | 
			
		||||
        const icon = item.attributes?.find((a) => a.name === "iconClass");
 | 
			
		||||
        expect(icon?.value).toBe("bx bx-star");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -66,8 +66,7 @@ export function parseNoteMeta(noteMeta: NoteMeta, docNameRoot: string): HiddenSu
 | 
			
		||||
 | 
			
		||||
    // Handle text notes
 | 
			
		||||
    if (noteMeta.type === "text" && noteMeta.dataFileName) {
 | 
			
		||||
        const docPath = `${docNameRoot}/${path.basename(noteMeta.dataFileName, ".html")}`
 | 
			
		||||
            .substring(1);
 | 
			
		||||
        const docPath = `${docNameRoot}/${path.basename(noteMeta.dataFileName, ".html")}`.substring(1);
 | 
			
		||||
        item.attributes?.push({
 | 
			
		||||
            type: "label",
 | 
			
		||||
            name: "docName",
 | 
			
		||||
@ -84,7 +83,7 @@ export function parseNoteMeta(noteMeta: NoteMeta, docNameRoot: string): HiddenSu
 | 
			
		||||
    if (noteMeta.children) {
 | 
			
		||||
        const children: HiddenSubtreeItem[] = [];
 | 
			
		||||
        for (const childMeta of noteMeta.children) {
 | 
			
		||||
            let newDocNameRoot = (noteMeta.dirFileName ? `${docNameRoot}/${noteMeta.dirFileName}` : docNameRoot);
 | 
			
		||||
            let newDocNameRoot = noteMeta.dirFileName ? `${docNameRoot}/${noteMeta.dirFileName}` : docNameRoot;
 | 
			
		||||
            children.push(parseNoteMeta(childMeta, newDocNameRoot));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -734,7 +734,7 @@ function updateNoteData(noteId: string, content: string, attachments: Attachment
 | 
			
		||||
    note.setContent(newContent, { forceFrontendReload });
 | 
			
		||||
 | 
			
		||||
    if (attachments?.length > 0) {
 | 
			
		||||
      const existingAttachmentsByTitle = toMap(note.getAttachments({ includeContentLength: false }), "title");
 | 
			
		||||
        const existingAttachmentsByTitle = toMap(note.getAttachments({ includeContentLength: false }), "title");
 | 
			
		||||
 | 
			
		||||
        for (const { attachmentId, role, mime, title, position, content } of attachments) {
 | 
			
		||||
            const existingAttachment = existingAttachmentsByTitle.get(title);
 | 
			
		||||
 | 
			
		||||
@ -100,7 +100,6 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
 | 
			
		||||
    // Share settings
 | 
			
		||||
    redirectBareDomain: boolean;
 | 
			
		||||
    showLoginInShareTheme: boolean;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type OptionNames = keyof OptionDefinitions;
 | 
			
		||||
 | 
			
		||||
@ -14,11 +14,14 @@ import { default as parseInternal, type ParseOpts } from "./parse.js";
 | 
			
		||||
 | 
			
		||||
describe("Parser", () => {
 | 
			
		||||
    it("fulltext parser without content", () => {
 | 
			
		||||
        const rootExp = parse({
 | 
			
		||||
            fulltextTokens: tokens(["hello", "hi"]),
 | 
			
		||||
            expressionTokens: [],
 | 
			
		||||
            searchContext: new SearchContext()
 | 
			
		||||
        }, AndExp);
 | 
			
		||||
        const rootExp = parse(
 | 
			
		||||
            {
 | 
			
		||||
                fulltextTokens: tokens(["hello", "hi"]),
 | 
			
		||||
                expressionTokens: [],
 | 
			
		||||
                searchContext: new SearchContext()
 | 
			
		||||
            },
 | 
			
		||||
            AndExp
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        expectExpression(rootExp.subExpressions[0], PropertyComparisonExp);
 | 
			
		||||
        const orExp = expectExpression(rootExp.subExpressions[2], OrExp);
 | 
			
		||||
@ -27,11 +30,14 @@ describe("Parser", () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("fulltext parser with content", () => {
 | 
			
		||||
        const rootExp = parse({
 | 
			
		||||
            fulltextTokens: tokens(["hello", "hi"]),
 | 
			
		||||
            expressionTokens: [],
 | 
			
		||||
            searchContext: new SearchContext()
 | 
			
		||||
        }, AndExp);
 | 
			
		||||
        const rootExp = parse(
 | 
			
		||||
            {
 | 
			
		||||
                fulltextTokens: tokens(["hello", "hi"]),
 | 
			
		||||
                expressionTokens: [],
 | 
			
		||||
                searchContext: new SearchContext()
 | 
			
		||||
            },
 | 
			
		||||
            AndExp
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        assertIsArchived(rootExp.subExpressions[0]);
 | 
			
		||||
 | 
			
		||||
@ -45,11 +51,14 @@ describe("Parser", () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("simple label comparison", () => {
 | 
			
		||||
        const rootExp = parse({
 | 
			
		||||
            fulltextTokens: [],
 | 
			
		||||
            expressionTokens: tokens(["#mylabel", "=", "text"]),
 | 
			
		||||
            searchContext: new SearchContext()
 | 
			
		||||
        }, AndExp);
 | 
			
		||||
        const rootExp = parse(
 | 
			
		||||
            {
 | 
			
		||||
                fulltextTokens: [],
 | 
			
		||||
                expressionTokens: tokens(["#mylabel", "=", "text"]),
 | 
			
		||||
                searchContext: new SearchContext()
 | 
			
		||||
            },
 | 
			
		||||
            AndExp
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        assertIsArchived(rootExp.subExpressions[0]);
 | 
			
		||||
        const labelComparisonExp = expectExpression(rootExp.subExpressions[2], LabelComparisonExp);
 | 
			
		||||
@ -59,11 +68,14 @@ describe("Parser", () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("simple attribute negation", () => {
 | 
			
		||||
        let rootExp = parse({
 | 
			
		||||
            fulltextTokens: [],
 | 
			
		||||
            expressionTokens: tokens(["#!mylabel"]),
 | 
			
		||||
            searchContext: new SearchContext()
 | 
			
		||||
        }, AndExp);
 | 
			
		||||
        let rootExp = parse(
 | 
			
		||||
            {
 | 
			
		||||
                fulltextTokens: [],
 | 
			
		||||
                expressionTokens: tokens(["#!mylabel"]),
 | 
			
		||||
                searchContext: new SearchContext()
 | 
			
		||||
            },
 | 
			
		||||
            AndExp
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        assertIsArchived(rootExp.subExpressions[0]);
 | 
			
		||||
        let notExp = expectExpression(rootExp.subExpressions[2], NotExp);
 | 
			
		||||
@ -71,11 +83,14 @@ describe("Parser", () => {
 | 
			
		||||
        expect(attributeExistsExp.attributeType).toEqual("label");
 | 
			
		||||
        expect(attributeExistsExp.attributeName).toEqual("mylabel");
 | 
			
		||||
 | 
			
		||||
        rootExp = parse({
 | 
			
		||||
            fulltextTokens: [],
 | 
			
		||||
            expressionTokens: tokens(["~!myrelation"]),
 | 
			
		||||
            searchContext: new SearchContext()
 | 
			
		||||
        }, AndExp);
 | 
			
		||||
        rootExp = parse(
 | 
			
		||||
            {
 | 
			
		||||
                fulltextTokens: [],
 | 
			
		||||
                expressionTokens: tokens(["~!myrelation"]),
 | 
			
		||||
                searchContext: new SearchContext()
 | 
			
		||||
            },
 | 
			
		||||
            AndExp
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        assertIsArchived(rootExp.subExpressions[0]);
 | 
			
		||||
        notExp = expectExpression(rootExp.subExpressions[2], NotExp);
 | 
			
		||||
@ -85,11 +100,14 @@ describe("Parser", () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("simple label AND", () => {
 | 
			
		||||
        const rootExp = parse({
 | 
			
		||||
            fulltextTokens: [],
 | 
			
		||||
            expressionTokens: tokens(["#first", "=", "text", "and", "#second", "=", "text"]),
 | 
			
		||||
            searchContext: new SearchContext()
 | 
			
		||||
        }, AndExp);
 | 
			
		||||
        const rootExp = parse(
 | 
			
		||||
            {
 | 
			
		||||
                fulltextTokens: [],
 | 
			
		||||
                expressionTokens: tokens(["#first", "=", "text", "and", "#second", "=", "text"]),
 | 
			
		||||
                searchContext: new SearchContext()
 | 
			
		||||
            },
 | 
			
		||||
            AndExp
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        assertIsArchived(rootExp.subExpressions[0]);
 | 
			
		||||
 | 
			
		||||
@ -101,11 +119,14 @@ describe("Parser", () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("simple label AND without explicit AND", () => {
 | 
			
		||||
        const rootExp = parse({
 | 
			
		||||
            fulltextTokens: [],
 | 
			
		||||
            expressionTokens: tokens(["#first", "=", "text", "#second", "=", "text"]),
 | 
			
		||||
            searchContext: new SearchContext()
 | 
			
		||||
        }, AndExp);
 | 
			
		||||
        const rootExp = parse(
 | 
			
		||||
            {
 | 
			
		||||
                fulltextTokens: [],
 | 
			
		||||
                expressionTokens: tokens(["#first", "=", "text", "#second", "=", "text"]),
 | 
			
		||||
                searchContext: new SearchContext()
 | 
			
		||||
            },
 | 
			
		||||
            AndExp
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        assertIsArchived(rootExp.subExpressions[0]);
 | 
			
		||||
 | 
			
		||||
@ -117,11 +138,14 @@ describe("Parser", () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("simple label OR", () => {
 | 
			
		||||
        const rootExp = parse({
 | 
			
		||||
            fulltextTokens: [],
 | 
			
		||||
            expressionTokens: tokens(["#first", "=", "text", "or", "#second", "=", "text"]),
 | 
			
		||||
            searchContext: new SearchContext()
 | 
			
		||||
        }, AndExp);
 | 
			
		||||
        const rootExp = parse(
 | 
			
		||||
            {
 | 
			
		||||
                fulltextTokens: [],
 | 
			
		||||
                expressionTokens: tokens(["#first", "=", "text", "or", "#second", "=", "text"]),
 | 
			
		||||
                searchContext: new SearchContext()
 | 
			
		||||
            },
 | 
			
		||||
            AndExp
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        assertIsArchived(rootExp.subExpressions[0]);
 | 
			
		||||
 | 
			
		||||
@ -132,11 +156,14 @@ describe("Parser", () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("fulltext and simple label", () => {
 | 
			
		||||
        const rootExp = parse({
 | 
			
		||||
            fulltextTokens: tokens(["hello"]),
 | 
			
		||||
            expressionTokens: tokens(["#mylabel", "=", "text"]),
 | 
			
		||||
            searchContext: new SearchContext()
 | 
			
		||||
        }, AndExp);
 | 
			
		||||
        const rootExp = parse(
 | 
			
		||||
            {
 | 
			
		||||
                fulltextTokens: tokens(["hello"]),
 | 
			
		||||
                expressionTokens: tokens(["#mylabel", "=", "text"]),
 | 
			
		||||
                searchContext: new SearchContext()
 | 
			
		||||
            },
 | 
			
		||||
            AndExp
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const [firstSub, _, thirdSub, fourth] = expectSubexpressions(rootExp, PropertyComparisonExp, undefined, OrExp, LabelComparisonExp);
 | 
			
		||||
 | 
			
		||||
@ -149,11 +176,14 @@ describe("Parser", () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("label sub-expression", () => {
 | 
			
		||||
        const rootExp = parse({
 | 
			
		||||
            fulltextTokens: [],
 | 
			
		||||
            expressionTokens: tokens(["#first", "=", "text", "or", ["#second", "=", "text", "and", "#third", "=", "text"]]),
 | 
			
		||||
            searchContext: new SearchContext()
 | 
			
		||||
        }, AndExp);
 | 
			
		||||
        const rootExp = parse(
 | 
			
		||||
            {
 | 
			
		||||
                fulltextTokens: [],
 | 
			
		||||
                expressionTokens: tokens(["#first", "=", "text", "or", ["#second", "=", "text", "and", "#third", "=", "text"]]),
 | 
			
		||||
                searchContext: new SearchContext()
 | 
			
		||||
            },
 | 
			
		||||
            AndExp
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        assertIsArchived(rootExp.subExpressions[0]);
 | 
			
		||||
 | 
			
		||||
@ -168,11 +198,14 @@ describe("Parser", () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("label sub-expression without explicit operator", () => {
 | 
			
		||||
        const rootExp = parse({
 | 
			
		||||
            fulltextTokens: [],
 | 
			
		||||
            expressionTokens: tokens(["#first", ["#second", "or", "#third"], "#fourth"]),
 | 
			
		||||
            searchContext: new SearchContext()
 | 
			
		||||
        }, AndExp);
 | 
			
		||||
        const rootExp = parse(
 | 
			
		||||
            {
 | 
			
		||||
                fulltextTokens: [],
 | 
			
		||||
                expressionTokens: tokens(["#first", ["#second", "or", "#third"], "#fourth"]),
 | 
			
		||||
                searchContext: new SearchContext()
 | 
			
		||||
            },
 | 
			
		||||
            AndExp
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        assertIsArchived(rootExp.subExpressions[0]);
 | 
			
		||||
 | 
			
		||||
@ -189,11 +222,14 @@ describe("Parser", () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("parses limit without order by", () => {
 | 
			
		||||
        const rootExp = parse({
 | 
			
		||||
            fulltextTokens: tokens(["hello", "hi"]),
 | 
			
		||||
            expressionTokens: [],
 | 
			
		||||
            searchContext: new SearchContext({ limit: 2 })
 | 
			
		||||
        }, OrderByAndLimitExp);
 | 
			
		||||
        const rootExp = parse(
 | 
			
		||||
            {
 | 
			
		||||
                fulltextTokens: tokens(["hello", "hi"]),
 | 
			
		||||
                expressionTokens: [],
 | 
			
		||||
                searchContext: new SearchContext({ limit: 2 })
 | 
			
		||||
            },
 | 
			
		||||
            OrderByAndLimitExp
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        expect(rootExp.limit).toBe(2);
 | 
			
		||||
        expect(rootExp.subExpression).toBeInstanceOf(AndExp);
 | 
			
		||||
@ -236,15 +272,18 @@ describe("Invalid expressions", () => {
 | 
			
		||||
 | 
			
		||||
        expect(searchContext.error).toEqual(`Error near token "note" in "#first = note.relations.second", it's possible to compare with constant only.`);
 | 
			
		||||
 | 
			
		||||
        const rootExp = parse({
 | 
			
		||||
            fulltextTokens: [],
 | 
			
		||||
            expressionTokens: [
 | 
			
		||||
                { token: "#first", inQuotes: false },
 | 
			
		||||
                { token: "=", inQuotes: false },
 | 
			
		||||
                { token: "#second", inQuotes: true }
 | 
			
		||||
            ],
 | 
			
		||||
            searchContext: new SearchContext()
 | 
			
		||||
        }, AndExp);
 | 
			
		||||
        const rootExp = parse(
 | 
			
		||||
            {
 | 
			
		||||
                fulltextTokens: [],
 | 
			
		||||
                expressionTokens: [
 | 
			
		||||
                    { token: "#first", inQuotes: false },
 | 
			
		||||
                    { token: "=", inQuotes: false },
 | 
			
		||||
                    { token: "#second", inQuotes: true }
 | 
			
		||||
                ],
 | 
			
		||||
                searchContext: new SearchContext()
 | 
			
		||||
            },
 | 
			
		||||
            AndExp
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        assertIsArchived(rootExp.subExpressions[0]);
 | 
			
		||||
 | 
			
		||||
@ -327,16 +366,13 @@ function expectExpression<T extends Expression>(exp: Expression, type: ClassType
 | 
			
		||||
 * @param fourthType the type of the fourth subexpression.
 | 
			
		||||
 * @returns an array of all the subexpressions (in order) typecasted to their expected type.
 | 
			
		||||
 */
 | 
			
		||||
function expectSubexpressions<FirstT extends Expression,
 | 
			
		||||
                              SecondT extends Expression,
 | 
			
		||||
                              ThirdT extends Expression,
 | 
			
		||||
                              FourthT extends Expression>(
 | 
			
		||||
                                exp: AndExp,
 | 
			
		||||
                                firstType: ClassType<FirstT>,
 | 
			
		||||
                                secondType?: ClassType<SecondT>,
 | 
			
		||||
                                thirdType?: ClassType<ThirdT>,
 | 
			
		||||
                                fourthType?: ClassType<FourthT>): [ FirstT, SecondT, ThirdT, FourthT ]
 | 
			
		||||
{
 | 
			
		||||
function expectSubexpressions<FirstT extends Expression, SecondT extends Expression, ThirdT extends Expression, FourthT extends Expression>(
 | 
			
		||||
    exp: AndExp,
 | 
			
		||||
    firstType: ClassType<FirstT>,
 | 
			
		||||
    secondType?: ClassType<SecondT>,
 | 
			
		||||
    thirdType?: ClassType<ThirdT>,
 | 
			
		||||
    fourthType?: ClassType<FourthT>
 | 
			
		||||
): [FirstT, SecondT, ThirdT, FourthT] {
 | 
			
		||||
    expectExpression(exp.subExpressions[0], firstType);
 | 
			
		||||
    if (secondType) {
 | 
			
		||||
        expectExpression(exp.subExpressions[1], secondType);
 | 
			
		||||
@ -347,10 +383,5 @@ function expectSubexpressions<FirstT extends Expression,
 | 
			
		||||
    if (fourthType) {
 | 
			
		||||
        expectExpression(exp.subExpressions[3], fourthType);
 | 
			
		||||
    }
 | 
			
		||||
    return [
 | 
			
		||||
        exp.subExpressions[0] as FirstT,
 | 
			
		||||
        exp.subExpressions[1] as SecondT,
 | 
			
		||||
        exp.subExpressions[2] as ThirdT,
 | 
			
		||||
        exp.subExpressions[3] as FourthT
 | 
			
		||||
    ]
 | 
			
		||||
    return [exp.subExpressions[0] as FirstT, exp.subExpressions[1] as SecondT, exp.subExpressions[2] as ThirdT, exp.subExpressions[3] as FourthT];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -427,7 +427,7 @@ export interface ParseOpts {
 | 
			
		||||
    fulltextTokens: TokenData[];
 | 
			
		||||
    expressionTokens: TokenStructure;
 | 
			
		||||
    searchContext: SearchContext;
 | 
			
		||||
    originalQuery?: string
 | 
			
		||||
    originalQuery?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function parse({ fulltextTokens, expressionTokens, searchContext }: ParseOpts) {
 | 
			
		||||
@ -450,7 +450,7 @@ function parse({ fulltextTokens, expressionTokens, searchContext }: ParseOpts) {
 | 
			
		||||
 | 
			
		||||
    if (searchContext.limit && !searchContext.orderBy) {
 | 
			
		||||
        const filterExp = exp;
 | 
			
		||||
        exp = new OrderByAndLimitExp([], searchContext.limit || undefined );
 | 
			
		||||
        exp = new OrderByAndLimitExp([], searchContext.limit || undefined);
 | 
			
		||||
        (exp as any).subExpression = filterExp;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -30,7 +30,7 @@ function buildIntegrationTestDatabase(dbPath?: string) {
 | 
			
		||||
    return new Database(dbBuffer);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function rebuildIntegrationTestDatabase(dbPath: string) {
 | 
			
		||||
function rebuildIntegrationTestDatabase(dbPath?: string) {
 | 
			
		||||
    if (dbConnection) {
 | 
			
		||||
        dbConnection.close();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ import config from "./config.js";
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
function get(name: keyof typeof config.Sync) {
 | 
			
		||||
  return (config["Sync"] && config["Sync"][name]) || optionService.getOption(name);
 | 
			
		||||
    return (config["Sync"] && config["Sync"][name]) || optionService.getOption(name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
 | 
			
		||||
@ -3,8 +3,7 @@ import BTask from "../becca/entities/btask.js";
 | 
			
		||||
import type { TaskRow } from "../becca/entities/rows.js";
 | 
			
		||||
 | 
			
		||||
export function getTasks(parentNoteId: string) {
 | 
			
		||||
    return becca.getTasks()
 | 
			
		||||
        .filter((task) => task.parentNoteId === parentNoteId && !task.isDone);
 | 
			
		||||
    return becca.getTasks().filter((task) => task.parentNoteId === parentNoteId && !task.isDone);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface CreateTaskParams {
 | 
			
		||||
@ -19,7 +18,7 @@ export function createNewTask(params: CreateTaskParams) {
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        task
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function toggleTaskDone(taskId: string) {
 | 
			
		||||
 | 
			
		||||
@ -33,7 +33,7 @@ function getTrayIconPath() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getIconPath(name: string) {
 | 
			
		||||
    const suffix = (!isMac && nativeTheme.shouldUseDarkColors ? "-inverted" : "");
 | 
			
		||||
    const suffix = !isMac && nativeTheme.shouldUseDarkColors ? "-inverted" : "";
 | 
			
		||||
    return path.join(path.dirname(fileURLToPath(import.meta.url)), "../..", "images", "app-icons", "tray", `${name}Template${suffix}.png`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -110,7 +110,6 @@ function updateTrayMenu() {
 | 
			
		||||
                    click: () => openInSameTab(bookmarkNote)
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return menuItems;
 | 
			
		||||
@ -139,7 +138,7 @@ function updateTrayMenu() {
 | 
			
		||||
                type: "normal",
 | 
			
		||||
                sublabel: formatter.format(date),
 | 
			
		||||
                click: () => openInSameTab(recentNote)
 | 
			
		||||
            })
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return menuItems;
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user