mirror of
https://github.com/zadam/trilium.git
synced 2025-11-01 02:45:54 +01:00
Merge branch 'develop' into ai-llm-integration
This commit is contained in:
5
.github/workflows/nightly.yml
vendored
5
.github/workflows/nightly.yml
vendored
@@ -5,6 +5,9 @@ on:
|
|||||||
- cron: "0 2 * * *" # run at 2 AM UTC
|
- cron: "0 2 * * *" # run at 2 AM UTC
|
||||||
# This can be used to allow manually triggering nightlies from the web interface
|
# This can be used to allow manually triggering nightlies from the web interface
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- .github/actions/build-electron/*
|
||||||
env:
|
env:
|
||||||
GITHUB_UPLOAD_URL: https://uploads.github.com/repos/TriliumNext/Notes/releases/179589950/assets{?name,label}
|
GITHUB_UPLOAD_URL: https://uploads.github.com/repos/TriliumNext/Notes/releases/179589950/assets{?name,label}
|
||||||
GITHUB_RELEASE_ID: 179589950
|
GITHUB_RELEASE_ID: 179589950
|
||||||
@@ -56,6 +59,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish release
|
- name: Publish release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
make_latest: false
|
make_latest: false
|
||||||
prerelease: true
|
prerelease: true
|
||||||
@@ -88,6 +92,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish release
|
- name: Publish release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
make_latest: false
|
make_latest: false
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
cd src/public
|
|
||||||
echo Summary
|
|
||||||
cloc HEAD \
|
|
||||||
--git --md \
|
|
||||||
--include-lang=javascript,typescript
|
|
||||||
|
|
||||||
echo By file
|
|
||||||
cloc HEAD \
|
|
||||||
--git --md \
|
|
||||||
--include-lang=javascript,typescript \
|
|
||||||
--by-file | grep \.js\|
|
|
||||||
@@ -76,14 +76,11 @@ try {
|
|||||||
"node_modules/dayjs/",
|
"node_modules/dayjs/",
|
||||||
"node_modules/boxicons/css/",
|
"node_modules/boxicons/css/",
|
||||||
"node_modules/boxicons/fonts/",
|
"node_modules/boxicons/fonts/",
|
||||||
"node_modules/mermaid/dist/",
|
|
||||||
"node_modules/jquery/dist/",
|
"node_modules/jquery/dist/",
|
||||||
"node_modules/jquery-hotkeys/",
|
"node_modules/jquery-hotkeys/",
|
||||||
"node_modules/split.js/dist/",
|
"node_modules/split.js/dist/",
|
||||||
"node_modules/panzoom/dist/",
|
|
||||||
"node_modules/i18next/",
|
"node_modules/i18next/",
|
||||||
"node_modules/i18next-http-backend/",
|
"node_modules/i18next-http-backend/",
|
||||||
"node_modules/jsplumb/dist/",
|
|
||||||
"node_modules/vanilla-js-wheel-zoom/dist/",
|
"node_modules/vanilla-js-wheel-zoom/dist/",
|
||||||
"node_modules/mark.js/dist/",
|
"node_modules/mark.js/dist/",
|
||||||
"node_modules/normalize.css/",
|
"node_modules/normalize.css/",
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ noAuthentication=false
|
|||||||
# set to true to disable backups (e.g. because of limited space on server)
|
# set to true to disable backups (e.g. because of limited space on server)
|
||||||
noBackup=false
|
noBackup=false
|
||||||
|
|
||||||
# Disable automatically generating desktop icon
|
|
||||||
# noDesktopIcon=true
|
|
||||||
|
|
||||||
[Network]
|
[Network]
|
||||||
# host setting is relevant only for web deployments - set the host on which the server will listen
|
# host setting is relevant only for web deployments - set the host on which the server will listen
|
||||||
# host=0.0.0.0
|
# host=0.0.0.0
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ It's possible to customize the title of generated date notes by defining a `#dat
|
|||||||
|
|
||||||
It is also possible to customize the title of generated month notes through the `#monthPattern` attribute, much like `#datePattern`. The options are:
|
It is also possible to customize the title of generated month notes through the `#monthPattern` attribute, much like `#datePattern`. The options are:
|
||||||
|
|
||||||
|
* `{isoMonth}` results in an ISO 8061 formatted month (e.g. "2025-03" for March 2025)
|
||||||
* `{monthNumberPadded}` results in a number like `09` for September, and `11` for November
|
* `{monthNumberPadded}` results in a number like `09` for September, and `11` for November
|
||||||
* `{month}` results in the full month name (e.g. `September` or `October`)
|
* `{month}` results in the full month name (e.g. `September` or `October`)
|
||||||
* `{shortMonth3}` is replaced with the first 3 letters of the month, e.g. Jan, Feb, etc.
|
* `{shortMonth3}` is replaced with the first 3 letters of the month, e.g. Jan, Feb, etc.
|
||||||
@@ -56,4 +57,4 @@ Trilium has some special support for day notes in the form of [backend Script AP
|
|||||||
|
|
||||||
Day (and year, month) notes are created with a label - e.g. `#dateNote="2018-08-16"` this can then be used by other scripts to add new notes to day note etc.
|
Day (and year, month) notes are created with a label - e.g. `#dateNote="2018-08-16"` this can then be used by other scripts to add new notes to day note etc.
|
||||||
|
|
||||||
Journal also has relation `child:child:child:template=Day template` (see \[\[attribute inheritance\]\]) which effectively adds \[\[template\]\] to day notes (grand-grand-grand children of Journal).
|
Journal also has relation `child:child:child:template=Day template` (see \[\[attribute inheritance\]\]) which effectively adds \[\[template\]\] to day notes (grand-grand-grand children of Journal).
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ async function testAriaSnapshot({ page, context, noteTitle, snapshot }: AriaTest
|
|||||||
await app.goto();
|
await app.goto();
|
||||||
await app.goToNoteInNewTab(noteTitle);
|
await app.goToNoteInNewTab(noteTitle);
|
||||||
|
|
||||||
const svgData = app.currentNoteSplit.locator(".mermaid-render svg");
|
const svgData = app.currentNoteSplit.locator(".render-container svg");
|
||||||
await expect(svgData).toBeVisible();
|
await expect(svgData).toBeVisible();
|
||||||
await expect(svgData).toMatchAriaSnapshot(snapshot);
|
await expect(svgData).toMatchAriaSnapshot(snapshot);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export default class App {
|
|||||||
async goToNoteInNewTab(noteTitle: string) {
|
async goToNoteInNewTab(noteTitle: string) {
|
||||||
const autocomplete = this.currentNoteSplit.locator(".note-autocomplete");
|
const autocomplete = this.currentNoteSplit.locator(".note-autocomplete");
|
||||||
await autocomplete.fill(noteTitle);
|
await autocomplete.fill(noteTitle);
|
||||||
|
await expect(this.currentNoteSplit.locator(".note-detail-empty-results")).toContainText(noteTitle);
|
||||||
await autocomplete.press("ArrowDown");
|
await autocomplete.press("ArrowDown");
|
||||||
await autocomplete.press("Enter");
|
await autocomplete.press("Enter");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import electron from "electron";
|
|||||||
import electronDebug from "electron-debug";
|
import electronDebug from "electron-debug";
|
||||||
import electronDl from "electron-dl";
|
import electronDl from "electron-dl";
|
||||||
import sqlInit from "./src/services/sql_init.js";
|
import sqlInit from "./src/services/sql_init.js";
|
||||||
import appIconService from "./src/services/app_icon.js";
|
|
||||||
import windowService from "./src/services/window.js";
|
import windowService from "./src/services/window.js";
|
||||||
import tray from "./src/services/tray.js";
|
import tray from "./src/services/tray.js";
|
||||||
|
|
||||||
@@ -19,8 +18,6 @@ if ((await import("electron-squirrel-startup")).default) {
|
|||||||
// Adds debug features like hotkeys for triggering dev tools and reload
|
// Adds debug features like hotkeys for triggering dev tools and reload
|
||||||
electronDebug();
|
electronDebug();
|
||||||
|
|
||||||
appIconService.installLocalAppIcon();
|
|
||||||
|
|
||||||
electronDl({ saveAs: true });
|
electronDl({ saveAs: true });
|
||||||
|
|
||||||
// needed for excalidraw export https://github.com/zadam/trilium/issues/4271
|
// needed for excalidraw export https://github.com/zadam/trilium/issues/4271
|
||||||
|
|||||||
16
libraries/ckeditor/ckeditor-content.css
vendored
16
libraries/ckeditor/ckeditor-content.css
vendored
@@ -17,7 +17,7 @@
|
|||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ck-content .admonition {
|
.admonition {
|
||||||
--accent-color: var(--card-border-color);
|
--accent-color: var(--card-border-color);
|
||||||
border: 1px solid var(--accent-color);
|
border: 1px solid var(--accent-color);
|
||||||
box-shadow: var(--card-box-shadow);
|
box-shadow: var(--card-box-shadow);
|
||||||
@@ -29,19 +29,19 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ck-content .admonition p:last-child {
|
.admonition p:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ck-content .admonition p, h2 {
|
.admonition p, h2 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ck-content .admonition.note { --accent-color: #69c7ff; }
|
.admonition.note { --accent-color: #69c7ff; }
|
||||||
.ck-content .admonition.tip { --accent-color: #40c025; }
|
.admonition.tip { --accent-color: #40c025; }
|
||||||
.ck-content .admonition.important { --accent-color: #9839f7; }
|
.admonition.important { --accent-color: #9839f7; }
|
||||||
.ck-content .admonition.caution { --accent-color: #ff2e2e; }
|
.admonition.caution { --accent-color: #ff2e2e; }
|
||||||
.ck-content .admonition.warning { --accent-color: #e2aa03; }
|
.admonition.warning { --accent-color: #e2aa03; }
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* CKEditor 5 (v41.0.0) content styles.
|
* CKEditor 5 (v41.0.0) content styles.
|
||||||
|
|||||||
2
libraries/ckeditor/ckeditor.js
vendored
2
libraries/ckeditor/ckeditor.js
vendored
File diff suppressed because one or more lines are too long
2
libraries/ckeditor/ckeditor.js.map
vendored
2
libraries/ckeditor/ckeditor.js.map
vendored
File diff suppressed because one or more lines are too long
106
package-lock.json
generated
106
package-lock.json
generated
@@ -25,7 +25,7 @@
|
|||||||
"cheerio": "1.0.0",
|
"cheerio": "1.0.0",
|
||||||
"chokidar": "4.0.3",
|
"chokidar": "4.0.3",
|
||||||
"cls-hooked": "4.2.2",
|
"cls-hooked": "4.2.2",
|
||||||
"codemirror": "5.65.18",
|
"codemirror": "5.65.19",
|
||||||
"compression": "1.8.0",
|
"compression": "1.8.0",
|
||||||
"cookie-parser": "1.4.7",
|
"cookie-parser": "1.4.7",
|
||||||
"csrf-csrf": "3.1.0",
|
"csrf-csrf": "3.1.0",
|
||||||
@@ -39,11 +39,11 @@
|
|||||||
"electron-squirrel-startup": "1.0.1",
|
"electron-squirrel-startup": "1.0.1",
|
||||||
"electron-window-state": "5.0.3",
|
"electron-window-state": "5.0.3",
|
||||||
"escape-html": "1.0.3",
|
"escape-html": "1.0.3",
|
||||||
"eslint-linter-browserify": "9.22.0",
|
"eslint-linter-browserify": "9.23.0",
|
||||||
"express": "4.21.2",
|
"express": "4.21.2",
|
||||||
"express-rate-limit": "7.5.0",
|
"express-rate-limit": "7.5.0",
|
||||||
"express-session": "1.18.1",
|
"express-session": "1.18.1",
|
||||||
"force-graph": "1.49.4",
|
"force-graph": "1.49.5",
|
||||||
"fs-extra": "11.3.0",
|
"fs-extra": "11.3.0",
|
||||||
"helmet": "8.1.0",
|
"helmet": "8.1.0",
|
||||||
"html": "1.0.0",
|
"html": "1.0.0",
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
"marked": "15.0.7",
|
"marked": "15.0.7",
|
||||||
"mermaid": "11.5.0",
|
"mermaid": "11.5.0",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
"multer": "1.4.5-lts.1",
|
"multer": "1.4.5-lts.2",
|
||||||
"normalize-strings": "1.1.1",
|
"normalize-strings": "1.1.1",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"panzoom": "9.4.3",
|
"panzoom": "9.4.3",
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
"@electron-forge/maker-zip": "7.7.0",
|
"@electron-forge/maker-zip": "7.7.0",
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "7.7.0",
|
"@electron-forge/plugin-auto-unpack-natives": "7.7.0",
|
||||||
"@electron/rebuild": "3.7.1",
|
"@electron/rebuild": "3.7.1",
|
||||||
"@eslint/js": "9.22.0",
|
"@eslint/js": "9.23.0",
|
||||||
"@fullcalendar/core": "6.1.15",
|
"@fullcalendar/core": "6.1.15",
|
||||||
"@fullcalendar/daygrid": "6.1.15",
|
"@fullcalendar/daygrid": "6.1.15",
|
||||||
"@fullcalendar/interaction": "6.1.15",
|
"@fullcalendar/interaction": "6.1.15",
|
||||||
@@ -138,11 +138,11 @@
|
|||||||
"@types/jquery": "3.5.32",
|
"@types/jquery": "3.5.32",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsdom": "21.1.7",
|
"@types/jsdom": "21.1.7",
|
||||||
"@types/leaflet": "1.9.16",
|
"@types/leaflet": "1.9.17",
|
||||||
"@types/leaflet-gpx": "1.3.7",
|
"@types/leaflet-gpx": "1.3.7",
|
||||||
"@types/mime-types": "2.1.4",
|
"@types/mime-types": "2.1.4",
|
||||||
"@types/multer": "1.4.12",
|
"@types/multer": "1.4.12",
|
||||||
"@types/node": "22.13.10",
|
"@types/node": "22.13.11",
|
||||||
"@types/react": "18.3.19",
|
"@types/react": "18.3.19",
|
||||||
"@types/react-dom": "18.3.5",
|
"@types/react-dom": "18.3.5",
|
||||||
"@types/safe-compare": "1.1.2",
|
"@types/safe-compare": "1.1.2",
|
||||||
@@ -164,8 +164,8 @@
|
|||||||
"bootstrap": "5.3.3",
|
"bootstrap": "5.3.3",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"css-loader": "7.1.2",
|
"css-loader": "7.1.2",
|
||||||
"electron": "35.0.1",
|
"electron": "35.0.3",
|
||||||
"eslint": "9.22.0",
|
"eslint": "9.23.0",
|
||||||
"esm": "3.2.25",
|
"esm": "3.2.25",
|
||||||
"globals": "16.0.0",
|
"globals": "16.0.0",
|
||||||
"happy-dom": "17.4.4",
|
"happy-dom": "17.4.4",
|
||||||
@@ -184,13 +184,13 @@
|
|||||||
"sass": "1.86.0",
|
"sass": "1.86.0",
|
||||||
"sass-loader": "16.0.5",
|
"sass-loader": "16.0.5",
|
||||||
"split.js": "1.6.5",
|
"split.js": "1.6.5",
|
||||||
"supertest": "7.0.0",
|
"supertest": "7.1.0",
|
||||||
"svg-pan-zoom": "3.6.2",
|
"svg-pan-zoom": "3.6.2",
|
||||||
"swagger-jsdoc": "6.2.8",
|
"swagger-jsdoc": "6.2.8",
|
||||||
"ts-loader": "9.5.2",
|
"ts-loader": "9.5.2",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"tsx": "4.19.3",
|
"tsx": "4.19.3",
|
||||||
"typedoc": "0.28.0",
|
"typedoc": "0.28.1",
|
||||||
"typescript": "5.8.2",
|
"typescript": "5.8.2",
|
||||||
"typescript-eslint": "8.27.0",
|
"typescript-eslint": "8.27.0",
|
||||||
"vitest": "3.0.9",
|
"vitest": "3.0.9",
|
||||||
@@ -2207,9 +2207,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-helpers": {
|
"node_modules/@eslint/config-helpers": {
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.0.tgz",
|
||||||
"integrity": "sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==",
|
"integrity": "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2230,9 +2230,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/eslintrc": {
|
"node_modules/@eslint/eslintrc": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
|
||||||
"integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==",
|
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2291,9 +2291,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "9.22.0",
|
"version": "9.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz",
|
||||||
"integrity": "sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==",
|
"integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -5820,9 +5820,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/leaflet": {
|
"node_modules/@types/leaflet": {
|
||||||
"version": "1.9.16",
|
"version": "1.9.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.16.tgz",
|
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.17.tgz",
|
||||||
"integrity": "sha512-wzZoyySUxkgMZ0ihJ7IaUIblG8Rdc8AbbZKLneyn+QjYsj5q1QU7TEKYqwTr10BGSzY5LI7tJk9Ifo+mEjdFRw==",
|
"integrity": "sha512-IJ4K6t7I3Fh5qXbQ1uwL3CFVbCi6haW9+53oLWgdKlLP7EaS21byWFJxxqOx9y8I0AP0actXSJLVMbyvxhkUTA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5925,9 +5925,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.13.10",
|
"version": "22.13.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.11.tgz",
|
||||||
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
|
"integrity": "sha512-iEUCUJoU0i3VnrCmgoWCXttklWcvoCIx4jzcP22fioIVSdTmjgoEvmAO/QPw6TcS9k5FrNgn4w7q5lGOd1CT5g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.20.0"
|
"undici-types": "~6.20.0"
|
||||||
@@ -8561,9 +8561,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/codemirror": {
|
"node_modules/codemirror": {
|
||||||
"version": "5.65.18",
|
"version": "5.65.19",
|
||||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.18.tgz",
|
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.19.tgz",
|
||||||
"integrity": "sha512-Gaz4gHnkbHMGgahNt3CA5HBk5lLQBqmD/pBgeB4kQU6OedZmqMBjlRF0LSrp2tJ4wlLNPm2FfaUd1pDy0mdlpA==",
|
"integrity": "sha512-+aFkvqhaAVr1gferNMuN8vkTSrWIFvzlMV9I2KBLCWS2WpZ2+UAkZjlMZmEuT+gcXTi6RrGQCkWq1/bDtGqhIA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
@@ -10151,9 +10151,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron": {
|
"node_modules/electron": {
|
||||||
"version": "35.0.1",
|
"version": "35.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/electron/-/electron-35.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/electron/-/electron-35.0.3.tgz",
|
||||||
"integrity": "sha512-iQonj6lnPhqfqha2KXx6LzV1dnu6UPTCWK+b7f9Zvg828umGemi22DKbcJ3/q+Opn7iUVTWyqp9z1JQqkIi6OA==",
|
"integrity": "sha512-kjQAYEWXSr2TyK19IZoF85dzFIBaYuX7Yp/C+34b5Y/jmI2z270CGie+RjmEGMMitsy0G8YJKftukhYMuWlK6g==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -11192,19 +11192,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "9.22.0",
|
"version": "9.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz",
|
||||||
"integrity": "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==",
|
"integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
"@eslint/config-array": "^0.19.2",
|
"@eslint/config-array": "^0.19.2",
|
||||||
"@eslint/config-helpers": "^0.1.0",
|
"@eslint/config-helpers": "^0.2.0",
|
||||||
"@eslint/core": "^0.12.0",
|
"@eslint/core": "^0.12.0",
|
||||||
"@eslint/eslintrc": "^3.3.0",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "9.22.0",
|
"@eslint/js": "9.23.0",
|
||||||
"@eslint/plugin-kit": "^0.2.7",
|
"@eslint/plugin-kit": "^0.2.7",
|
||||||
"@humanfs/node": "^0.16.6",
|
"@humanfs/node": "^0.16.6",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
@@ -11253,9 +11253,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-linter-browserify": {
|
"node_modules/eslint-linter-browserify": {
|
||||||
"version": "9.22.0",
|
"version": "9.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-linter-browserify/-/eslint-linter-browserify-9.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-linter-browserify/-/eslint-linter-browserify-9.23.0.tgz",
|
||||||
"integrity": "sha512-b70x+ilh1XkugEZZvGJ6LNPE1+jxjsn4KIdj1OBMBGbzYj7l2lr3N/Y4NHKhFxq2a4v/J8WqojIOvy0AFDmrXw==",
|
"integrity": "sha512-5dpvA43y3psh58cl5HILg0Fk82KUP30YqCMQw17mt5H3kG16G3YfQgSE+sKV/6U3fT/SBitFrQA9PX0BZ7xUBQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/eslint-scope": {
|
"node_modules/eslint-scope": {
|
||||||
@@ -12231,9 +12231,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/force-graph": {
|
"node_modules/force-graph": {
|
||||||
"version": "1.49.4",
|
"version": "1.49.5",
|
||||||
"resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.49.4.tgz",
|
"resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.49.5.tgz",
|
||||||
"integrity": "sha512-TMbbXg3n0pjI8cmgNlv1IKEGewnd9LdwKVJ4cj4XzZXqP/Q5aSjsyuxzIITtkfDJ+KDsiLql1FHu19Lqrq41uQ==",
|
"integrity": "sha512-mCTLxsaOPfp4Jq4FND8sHTpa8aZDLNXgkwAN98IDZ8Ve3nralz0gNsmE4Nx6NFm48olJ0gzCQYYLJrrYDqifew==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tweenjs/tween.js": "18 - 25",
|
"@tweenjs/tween.js": "18 - 25",
|
||||||
@@ -15868,9 +15868,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/multer": {
|
"node_modules/multer": {
|
||||||
"version": "1.4.5-lts.1",
|
"version": "1.4.5-lts.2",
|
||||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz",
|
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
|
||||||
"integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==",
|
"integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"append-field": "^1.0.0",
|
"append-field": "^1.0.0",
|
||||||
@@ -19742,9 +19742,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/supertest": {
|
"node_modules/supertest": {
|
||||||
"version": "7.0.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.0.tgz",
|
||||||
"integrity": "sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==",
|
"integrity": "sha512-5QeSO8hSrKghtcWEoPiO036fxH0Ii2wVQfFZSP0oqQhmjk8bOLhDFXr4JrvaFmPuEWUoq4znY3uSi8UzLKxGqw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -20617,9 +20617,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typedoc": {
|
"node_modules/typedoc": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.1.tgz",
|
||||||
"integrity": "sha512-UU+xxZXrpnUhEulBYRwY2afoYFC24J2fTFovOs3llj2foGShCoKVQL6cQCfQ+sBAOdiFn2dETpZ9xhah+CL3RQ==",
|
"integrity": "sha512-Mn2VPNMaxoe/hlBiLriG4U55oyAa3Xo+8HbtEwV7F5WEOPXqtxzGuMZhJYHaqFJpajeQ6ZDUC2c990NAtTbdgw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
22
package.json
22
package.json
@@ -85,7 +85,7 @@
|
|||||||
"cheerio": "1.0.0",
|
"cheerio": "1.0.0",
|
||||||
"chokidar": "4.0.3",
|
"chokidar": "4.0.3",
|
||||||
"cls-hooked": "4.2.2",
|
"cls-hooked": "4.2.2",
|
||||||
"codemirror": "5.65.18",
|
"codemirror": "5.65.19",
|
||||||
"compression": "1.8.0",
|
"compression": "1.8.0",
|
||||||
"cookie-parser": "1.4.7",
|
"cookie-parser": "1.4.7",
|
||||||
"csrf-csrf": "3.1.0",
|
"csrf-csrf": "3.1.0",
|
||||||
@@ -99,11 +99,11 @@
|
|||||||
"electron-squirrel-startup": "1.0.1",
|
"electron-squirrel-startup": "1.0.1",
|
||||||
"electron-window-state": "5.0.3",
|
"electron-window-state": "5.0.3",
|
||||||
"escape-html": "1.0.3",
|
"escape-html": "1.0.3",
|
||||||
"eslint-linter-browserify": "9.22.0",
|
"eslint-linter-browserify": "9.23.0",
|
||||||
"express": "4.21.2",
|
"express": "4.21.2",
|
||||||
"express-rate-limit": "7.5.0",
|
"express-rate-limit": "7.5.0",
|
||||||
"express-session": "1.18.1",
|
"express-session": "1.18.1",
|
||||||
"force-graph": "1.49.4",
|
"force-graph": "1.49.5",
|
||||||
"fs-extra": "11.3.0",
|
"fs-extra": "11.3.0",
|
||||||
"helmet": "8.1.0",
|
"helmet": "8.1.0",
|
||||||
"html": "1.0.0",
|
"html": "1.0.0",
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
"marked": "15.0.7",
|
"marked": "15.0.7",
|
||||||
"mermaid": "11.5.0",
|
"mermaid": "11.5.0",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
"multer": "1.4.5-lts.1",
|
"multer": "1.4.5-lts.2",
|
||||||
"normalize-strings": "1.1.1",
|
"normalize-strings": "1.1.1",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"panzoom": "9.4.3",
|
"panzoom": "9.4.3",
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
"@electron-forge/maker-zip": "7.7.0",
|
"@electron-forge/maker-zip": "7.7.0",
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "7.7.0",
|
"@electron-forge/plugin-auto-unpack-natives": "7.7.0",
|
||||||
"@electron/rebuild": "3.7.1",
|
"@electron/rebuild": "3.7.1",
|
||||||
"@eslint/js": "9.22.0",
|
"@eslint/js": "9.23.0",
|
||||||
"@fullcalendar/core": "6.1.15",
|
"@fullcalendar/core": "6.1.15",
|
||||||
"@fullcalendar/daygrid": "6.1.15",
|
"@fullcalendar/daygrid": "6.1.15",
|
||||||
"@fullcalendar/interaction": "6.1.15",
|
"@fullcalendar/interaction": "6.1.15",
|
||||||
@@ -195,11 +195,11 @@
|
|||||||
"@types/jquery": "3.5.32",
|
"@types/jquery": "3.5.32",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsdom": "21.1.7",
|
"@types/jsdom": "21.1.7",
|
||||||
"@types/leaflet": "1.9.16",
|
"@types/leaflet": "1.9.17",
|
||||||
"@types/leaflet-gpx": "1.3.7",
|
"@types/leaflet-gpx": "1.3.7",
|
||||||
"@types/mime-types": "2.1.4",
|
"@types/mime-types": "2.1.4",
|
||||||
"@types/multer": "1.4.12",
|
"@types/multer": "1.4.12",
|
||||||
"@types/node": "22.13.10",
|
"@types/node": "22.13.11",
|
||||||
"@types/react": "18.3.19",
|
"@types/react": "18.3.19",
|
||||||
"@types/react-dom": "18.3.5",
|
"@types/react-dom": "18.3.5",
|
||||||
"@types/safe-compare": "1.1.2",
|
"@types/safe-compare": "1.1.2",
|
||||||
@@ -221,8 +221,8 @@
|
|||||||
"bootstrap": "5.3.3",
|
"bootstrap": "5.3.3",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"css-loader": "7.1.2",
|
"css-loader": "7.1.2",
|
||||||
"electron": "35.0.1",
|
"electron": "35.0.3",
|
||||||
"eslint": "9.22.0",
|
"eslint": "9.23.0",
|
||||||
"esm": "3.2.25",
|
"esm": "3.2.25",
|
||||||
"globals": "16.0.0",
|
"globals": "16.0.0",
|
||||||
"happy-dom": "17.4.4",
|
"happy-dom": "17.4.4",
|
||||||
@@ -241,13 +241,13 @@
|
|||||||
"sass": "1.86.0",
|
"sass": "1.86.0",
|
||||||
"sass-loader": "16.0.5",
|
"sass-loader": "16.0.5",
|
||||||
"split.js": "1.6.5",
|
"split.js": "1.6.5",
|
||||||
"supertest": "7.0.0",
|
"supertest": "7.1.0",
|
||||||
"svg-pan-zoom": "3.6.2",
|
"svg-pan-zoom": "3.6.2",
|
||||||
"swagger-jsdoc": "6.2.8",
|
"swagger-jsdoc": "6.2.8",
|
||||||
"ts-loader": "9.5.2",
|
"ts-loader": "9.5.2",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"tsx": "4.19.3",
|
"tsx": "4.19.3",
|
||||||
"typedoc": "0.28.0",
|
"typedoc": "0.28.1",
|
||||||
"typescript": "5.8.2",
|
"typescript": "5.8.2",
|
||||||
"typescript-eslint": "8.27.0",
|
"typescript-eslint": "8.27.0",
|
||||||
"vitest": "3.0.9",
|
"vitest": "3.0.9",
|
||||||
|
|||||||
173
packages/turndown-plugin-gfm/package-lock.json
generated
173
packages/turndown-plugin-gfm/package-lock.json
generated
@@ -163,9 +163,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.37.0.tgz",
|
||||||
"integrity": "sha512-jgrXjjcEwN6XpZXL0HUeOVGfjXhPyxAbbhD0BlXUB+abTOpbPiN5Wb3kOT7yb+uEtATNYF5x5gIfwutmuBA26w==",
|
"integrity": "sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -177,9 +177,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm64": {
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.37.0.tgz",
|
||||||
"integrity": "sha512-NyfuLvdPdNUfUNeYKUwPwKsE5SXa2J6bCt2LdB/N+AxShnkpiczi3tcLJrm5mA+eqpy0HmaIY9F6XCa32N5yzg==",
|
"integrity": "sha512-6U3SlVyMxezt8Y+/iEBcbp945uZjJwjZimu76xoG7tO1av9VO691z8PkhzQ85ith2I8R2RddEPeSfcbyPfD4hA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -191,9 +191,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.37.0.tgz",
|
||||||
"integrity": "sha512-JQ1Jk5G4bGrD4pWJQzWsD8I1n1mgPXq33+/vP4sk8j/z/C2siRuxZtaUA7yMTf71TCZTZl/4e1bfzwUmFb3+rw==",
|
"integrity": "sha512-+iTQ5YHuGmPt10NTzEyMPbayiNTcOZDWsbxZYR1ZnmLnZxG17ivrPSWFO9j6GalY0+gV3Jtwrrs12DBscxnlYA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -205,9 +205,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-x64": {
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.37.0.tgz",
|
||||||
"integrity": "sha512-6c6wMZa1lrtiRsbDziCmjE53YbTkxMYhhnWnSW8R/yqsM7a6mSJ3uAVT0t8Y/DGt7gxUWYuFM4bwWk9XCJrFKA==",
|
"integrity": "sha512-m8W2UbxLDcmRKVjgl5J/k4B8d7qX2EcJve3Sut7YGrQoPtCIQGPH5AMzuFvYRWZi0FVS0zEY4c8uttPfX6bwYQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -219,9 +219,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.37.0.tgz",
|
||||||
"integrity": "sha512-KXVsijKeJXOl8QzXTsA+sHVDsFOmMCdBRgFmBb+mfEb/7geR7+C8ypAml4fquUt14ZyVXaw2o1FWhqAfOvA4sg==",
|
"integrity": "sha512-FOMXGmH15OmtQWEt174v9P1JqqhlgYge/bUjIbiVD1nI1NeJ30HYT9SJlZMqdo1uQFyt9cz748F1BHghWaDnVA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -233,9 +233,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.37.0.tgz",
|
||||||
"integrity": "sha512-dVeWq1ebbvByI+ndz4IJcD4a09RJgRYmLccwlQ8bPd4olz3Y213uf1iwvc7ZaxNn2ab7bjc08PrtBgMu6nb4pQ==",
|
"integrity": "sha512-SZMxNttjPKvV14Hjck5t70xS3l63sbVwl98g3FlVVx2YIDmfUIy29jQrsw06ewEYQ8lQSuY9mpAPlmgRD2iSsA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -247,9 +247,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.37.0.tgz",
|
||||||
"integrity": "sha512-bvXVU42mOVcF4le6XSjscdXjqx8okv4n5vmwgzcmtvFdifQ5U4dXFYaCB87namDRKlUL9ybVtLQ9ztnawaSzvg==",
|
"integrity": "sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -261,9 +261,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.37.0.tgz",
|
||||||
"integrity": "sha512-JFIQrDJYrxOnyDQGYkqnNBtjDwTgbasdbUiQvcU8JmGDfValfH1lNpng+4FWlhaVIR4KPkeddYjsVVbmJYvDcg==",
|
"integrity": "sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -275,9 +275,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.37.0.tgz",
|
||||||
"integrity": "sha512-KqjYVh3oM1bj//5X7k79PSCZ6CvaVzb7Qs7VMWS+SlWB5M8p3FqufLP9VNp4CazJ0CsPDLwVD9r3vX7Ci4J56A==",
|
"integrity": "sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -289,9 +289,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.37.0.tgz",
|
||||||
"integrity": "sha512-QiGnhScND+mAAtfHqeT+cB1S9yFnNQ/EwCg5yE3MzoaZZnIV0RV9O5alJAoJKX/sBONVKeZdMfO8QSaWEygMhw==",
|
"integrity": "sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -303,9 +303,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.37.0.tgz",
|
||||||
"integrity": "sha512-1ZPyEDWF8phd4FQtTzMh8FQwqzvIjLsl6/84gzUxnMNFBtExBtpL51H67mV9xipuxl1AEAerRBgBwFNpkw8+Lg==",
|
"integrity": "sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
@@ -317,9 +317,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.37.0.tgz",
|
||||||
"integrity": "sha512-VMPMEIUpPFKpPI9GZMhJrtu8rxnp6mJR3ZzQPykq4xc2GmdHj3Q4cA+7avMyegXy4n1v+Qynr9fR88BmyO74tg==",
|
"integrity": "sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -331,9 +331,23 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.37.0.tgz",
|
||||||
"integrity": "sha512-ttE6ayb/kHwNRJGYLpuAvB7SMtOeQnVXEIpMtAvx3kepFQeowVED0n1K9nAdraHUPJ5hydEMxBpIR7o4nrm8uA==",
|
"integrity": "sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||||
|
"version": "4.37.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.37.0.tgz",
|
||||||
|
"integrity": "sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -345,9 +359,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.37.0.tgz",
|
||||||
"integrity": "sha512-4a5gf2jpS0AIe7uBjxDeUMNcFmaRTbNv7NxI5xOCs4lhzsVyGR/0qBXduPnoWf6dGC365saTiwag8hP1imTgag==",
|
"integrity": "sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -359,9 +373,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.37.0.tgz",
|
||||||
"integrity": "sha512-5KtoW8UWmwFKQ96aQL3LlRXX16IMwyzMq/jSSVIIyAANiE1doaQsx/KRyhAvpHlPjPiSU/AYX/8m+lQ9VToxFQ==",
|
"integrity": "sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -373,9 +387,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.37.0.tgz",
|
||||||
"integrity": "sha512-sycrYZPrv2ag4OCvaN5js+f01eoZ2U+RmT5as8vhxiFz+kxwlHrsxOwKPSA8WyS+Wc6Epid9QeI/IkQ9NkgYyQ==",
|
"integrity": "sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -387,9 +401,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.37.0.tgz",
|
||||||
"integrity": "sha512-qbqt4N7tokFwwSVlWDsjfoHgviS3n/vZ8LK0h1uLG9TYIRuUTJC88E1xb3LM2iqZ/WTqNQjYrtmtGmrmmawB6A==",
|
"integrity": "sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -401,9 +415,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.37.0.tgz",
|
||||||
"integrity": "sha512-t+RY0JuRamIocMuQcfwYSOkmdX9dtkr1PbhKW42AMvaDQa+jOdpUYysroTF/nuPpAaQMWp7ye+ndlmmthieJrQ==",
|
"integrity": "sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -415,9 +429,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.37.0.tgz",
|
||||||
"integrity": "sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw==",
|
"integrity": "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -4944,9 +4958,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.36.0",
|
"version": "4.37.0",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.36.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.37.0.tgz",
|
||||||
"integrity": "sha512-zwATAXNQxUcd40zgtQG0ZafcRK4g004WtEl7kbuhTWPvf07PsfohXl39jVUvPF7jvNAIkKPQ2XrsDlWuxBd++Q==",
|
"integrity": "sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4960,25 +4974,26 @@
|
|||||||
"npm": ">=8.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-android-arm-eabi": "4.36.0",
|
"@rollup/rollup-android-arm-eabi": "4.37.0",
|
||||||
"@rollup/rollup-android-arm64": "4.36.0",
|
"@rollup/rollup-android-arm64": "4.37.0",
|
||||||
"@rollup/rollup-darwin-arm64": "4.36.0",
|
"@rollup/rollup-darwin-arm64": "4.37.0",
|
||||||
"@rollup/rollup-darwin-x64": "4.36.0",
|
"@rollup/rollup-darwin-x64": "4.37.0",
|
||||||
"@rollup/rollup-freebsd-arm64": "4.36.0",
|
"@rollup/rollup-freebsd-arm64": "4.37.0",
|
||||||
"@rollup/rollup-freebsd-x64": "4.36.0",
|
"@rollup/rollup-freebsd-x64": "4.37.0",
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": "4.36.0",
|
"@rollup/rollup-linux-arm-gnueabihf": "4.37.0",
|
||||||
"@rollup/rollup-linux-arm-musleabihf": "4.36.0",
|
"@rollup/rollup-linux-arm-musleabihf": "4.37.0",
|
||||||
"@rollup/rollup-linux-arm64-gnu": "4.36.0",
|
"@rollup/rollup-linux-arm64-gnu": "4.37.0",
|
||||||
"@rollup/rollup-linux-arm64-musl": "4.36.0",
|
"@rollup/rollup-linux-arm64-musl": "4.37.0",
|
||||||
"@rollup/rollup-linux-loongarch64-gnu": "4.36.0",
|
"@rollup/rollup-linux-loongarch64-gnu": "4.37.0",
|
||||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.36.0",
|
"@rollup/rollup-linux-powerpc64le-gnu": "4.37.0",
|
||||||
"@rollup/rollup-linux-riscv64-gnu": "4.36.0",
|
"@rollup/rollup-linux-riscv64-gnu": "4.37.0",
|
||||||
"@rollup/rollup-linux-s390x-gnu": "4.36.0",
|
"@rollup/rollup-linux-riscv64-musl": "4.37.0",
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.36.0",
|
"@rollup/rollup-linux-s390x-gnu": "4.37.0",
|
||||||
"@rollup/rollup-linux-x64-musl": "4.36.0",
|
"@rollup/rollup-linux-x64-gnu": "4.37.0",
|
||||||
"@rollup/rollup-win32-arm64-msvc": "4.36.0",
|
"@rollup/rollup-linux-x64-musl": "4.37.0",
|
||||||
"@rollup/rollup-win32-ia32-msvc": "4.36.0",
|
"@rollup/rollup-win32-arm64-msvc": "4.37.0",
|
||||||
"@rollup/rollup-win32-x64-msvc": "4.36.0",
|
"@rollup/rollup-win32-ia32-msvc": "4.37.0",
|
||||||
|
"@rollup/rollup-win32-x64-msvc": "4.37.0",
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -194,6 +194,8 @@ export type CommandMappings = {
|
|||||||
showPasswordNotSet: CommandData;
|
showPasswordNotSet: CommandData;
|
||||||
showProtectedSessionPasswordDialog: CommandData;
|
showProtectedSessionPasswordDialog: CommandData;
|
||||||
showUploadAttachmentsDialog: CommandData & { noteId: string };
|
showUploadAttachmentsDialog: CommandData & { noteId: string };
|
||||||
|
showIncludeNoteDialog: CommandData & { textTypeWidget: EditableTextTypeWidget };
|
||||||
|
showAddLinkDialog: CommandData & { textTypeWidget: EditableTextTypeWidget, text: string };
|
||||||
closeProtectedSessionPasswordDialog: CommandData;
|
closeProtectedSessionPasswordDialog: CommandData;
|
||||||
copyImageReferenceToClipboard: CommandData;
|
copyImageReferenceToClipboard: CommandData;
|
||||||
copyImageToClipboard: CommandData;
|
copyImageToClipboard: CommandData;
|
||||||
@@ -342,9 +344,8 @@ type EventMappings = {
|
|||||||
noteContextRemoved: {
|
noteContextRemoved: {
|
||||||
ntxIds: string[];
|
ntxIds: string[];
|
||||||
};
|
};
|
||||||
exportSvg: {
|
exportSvg: { ntxId: string | null | undefined; };
|
||||||
ntxId: string | null | undefined;
|
exportPng: { ntxId: string | null | undefined; };
|
||||||
};
|
|
||||||
geoMapCreateChildNote: {
|
geoMapCreateChildNote: {
|
||||||
ntxId: string | null | undefined; // TODO: deduplicate ntxId
|
ntxId: string | null | undefined; // TODO: deduplicate ntxId
|
||||||
};
|
};
|
||||||
@@ -365,6 +366,9 @@ type EventMappings = {
|
|||||||
textTypeWidget: EditableTextTypeWidget;
|
textTypeWidget: EditableTextTypeWidget;
|
||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
|
showIncludeDialog: {
|
||||||
|
textTypeWidget: EditableTextTypeWidget;
|
||||||
|
};
|
||||||
openBulkActionsDialog: {
|
openBulkActionsDialog: {
|
||||||
selectedOrActiveNoteIds: string[];
|
selectedOrActiveNoteIds: string[];
|
||||||
};
|
};
|
||||||
@@ -400,7 +404,7 @@ type FilterByValueType<T, ValueType> = { [K in keyof T]: T[K] extends ValueType
|
|||||||
*/
|
*/
|
||||||
export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMappings, FilterByValueType<CommandMappings, T>>;
|
export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMappings, FilterByValueType<CommandMappings, T>>;
|
||||||
|
|
||||||
class AppContext extends Component {
|
export class AppContext extends Component {
|
||||||
isMainWindow: boolean;
|
isMainWindow: boolean;
|
||||||
components: Component[];
|
components: Component[];
|
||||||
beforeUnloadListeners: WeakRef<BeforeUploadListener>[];
|
beforeUnloadListeners: WeakRef<BeforeUploadListener>[];
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export interface SetNoteOpts {
|
|||||||
viewScope?: ViewScope;
|
viewScope?: ViewScope;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetTextEditorCallback = () => void;
|
export type GetTextEditorCallback = (editor: TextEditor) => void;
|
||||||
|
|
||||||
class NoteContext extends Component implements EventListener<"entitiesReloaded"> {
|
class NoteContext extends Component implements EventListener<"entitiesReloaded"> {
|
||||||
ntxId: string | null;
|
ntxId: string | null;
|
||||||
|
|||||||
@@ -76,6 +76,8 @@
|
|||||||
the <code>#monthPattern</code> attribute, much like <code>#datePattern</code>.
|
the <code>#monthPattern</code> attribute, much like <code>#datePattern</code>.
|
||||||
The options are:</p>
|
The options are:</p>
|
||||||
<ul>
|
<ul>
|
||||||
|
<li><code>{isoMonth}</code> results in an ISO 8061 formatted month (e.g.
|
||||||
|
"2025-03" for March 2025)</li>
|
||||||
<li><code>{monthNumberPadded}</code> results in a number like <code>09</code> for
|
<li><code>{monthNumberPadded}</code> results in a number like <code>09</code> for
|
||||||
September, and <code>11</code> for November</li>
|
September, and <code>11</code> for November</li>
|
||||||
<li><code>{month}</code> results in the full month name (e.g. <code>September</code> or <code>October</code>)</li>
|
<li><code>{month}</code> results in the full month name (e.g. <code>September</code> or <code>October</code>)</li>
|
||||||
@@ -98,4 +100,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
16
src/public/app/doc_notes/en/User Guide/style.css
generated
16
src/public/app/doc_notes/en/User Guide/style.css
generated
@@ -17,7 +17,7 @@
|
|||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ck-content .admonition {
|
.admonition {
|
||||||
--accent-color: var(--card-border-color);
|
--accent-color: var(--card-border-color);
|
||||||
border: 1px solid var(--accent-color);
|
border: 1px solid var(--accent-color);
|
||||||
box-shadow: var(--card-box-shadow);
|
box-shadow: var(--card-box-shadow);
|
||||||
@@ -29,19 +29,19 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ck-content .admonition p:last-child {
|
.admonition p:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ck-content .admonition p, h2 {
|
.admonition p, h2 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ck-content .admonition.note { --accent-color: #69c7ff; }
|
.admonition.note { --accent-color: #69c7ff; }
|
||||||
.ck-content .admonition.tip { --accent-color: #40c025; }
|
.admonition.tip { --accent-color: #40c025; }
|
||||||
.ck-content .admonition.important { --accent-color: #9839f7; }
|
.admonition.important { --accent-color: #9839f7; }
|
||||||
.ck-content .admonition.caution { --accent-color: #ff2e2e; }
|
.admonition.caution { --accent-color: #ff2e2e; }
|
||||||
.ck-content .admonition.warning { --accent-color: #e2aa03; }
|
.admonition.warning { --accent-color: #e2aa03; }
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* CKEditor 5 (v41.0.0) content styles.
|
* CKEditor 5 (v41.0.0) content styles.
|
||||||
|
|||||||
@@ -36,11 +36,10 @@ import NoteMapRibbonWidget from "../widgets/ribbon_widgets/note_map.js";
|
|||||||
import NotePathsWidget from "../widgets/ribbon_widgets/note_paths.js";
|
import NotePathsWidget from "../widgets/ribbon_widgets/note_paths.js";
|
||||||
import SimilarNotesWidget from "../widgets/ribbon_widgets/similar_notes.js";
|
import SimilarNotesWidget from "../widgets/ribbon_widgets/similar_notes.js";
|
||||||
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
||||||
import EditButton from "../widgets/buttons/edit_button.js";
|
import EditButton from "../widgets/floating_buttons/edit_button.js";
|
||||||
import EditedNotesWidget from "../widgets/ribbon_widgets/edited_notes.js";
|
import EditedNotesWidget from "../widgets/ribbon_widgets/edited_notes.js";
|
||||||
import ShowTocWidgetButton from "../widgets/buttons/show_toc_widget_button.js";
|
import ShowTocWidgetButton from "../widgets/buttons/show_toc_widget_button.js";
|
||||||
import ShowHighlightsListWidgetButton from "../widgets/buttons/show_highlights_list_widget_button.js";
|
import ShowHighlightsListWidgetButton from "../widgets/buttons/show_highlights_list_widget_button.js";
|
||||||
import MermaidWidget from "../widgets/mermaid.js";
|
|
||||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||||
import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js";
|
import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js";
|
||||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||||
@@ -89,13 +88,21 @@ import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js";
|
|||||||
import ContextualHelpButton from "../widgets/floating_buttons/help_button.js";
|
import ContextualHelpButton from "../widgets/floating_buttons/help_button.js";
|
||||||
import CloseZenButton from "../widgets/close_zen_button.js";
|
import CloseZenButton from "../widgets/close_zen_button.js";
|
||||||
import rightPaneTabManager from "../services/right_pane_tab_manager.js";
|
import rightPaneTabManager from "../services/right_pane_tab_manager.js";
|
||||||
|
import type { AppContext } from "./../components/app_context.js";
|
||||||
|
import type { WidgetsByParent } from "../services/bundle.js";
|
||||||
|
import SwitchSplitOrientationButton from "../widgets/floating_buttons/switch_layout_button.js";
|
||||||
|
import ToggleReadOnlyButton from "../widgets/floating_buttons/toggle_read_only_button.js";
|
||||||
|
import PngExportButton from "../widgets/floating_buttons/png_export_button.js";
|
||||||
|
|
||||||
export default class DesktopLayout {
|
export default class DesktopLayout {
|
||||||
constructor(customWidgets) {
|
|
||||||
|
private customWidgets: WidgetsByParent;
|
||||||
|
|
||||||
|
constructor(customWidgets: WidgetsByParent) {
|
||||||
this.customWidgets = customWidgets;
|
this.customWidgets = customWidgets;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRootWidget(appContext) {
|
getRootWidget(appContext: AppContext) {
|
||||||
appContext.noteTreeWidget = new NoteTreeWidget();
|
appContext.noteTreeWidget = new NoteTreeWidget();
|
||||||
|
|
||||||
// Initialize the right pane tab manager after widget render
|
// Initialize the right pane tab manager after widget render
|
||||||
@@ -209,6 +216,8 @@ export default class DesktopLayout {
|
|||||||
.child(new WatchedFileUpdateStatusWidget())
|
.child(new WatchedFileUpdateStatusWidget())
|
||||||
.child(
|
.child(
|
||||||
new FloatingButtons()
|
new FloatingButtons()
|
||||||
|
.child(new SwitchSplitOrientationButton())
|
||||||
|
.child(new ToggleReadOnlyButton())
|
||||||
.child(new EditButton())
|
.child(new EditButton())
|
||||||
.child(new ShowTocWidgetButton())
|
.child(new ShowTocWidgetButton())
|
||||||
.child(new ShowHighlightsListWidgetButton())
|
.child(new ShowHighlightsListWidgetButton())
|
||||||
@@ -217,11 +226,11 @@ export default class DesktopLayout {
|
|||||||
.child(new GeoMapButtons())
|
.child(new GeoMapButtons())
|
||||||
.child(new CopyImageReferenceButton())
|
.child(new CopyImageReferenceButton())
|
||||||
.child(new SvgExportButton())
|
.child(new SvgExportButton())
|
||||||
|
.child(new PngExportButton())
|
||||||
.child(new BacklinksWidget())
|
.child(new BacklinksWidget())
|
||||||
.child(new ContextualHelpButton())
|
.child(new ContextualHelpButton())
|
||||||
.child(new HideFloatingButtonsButton())
|
.child(new HideFloatingButtonsButton())
|
||||||
)
|
)
|
||||||
.child(new MermaidWidget())
|
|
||||||
.child(
|
.child(
|
||||||
new ScrollingContainer()
|
new ScrollingContainer()
|
||||||
.filling()
|
.filling()
|
||||||
@@ -293,7 +302,7 @@ export default class DesktopLayout {
|
|||||||
.child(new CloseZenButton());
|
.child(new CloseZenButton());
|
||||||
}
|
}
|
||||||
|
|
||||||
#buildLauncherPane(isHorizontal) {
|
#buildLauncherPane(isHorizontal: boolean) {
|
||||||
let launcherPane;
|
let launcherPane;
|
||||||
|
|
||||||
if (isHorizontal) {
|
if (isHorizontal) {
|
||||||
@@ -11,12 +11,11 @@ import ProtectedSessionPasswordDialog from "../widgets/dialogs/protected_session
|
|||||||
import ConfirmDialog from "../widgets/dialogs/confirm.js";
|
import ConfirmDialog from "../widgets/dialogs/confirm.js";
|
||||||
import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js";
|
import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js";
|
||||||
import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
|
import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
|
||||||
import EditButton from "../widgets/buttons/edit_button.js";
|
import EditButton from "../widgets/floating_buttons/edit_button.js";
|
||||||
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
|
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
|
||||||
import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js";
|
import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js";
|
||||||
import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js";
|
import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js";
|
||||||
import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js";
|
import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js";
|
||||||
import MermaidWidget from "../widgets/mermaid.js";
|
|
||||||
import NoteListWidget from "../widgets/note_list.js";
|
import NoteListWidget from "../widgets/note_list.js";
|
||||||
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
||||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
||||||
@@ -131,7 +130,7 @@ export default class MobileLayout {
|
|||||||
.id("mobile-rest-container")
|
.id("mobile-rest-container")
|
||||||
.child(
|
.child(
|
||||||
new SidebarContainer("tree", "column")
|
new SidebarContainer("tree", "column")
|
||||||
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-5 col-md-4 col-lg-3 col-xl-3")
|
.class("d-md-flex d-lg-flex d-xl-flex col-12 col-sm-5 col-md-4 col-lg-3 col-xl-3")
|
||||||
.id("mobile-sidebar-wrapper")
|
.id("mobile-sidebar-wrapper")
|
||||||
.css("max-height", "100%")
|
.css("max-height", "100%")
|
||||||
.css("padding-left", "0")
|
.css("padding-left", "0")
|
||||||
@@ -143,10 +142,6 @@ export default class MobileLayout {
|
|||||||
new ScreenContainer("detail", "column")
|
new ScreenContainer("detail", "column")
|
||||||
.id("detail-container")
|
.id("detail-container")
|
||||||
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-9")
|
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-9")
|
||||||
.css("padding-left", "0")
|
|
||||||
.css("padding-right", "0")
|
|
||||||
.css("max-height", "100%")
|
|
||||||
.css("position", "relative")
|
|
||||||
.child(
|
.child(
|
||||||
new FlexContainer("row")
|
new FlexContainer("row")
|
||||||
.contentSized()
|
.contentSized()
|
||||||
@@ -165,7 +160,6 @@ export default class MobileLayout {
|
|||||||
.child(new BacklinksWidget())
|
.child(new BacklinksWidget())
|
||||||
.child(new HideFloatingButtonsButton())
|
.child(new HideFloatingButtonsButton())
|
||||||
)
|
)
|
||||||
.child(new MermaidWidget())
|
|
||||||
.child(new PromotedAttributesWidget())
|
.child(new PromotedAttributesWidget())
|
||||||
.child(
|
.child(
|
||||||
new ScrollingContainer()
|
new ScrollingContainer()
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import type { CommandNames } from "../components/app_context.js";
|
|
||||||
import keyboardActionService from "../services/keyboard_actions.js";
|
import keyboardActionService from "../services/keyboard_actions.js";
|
||||||
import note_tooltip from "../services/note_tooltip.js";
|
import note_tooltip from "../services/note_tooltip.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
|
|
||||||
interface ContextMenuOptions<T extends CommandNames> {
|
interface ContextMenuOptions<T> {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
orientation?: "left";
|
orientation?: "left";
|
||||||
@@ -17,7 +16,7 @@ interface MenuSeparatorItem {
|
|||||||
title: "----";
|
title: "----";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuCommandItem<T extends CommandNames> {
|
export interface MenuCommandItem<T> {
|
||||||
title: string;
|
title: string;
|
||||||
command?: T;
|
command?: T;
|
||||||
type?: string;
|
type?: string;
|
||||||
@@ -30,8 +29,8 @@ export interface MenuCommandItem<T extends CommandNames> {
|
|||||||
spellingSuggestion?: string;
|
spellingSuggestion?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MenuItem<T extends CommandNames> = MenuCommandItem<T> | MenuSeparatorItem;
|
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem;
|
||||||
export type MenuHandler<T extends CommandNames> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
|
export type MenuHandler<T> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
|
||||||
export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;
|
export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;
|
||||||
|
|
||||||
class ContextMenu {
|
class ContextMenu {
|
||||||
@@ -55,7 +54,7 @@ class ContextMenu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async show<T extends CommandNames>(options: ContextMenuOptions<T>) {
|
async show<T>(options: ContextMenuOptions<T>) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
|
||||||
note_tooltip.dismissAllTooltips();
|
note_tooltip.dismissAllTooltips();
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ interface Token {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Attribute {
|
export interface Attribute {
|
||||||
|
attributeId?: string;
|
||||||
type: AttributeType;
|
type: AttributeType;
|
||||||
name: string;
|
name: string;
|
||||||
isInheritable: boolean;
|
isInheritable?: boolean;
|
||||||
value?: string;
|
value?: string;
|
||||||
startIndex?: number;
|
startIndex?: number;
|
||||||
endIndex?: number;
|
endIndex?: number;
|
||||||
|
|||||||
@@ -23,6 +23,23 @@ async function removeAttributeById(noteId: string, attributeId: string) {
|
|||||||
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
|
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a label identified by its name from the given note, if it exists. Note that the label must be owned, i.e.
|
||||||
|
* it will not remove inherited attributes.
|
||||||
|
*
|
||||||
|
* @param note the note from which to remove the label.
|
||||||
|
* @param labelName the name of the label to remove.
|
||||||
|
* @returns `true` if an attribute was identified and removed, `false` otherwise.
|
||||||
|
*/
|
||||||
|
function removeOwnedLabelByName(note: FNote, labelName: string) {
|
||||||
|
const label = note.getOwnedLabel(labelName);
|
||||||
|
if (label) {
|
||||||
|
removeAttributeById(note.noteId, label.attributeId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the attribute of the given note to the provided value if its truthy, or removes the attribute if the value is falsy.
|
* Sets the attribute of the given note to the provided value if its truthy, or removes the attribute if the value is falsy.
|
||||||
* For an attribute with an empty value, pass an empty string instead.
|
* For an attribute with an empty value, pass an empty string instead.
|
||||||
@@ -90,5 +107,6 @@ export default {
|
|||||||
setLabel,
|
setLabel,
|
||||||
setAttribute,
|
setAttribute,
|
||||||
removeAttributeById,
|
removeAttributeById,
|
||||||
|
removeOwnedLabelByName,
|
||||||
isAffecting
|
isAffecting
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ async function executeStartupBundles() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WidgetsByParent {
|
export class WidgetsByParent {
|
||||||
private byParent: Record<string, Widget[]>;
|
private byParent: Record<string, Widget[]>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
|
|||||||
import { normalizeMimeTypeForCKEditor } from "./mime_type_definitions.js";
|
import { normalizeMimeTypeForCKEditor } from "./mime_type_definitions.js";
|
||||||
import renderDoc from "./doc_renderer.js";
|
import renderDoc from "./doc_renderer.js";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
import type { Mermaid } from "mermaid";
|
||||||
|
|
||||||
let idCounter = 1;
|
let idCounter = 1;
|
||||||
|
|
||||||
@@ -226,7 +227,7 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function renderMermaid(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>) {
|
async function renderMermaid(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>) {
|
||||||
await libraryLoader.requireLibrary(libraryLoader.MERMAID);
|
const mermaid = (await import("mermaid")).default;
|
||||||
|
|
||||||
const blob = await note.getBlob();
|
const blob = await note.getBlob();
|
||||||
const content = blob?.content || "";
|
const content = blob?.content || "";
|
||||||
@@ -236,10 +237,10 @@ async function renderMermaid(note: FNote | FAttachment, $renderedContent: JQuery
|
|||||||
const documentStyle = window.getComputedStyle(document.documentElement);
|
const documentStyle = window.getComputedStyle(document.documentElement);
|
||||||
const mermaidTheme = documentStyle.getPropertyValue("--mermaid-theme");
|
const mermaidTheme = documentStyle.getPropertyValue("--mermaid-theme");
|
||||||
|
|
||||||
mermaid.mermaidAPI.initialize({ startOnLoad: false, theme: mermaidTheme.trim(), securityLevel: "antiscript" });
|
mermaid.mermaidAPI.initialize({ startOnLoad: false, theme: mermaidTheme.trim() as "default", securityLevel: "antiscript" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await loadElkIfNeeded(content);
|
await loadElkIfNeeded(mermaid, content);
|
||||||
const { svg } = await mermaid.mermaidAPI.render("in-mermaid-graph-" + idCounter++, content);
|
const { svg } = await mermaid.mermaidAPI.render("in-mermaid-graph-" + idCounter++, content);
|
||||||
|
|
||||||
$renderedContent.append($(postprocessMermaidSvg(svg)));
|
$renderedContent.append($(postprocessMermaidSvg(svg)));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import appContext from "../components/app_context.js";
|
import appContext from "../components/app_context.js";
|
||||||
import type { ConfirmDialogOptions, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
|
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
|
||||||
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||||
|
|
||||||
async function info(message: string) {
|
async function info(message: string) {
|
||||||
@@ -16,7 +16,7 @@ async function confirm(message: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function confirmDeleteNoteBoxWithNote(title: string) {
|
async function confirmDeleteNoteBoxWithNote(title: string) {
|
||||||
return new Promise((res) => appContext.triggerCommand("showConfirmDeleteNoteBoxWithNoteDialog", { title, callback: res }));
|
return new Promise<ConfirmDialogResult | undefined>((res) => appContext.triggerCommand("showConfirmDeleteNoteBoxWithNoteDialog", { title, callback: res }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function prompt(props: PromptDialogOptions) {
|
async function prompt(props: PromptDialogOptions) {
|
||||||
|
|||||||
@@ -55,4 +55,8 @@ describe("Linter", () => {
|
|||||||
expect(await lint(`module.exports("Hi");`, "application/javascript;env=backend")).toStrictEqual([]);
|
expect(await lint(`module.exports("Hi");`, "application/javascript;env=backend")).toStrictEqual([]);
|
||||||
expect(await lint(`module.exports("Hi");`, "application/javascript;env=frontend")).toStrictEqual([]);
|
expect(await lint(`module.exports("Hi");`, "application/javascript;env=frontend")).toStrictEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("ignores TypeScript file", async () => {
|
||||||
|
expect(await lint("export async function lint(code: string, mimeType: string) {}", "text/typescript-jsx")).toStrictEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ export async function lint(code: string, mimeType: string) {
|
|||||||
module: "readonly"
|
module: "readonly"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Unsupported languages
|
||||||
|
if (mimeType.startsWith("text/typescript")) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom globals
|
||||||
if (mimeType === "application/javascript;env=frontend") {
|
if (mimeType === "application/javascript;env=frontend") {
|
||||||
globals = { ...globals, ...globalDefinitions.jquery };
|
globals = { ...globals, ...globalDefinitions.jquery };
|
||||||
} else if (mimeType === "application/javascript;env=backend") {
|
} else if (mimeType === "application/javascript;env=backend") {
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import utils from "./utils.js";
|
|||||||
import appContext from "../components/app_context.js";
|
import appContext from "../components/app_context.js";
|
||||||
import { t } from "./i18n.js";
|
import { t } from "./i18n.js";
|
||||||
|
|
||||||
interface UploadFilesOptions {
|
type BooleanLike = boolean | "true" | "false";
|
||||||
safeImport?: boolean;
|
|
||||||
shrinkImages: boolean | "true" | "false";
|
export interface UploadFilesOptions {
|
||||||
textImportedAsText?: boolean;
|
safeImport?: BooleanLike;
|
||||||
codeImportedAsCode?: boolean;
|
shrinkImages: BooleanLike;
|
||||||
explodeArchives?: boolean;
|
textImportedAsText?: BooleanLike;
|
||||||
replaceUnderscoresWithSpaces?: boolean;
|
codeImportedAsCode?: BooleanLike;
|
||||||
|
explodeArchives?: BooleanLike;
|
||||||
|
replaceUnderscoresWithSpaces?: BooleanLike;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadFiles(entityType: string, parentNoteId: string, files: string[] | File[], options: UploadFilesOptions) {
|
export async function uploadFiles(entityType: string, parentNoteId: string, files: string[] | File[], options: UploadFilesOptions) {
|
||||||
|
|||||||
@@ -42,11 +42,6 @@ const CODE_MIRROR: Library = {
|
|||||||
css: ["node_modules/codemirror/lib/codemirror.css", "node_modules/codemirror/addon/lint/lint.css"]
|
css: ["node_modules/codemirror/lib/codemirror.css", "node_modules/codemirror/addon/lint/lint.css"]
|
||||||
};
|
};
|
||||||
|
|
||||||
const RELATION_MAP: Library = {
|
|
||||||
js: ["node_modules/jsplumb/dist/js/jsplumb.min.js", "node_modules/panzoom/dist/panzoom.min.js"],
|
|
||||||
css: ["stylesheets/relation_map.css"]
|
|
||||||
};
|
|
||||||
|
|
||||||
const CALENDAR_WIDGET: Library = {
|
const CALENDAR_WIDGET: Library = {
|
||||||
css: ["stylesheets/calendar.css"]
|
css: ["stylesheets/calendar.css"]
|
||||||
};
|
};
|
||||||
@@ -60,10 +55,6 @@ const WHEEL_ZOOM: Library = {
|
|||||||
js: ["node_modules/vanilla-js-wheel-zoom/dist/wheel-zoom.min.js"]
|
js: ["node_modules/vanilla-js-wheel-zoom/dist/wheel-zoom.min.js"]
|
||||||
};
|
};
|
||||||
|
|
||||||
const MERMAID: Library = {
|
|
||||||
js: ["node_modules/mermaid/dist/mermaid.min.js"]
|
|
||||||
};
|
|
||||||
|
|
||||||
const MARKJS: Library = {
|
const MARKJS: Library = {
|
||||||
js: ["node_modules/mark.js/dist/jquery.mark.es6.min.js"]
|
js: ["node_modules/mark.js/dist/jquery.mark.es6.min.js"]
|
||||||
};
|
};
|
||||||
@@ -183,11 +174,9 @@ export default {
|
|||||||
loadHighlightingTheme,
|
loadHighlightingTheme,
|
||||||
CKEDITOR,
|
CKEDITOR,
|
||||||
CODE_MIRROR,
|
CODE_MIRROR,
|
||||||
RELATION_MAP,
|
|
||||||
CALENDAR_WIDGET,
|
CALENDAR_WIDGET,
|
||||||
KATEX,
|
KATEX,
|
||||||
WHEEL_ZOOM,
|
WHEEL_ZOOM,
|
||||||
MERMAID,
|
|
||||||
MARKJS,
|
MARKJS,
|
||||||
HIGHLIGHT_JS,
|
HIGHLIGHT_JS,
|
||||||
LEAFLET
|
LEAFLET
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToLink(evt: MouseEvent | JQuery.ClickEvent) {
|
function goToLink(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent) {
|
||||||
const $link = $(evt.target as any).closest("a,.block-link");
|
const $link = $(evt.target as any).closest("a,.block-link");
|
||||||
const hrefLink = $link.attr("href") || $link.attr("data-href");
|
const hrefLink = $link.attr("href") || $link.attr("data-href");
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,26 @@
|
|||||||
|
import type { MermaidConfig } from "mermaid";
|
||||||
|
import type { Mermaid } from "mermaid";
|
||||||
|
|
||||||
let elkLoaded = false;
|
let elkLoaded = false;
|
||||||
|
|
||||||
|
export function getMermaidConfig(): MermaidConfig {
|
||||||
|
const documentStyle = window.getComputedStyle(document.documentElement);
|
||||||
|
const mermaidTheme = documentStyle.getPropertyValue("--mermaid-theme") as "default";
|
||||||
|
|
||||||
|
return {
|
||||||
|
theme: mermaidTheme.trim() as "default",
|
||||||
|
securityLevel: "antiscript",
|
||||||
|
flowchart: { useMaxWidth: false },
|
||||||
|
sequence: { useMaxWidth: false },
|
||||||
|
gantt: { useMaxWidth: false },
|
||||||
|
class: { useMaxWidth: false },
|
||||||
|
state: { useMaxWidth: false },
|
||||||
|
pie: { useMaxWidth: true },
|
||||||
|
journey: { useMaxWidth: false },
|
||||||
|
gitGraph: { useMaxWidth: false }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether the ELK extension of Mermaid.js needs to be loaded (which is a relatively large library), based on the
|
* Determines whether the ELK extension of Mermaid.js needs to be loaded (which is a relatively large library), based on the
|
||||||
* front-matter of the diagram and loads the library if needed.
|
* front-matter of the diagram and loads the library if needed.
|
||||||
@@ -9,7 +30,7 @@ let elkLoaded = false;
|
|||||||
*
|
*
|
||||||
* @param mermaidContent the plain text of the mermaid diagram, potentially including a frontmatter.
|
* @param mermaidContent the plain text of the mermaid diagram, potentially including a frontmatter.
|
||||||
*/
|
*/
|
||||||
export async function loadElkIfNeeded(mermaidContent: string) {
|
export async function loadElkIfNeeded(mermaid: Mermaid, mermaidContent: string) {
|
||||||
if (elkLoaded) {
|
if (elkLoaded) {
|
||||||
// Exit immediately since the ELK library is already loaded.
|
// Exit immediately since the ELK library is already loaded.
|
||||||
return;
|
return;
|
||||||
@@ -18,7 +39,7 @@ export async function loadElkIfNeeded(mermaidContent: string) {
|
|||||||
const parsedContent = await mermaid.parse(mermaidContent, {
|
const parsedContent = await mermaid.parse(mermaidContent, {
|
||||||
suppressErrors: true
|
suppressErrors: true
|
||||||
});
|
});
|
||||||
if (parsedContent?.config?.layout === "elk") {
|
if (parsedContent && parsedContent.config?.layout === "elk") {
|
||||||
elkLoaded = true;
|
elkLoaded = true;
|
||||||
mermaid.registerLayoutLoaders((await import("@mermaid-js/layout-elk")).default);
|
mermaid.registerLayoutLoaders((await import("@mermaid-js/layout-elk")).default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "url";
|
type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url";
|
||||||
type Multiplicity = "single" | "multi";
|
type Multiplicity = "single" | "multi";
|
||||||
|
|
||||||
interface DefinitionObject {
|
interface DefinitionObject {
|
||||||
|
|||||||
@@ -609,9 +609,20 @@ function createImageSrcUrl(note: { noteId: string; title: string }) {
|
|||||||
*/
|
*/
|
||||||
function downloadSvg(nameWithoutExtension: string, svgContent: string) {
|
function downloadSvg(nameWithoutExtension: string, svgContent: string) {
|
||||||
const filename = `${nameWithoutExtension}.svg`;
|
const filename = `${nameWithoutExtension}.svg`;
|
||||||
|
const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`;
|
||||||
|
triggerDownload(filename, dataUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads the given data URL on the client device, with a custom file name.
|
||||||
|
*
|
||||||
|
* @param fileName the name to give the downloaded file.
|
||||||
|
* @param dataUrl the data URI to download.
|
||||||
|
*/
|
||||||
|
function triggerDownload(fileName: string, dataUrl: string) {
|
||||||
const element = document.createElement("a");
|
const element = document.createElement("a");
|
||||||
element.setAttribute("href", `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`);
|
element.setAttribute("href", dataUrl);
|
||||||
element.setAttribute("download", filename);
|
element.setAttribute("download", fileName);
|
||||||
|
|
||||||
element.style.display = "none";
|
element.style.display = "none";
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
@@ -621,6 +632,56 @@ function downloadSvg(nameWithoutExtension: string, svgContent: string) {
|
|||||||
document.body.removeChild(element);
|
document.body.removeChild(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a string representation of an SVG, renders the SVG to PNG and triggers a download of the file on the client device.
|
||||||
|
*
|
||||||
|
* Note that the SVG must specify its width and height as attributes in order for it to be rendered.
|
||||||
|
*
|
||||||
|
* @param nameWithoutExtension the name of the file. The .png suffix is automatically added to it.
|
||||||
|
* @param svgContent the content of the SVG file download.
|
||||||
|
* @returns `true` if the operation succeeded (width/height present), or `false` if the download was not triggered.
|
||||||
|
*/
|
||||||
|
function downloadSvgAsPng(nameWithoutExtension: string, svgContent: string) {
|
||||||
|
const mime = "image/svg+xml";
|
||||||
|
|
||||||
|
// First, we need to determine the width and the height from the input SVG.
|
||||||
|
const svgDocument = (new DOMParser()).parseFromString(svgContent, mime);
|
||||||
|
const width = svgDocument.documentElement?.getAttribute("width");
|
||||||
|
const height = svgDocument.documentElement?.getAttribute("height");
|
||||||
|
|
||||||
|
if (!width || !height) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the image to a blob.
|
||||||
|
const svgBlob = new Blob([ svgContent ], {
|
||||||
|
type: mime
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create an image element and load the SVG.
|
||||||
|
const imageEl = new Image();
|
||||||
|
imageEl.width = parseFloat(width);
|
||||||
|
imageEl.height = parseFloat(height);
|
||||||
|
imageEl.src = URL.createObjectURL(svgBlob);
|
||||||
|
imageEl.onload = () => {
|
||||||
|
// Draw the image with a canvas.
|
||||||
|
const canvasEl = document.createElement("canvas");
|
||||||
|
canvasEl.width = imageEl.width;
|
||||||
|
canvasEl.height = imageEl.height;
|
||||||
|
document.body.appendChild(canvasEl);
|
||||||
|
|
||||||
|
const ctx = canvasEl.getContext("2d");
|
||||||
|
ctx?.drawImage(imageEl, 0, 0);
|
||||||
|
URL.revokeObjectURL(imageEl.src);
|
||||||
|
|
||||||
|
const imgUri = canvasEl.toDataURL("image/png")
|
||||||
|
triggerDownload(`${nameWithoutExtension}.png`, imgUri);
|
||||||
|
document.body.removeChild(canvasEl);
|
||||||
|
};
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compares two semantic version strings.
|
* Compares two semantic version strings.
|
||||||
* Returns:
|
* Returns:
|
||||||
@@ -719,6 +780,7 @@ export default {
|
|||||||
copyHtmlToClipboard,
|
copyHtmlToClipboard,
|
||||||
createImageSrcUrl,
|
createImageSrcUrl,
|
||||||
downloadSvg,
|
downloadSvg,
|
||||||
|
downloadSvgAsPng,
|
||||||
compareVersions,
|
compareVersions,
|
||||||
isUpdateAvailable,
|
isUpdateAvailable,
|
||||||
isLaunchBarConfig
|
isLaunchBarConfig
|
||||||
|
|||||||
306
src/public/app/types.d.ts
vendored
306
src/public/app/types.d.ts
vendored
@@ -7,6 +7,8 @@ import server from "./services/server.ts";
|
|||||||
import library_loader, { Library } from "./services/library_loader.ts";
|
import library_loader, { Library } from "./services/library_loader.ts";
|
||||||
import type { init } from "i18next";
|
import type { init } from "i18next";
|
||||||
import type { lint } from "./services/eslint.ts";
|
import type { lint } from "./services/eslint.ts";
|
||||||
|
import type { RelationType } from "./widgets/type_widgets/relation_map.ts";
|
||||||
|
import type { Mermaid } from "mermaid";
|
||||||
|
|
||||||
interface ElectronProcess {
|
interface ElectronProcess {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -46,6 +48,7 @@ interface CustomGlobals {
|
|||||||
TRILIUM_SAFE_MODE: boolean;
|
TRILIUM_SAFE_MODE: boolean;
|
||||||
platform?: typeof process.platform;
|
platform?: typeof process.platform;
|
||||||
linter: typeof lint;
|
linter: typeof lint;
|
||||||
|
hasNativeTitleBar: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RequireMethod = (moduleName: string) => any;
|
type RequireMethod = (moduleName: string) => any;
|
||||||
@@ -74,7 +77,7 @@ declare global {
|
|||||||
|
|
||||||
interface AutoCompleteArg {
|
interface AutoCompleteArg {
|
||||||
displayKey: "name" | "value" | "notePathTitle";
|
displayKey: "name" | "value" | "notePathTitle";
|
||||||
cache: boolean;
|
cache?: boolean;
|
||||||
source: (term: string, cb: AutoCompleteCallback) => void,
|
source: (term: string, cb: AutoCompleteCallback) => void,
|
||||||
templates?: {
|
templates?: {
|
||||||
suggestion: (suggestion: Suggestion) => string | undefined
|
suggestion: (suggestion: Suggestion) => string | undefined
|
||||||
@@ -95,7 +98,11 @@ declare global {
|
|||||||
className: string;
|
className: string;
|
||||||
separateWordSearch: boolean;
|
separateWordSearch: boolean;
|
||||||
caseSensitive: boolean;
|
caseSensitive: boolean;
|
||||||
})
|
done?: () => void;
|
||||||
|
});
|
||||||
|
unmark(opts?: {
|
||||||
|
done: () => void;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface JQueryStatic {
|
interface JQueryStatic {
|
||||||
@@ -132,72 +139,76 @@ declare global {
|
|||||||
zoomOnClick: boolean
|
zoomOnClick: boolean
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
interface MermaidApi {
|
interface CKCodeBlockLanguage {
|
||||||
initialize(opts: {
|
language: string;
|
||||||
startOnLoad: boolean,
|
label: string;
|
||||||
theme: string,
|
|
||||||
securityLevel: "antiscript"
|
|
||||||
}): void;
|
|
||||||
render(selector: string, data: string);
|
|
||||||
}
|
}
|
||||||
interface MermaidLoader {
|
|
||||||
|
|
||||||
}
|
interface CKWatchdog {
|
||||||
interface MermaidChartConfig {
|
constructor(editorClass: CKEditorInstance, opts: {
|
||||||
useMaxWidth: boolean;
|
minimumNonErrorTimePeriod: number;
|
||||||
}
|
crashNumberLimit: number,
|
||||||
interface MermaidConfig {
|
saveInterval: number
|
||||||
theme: string;
|
});
|
||||||
securityLevel: "antiscript",
|
on(event: string, callback: () => void);
|
||||||
flow: MermaidChartConfig;
|
state: string;
|
||||||
sequence: MermaidChartConfig;
|
crashes: unknown[];
|
||||||
gantt: MermaidChartConfig;
|
editor: TextEditor;
|
||||||
class: MermaidChartConfig;
|
setCreator(callback: (elementOrData, editorConfig) => void);
|
||||||
state: MermaidChartConfig;
|
create(el: HTMLElement, opts: {
|
||||||
pie: MermaidChartConfig;
|
placeholder: string,
|
||||||
journey: MermaidChartConfig;
|
mention: MentionConfig,
|
||||||
git: MermaidChartConfig;
|
codeBlock: {
|
||||||
}
|
languages: CKCodeBlockLanguage[]
|
||||||
var mermaid: {
|
},
|
||||||
mermaidAPI: MermaidApi;
|
math: {
|
||||||
registerLayoutLoaders(loader: MermaidLoader);
|
engine: string,
|
||||||
init(config: MermaidConfig, el: HTMLElement | JQuery<HTMLElement>);
|
outputType: string,
|
||||||
parse(content: string, opts: {
|
lazyLoad: () => Promise<void>,
|
||||||
suppressErrors: true
|
forceOutputType: boolean,
|
||||||
}): Promise<{
|
enablePreview: boolean
|
||||||
config: {
|
},
|
||||||
layout: string;
|
mermaid: {
|
||||||
|
lazyLoad: () => Promise<Mermaid>,
|
||||||
|
config: MermaidConfig
|
||||||
}
|
}
|
||||||
}>
|
});
|
||||||
};
|
destroy();
|
||||||
|
}
|
||||||
|
|
||||||
var CKEditor: {
|
var CKEditor: {
|
||||||
BalloonEditor: {
|
BalloonEditor: CKEditorInstance;
|
||||||
create(el: HTMLElement, config: {
|
DecoupledEditor: CKEditorInstance;
|
||||||
removePlugins?: string[];
|
EditorWatchdog: typeof CKWatchdog;
|
||||||
toolbar: {
|
|
||||||
items: any[];
|
|
||||||
},
|
|
||||||
placeholder: string;
|
|
||||||
mention: MentionConfig
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var CKEditorInspector: {
|
||||||
|
attach(editor: TextEditor);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CodeMirrorOpts {
|
||||||
|
value: string;
|
||||||
|
viewportMargin: number;
|
||||||
|
indentUnit: number;
|
||||||
|
matchBrackets: boolean;
|
||||||
|
matchTags: { bothTags: boolean };
|
||||||
|
highlightSelectionMatches: {
|
||||||
|
showToken: boolean;
|
||||||
|
annotateScrollbar: boolean;
|
||||||
|
};
|
||||||
|
lineNumbers: boolean;
|
||||||
|
lineWrapping: boolean;
|
||||||
|
keyMap?: "vim" | "default";
|
||||||
|
lint?: boolean;
|
||||||
|
gutters?: string[];
|
||||||
|
tabindex?: number;
|
||||||
|
dragDrop?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
readOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
var CodeMirror: {
|
var CodeMirror: {
|
||||||
(el: HTMLElement, opts: {
|
(el: HTMLElement, opts: CodeMirrorOpts): CodeMirrorInstance;
|
||||||
value: string;
|
|
||||||
viewportMargin: number;
|
|
||||||
indentUnit: number;
|
|
||||||
matchBrackets: boolean;
|
|
||||||
matchTags: { bothTags: boolean };
|
|
||||||
highlightSelectionMatches: {
|
|
||||||
showToken: boolean;
|
|
||||||
annotateScrollbar: boolean;
|
|
||||||
};
|
|
||||||
lineNumbers: boolean;
|
|
||||||
lineWrapping: boolean;
|
|
||||||
}): CodeMirrorInstance;
|
|
||||||
keyMap: {
|
keyMap: {
|
||||||
default: Record<string, string>;
|
default: Record<string, string>;
|
||||||
};
|
};
|
||||||
@@ -205,6 +216,8 @@ declare global {
|
|||||||
modeInfo: ModeInfo[];
|
modeInfo: ModeInfo[];
|
||||||
findModeByMIME(mime: string): ModeInfo;
|
findModeByMIME(mime: string): ModeInfo;
|
||||||
autoLoadMode(instance: CodeMirrorInstance, mode: string)
|
autoLoadMode(instance: CodeMirrorInstance, mode: string)
|
||||||
|
registerHelper(type: string, filter: string | null, callback: (text: string, options: object) => unknown);
|
||||||
|
Pos(line: number, col: number);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ModeInfo {
|
interface ModeInfo {
|
||||||
@@ -221,9 +234,24 @@ declare global {
|
|||||||
setOption(name: string, value: string);
|
setOption(name: string, value: string);
|
||||||
refresh();
|
refresh();
|
||||||
focus();
|
focus();
|
||||||
|
getCursor(): { line: number, col: number, ch: number };
|
||||||
setCursor(line: number, col: number);
|
setCursor(line: number, col: number);
|
||||||
|
getSelection(): string;
|
||||||
lineCount(): number;
|
lineCount(): number;
|
||||||
on(event: string, callback: () => void);
|
on(event: string, callback: () => void);
|
||||||
|
operation(callback: () => void);
|
||||||
|
scrollIntoView(pos: number);
|
||||||
|
doc: {
|
||||||
|
getValue(): string;
|
||||||
|
markText(
|
||||||
|
from: { line: number, ch: number } | number,
|
||||||
|
to: { line: number, ch: number } | number,
|
||||||
|
opts: {
|
||||||
|
className: string
|
||||||
|
});
|
||||||
|
setSelection(from: number, to: number);
|
||||||
|
replaceRange(text: string, from: number, to: number);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var katex: {
|
var katex: {
|
||||||
@@ -232,11 +260,22 @@ declare global {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type TextEditorElement = {};
|
interface Range {
|
||||||
|
toJSON(): object;
|
||||||
|
getItems(): TextNode[];
|
||||||
|
}
|
||||||
interface Writer {
|
interface Writer {
|
||||||
setAttribute(name: string, value: string, el: TextEditorElement);
|
setAttribute(name: string, value: string, el: CKNode);
|
||||||
createPositionAt(el: TextEditorElement, opt?: "end");
|
createPositionAt(el: CKNode, opt?: "end" | number);
|
||||||
setSelection(pos: number);
|
setSelection(pos: number, pos?: number);
|
||||||
|
insertText(text: string, opts: Record<string, unknown> | undefined | TextPosition, position?: TextPosition);
|
||||||
|
addMarker(name: string, opts: {
|
||||||
|
range: Range;
|
||||||
|
usingOperation: boolean;
|
||||||
|
});
|
||||||
|
removeMarker(name: string);
|
||||||
|
createRange(start: number, end: number): Range;
|
||||||
|
createElement(type: string, opts: Record<string, string | null | undefined>);
|
||||||
}
|
}
|
||||||
interface TextNode {
|
interface TextNode {
|
||||||
previousSibling?: TextNode;
|
previousSibling?: TextNode;
|
||||||
@@ -252,29 +291,98 @@ declare global {
|
|||||||
interface TextPosition {
|
interface TextPosition {
|
||||||
textNode: TextNode;
|
textNode: TextNode;
|
||||||
offset: number;
|
offset: number;
|
||||||
|
compareWith(pos: TextPosition): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TextRange {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Marker {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CKNode {
|
||||||
|
name: string;
|
||||||
|
childCount: number;
|
||||||
|
isEmpty: boolean;
|
||||||
|
toJSON(): object;
|
||||||
|
is(type: string, name?: string);
|
||||||
|
getAttribute(name: string): string;
|
||||||
|
getChild(index: number): CKNode;
|
||||||
|
data: string;
|
||||||
|
startOffset: number;
|
||||||
|
root: {
|
||||||
|
document: {
|
||||||
|
model: {
|
||||||
|
createRangeIn(el: CKNode): TextRange;
|
||||||
|
markers: {
|
||||||
|
getMarkersIntersectingRange(range: TextRange): Marker[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CKEvent {
|
||||||
|
stop(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginEventData {
|
||||||
|
title: string;
|
||||||
|
message: {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface TextEditor {
|
interface TextEditor {
|
||||||
|
create(el: HTMLElement, config: {
|
||||||
|
removePlugins?: string[];
|
||||||
|
toolbar: {
|
||||||
|
items: any[];
|
||||||
|
},
|
||||||
|
placeholder: string;
|
||||||
|
mention: MentionConfig
|
||||||
|
});
|
||||||
|
enableReadOnlyMode(reason: string);
|
||||||
model: {
|
model: {
|
||||||
document: {
|
document: {
|
||||||
on(event: string, cb: () => void);
|
on(event: string, cb: () => void);
|
||||||
getRoot(): TextEditorElement;
|
getRoot(): CKNode;
|
||||||
|
registerPostFixer(callback: (writer: Writer) => boolean);
|
||||||
selection: {
|
selection: {
|
||||||
getFirstPosition(): undefined | TextPosition;
|
getFirstPosition(): undefined | TextPosition;
|
||||||
|
getLastPosition(): undefined | TextPosition;
|
||||||
|
getSelectedElement(): CKNode;
|
||||||
|
hasAttribute(attribute: string): boolean;
|
||||||
|
getAttribute(attribute: string): string;
|
||||||
|
getFirstRange(): Range;
|
||||||
|
isCollapsed: boolean;
|
||||||
|
};
|
||||||
|
differ: {
|
||||||
|
getChanges(): {
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
position: {
|
||||||
|
nodeAfter: CKNode;
|
||||||
|
parent: CKNode;
|
||||||
|
toJSON(): Object;
|
||||||
|
}
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
insertContent(modelFragment: any, selection?: any);
|
||||||
change(cb: (writer: Writer) => void)
|
change(cb: (writer: Writer) => void)
|
||||||
},
|
},
|
||||||
editing: {
|
editing: {
|
||||||
view: {
|
view: {
|
||||||
document: {
|
document: {
|
||||||
on(event: string, cb: (event: {
|
on(event: string, cb: (event: CKEvent, data: {
|
||||||
stop();
|
|
||||||
}, data: {
|
|
||||||
preventDefault();
|
preventDefault();
|
||||||
}) => void, opts?: {
|
}) => void, opts?: {
|
||||||
priority: "high"
|
priority: "high"
|
||||||
});
|
});
|
||||||
getRoot(): TextEditorElement
|
getRoot(): CKNode
|
||||||
},
|
},
|
||||||
domRoots: {
|
domRoots: {
|
||||||
values: () => {
|
values: () => {
|
||||||
@@ -283,16 +391,55 @@ declare global {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
change(cb: (writer: Writer) => void)
|
change(cb: (writer: Writer) => void);
|
||||||
|
scrollToTheSelection(): void;
|
||||||
|
focus(): void;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
plugins: {
|
||||||
|
get(command: string)
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
processor: {
|
||||||
|
toView(html: string);
|
||||||
|
};
|
||||||
|
toModel(viewFeragment: any);
|
||||||
|
},
|
||||||
|
conversion: {
|
||||||
|
for(filter: string): {
|
||||||
|
markerToHighlight(data: {
|
||||||
|
model: string;
|
||||||
|
view: (data: {
|
||||||
|
markerName: string;
|
||||||
|
}) => void;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
getData(): string;
|
getData(): string;
|
||||||
setData(data: string): void;
|
setData(data: string): void;
|
||||||
getSelectedHtml(): string;
|
getSelectedHtml(): string;
|
||||||
removeSelection(): void;
|
removeSelection(): void;
|
||||||
|
execute<T>(action: string, ...args: unknown[]): T;
|
||||||
|
focus(): void;
|
||||||
sourceElement: HTMLElement;
|
sourceElement: HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface EditingState {
|
||||||
|
highlightedResult: string;
|
||||||
|
results: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CKFindResult {
|
||||||
|
results: {
|
||||||
|
get(number): {
|
||||||
|
marker: {
|
||||||
|
getStart(): TextPosition;
|
||||||
|
getRange(): number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} & [];
|
||||||
|
}
|
||||||
|
|
||||||
interface MentionItem {
|
interface MentionItem {
|
||||||
action?: string;
|
action?: string;
|
||||||
noteTitle?: string;
|
noteTitle?: string;
|
||||||
@@ -313,4 +460,23 @@ declare global {
|
|||||||
minimumCharacters: number;
|
minimumCharacters: number;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Panzoom
|
||||||
|
*/
|
||||||
|
|
||||||
|
function panzoom(el: HTMLElement, opts: {
|
||||||
|
maxZoom: number,
|
||||||
|
minZoom: number,
|
||||||
|
smoothScroll: false,
|
||||||
|
filterKey: (e: { altKey: boolean }, dx: number, dy: number, dz: number) => void;
|
||||||
|
});
|
||||||
|
|
||||||
|
interface PanZoom {
|
||||||
|
zoomTo(x: number, y: number, scale: number);
|
||||||
|
moveTo(x: number, y: number);
|
||||||
|
on(event: string, callback: () => void);
|
||||||
|
getTransform(): unknown;
|
||||||
|
dispose(): void;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ const ATTR_HELP: Record<string, Record<string, string>> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface AttributeDetailOpts {
|
interface AttributeDetailOpts {
|
||||||
allAttributes: Attribute[];
|
allAttributes?: Attribute[];
|
||||||
attribute: Attribute;
|
attribute: Attribute;
|
||||||
isOwned: boolean;
|
isOwned: boolean;
|
||||||
x: number;
|
x: number;
|
||||||
@@ -338,7 +338,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
private relatedNotesSpacedUpdate!: SpacedUpdate;
|
private relatedNotesSpacedUpdate!: SpacedUpdate;
|
||||||
private attribute!: Attribute;
|
private attribute!: Attribute;
|
||||||
private allAttributes!: Attribute[];
|
private allAttributes?: Attribute[];
|
||||||
private attrType!: ReturnType<AttributeDetailWidget["getAttrType"]>;
|
private attrType!: ReturnType<AttributeDetailWidget["getAttrType"]>;
|
||||||
|
|
||||||
async refresh() {
|
async refresh() {
|
||||||
@@ -434,7 +434,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
this.attribute.value = pathChunks[pathChunks.length - 1]; // noteId
|
this.attribute.value = pathChunks[pathChunks.length - 1]; // noteId
|
||||||
|
|
||||||
this.triggerCommand("updateAttributeList", { attributes: this.allAttributes });
|
this.triggerCommand("updateAttributeList", { attributes: this.allAttributes ?? [] });
|
||||||
this.updateRelatedNotes();
|
this.updateRelatedNotes();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -454,7 +454,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
|||||||
this.$deleteButton = this.$widget.find(".attr-delete-button");
|
this.$deleteButton = this.$widget.find(".attr-delete-button");
|
||||||
this.$deleteButton.on("click", async () => {
|
this.$deleteButton.on("click", async () => {
|
||||||
await this.triggerCommand("updateAttributeList", {
|
await this.triggerCommand("updateAttributeList", {
|
||||||
attributes: this.allAttributes.filter((attr) => attr !== this.attribute)
|
attributes: (this.allAttributes || []).filter((attr) => attr !== this.attribute)
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.triggerCommand("saveAttributes");
|
await this.triggerCommand("saveAttributes");
|
||||||
@@ -714,7 +714,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
|||||||
this.attribute.value = String(this.$inputValue.val());
|
this.attribute.value = String(this.$inputValue.val());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.triggerCommand("updateAttributeList", { attributes: this.allAttributes });
|
this.triggerCommand("updateAttributeList", { attributes: this.allAttributes ?? [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
buildDefinitionValue() {
|
buildDefinitionValue() {
|
||||||
|
|||||||
@@ -206,7 +206,8 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
|
|||||||
doRender() {}
|
doRender() {}
|
||||||
|
|
||||||
toggleInt(show: boolean | null | undefined) {
|
toggleInt(show: boolean | null | undefined) {
|
||||||
this.$widget.toggleClass("hidden-int", !show);
|
this.$widget.toggleClass("hidden-int", !show)
|
||||||
|
.toggleClass("visible", !!show);
|
||||||
}
|
}
|
||||||
|
|
||||||
isHiddenInt() {
|
isHiddenInt() {
|
||||||
@@ -214,7 +215,8 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleExt(show: boolean) {
|
toggleExt(show: boolean) {
|
||||||
this.$widget.toggleClass("hidden-ext", !show);
|
this.$widget.toggleClass("hidden-ext", !show)
|
||||||
|
.toggleClass("visible", !!show);
|
||||||
}
|
}
|
||||||
|
|
||||||
isHiddenExt() {
|
isHiddenExt() {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default class ShowHighlightsListWidgetButton extends OnClickButtonWidget
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.icon("bx-highlight")
|
this.icon("bx-bookmarks")
|
||||||
.title(t("show_highlights_list_widget_button.show_highlights_list"))
|
.title(t("show_highlights_list_widget_button.show_highlights_list"))
|
||||||
.titlePlacement("bottom")
|
.titlePlacement("bottom")
|
||||||
.onClick(() => {
|
.onClick(() => {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default class ShowTocWidgetButton extends OnClickButtonWidget {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.icon("bx-objects-horizontal-left")
|
this.icon("bx-tn-toc")
|
||||||
.title(t("show_toc_widget_button.show_toc"))
|
.title(t("show_toc_widget_button.show_toc"))
|
||||||
.titlePlacement("bottom")
|
.titlePlacement("bottom")
|
||||||
.onClick(() => {
|
.onClick(() => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type CommandButtonWidget from "../buttons/command_button.js";
|
|||||||
import type FNote from "../../entities/fnote.js";
|
import type FNote from "../../entities/fnote.js";
|
||||||
import type { NoteType } from "../../entities/fnote.js";
|
import type { NoteType } from "../../entities/fnote.js";
|
||||||
import type { EventData, EventNames } from "../../components/app_context.js";
|
import type { EventData, EventNames } from "../../components/app_context.js";
|
||||||
|
import type NoteActionsWidget from "../buttons/note_actions.js";
|
||||||
|
|
||||||
const TPL = `
|
const TPL = `
|
||||||
<div class="ribbon-container">
|
<div class="ribbon-container">
|
||||||
@@ -116,13 +117,15 @@ const TPL = `
|
|||||||
<div class="ribbon-body-container"></div>
|
<div class="ribbon-body-container"></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
|
type ButtonWidget = (CommandButtonWidget | NoteActionsWidget);
|
||||||
|
|
||||||
export default class RibbonContainer extends NoteContextAwareWidget {
|
export default class RibbonContainer extends NoteContextAwareWidget {
|
||||||
|
|
||||||
private lastActiveComponentId?: string | null;
|
private lastActiveComponentId?: string | null;
|
||||||
private lastNoteType?: NoteType;
|
private lastNoteType?: NoteType;
|
||||||
|
|
||||||
private ribbonWidgets: NoteContextAwareWidget[];
|
private ribbonWidgets: NoteContextAwareWidget[];
|
||||||
private buttonWidgets: CommandButtonWidget[];
|
private buttonWidgets: ButtonWidget[];
|
||||||
private $tabContainer!: JQuery<HTMLElement>;
|
private $tabContainer!: JQuery<HTMLElement>;
|
||||||
private $buttonContainer!: JQuery<HTMLElement>;
|
private $buttonContainer!: JQuery<HTMLElement>;
|
||||||
private $bodyContainer!: JQuery<HTMLElement>;
|
private $bodyContainer!: JQuery<HTMLElement>;
|
||||||
@@ -148,7 +151,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
button(widget: CommandButtonWidget) {
|
button(widget: ButtonWidget) {
|
||||||
super.child(widget);
|
super.child(widget);
|
||||||
|
|
||||||
this.buttonWidgets.push(widget);
|
this.buttonWidgets.push(widget);
|
||||||
|
|||||||
@@ -80,13 +80,13 @@ export default class AddLinkDialog extends BasicWidget {
|
|||||||
if (this.$autoComplete.getSelectedNotePath()) {
|
if (this.$autoComplete.getSelectedNotePath()) {
|
||||||
this.$widget.modal("hide");
|
this.$widget.modal("hide");
|
||||||
|
|
||||||
const linkTitle = this.getLinkType() === "reference-link" ? null : this.$linkTitle.val();
|
const linkTitle = this.getLinkType() === "reference-link" ? null : this.$linkTitle.val() as string;
|
||||||
|
|
||||||
this.textTypeWidget?.addLink(this.$autoComplete.getSelectedNotePath()!, linkTitle);
|
this.textTypeWidget?.addLink(this.$autoComplete.getSelectedNotePath()!, linkTitle);
|
||||||
} else if (this.$autoComplete.getSelectedExternalLink()) {
|
} else if (this.$autoComplete.getSelectedExternalLink()) {
|
||||||
this.$widget.modal("hide");
|
this.$widget.modal("hide");
|
||||||
|
|
||||||
this.textTypeWidget?.addLink(this.$autoComplete.getSelectedExternalLink()!, this.$linkTitle.val(), true);
|
this.textTypeWidget?.addLink(this.$autoComplete.getSelectedExternalLink()!, this.$linkTitle.val() as string, true);
|
||||||
} else {
|
} else {
|
||||||
logError("No link to add.");
|
logError("No link to add.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ const TPL = `
|
|||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
export type ConfirmDialogCallback = (val?: false | ConfirmDialogOptions) => void;
|
export type ConfirmDialogResult = false | ConfirmDialogOptions;
|
||||||
|
export type ConfirmDialogCallback = (val?: ConfirmDialogResult) => void;
|
||||||
|
|
||||||
export interface ConfirmDialogOptions {
|
export interface ConfirmDialogOptions {
|
||||||
confirmed: boolean;
|
confirmed: boolean;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import utils, { escapeQuotes } from "../../services/utils.js";
|
import utils, { escapeQuotes } from "../../services/utils.js";
|
||||||
import treeService from "../../services/tree.js";
|
import treeService from "../../services/tree.js";
|
||||||
import importService from "../../services/import.js";
|
import importService, { type UploadFilesOptions } from "../../services/import.js";
|
||||||
import options from "../../services/options.js";
|
import options from "../../services/options.js";
|
||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import { Modal, Tooltip } from "bootstrap";
|
import { Modal, Tooltip } from "bootstrap";
|
||||||
|
import type { EventData } from "../../components/app_context.js";
|
||||||
|
|
||||||
const TPL = `
|
const TPL = `
|
||||||
<div class="import-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
<div class="import-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||||
@@ -79,6 +80,20 @@ const TPL = `
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
export default class ImportDialog extends BasicWidget {
|
export default class ImportDialog extends BasicWidget {
|
||||||
|
|
||||||
|
private parentNoteId: string | null;
|
||||||
|
|
||||||
|
private $form!: JQuery<HTMLElement>;
|
||||||
|
private $noteTitle!: JQuery<HTMLElement>;
|
||||||
|
private $fileUploadInput!: JQuery<HTMLInputElement>;
|
||||||
|
private $importButton!: JQuery<HTMLElement>;
|
||||||
|
private $safeImportCheckbox!: JQuery<HTMLElement>;
|
||||||
|
private $shrinkImagesCheckbox!: JQuery<HTMLElement>;
|
||||||
|
private $textImportedAsTextCheckbox!: JQuery<HTMLElement>;
|
||||||
|
private $codeImportedAsCodeCheckbox!: JQuery<HTMLElement>;
|
||||||
|
private $explodeArchivesCheckbox!: JQuery<HTMLElement>;
|
||||||
|
private $replaceUnderscoresWithSpacesCheckbox!: JQuery<HTMLElement>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@@ -87,7 +102,7 @@ export default class ImportDialog extends BasicWidget {
|
|||||||
|
|
||||||
doRender() {
|
doRender() {
|
||||||
this.$widget = $(TPL);
|
this.$widget = $(TPL);
|
||||||
Modal.getOrCreateInstance(this.$widget);
|
Modal.getOrCreateInstance(this.$widget[0]);
|
||||||
|
|
||||||
this.$form = this.$widget.find(".import-form");
|
this.$form = this.$widget.find(".import-form");
|
||||||
this.$noteTitle = this.$widget.find(".import-note-title");
|
this.$noteTitle = this.$widget.find(".import-note-title");
|
||||||
@@ -104,7 +119,9 @@ export default class ImportDialog extends BasicWidget {
|
|||||||
// disabling so that import is not triggered again.
|
// disabling so that import is not triggered again.
|
||||||
this.$importButton.attr("disabled", "disabled");
|
this.$importButton.attr("disabled", "disabled");
|
||||||
|
|
||||||
this.importIntoNote(this.parentNoteId);
|
if (this.parentNoteId) {
|
||||||
|
this.importIntoNote(this.parentNoteId);
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
@@ -124,7 +141,7 @@ export default class ImportDialog extends BasicWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async showImportDialogEvent({ noteId }) {
|
async showImportDialogEvent({ noteId }: EventData<"showImportDialog">) {
|
||||||
this.parentNoteId = noteId;
|
this.parentNoteId = noteId;
|
||||||
|
|
||||||
this.$fileUploadInput.val("").trigger("change"); // to trigger Import button disabling listener below
|
this.$fileUploadInput.val("").trigger("change"); // to trigger Import button disabling listener below
|
||||||
@@ -141,12 +158,12 @@ export default class ImportDialog extends BasicWidget {
|
|||||||
utils.openDialog(this.$widget);
|
utils.openDialog(this.$widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
async importIntoNote(parentNoteId) {
|
async importIntoNote(parentNoteId: string) {
|
||||||
const files = Array.from(this.$fileUploadInput[0].files); // shallow copy since we're resetting the upload button below
|
const files = Array.from(this.$fileUploadInput[0].files ?? []); // shallow copy since we're resetting the upload button below
|
||||||
|
|
||||||
const boolToString = ($el) => ($el.is(":checked") ? "true" : "false");
|
const boolToString = ($el: JQuery<HTMLElement>) => ($el.is(":checked") ? "true" : "false");
|
||||||
|
|
||||||
const options = {
|
const options: UploadFilesOptions = {
|
||||||
safeImport: boolToString(this.$safeImportCheckbox),
|
safeImport: boolToString(this.$safeImportCheckbox),
|
||||||
shrinkImages: boolToString(this.$shrinkImagesCheckbox),
|
shrinkImages: boolToString(this.$shrinkImagesCheckbox),
|
||||||
textImportedAsText: boolToString(this.$textImportedAsTextCheckbox),
|
textImportedAsText: boolToString(this.$textImportedAsTextCheckbox),
|
||||||
@@ -5,6 +5,8 @@ import utils from "../../services/utils.js";
|
|||||||
import froca from "../../services/froca.js";
|
import froca from "../../services/froca.js";
|
||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import { Modal } from "bootstrap";
|
import { Modal } from "bootstrap";
|
||||||
|
import type { EventData } from "../../components/app_context.js";
|
||||||
|
import type EditableTextTypeWidget from "../type_widgets/editable_text.js";
|
||||||
|
|
||||||
const TPL = `
|
const TPL = `
|
||||||
<div class="include-note-dialog modal mx-auto" tabindex="-1" role="dialog">
|
<div class="include-note-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||||
@@ -53,9 +55,15 @@ const TPL = `
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
export default class IncludeNoteDialog extends BasicWidget {
|
export default class IncludeNoteDialog extends BasicWidget {
|
||||||
|
|
||||||
|
private modal!: bootstrap.Modal;
|
||||||
|
private $form!: JQuery<HTMLElement>;
|
||||||
|
private $autoComplete!: JQuery<HTMLElement>;
|
||||||
|
private textTypeWidget?: EditableTextTypeWidget;
|
||||||
|
|
||||||
doRender() {
|
doRender() {
|
||||||
this.$widget = $(TPL);
|
this.$widget = $(TPL);
|
||||||
this.modal = Modal.getOrCreateInstance(this.$widget);
|
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
|
||||||
this.$form = this.$widget.find(".include-note-form");
|
this.$form = this.$widget.find(".include-note-form");
|
||||||
this.$autoComplete = this.$widget.find(".include-note-autocomplete");
|
this.$autoComplete = this.$widget.find(".include-note-autocomplete");
|
||||||
this.$form.on("submit", () => {
|
this.$form.on("submit", () => {
|
||||||
@@ -72,7 +80,7 @@ export default class IncludeNoteDialog extends BasicWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async showIncludeNoteDialogEvent({ textTypeWidget }) {
|
async showIncludeNoteDialogEvent({ textTypeWidget }: EventData<"showIncludeDialog">) {
|
||||||
this.textTypeWidget = textTypeWidget;
|
this.textTypeWidget = textTypeWidget;
|
||||||
await this.refresh();
|
await this.refresh();
|
||||||
utils.openDialog(this.$widget);
|
utils.openDialog(this.$widget);
|
||||||
@@ -80,7 +88,7 @@ export default class IncludeNoteDialog extends BasicWidget {
|
|||||||
this.$autoComplete.trigger("focus").trigger("select"); // to be able to quickly remove entered text
|
this.$autoComplete.trigger("focus").trigger("select"); // to be able to quickly remove entered text
|
||||||
}
|
}
|
||||||
|
|
||||||
async refresh(widget) {
|
async refresh() {
|
||||||
this.$autoComplete.val("");
|
this.$autoComplete.val("");
|
||||||
noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, {
|
noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, {
|
||||||
hideGoToSelectedNoteButton: true,
|
hideGoToSelectedNoteButton: true,
|
||||||
@@ -89,17 +97,20 @@ export default class IncludeNoteDialog extends BasicWidget {
|
|||||||
noteAutocompleteService.showRecentNotes(this.$autoComplete);
|
noteAutocompleteService.showRecentNotes(this.$autoComplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
async includeNote(notePath) {
|
async includeNote(notePath: string) {
|
||||||
const noteId = treeService.getNoteIdFromUrl(notePath);
|
const noteId = treeService.getNoteIdFromUrl(notePath);
|
||||||
|
if (!noteId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const note = await froca.getNote(noteId);
|
const note = await froca.getNote(noteId);
|
||||||
const boxSize = $("input[name='include-note-box-size']:checked").val();
|
const boxSize = $("input[name='include-note-box-size']:checked").val() as string;
|
||||||
|
|
||||||
if (["image", "canvas", "mermaid"].includes(note.type)) {
|
if (["image", "canvas", "mermaid"].includes(note?.type ?? "")) {
|
||||||
// there's no benefit to use insert note functionlity for images,
|
// there's no benefit to use insert note functionlity for images,
|
||||||
// so we'll just add an IMG tag
|
// so we'll just add an IMG tag
|
||||||
this.textTypeWidget.addImage(noteId);
|
this.textTypeWidget?.addImage(noteId);
|
||||||
} else {
|
} else {
|
||||||
this.textTypeWidget.addIncludeNote(noteId, boxSize);
|
this.textTypeWidget?.addIncludeNote(noteId, boxSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,13 @@ const TPL = `<div class="jump-to-note-dialog modal mx-auto" tabindex="-1" role="
|
|||||||
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
|
const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120;
|
||||||
|
|
||||||
export default class JumpToNoteDialog extends BasicWidget {
|
export default class JumpToNoteDialog extends BasicWidget {
|
||||||
|
|
||||||
|
private lastOpenedTs: number;
|
||||||
|
private modal!: bootstrap.Modal;
|
||||||
|
private $autoComplete!: JQuery<HTMLElement>;
|
||||||
|
private $results!: JQuery<HTMLElement>;
|
||||||
|
private $showInFullTextButton!: JQuery<HTMLElement>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@@ -36,7 +43,7 @@ export default class JumpToNoteDialog extends BasicWidget {
|
|||||||
|
|
||||||
doRender() {
|
doRender() {
|
||||||
this.$widget = $(TPL);
|
this.$widget = $(TPL);
|
||||||
this.modal = Modal.getOrCreateInstance(this.$widget);
|
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
|
||||||
|
|
||||||
this.$autoComplete = this.$widget.find(".jump-to-note-autocomplete");
|
this.$autoComplete = this.$widget.find(".jump-to-note-autocomplete");
|
||||||
this.$results = this.$widget.find(".jump-to-note-results");
|
this.$results = this.$widget.find(".jump-to-note-results");
|
||||||
@@ -54,17 +61,17 @@ export default class JumpToNoteDialog extends BasicWidget {
|
|||||||
|
|
||||||
function reposition() {
|
function reposition() {
|
||||||
const offset = 100;
|
const offset = 100;
|
||||||
const modalHeight = window.visualViewport.height - offset;
|
const modalHeight = (window.visualViewport?.height ?? 0) - offset;
|
||||||
const safeAreaInsetBottom = window.visualViewport.height - window.innerHeight;
|
const safeAreaInsetBottom = (window.visualViewport?.height ?? 0) - window.innerHeight;
|
||||||
el.style.height = `${modalHeight}px`;
|
el.style.height = `${modalHeight}px`;
|
||||||
el.style.bottom = `${window.visualViewport.height - modalHeight - safeAreaInsetBottom - offset}px`;
|
el.style.bottom = `${(window.visualViewport?.height ?? 0) - modalHeight - safeAreaInsetBottom - offset}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$autoComplete.on("focus", () => {
|
this.$autoComplete.on("focus", () => {
|
||||||
reposition();
|
reposition();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.visualViewport.addEventListener("resize", () => {
|
window.visualViewport?.addEventListener("resize", () => {
|
||||||
reposition();
|
reposition();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,7 +91,7 @@ export default class JumpToNoteDialog extends BasicWidget {
|
|||||||
allowCreatingNotes: true,
|
allowCreatingNotes: true,
|
||||||
hideGoToSelectedNoteButton: true,
|
hideGoToSelectedNoteButton: true,
|
||||||
allowJumpToSearchNotes: true,
|
allowJumpToSearchNotes: true,
|
||||||
container: this.$results
|
container: this.$results[0]
|
||||||
})
|
})
|
||||||
// clear any event listener added in previous invocation of this function
|
// clear any event listener added in previous invocation of this function
|
||||||
.off("autocomplete:noteselected")
|
.off("autocomplete:noteselected")
|
||||||
@@ -93,7 +100,7 @@ export default class JumpToNoteDialog extends BasicWidget {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
appContext.tabManager.getActiveContext().setNote(suggestion.notePath);
|
appContext.tabManager.getActiveContext()?.setNote(suggestion.notePath);
|
||||||
});
|
});
|
||||||
|
|
||||||
// if you open the Jump To dialog soon after using it previously, it can often mean that you
|
// if you open the Jump To dialog soon after using it previously, it can often mean that you
|
||||||
@@ -112,15 +119,14 @@ export default class JumpToNoteDialog extends BasicWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showInFullText(e) {
|
showInFullText(e: JQuery.TriggeredEvent) {
|
||||||
// stop from propagating upwards (dangerous, especially with ctrl+enter executable javascript notes)
|
// stop from propagating upwards (dangerous, especially with ctrl+enter executable javascript notes)
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
const searchString = this.$autoComplete.val();
|
const searchString = String(this.$autoComplete.val());
|
||||||
|
|
||||||
this.triggerCommand("searchNotes", { searchString });
|
this.triggerCommand("searchNotes", { searchString });
|
||||||
|
|
||||||
this.modal.hide();
|
this.modal.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,17 @@ const TPL = `
|
|||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
|
interface RenderMarkdownResponse {
|
||||||
|
htmlContent: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class MarkdownImportDialog extends BasicWidget {
|
export default class MarkdownImportDialog extends BasicWidget {
|
||||||
|
|
||||||
|
private lastOpenedTs: number;
|
||||||
|
private modal!: bootstrap.Modal;
|
||||||
|
private $importTextarea!: JQuery<HTMLElement>;
|
||||||
|
private $importButton!: JQuery<HTMLElement>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@@ -36,7 +46,7 @@ export default class MarkdownImportDialog extends BasicWidget {
|
|||||||
|
|
||||||
doRender() {
|
doRender() {
|
||||||
this.$widget = $(TPL);
|
this.$widget = $(TPL);
|
||||||
this.modal = Modal.getOrCreateInstance(this.$widget);
|
this.modal = Modal.getOrCreateInstance(this.$widget[0]);
|
||||||
this.$importTextarea = this.$widget.find(".markdown-import-textarea");
|
this.$importTextarea = this.$widget.find(".markdown-import-textarea");
|
||||||
this.$importButton = this.$widget.find(".markdown-import-button");
|
this.$importButton = this.$widget.find(".markdown-import-button");
|
||||||
|
|
||||||
@@ -47,10 +57,13 @@ export default class MarkdownImportDialog extends BasicWidget {
|
|||||||
shortcutService.bindElShortcut(this.$widget, "ctrl+return", () => this.sendForm());
|
shortcutService.bindElShortcut(this.$widget, "ctrl+return", () => this.sendForm());
|
||||||
}
|
}
|
||||||
|
|
||||||
async convertMarkdownToHtml(markdownContent) {
|
async convertMarkdownToHtml(markdownContent: string) {
|
||||||
const { htmlContent } = await server.post("other/render-markdown", { markdownContent });
|
const { htmlContent } = await server.post<RenderMarkdownResponse>("other/render-markdown", { markdownContent });
|
||||||
|
|
||||||
const textEditor = await appContext.tabManager.getActiveContext().getTextEditor();
|
const textEditor = await appContext.tabManager.getActiveContext()?.getTextEditor();
|
||||||
|
if (!textEditor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const viewFragment = textEditor.data.processor.toView(htmlContent);
|
const viewFragment = textEditor.data.processor.toView(htmlContent);
|
||||||
const modelFragment = textEditor.data.toModel(viewFragment);
|
const modelFragment = textEditor.data.toModel(viewFragment);
|
||||||
@@ -80,7 +93,7 @@ export default class MarkdownImportDialog extends BasicWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sendForm() {
|
async sendForm() {
|
||||||
const text = this.$importTextarea.val();
|
const text = String(this.$importTextarea.val());
|
||||||
|
|
||||||
this.modal.hide();
|
this.modal.hide();
|
||||||
|
|
||||||
@@ -6,6 +6,7 @@ import branchService from "../../services/branches.js";
|
|||||||
import treeService from "../../services/tree.js";
|
import treeService from "../../services/tree.js";
|
||||||
import BasicWidget from "../basic_widget.js";
|
import BasicWidget from "../basic_widget.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
|
import type { EventData } from "../../components/app_context.js";
|
||||||
|
|
||||||
const TPL = `
|
const TPL = `
|
||||||
<div class="move-to-dialog modal mx-auto" tabindex="-1" role="dialog">
|
<div class="move-to-dialog modal mx-auto" tabindex="-1" role="dialog">
|
||||||
@@ -39,6 +40,12 @@ const TPL = `
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
export default class MoveToDialog extends BasicWidget {
|
export default class MoveToDialog extends BasicWidget {
|
||||||
|
|
||||||
|
private movedBranchIds: string[] | null;
|
||||||
|
private $form!: JQuery<HTMLElement>;
|
||||||
|
private $noteAutoComplete!: JQuery<HTMLElement>;
|
||||||
|
private $noteList!: JQuery<HTMLElement>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@@ -58,7 +65,13 @@ export default class MoveToDialog extends BasicWidget {
|
|||||||
this.$widget.modal("hide");
|
this.$widget.modal("hide");
|
||||||
|
|
||||||
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);
|
const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);
|
||||||
froca.getBranchId(parentNoteId, noteId).then((branchId) => this.moveNotesTo(branchId));
|
if (parentNoteId) {
|
||||||
|
froca.getBranchId(parentNoteId, noteId).then((branchId) => {
|
||||||
|
if (branchId) {
|
||||||
|
this.moveNotesTo(branchId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
logError(t("move_to.error_no_path"));
|
logError(t("move_to.error_no_path"));
|
||||||
}
|
}
|
||||||
@@ -67,7 +80,7 @@ export default class MoveToDialog extends BasicWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async moveBranchIdsToEvent({ branchIds }) {
|
async moveBranchIdsToEvent({ branchIds }: EventData<"moveBranchIdsTo">) {
|
||||||
this.movedBranchIds = branchIds;
|
this.movedBranchIds = branchIds;
|
||||||
|
|
||||||
utils.openDialog(this.$widget);
|
utils.openDialog(this.$widget);
|
||||||
@@ -78,7 +91,14 @@ export default class MoveToDialog extends BasicWidget {
|
|||||||
|
|
||||||
for (const branchId of this.movedBranchIds) {
|
for (const branchId of this.movedBranchIds) {
|
||||||
const branch = froca.getBranch(branchId);
|
const branch = froca.getBranch(branchId);
|
||||||
|
if (!branch) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const note = await froca.getNote(branch.noteId);
|
const note = await froca.getNote(branch.noteId);
|
||||||
|
if (!note) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
this.$noteList.append($("<li>").text(note.title));
|
this.$noteList.append($("<li>").text(note.title));
|
||||||
}
|
}
|
||||||
@@ -87,12 +107,14 @@ export default class MoveToDialog extends BasicWidget {
|
|||||||
noteAutocompleteService.showRecentNotes(this.$noteAutoComplete);
|
noteAutocompleteService.showRecentNotes(this.$noteAutoComplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
async moveNotesTo(parentBranchId) {
|
async moveNotesTo(parentBranchId: string) {
|
||||||
await branchService.moveToParentNote(this.movedBranchIds, parentBranchId);
|
if (this.movedBranchIds) {
|
||||||
|
await branchService.moveToParentNote(this.movedBranchIds, parentBranchId);
|
||||||
|
}
|
||||||
|
|
||||||
const parentBranch = froca.getBranch(parentBranchId);
|
const parentBranch = froca.getBranch(parentBranchId);
|
||||||
const parentNote = await parentBranch.getNote();
|
const parentNote = await parentBranch?.getNote();
|
||||||
|
|
||||||
toastService.showMessage(`${t("move_to.move_success_message")} ${parentNote.title}`);
|
toastService.showMessage(`${t("move_to.move_success_message")} ${parentNote?.title}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,10 +9,16 @@ import attributeService from "../services/attributes.js";
|
|||||||
import FindInText from "./find_in_text.js";
|
import FindInText from "./find_in_text.js";
|
||||||
import FindInCode from "./find_in_code.js";
|
import FindInCode from "./find_in_code.js";
|
||||||
import FindInHtml from "./find_in_html.js";
|
import FindInHtml from "./find_in_html.js";
|
||||||
|
import type { EventData } from "../components/app_context.js";
|
||||||
|
|
||||||
const findWidgetDelayMillis = 200;
|
const findWidgetDelayMillis = 200;
|
||||||
const waitForEnter = findWidgetDelayMillis < 0;
|
const waitForEnter = findWidgetDelayMillis < 0;
|
||||||
|
|
||||||
|
export interface FindResult {
|
||||||
|
totalFound: number;
|
||||||
|
currentFound: number;
|
||||||
|
}
|
||||||
|
|
||||||
// tabIndex=-1 on the checkbox labels is necessary, so when clicking on the label,
|
// tabIndex=-1 on the checkbox labels is necessary, so when clicking on the label,
|
||||||
// the focusout handler is called with relatedTarget equal to the label instead
|
// the focusout handler is called with relatedTarget equal to the label instead
|
||||||
// of undefined. It's -1 instead of > 0, so they don't tabstop
|
// of undefined. It's -1 instead of > 0, so they don't tabstop
|
||||||
@@ -92,6 +98,28 @@ const TPL = `
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
export default class FindWidget extends NoteContextAwareWidget {
|
export default class FindWidget extends NoteContextAwareWidget {
|
||||||
|
|
||||||
|
private searchTerm: string | null;
|
||||||
|
|
||||||
|
private textHandler: FindInText;
|
||||||
|
private codeHandler: FindInCode;
|
||||||
|
private htmlHandler: FindInHtml;
|
||||||
|
private handler?: FindInText | FindInCode | FindInHtml;
|
||||||
|
private timeoutId?: number | null;
|
||||||
|
|
||||||
|
private $input!: JQuery<HTMLElement>;
|
||||||
|
private $currentFound!: JQuery<HTMLElement>;
|
||||||
|
private $totalFound!: JQuery<HTMLElement>;
|
||||||
|
private $caseSensitiveCheckbox!: JQuery<HTMLElement>;
|
||||||
|
private $matchWordsCheckbox!: JQuery<HTMLElement>;
|
||||||
|
private $previousButton!: JQuery<HTMLElement>;
|
||||||
|
private $nextButton!: JQuery<HTMLElement>;
|
||||||
|
private $closeButton!: JQuery<HTMLElement>;
|
||||||
|
private $replaceWidgetBox!: JQuery<HTMLElement>;
|
||||||
|
private $replaceTextInput!: JQuery<HTMLElement>;
|
||||||
|
private $replaceAllButton!: JQuery<HTMLElement>;
|
||||||
|
private $replaceButton!: JQuery<HTMLElement>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@@ -160,24 +188,24 @@ export default class FindWidget extends NoteContextAwareWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!["text", "code", "render"].includes(this.note.type)) {
|
if (!["text", "code", "render"].includes(this.note?.type ?? "")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.handler = await this.getHandler();
|
this.handler = await this.getHandler();
|
||||||
|
|
||||||
const isReadOnly = await this.noteContext.isReadOnly();
|
const isReadOnly = await this.noteContext?.isReadOnly();
|
||||||
|
|
||||||
let selectedText = "";
|
let selectedText = "";
|
||||||
if (this.note.type === "code" && !isReadOnly) {
|
if (this.note?.type === "code" && !isReadOnly && this.noteContext) {
|
||||||
const codeEditor = await this.noteContext.getCodeEditor();
|
const codeEditor = await this.noteContext.getCodeEditor();
|
||||||
selectedText = codeEditor.getSelection();
|
selectedText = codeEditor.getSelection();
|
||||||
} else {
|
} else {
|
||||||
selectedText = window.getSelection().toString() || "";
|
selectedText = window.getSelection()?.toString() || "";
|
||||||
}
|
}
|
||||||
this.$widget.show();
|
this.$widget.show();
|
||||||
this.$input.focus();
|
this.$input.focus();
|
||||||
if (["text", "code"].includes(this.note.type) && !isReadOnly) {
|
if (["text", "code"].includes(this.note?.type ?? "") && !isReadOnly) {
|
||||||
this.$replaceWidgetBox.show();
|
this.$replaceWidgetBox.show();
|
||||||
} else {
|
} else {
|
||||||
this.$replaceWidgetBox.hide();
|
this.$replaceWidgetBox.hide();
|
||||||
@@ -208,16 +236,16 @@ export default class FindWidget extends NoteContextAwareWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getHandler() {
|
async getHandler() {
|
||||||
if (this.note.type === "render") {
|
if (this.note?.type === "render") {
|
||||||
return this.htmlHandler;
|
return this.htmlHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
const readOnly = await this.noteContext.isReadOnly();
|
const readOnly = await this.noteContext?.isReadOnly();
|
||||||
|
|
||||||
if (readOnly) {
|
if (readOnly) {
|
||||||
return this.htmlHandler;
|
return this.htmlHandler;
|
||||||
} else {
|
} else {
|
||||||
return this.note.type === "code" ? this.codeHandler : this.textHandler;
|
return this.note?.type === "code" ? this.codeHandler : this.textHandler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +256,7 @@ export default class FindWidget extends NoteContextAwareWidget {
|
|||||||
if (!waitForEnter) {
|
if (!waitForEnter) {
|
||||||
// Clear the previous timeout if any, it's ok if timeoutId is
|
// Clear the previous timeout if any, it's ok if timeoutId is
|
||||||
// null or undefined
|
// null or undefined
|
||||||
clearTimeout(this.timeoutId);
|
clearTimeout(this.timeoutId as unknown as NodeJS.Timeout); // TODO: Fix once client is separated from Node.js types.
|
||||||
|
|
||||||
// Defer the search a few millis so the search doesn't start
|
// Defer the search a few millis so the search doesn't start
|
||||||
// immediately, as this can cause search word typing lag with
|
// immediately, as this can cause search word typing lag with
|
||||||
@@ -237,15 +265,14 @@ export default class FindWidget extends NoteContextAwareWidget {
|
|||||||
this.timeoutId = setTimeout(async () => {
|
this.timeoutId = setTimeout(async () => {
|
||||||
this.timeoutId = null;
|
this.timeoutId = null;
|
||||||
await this.performFind();
|
await this.performFind();
|
||||||
}, findWidgetDelayMillis);
|
}, findWidgetDelayMillis) as unknown as number; // TODO: Fix once client is separated from Node.js types.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param direction +1 for next, -1 for previous
|
* @param direction +1 for next, -1 for previous
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
*/
|
||||||
async findNext(direction) {
|
async findNext(direction: 1 | -1) {
|
||||||
if (this.$totalFound.text() == "?") {
|
if (this.$totalFound.text() == "?") {
|
||||||
await this.performFind();
|
await this.performFind();
|
||||||
return;
|
return;
|
||||||
@@ -268,16 +295,19 @@ export default class FindWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
this.$currentFound.text(nextFound + 1);
|
this.$currentFound.text(nextFound + 1);
|
||||||
|
|
||||||
await this.handler.findNext(direction, currentFound, nextFound);
|
await this.handler?.findNext(direction, currentFound, nextFound);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Perform the find and highlight the find results. */
|
/** Perform the find and highlight the find results. */
|
||||||
async performFind() {
|
async performFind() {
|
||||||
const searchTerm = this.$input.val();
|
const searchTerm = String(this.$input.val());
|
||||||
const matchCase = this.$caseSensitiveCheckbox.prop("checked");
|
const matchCase = this.$caseSensitiveCheckbox.prop("checked");
|
||||||
const wholeWord = this.$matchWordsCheckbox.prop("checked");
|
const wholeWord = this.$matchWordsCheckbox.prop("checked");
|
||||||
|
|
||||||
|
if (!this.handler) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { totalFound, currentFound } = await this.handler.performFind(searchTerm, matchCase, wholeWord);
|
const { totalFound, currentFound } = await this.handler.performFind(searchTerm, matchCase, wholeWord);
|
||||||
|
|
||||||
this.$totalFound.text(totalFound);
|
this.$totalFound.text(totalFound);
|
||||||
@@ -297,28 +327,34 @@ export default class FindWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
this.searchTerm = null;
|
this.searchTerm = null;
|
||||||
|
|
||||||
await this.handler.findBoxClosed(totalFound, currentFound);
|
await this.handler?.findBoxClosed(totalFound, currentFound);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async replace() {
|
async replace() {
|
||||||
const replaceText = this.$replaceTextInput.val();
|
const replaceText = String(this.$replaceTextInput.val());
|
||||||
await this.handler.replace(replaceText);
|
if (this.handler && "replace" in this.handler) {
|
||||||
|
await this.handler.replace(replaceText);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async replaceAll() {
|
async replaceAll() {
|
||||||
const replaceText = this.$replaceTextInput.val();
|
const replaceText = String(this.$replaceTextInput.val());
|
||||||
await this.handler.replaceAll(replaceText);
|
if (this.handler && "replace" in this.handler) {
|
||||||
|
await this.handler.replaceAll(replaceText);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isEnabled() {
|
isEnabled() {
|
||||||
return super.isEnabled() && ["text", "code", "render"].includes(this.note.type);
|
return super.isEnabled() && ["text", "code", "render"].includes(this.note?.type ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
async entitiesReloadedEvent({ loadResults }) {
|
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||||
if (loadResults.isNoteContentReloaded(this.noteId)) {
|
if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) {
|
||||||
this.$totalFound.text("?");
|
this.$totalFound.text("?");
|
||||||
} else if (loadResults.getAttributeRows().find((attr) => attr.type === "label" && attr.name.toLowerCase().includes("readonly") && attributeService.isAffecting(attr, this.note))) {
|
} else if (loadResults.getAttributeRows().find((attr) => attr.type === "label"
|
||||||
|
&& (attr.name?.toLowerCase() ?? "").includes("readonly")
|
||||||
|
&& attributeService.isAffecting(attr, this.note))) {
|
||||||
this.closeSearch();
|
this.closeSearch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,35 +2,54 @@
|
|||||||
// uses for highlighting matches, use the same one on CodeMirror
|
// uses for highlighting matches, use the same one on CodeMirror
|
||||||
// for consistency
|
// for consistency
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
|
import type FindWidget from "./find.js";
|
||||||
|
|
||||||
const FIND_RESULT_SELECTED_CSS_CLASSNAME = "ck-find-result_selected";
|
const FIND_RESULT_SELECTED_CSS_CLASSNAME = "ck-find-result_selected";
|
||||||
const FIND_RESULT_CSS_CLASSNAME = "ck-find-result";
|
const FIND_RESULT_CSS_CLASSNAME = "ck-find-result";
|
||||||
|
|
||||||
|
// TODO: Deduplicate.
|
||||||
|
interface Match {
|
||||||
|
className: string;
|
||||||
|
clear(): void;
|
||||||
|
find(): {
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default class FindInCode {
|
export default class FindInCode {
|
||||||
constructor(parent) {
|
|
||||||
/** @property {FindWidget} */
|
private parent: FindWidget;
|
||||||
|
private findResult?: Match[] | null;
|
||||||
|
|
||||||
|
constructor(parent: FindWidget) {
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCodeEditor() {
|
async getCodeEditor() {
|
||||||
return this.parent.noteContext.getCodeEditor();
|
return this.parent.noteContext?.getCodeEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
async performFind(searchTerm, matchCase, wholeWord) {
|
async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
|
||||||
let findResult = null;
|
let findResult: Match[] | null = null;
|
||||||
let totalFound = 0;
|
let totalFound = 0;
|
||||||
let currentFound = -1;
|
let currentFound = -1;
|
||||||
|
|
||||||
// See https://codemirror.net/addon/search/searchcursor.js for tips
|
// See https://codemirror.net/addon/search/searchcursor.js for tips
|
||||||
const codeEditor = await this.getCodeEditor();
|
const codeEditor = await this.getCodeEditor();
|
||||||
|
if (!codeEditor) {
|
||||||
|
return { totalFound: 0, currentFound: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
const doc = codeEditor.doc;
|
const doc = codeEditor.doc;
|
||||||
const text = doc.getValue();
|
const text = doc.getValue();
|
||||||
|
|
||||||
// Clear all markers
|
// Clear all markers
|
||||||
if (this.findResult != null) {
|
if (this.findResult) {
|
||||||
codeEditor.operation(() => {
|
codeEditor.operation(() => {
|
||||||
for (let i = 0; i < this.findResult.length; ++i) {
|
const findResult = this.findResult as Match[];
|
||||||
const marker = this.findResult[i];
|
for (let i = 0; i < findResult.length; ++i) {
|
||||||
|
const marker = findResult[i];
|
||||||
marker.clear();
|
marker.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -49,7 +68,7 @@ export default class FindInCode {
|
|||||||
const re = new RegExp(wholeWordChar + searchTerm + wholeWordChar, "g" + (matchCase ? "" : "i"));
|
const re = new RegExp(wholeWordChar + searchTerm + wholeWordChar, "g" + (matchCase ? "" : "i"));
|
||||||
let curLine = 0;
|
let curLine = 0;
|
||||||
let curChar = 0;
|
let curChar = 0;
|
||||||
let curMatch = null;
|
let curMatch: RegExpExecArray | null = null;
|
||||||
findResult = [];
|
findResult = [];
|
||||||
// All those markText take several seconds on e.g., this ~500-line
|
// All those markText take several seconds on e.g., this ~500-line
|
||||||
// script, batch them inside an operation, so they become
|
// script, batch them inside an operation, so they become
|
||||||
@@ -73,7 +92,7 @@ export default class FindInCode {
|
|||||||
let toPos = { line: curLine, ch: curChar + curMatch[0].length };
|
let toPos = { line: curLine, ch: curChar + curMatch[0].length };
|
||||||
// or css = "color: #f3"
|
// or css = "color: #f3"
|
||||||
let marker = doc.markText(fromPos, toPos, { className: FIND_RESULT_CSS_CLASSNAME });
|
let marker = doc.markText(fromPos, toPos, { className: FIND_RESULT_CSS_CLASSNAME });
|
||||||
findResult.push(marker);
|
findResult?.push(marker);
|
||||||
|
|
||||||
// Set the first match beyond the cursor as the current match
|
// Set the first match beyond the cursor as the current match
|
||||||
if (currentFound === -1) {
|
if (currentFound === -1) {
|
||||||
@@ -99,7 +118,7 @@ export default class FindInCode {
|
|||||||
this.findResult = findResult;
|
this.findResult = findResult;
|
||||||
|
|
||||||
// Calculate curfound if not already, highlight it as selected
|
// Calculate curfound if not already, highlight it as selected
|
||||||
if (totalFound > 0) {
|
if (findResult && totalFound > 0) {
|
||||||
currentFound = Math.max(0, currentFound);
|
currentFound = Math.max(0, currentFound);
|
||||||
let marker = findResult[currentFound];
|
let marker = findResult[currentFound];
|
||||||
let pos = marker.find();
|
let pos = marker.find();
|
||||||
@@ -114,8 +133,12 @@ export default class FindInCode {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async findNext(direction, currentFound, nextFound) {
|
async findNext(direction: number, currentFound: number, nextFound: number) {
|
||||||
const codeEditor = await this.getCodeEditor();
|
const codeEditor = await this.getCodeEditor();
|
||||||
|
if (!codeEditor || !this.findResult) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const doc = codeEditor.doc;
|
const doc = codeEditor.doc;
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -137,18 +160,23 @@ export default class FindInCode {
|
|||||||
codeEditor.scrollIntoView(pos.from);
|
codeEditor.scrollIntoView(pos.from);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findBoxClosed(totalFound, currentFound) {
|
async findBoxClosed(totalFound: number, currentFound: number) {
|
||||||
const codeEditor = await this.getCodeEditor();
|
const codeEditor = await this.getCodeEditor();
|
||||||
|
|
||||||
if (totalFound > 0) {
|
if (codeEditor && totalFound > 0) {
|
||||||
const doc = codeEditor.doc;
|
const doc = codeEditor.doc;
|
||||||
const pos = this.findResult[currentFound].find();
|
const pos = this.findResult?.[currentFound].find();
|
||||||
// Note setting the selection sets the cursor to
|
// Note setting the selection sets the cursor to
|
||||||
// the end of the selection and scrolls it into
|
// the end of the selection and scrolls it into
|
||||||
// view
|
// view
|
||||||
doc.setSelection(pos.from, pos.to);
|
if (pos) {
|
||||||
|
doc.setSelection(pos.from, pos.to);
|
||||||
|
}
|
||||||
// Clear all markers
|
// Clear all markers
|
||||||
codeEditor.operation(() => {
|
codeEditor.operation(() => {
|
||||||
|
if (!this.findResult) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (let i = 0; i < this.findResult.length; ++i) {
|
for (let i = 0; i < this.findResult.length; ++i) {
|
||||||
let marker = this.findResult[i];
|
let marker = this.findResult[i];
|
||||||
marker.clear();
|
marker.clear();
|
||||||
@@ -157,9 +185,9 @@ export default class FindInCode {
|
|||||||
}
|
}
|
||||||
this.findResult = null;
|
this.findResult = null;
|
||||||
|
|
||||||
codeEditor.focus();
|
codeEditor?.focus();
|
||||||
}
|
}
|
||||||
async replace(replaceText) {
|
async replace(replaceText: string) {
|
||||||
// this.findResult may be undefined and null
|
// this.findResult may be undefined and null
|
||||||
if (!this.findResult || this.findResult.length === 0) {
|
if (!this.findResult || this.findResult.length === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -178,8 +206,10 @@ export default class FindInCode {
|
|||||||
let marker = this.findResult[currentFound];
|
let marker = this.findResult[currentFound];
|
||||||
let pos = marker.find();
|
let pos = marker.find();
|
||||||
const codeEditor = await this.getCodeEditor();
|
const codeEditor = await this.getCodeEditor();
|
||||||
const doc = codeEditor.doc;
|
const doc = codeEditor?.doc;
|
||||||
doc.replaceRange(replaceText, pos.from, pos.to);
|
if (doc) {
|
||||||
|
doc.replaceRange(replaceText, pos.from, pos.to);
|
||||||
|
}
|
||||||
marker.clear();
|
marker.clear();
|
||||||
|
|
||||||
let nextFound;
|
let nextFound;
|
||||||
@@ -194,17 +224,21 @@ export default class FindInCode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async replaceAll(replaceText) {
|
async replaceAll(replaceText: string) {
|
||||||
if (!this.findResult || this.findResult.length === 0) {
|
if (!this.findResult || this.findResult.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const codeEditor = await this.getCodeEditor();
|
const codeEditor = await this.getCodeEditor();
|
||||||
const doc = codeEditor.doc;
|
const doc = codeEditor?.doc;
|
||||||
codeEditor.operation(() => {
|
codeEditor?.operation(() => {
|
||||||
|
if (!this.findResult) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (let currentFound = 0; currentFound < this.findResult.length; currentFound++) {
|
for (let currentFound = 0; currentFound < this.findResult.length; currentFound++) {
|
||||||
let marker = this.findResult[currentFound];
|
let marker = this.findResult[currentFound];
|
||||||
let pos = marker.find();
|
let pos = marker.find();
|
||||||
doc.replaceRange(replaceText, pos.from, pos.to);
|
doc?.replaceRange(replaceText, pos.from, pos.to);
|
||||||
marker.clear();
|
marker.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -4,28 +4,34 @@
|
|||||||
import libraryLoader from "../services/library_loader.js";
|
import libraryLoader from "../services/library_loader.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
import appContext from "../components/app_context.js";
|
import appContext from "../components/app_context.js";
|
||||||
|
import type FindWidget from "./find.js";
|
||||||
|
import type { FindResult } from "./find.js";
|
||||||
|
|
||||||
const FIND_RESULT_SELECTED_CSS_CLASSNAME = "ck-find-result_selected";
|
const FIND_RESULT_SELECTED_CSS_CLASSNAME = "ck-find-result_selected";
|
||||||
const FIND_RESULT_CSS_CLASSNAME = "ck-find-result";
|
const FIND_RESULT_CSS_CLASSNAME = "ck-find-result";
|
||||||
|
|
||||||
export default class FindInHtml {
|
export default class FindInHtml {
|
||||||
constructor(parent) {
|
|
||||||
/** @property {FindWidget} */
|
private parent: FindWidget;
|
||||||
|
private currentIndex: number;
|
||||||
|
private $results: JQuery<HTMLElement> | null;
|
||||||
|
|
||||||
|
constructor(parent: FindWidget) {
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.currentIndex = 0;
|
this.currentIndex = 0;
|
||||||
this.$results = null;
|
this.$results = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async performFind(searchTerm, matchCase, wholeWord) {
|
async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
|
||||||
await libraryLoader.requireLibrary(libraryLoader.MARKJS);
|
await libraryLoader.requireLibrary(libraryLoader.MARKJS);
|
||||||
|
|
||||||
const $content = await this.parent.noteContext.getContentElement();
|
const $content = await this.parent?.noteContext?.getContentElement();
|
||||||
|
|
||||||
const wholeWordChar = wholeWord ? "\\b" : "";
|
const wholeWordChar = wholeWord ? "\\b" : "";
|
||||||
const regExp = new RegExp(wholeWordChar + utils.escapeRegExp(searchTerm) + wholeWordChar, matchCase ? "g" : "gi");
|
const regExp = new RegExp(wholeWordChar + utils.escapeRegExp(searchTerm) + wholeWordChar, matchCase ? "g" : "gi");
|
||||||
|
|
||||||
return new Promise((res) => {
|
return new Promise<FindResult>((res) => {
|
||||||
$content.unmark({
|
$content?.unmark({
|
||||||
done: () => {
|
done: () => {
|
||||||
$content.markRegExp(regExp, {
|
$content.markRegExp(regExp, {
|
||||||
element: "span",
|
element: "span",
|
||||||
@@ -48,8 +54,8 @@ export default class FindInHtml {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findNext(direction, currentFound, nextFound) {
|
async findNext(direction: -1 | 1, currentFound: number, nextFound: number) {
|
||||||
if (this.$results.length) {
|
if (this.$results?.length) {
|
||||||
this.currentIndex += direction;
|
this.currentIndex += direction;
|
||||||
|
|
||||||
if (this.currentIndex < 0) {
|
if (this.currentIndex < 0) {
|
||||||
@@ -64,13 +70,15 @@ export default class FindInHtml {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findBoxClosed(totalFound, currentFound) {
|
async findBoxClosed(totalFound: number, currentFound: number) {
|
||||||
const $content = await this.parent.noteContext.getContentElement();
|
const $content = await this.parent?.noteContext?.getContentElement();
|
||||||
$content.unmark();
|
if ($content) {
|
||||||
|
$content.unmark();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async jumpTo() {
|
async jumpTo() {
|
||||||
if (this.$results.length) {
|
if (this.$results?.length) {
|
||||||
const offsetTop = 100;
|
const offsetTop = 100;
|
||||||
const $current = this.$results.eq(this.currentIndex);
|
const $current = this.$results.eq(this.currentIndex);
|
||||||
this.$results.removeClass(FIND_RESULT_SELECTED_CSS_CLASSNAME);
|
this.$results.removeClass(FIND_RESULT_SELECTED_CSS_CLASSNAME);
|
||||||
@@ -79,10 +87,11 @@ export default class FindInHtml {
|
|||||||
$current.addClass(FIND_RESULT_SELECTED_CSS_CLASSNAME);
|
$current.addClass(FIND_RESULT_SELECTED_CSS_CLASSNAME);
|
||||||
const position = $current.position().top - offsetTop;
|
const position = $current.position().top - offsetTop;
|
||||||
|
|
||||||
const $content = await this.parent.noteContext.getContentElement();
|
const $content = await this.parent.noteContext?.getContentElement();
|
||||||
const $contentWiget = appContext.getComponentByEl($content);
|
if ($content) {
|
||||||
|
const $contentWidget = appContext.getComponentByEl($content[0]);
|
||||||
$contentWiget.triggerCommand("scrollContainerTo", { position });
|
$contentWidget.triggerCommand("scrollContainerTo", { position });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,38 @@
|
|||||||
|
import type { FindResult } from "./find.js";
|
||||||
|
import type FindWidget from "./find.js";
|
||||||
|
|
||||||
|
// TODO: Deduplicate.
|
||||||
|
interface Match {
|
||||||
|
className: string;
|
||||||
|
clear(): void;
|
||||||
|
find(): {
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default class FindInText {
|
export default class FindInText {
|
||||||
constructor(parent) {
|
|
||||||
/** @property {FindWidget} */
|
private parent: FindWidget;
|
||||||
|
private findResult?: CKFindResult | null;
|
||||||
|
private editingState?: EditingState;
|
||||||
|
|
||||||
|
constructor(parent: FindWidget) {
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTextEditor() {
|
async getTextEditor() {
|
||||||
return this.parent.noteContext.getTextEditor();
|
return this.parent?.noteContext?.getTextEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
async performFind(searchTerm, matchCase, wholeWord) {
|
async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean): Promise<FindResult> {
|
||||||
// Do this even if the searchTerm is empty so the markers are cleared and
|
// Do this even if the searchTerm is empty so the markers are cleared and
|
||||||
// the counters updated
|
// the counters updated
|
||||||
const textEditor = await this.getTextEditor();
|
const textEditor = await this.getTextEditor();
|
||||||
|
if (!textEditor) {
|
||||||
|
return { currentFound: 0, totalFound: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
const model = textEditor.model;
|
const model = textEditor.model;
|
||||||
let findResult = null;
|
let findResult = null;
|
||||||
let totalFound = 0;
|
let totalFound = 0;
|
||||||
@@ -31,14 +52,14 @@ export default class FindInText {
|
|||||||
// let m = text.match(re);
|
// let m = text.match(re);
|
||||||
// totalFound = m ? m.length : 0;
|
// totalFound = m ? m.length : 0;
|
||||||
const options = { matchCase: matchCase, wholeWords: wholeWord };
|
const options = { matchCase: matchCase, wholeWords: wholeWord };
|
||||||
findResult = textEditor.execute("find", searchTerm, options);
|
findResult = textEditor.execute<CKFindResult>("find", searchTerm, options);
|
||||||
totalFound = findResult.results.length;
|
totalFound = findResult.results.length;
|
||||||
// Find the result beyond the cursor
|
// Find the result beyond the cursor
|
||||||
const cursorPos = model.document.selection.getLastPosition();
|
const cursorPos = model.document.selection.getLastPosition();
|
||||||
for (let i = 0; i < findResult.results.length; ++i) {
|
for (let i = 0; i < findResult.results.length; ++i) {
|
||||||
const marker = findResult.results.get(i).marker;
|
const marker = findResult.results.get(i).marker;
|
||||||
const fromPos = marker.getStart();
|
const fromPos = marker.getStart();
|
||||||
if (fromPos.compareWith(cursorPos) !== "before") {
|
if (cursorPos && fromPos.compareWith(cursorPos) !== "before") {
|
||||||
currentFound = i;
|
currentFound = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -54,7 +75,7 @@ export default class FindInText {
|
|||||||
// XXX Do this accessing the private data?
|
// XXX Do this accessing the private data?
|
||||||
// See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js
|
// See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js
|
||||||
for (let i = 0; i < currentFound; ++i) {
|
for (let i = 0; i < currentFound; ++i) {
|
||||||
textEditor.execute("findNext", searchTerm);
|
textEditor?.execute("findNext", searchTerm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +85,7 @@ export default class FindInText {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async findNext(direction, currentFound, nextFound) {
|
async findNext(direction: number, currentFound: number, nextFound: number) {
|
||||||
const textEditor = await this.getTextEditor();
|
const textEditor = await this.getTextEditor();
|
||||||
|
|
||||||
// There are no parameters for findNext/findPrev
|
// There are no parameters for findNext/findPrev
|
||||||
@@ -72,20 +93,23 @@ export default class FindInText {
|
|||||||
// curFound wrap around above assumes findNext and
|
// curFound wrap around above assumes findNext and
|
||||||
// findPrevious wraparound, which is what they do
|
// findPrevious wraparound, which is what they do
|
||||||
if (direction > 0) {
|
if (direction > 0) {
|
||||||
textEditor.execute("findNext");
|
textEditor?.execute("findNext");
|
||||||
} else {
|
} else {
|
||||||
textEditor.execute("findPrevious");
|
textEditor?.execute("findPrevious");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findBoxClosed(totalFound, currentFound) {
|
async findBoxClosed(totalFound: number, currentFound: number) {
|
||||||
const textEditor = await this.getTextEditor();
|
const textEditor = await this.getTextEditor();
|
||||||
|
if (!textEditor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (totalFound > 0) {
|
if (totalFound > 0) {
|
||||||
// Clear the markers and set the caret to the
|
// Clear the markers and set the caret to the
|
||||||
// current occurrence
|
// current occurrence
|
||||||
const model = textEditor.model;
|
const model = textEditor.model;
|
||||||
const range = this.findResult.results.get(currentFound).marker.getRange();
|
const range = this.findResult?.results?.get(currentFound).marker.getRange();
|
||||||
// From
|
// From
|
||||||
// https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findandreplace.js#L92
|
// https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findandreplace.js#L92
|
||||||
// XXX Roll our own since already done for codeEditor and
|
// XXX Roll our own since already done for codeEditor and
|
||||||
@@ -93,9 +117,11 @@ export default class FindInText {
|
|||||||
let findAndReplaceEditing = textEditor.plugins.get("FindAndReplaceEditing");
|
let findAndReplaceEditing = textEditor.plugins.get("FindAndReplaceEditing");
|
||||||
findAndReplaceEditing.state.clear(model);
|
findAndReplaceEditing.state.clear(model);
|
||||||
findAndReplaceEditing.stop();
|
findAndReplaceEditing.stop();
|
||||||
model.change((writer) => {
|
if (range) {
|
||||||
writer.setSelection(range, 0);
|
model.change((writer) => {
|
||||||
});
|
writer.setSelection(range, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
textEditor.editing.view.scrollToTheSelection();
|
textEditor.editing.view.scrollToTheSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,17 +130,17 @@ export default class FindInText {
|
|||||||
textEditor.focus();
|
textEditor.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async replace(replaceText) {
|
async replace(replaceText: string) {
|
||||||
if (this.editingState !== undefined && this.editingState.highlightedResult !== null) {
|
if (this.editingState !== undefined && this.editingState.highlightedResult !== null) {
|
||||||
const textEditor = await this.getTextEditor();
|
const textEditor = await this.getTextEditor();
|
||||||
textEditor.execute("replace", replaceText, this.editingState.highlightedResult);
|
textEditor?.execute("replace", replaceText, this.editingState.highlightedResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async replaceAll(replaceText) {
|
async replaceAll(replaceText: string) {
|
||||||
if (this.editingState !== undefined && this.editingState.results.length > 0) {
|
if (this.editingState !== undefined && this.editingState.results.length > 0) {
|
||||||
const textEditor = await this.getTextEditor();
|
const textEditor = await this.getTextEditor();
|
||||||
textEditor.execute("replaceAll", replaceText, this.editingState.results);
|
textEditor?.execute("replaceAll", replaceText, this.editingState.results);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ const TPL = `
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<button data-trigger-command="runActiveNote" class="execute-button floating-button btn" title="${t("code_buttons.execute_button_title")}">
|
<button data-trigger-command="runActiveNote" class="execute-button floating-button btn" title="${t("code_buttons.execute_button_title")}">
|
||||||
<span class="bx bx-run"></span>
|
<span class="bx bx-play"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="trilium-api-docs-button floating-button btn" title="${t("code_buttons.trilium_api_docs_button_title")}">
|
<button class="trilium-api-docs-button floating-button btn" title="${t("code_buttons.trilium_api_docs_button_title")}">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import OnClickButtonWidget from "./onclick_button.js";
|
import OnClickButtonWidget from "../buttons/onclick_button.js";
|
||||||
import appContext from "../../components/app_context.js";
|
import appContext from "../../components/app_context.js";
|
||||||
import attributeService from "../../services/attributes.js";
|
import attributeService from "../../services/attributes.js";
|
||||||
import protectedSessionHolder from "../../services/protected_session_holder.js";
|
import protectedSessionHolder from "../../services/protected_session_holder.js";
|
||||||
@@ -15,7 +15,7 @@ export default class EditButton extends OnClickButtonWidget {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.icon("bx-edit-alt")
|
this.icon("bx-pencil")
|
||||||
.title(t("edit_button.edit_this_note"))
|
.title(t("edit_button.edit_this_note"))
|
||||||
.titlePlacement("bottom")
|
.titlePlacement("bottom")
|
||||||
.onClick((widget) => {
|
.onClick((widget) => {
|
||||||
@@ -3,6 +3,14 @@ import { t } from "../../services/i18n.js";
|
|||||||
import type FNote from "../../entities/fnote.js";
|
import type FNote from "../../entities/fnote.js";
|
||||||
import type BasicWidget from "../basic_widget.js";
|
import type BasicWidget from "../basic_widget.js";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Note:
|
||||||
|
*
|
||||||
|
* For floating button widgets that require content to overflow, the has-overflow CSS class should
|
||||||
|
* be applied to the root element of the widget. Additionally, this root element may need to
|
||||||
|
* properly handle rounded corners, as defined by the --border-radius CSS variable.
|
||||||
|
*/
|
||||||
|
|
||||||
const TPL = `
|
const TPL = `
|
||||||
<div class="floating-buttons no-print">
|
<div class="floating-buttons no-print">
|
||||||
<style>
|
<style>
|
||||||
@@ -39,10 +47,18 @@ const TPL = `
|
|||||||
top: 70px;
|
top: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.type-canvas .floating-buttons-children > * {
|
||||||
|
--border-radius: 0; /* Overridden by themes */
|
||||||
|
}
|
||||||
|
|
||||||
.floating-buttons-children > *:not(.hidden-int):not(.no-content-hidden) {
|
.floating-buttons-children > *:not(.hidden-int):not(.no-content-hidden) {
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.floating-buttons-children > *:not(.has-overflow) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.floating-buttons-children > button, .floating-buttons-children .floating-button {
|
.floating-buttons-children > button, .floating-buttons-children .floating-button {
|
||||||
font-size: 150%;
|
font-size: 150%;
|
||||||
padding: 5px 10px 4px 10px;
|
padding: 5px 10px 4px 10px;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const TPL = `\
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="geo-map-create-child-note floating-button btn bx bx-folder-plus"
|
class="geo-map-create-child-note floating-button btn bx bx-plus-circle"
|
||||||
title="${t("geo-map.create-child-note-title")}" />
|
title="${t("geo-map.create-child-note-title")}" />
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
|
|||||||
24
src/public/app/widgets/floating_buttons/png_export_button.ts
Normal file
24
src/public/app/widgets/floating_buttons/png_export_button.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { t } from "../../services/i18n.js";
|
||||||
|
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||||
|
|
||||||
|
const TPL = `
|
||||||
|
<button type="button"
|
||||||
|
class="export-svg-button"
|
||||||
|
title="${t("png_export_button.button_title")}">
|
||||||
|
<span class="bx bxs-file-png"></span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default class PngExportButton extends NoteContextAwareWidget {
|
||||||
|
isEnabled() {
|
||||||
|
return super.isEnabled() && ["mermaid", "mindMap"].includes(this.note?.type ?? "") && this.note?.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
doRender() {
|
||||||
|
super.doRender();
|
||||||
|
|
||||||
|
this.$widget = $(TPL);
|
||||||
|
this.$widget.on("click", () => this.triggerEvent("exportPng", { ntxId: this.ntxId }));
|
||||||
|
this.contentSized();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ const TPL = `
|
|||||||
<button type="button"
|
<button type="button"
|
||||||
class="export-svg-button"
|
class="export-svg-button"
|
||||||
title="${t("svg_export_button.button_title")}">
|
title="${t("svg_export_button.button_title")}">
|
||||||
<span class="bx bx-export"></span>
|
<span class="bx bxs-file-image"></span>
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import type { EventData } from "../../components/app_context.js";
|
||||||
|
import { t } from "../../services/i18n.js";
|
||||||
|
import options from "../../services/options.js";
|
||||||
|
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||||
|
|
||||||
|
const TPL = `
|
||||||
|
<button type="button"
|
||||||
|
class="switch-layout-button">
|
||||||
|
<span class="bx"></span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default class SwitchSplitOrientationButton extends NoteContextAwareWidget {
|
||||||
|
isEnabled() {
|
||||||
|
return super.isEnabled()
|
||||||
|
&& ["mermaid"].includes(this.note?.type ?? "")
|
||||||
|
&& this.note?.isContentAvailable()
|
||||||
|
&& !this.note?.hasLabel("readOnly")
|
||||||
|
&& this.noteContext?.viewScope?.viewMode === "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
doRender(): void {
|
||||||
|
super.doRender();
|
||||||
|
this.$widget = $(TPL);
|
||||||
|
this.$widget.on("click", () => {
|
||||||
|
const currentOrientation = options.get("splitEditorOrientation");
|
||||||
|
options.save("splitEditorOrientation", toggleOrientation(currentOrientation));
|
||||||
|
});
|
||||||
|
this.#adjustIcon();
|
||||||
|
this.contentSized();
|
||||||
|
}
|
||||||
|
|
||||||
|
#adjustIcon() {
|
||||||
|
const currentOrientation = options.get("splitEditorOrientation");
|
||||||
|
const upcomingOrientation = toggleOrientation(currentOrientation);
|
||||||
|
const $icon = this.$widget.find("span.bx");
|
||||||
|
$icon
|
||||||
|
.toggleClass("bxs-dock-bottom", upcomingOrientation === "vertical")
|
||||||
|
.toggleClass("bxs-dock-left", upcomingOrientation === "horizontal");
|
||||||
|
|
||||||
|
if (upcomingOrientation === "vertical") {
|
||||||
|
this.$widget.attr("title", t("switch_layout_button.title_vertical"));
|
||||||
|
} else {
|
||||||
|
this.$widget.attr("title", t("switch_layout_button.title_horizontal"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||||
|
if (loadResults.isOptionReloaded("splitEditorOrientation")) {
|
||||||
|
this.#adjustIcon();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleOrientation(orientation: string) {
|
||||||
|
if (orientation === "horizontal") {
|
||||||
|
return "vertical";
|
||||||
|
} else {
|
||||||
|
return "horizontal";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type FNote from "../../entities/fnote.js";
|
||||||
|
import attributes from "../../services/attributes.js";
|
||||||
|
import { t } from "../../services/i18n.js";
|
||||||
|
import OnClickButtonWidget from "../buttons/onclick_button.js";
|
||||||
|
|
||||||
|
export default class ToggleReadOnlyButton extends OnClickButtonWidget {
|
||||||
|
|
||||||
|
private isReadOnly?: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this
|
||||||
|
.title(() => this.isReadOnly ? t("toggle_read_only_button.unlock-editing") : t("toggle_read_only_button.lock-editing"))
|
||||||
|
.titlePlacement("bottom")
|
||||||
|
.icon(() => this.isReadOnly ? "bx-lock-open-alt" : "bx-lock-alt")
|
||||||
|
.onClick(() => this.#toggleReadOnly());
|
||||||
|
}
|
||||||
|
|
||||||
|
#toggleReadOnly() {
|
||||||
|
if (!this.noteId || !this.note) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isReadOnly) {
|
||||||
|
attributes.removeOwnedLabelByName(this.note, "readOnly");
|
||||||
|
} else {
|
||||||
|
attributes.setLabel(this.noteId, "readOnly");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshWithNote(note: FNote | null | undefined) {
|
||||||
|
const isReadOnly = !!note?.hasLabel("readOnly");
|
||||||
|
|
||||||
|
if (isReadOnly !== this.isReadOnly) {
|
||||||
|
this.isReadOnly = isReadOnly;
|
||||||
|
this.refreshIcon();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled() {
|
||||||
|
return super.isEnabled()
|
||||||
|
&& this.note?.type === "mermaid"
|
||||||
|
&& this.note?.isContentAvailable()
|
||||||
|
&& this.noteContext?.viewScope?.viewMode === "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import froca from "../../services/froca.js";
|
|||||||
import type FNote from "../../entities/fnote.js";
|
import type FNote from "../../entities/fnote.js";
|
||||||
|
|
||||||
const TPL = `
|
const TPL = `
|
||||||
<div class="backlinks-widget">
|
<div class="backlinks-widget has-overflow">
|
||||||
<style>
|
<style>
|
||||||
.backlinks-widget {
|
.backlinks-widget {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -61,7 +61,7 @@ const TPL = `
|
|||||||
<span class="backlinks-count"></span>
|
<span class="backlinks-count"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="backlinks-items" style="display: none;"></div>
|
<div class="backlinks-items dropdown-menu" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -121,7 +121,8 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggle(show: boolean) {
|
toggle(show: boolean) {
|
||||||
this.$widget.toggleClass("hidden-no-content", !show);
|
this.$widget.toggleClass("hidden-no-content", !show)
|
||||||
|
.toggleClass("visible", !!show);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearItems() {
|
clearItems() {
|
||||||
|
|||||||
@@ -1,261 +0,0 @@
|
|||||||
import { t } from "../services/i18n.js";
|
|
||||||
import libraryLoader from "../services/library_loader.js";
|
|
||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
|
||||||
import server from "../services/server.js";
|
|
||||||
import utils from "../services/utils.js";
|
|
||||||
import { loadElkIfNeeded, postprocessMermaidSvg } from "../services/mermaid.js";
|
|
||||||
import type FNote from "../entities/fnote.js";
|
|
||||||
import type { EventData } from "../components/app_context.js";
|
|
||||||
import ScrollingContainer from "./containers/scrolling_container.js";
|
|
||||||
import Split from "split.js";
|
|
||||||
import { DEFAULT_GUTTER_SIZE } from "../services/resizer.js";
|
|
||||||
|
|
||||||
const TPL = `<div class="mermaid-widget">
|
|
||||||
<style>
|
|
||||||
.mermaid-widget {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.mobile .mermaid-widget {
|
|
||||||
min-height: 200px;
|
|
||||||
flex-grow: 2;
|
|
||||||
flex-basis: 0;
|
|
||||||
border-bottom: 1px solid var(--main-border-color);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.desktop .mermaid-widget + .gutter {
|
|
||||||
border-bottom: 1px solid var(--main-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mermaid-render {
|
|
||||||
overflow: auto;
|
|
||||||
height: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="mermaid-error alert alert-warning">
|
|
||||||
<p><strong>${t("mermaid.diagram_error")}</strong></p>
|
|
||||||
<p class="error-content"></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mermaid-render"></div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
let idCounter = 1;
|
|
||||||
|
|
||||||
export default class MermaidWidget extends NoteContextAwareWidget {
|
|
||||||
|
|
||||||
private $display!: JQuery<HTMLElement>;
|
|
||||||
private $errorContainer!: JQuery<HTMLElement>;
|
|
||||||
private $errorMessage!: JQuery<HTMLElement>;
|
|
||||||
private dirtyAttachment?: boolean;
|
|
||||||
private zoomHandler?: () => void;
|
|
||||||
private zoomInstance?: SvgPanZoom.Instance;
|
|
||||||
private splitInstance?: Split.Instance;
|
|
||||||
private lastNote?: FNote;
|
|
||||||
|
|
||||||
isEnabled() {
|
|
||||||
return super.isEnabled() && this.note?.type === "mermaid" && this.note.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
|
|
||||||
}
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
this.contentSized();
|
|
||||||
this.$display = this.$widget.find(".mermaid-render");
|
|
||||||
this.$errorContainer = this.$widget.find(".mermaid-error");
|
|
||||||
this.$errorMessage = this.$errorContainer.find(".error-content");
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshWithNote(note: FNote) {
|
|
||||||
const isSameNote = (this.lastNote === note);
|
|
||||||
|
|
||||||
this.cleanup();
|
|
||||||
this.$errorContainer.hide();
|
|
||||||
|
|
||||||
await libraryLoader.requireLibrary(libraryLoader.MERMAID);
|
|
||||||
|
|
||||||
mermaid.mermaidAPI.initialize({
|
|
||||||
startOnLoad: false,
|
|
||||||
...(getMermaidConfig() as any)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isSameNote) {
|
|
||||||
this.$display.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$errorContainer.hide();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const svg = await this.renderSvg();
|
|
||||||
|
|
||||||
if (this.dirtyAttachment) {
|
|
||||||
const payload = {
|
|
||||||
role: "image",
|
|
||||||
title: "mermaid-export.svg",
|
|
||||||
mime: "image/svg+xml",
|
|
||||||
content: svg,
|
|
||||||
position: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
server.post(`notes/${this.noteId}/attachments?matchBy=title`, payload).then(() => {
|
|
||||||
this.dirtyAttachment = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$display.html(svg);
|
|
||||||
this.$display.attr("id", `mermaid-render-${idCounter}`);
|
|
||||||
|
|
||||||
// Fit the image to bounds.
|
|
||||||
const $svg = this.$display.find("svg");
|
|
||||||
$svg.attr("width", "100%").attr("height", "100%");
|
|
||||||
|
|
||||||
// Enable pan to zoom.
|
|
||||||
this.#setupPanZoom($svg[0], isSameNote);
|
|
||||||
} catch (e: any) {
|
|
||||||
console.warn(e);
|
|
||||||
this.#cleanUpZoom();
|
|
||||||
this.$display.empty();
|
|
||||||
this.$errorMessage.text(e.message);
|
|
||||||
this.$errorContainer.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#setupResizer();
|
|
||||||
this.lastNote = note;
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
super.cleanup();
|
|
||||||
if (this.zoomHandler) {
|
|
||||||
$(window).off("resize", this.zoomHandler);
|
|
||||||
this.zoomHandler = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#cleanUpZoom() {
|
|
||||||
if (this.zoomInstance) {
|
|
||||||
this.zoomInstance.destroy();
|
|
||||||
this.zoomInstance = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleInt(show: boolean | null | undefined): void {
|
|
||||||
super.toggleInt(show);
|
|
||||||
|
|
||||||
if (!show) {
|
|
||||||
this.cleanup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async renderSvg() {
|
|
||||||
idCounter++;
|
|
||||||
|
|
||||||
if (!this.note) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = await this.note.getBlob();
|
|
||||||
const content = blob?.content || "";
|
|
||||||
|
|
||||||
await loadElkIfNeeded(content);
|
|
||||||
const { svg } = await mermaid.mermaidAPI.render(`mermaid-graph-${idCounter}`, content);
|
|
||||||
return postprocessMermaidSvg(svg);
|
|
||||||
}
|
|
||||||
|
|
||||||
async #setupPanZoom(svgEl: SVGElement, isSameNote: boolean) {
|
|
||||||
// Clean up
|
|
||||||
let pan = null;
|
|
||||||
let zoom = null;
|
|
||||||
if (this.zoomInstance) {
|
|
||||||
// Store pan and zoom for same note, when the user is editing the note.
|
|
||||||
if (isSameNote && this.zoomInstance) {
|
|
||||||
pan = this.zoomInstance.getPan();
|
|
||||||
zoom = this.zoomInstance.getZoom();
|
|
||||||
}
|
|
||||||
this.#cleanUpZoom();
|
|
||||||
}
|
|
||||||
|
|
||||||
const svgPanZoom = (await import("svg-pan-zoom")).default;
|
|
||||||
const zoomInstance = svgPanZoom(svgEl, {
|
|
||||||
zoomEnabled: true,
|
|
||||||
controlIconsEnabled: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (pan && zoom) {
|
|
||||||
// Restore the pan and zoom.
|
|
||||||
zoomInstance.zoom(zoom);
|
|
||||||
zoomInstance.pan(pan);
|
|
||||||
} else {
|
|
||||||
// New instance, reposition properly.
|
|
||||||
zoomInstance.center();
|
|
||||||
zoomInstance.fit();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.zoomHandler = () => {
|
|
||||||
zoomInstance.resize();
|
|
||||||
zoomInstance.fit();
|
|
||||||
zoomInstance.center();
|
|
||||||
};
|
|
||||||
this.zoomInstance = zoomInstance;
|
|
||||||
$(window).on("resize", this.zoomHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
#setupResizer() {
|
|
||||||
if (!utils.isDesktop()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selfEl = this.$widget;
|
|
||||||
const scrollingContainer = this.parent?.children.find((ch) => ch instanceof ScrollingContainer)?.$widget;
|
|
||||||
|
|
||||||
if (!selfEl.length || !scrollingContainer?.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.splitInstance) {
|
|
||||||
this.splitInstance = Split([ selfEl[0], scrollingContainer[0] ], {
|
|
||||||
sizes: [ 50, 50 ],
|
|
||||||
direction: "vertical",
|
|
||||||
gutterSize: DEFAULT_GUTTER_SIZE,
|
|
||||||
onDragEnd: () => this.zoomHandler?.()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
|
||||||
if (this.noteId && loadResults.isNoteContentReloaded(this.noteId)) {
|
|
||||||
this.dirtyAttachment = true;
|
|
||||||
|
|
||||||
await this.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async exportSvgEvent({ ntxId }: EventData<"exportSvg">) {
|
|
||||||
if (!this.isNoteContext(ntxId) || this.note?.type !== "mermaid") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const svg = await this.renderSvg();
|
|
||||||
utils.downloadSvg(this.note.title, svg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMermaidConfig(): MermaidConfig {
|
|
||||||
const documentStyle = window.getComputedStyle(document.documentElement);
|
|
||||||
const mermaidTheme = documentStyle.getPropertyValue("--mermaid-theme");
|
|
||||||
|
|
||||||
return {
|
|
||||||
theme: mermaidTheme.trim(),
|
|
||||||
securityLevel: "antiscript",
|
|
||||||
// TODO: Are all these options correct?
|
|
||||||
flow: { useMaxWidth: false },
|
|
||||||
sequence: { useMaxWidth: false },
|
|
||||||
gantt: { useMaxWidth: false },
|
|
||||||
class: { useMaxWidth: false },
|
|
||||||
state: { useMaxWidth: false },
|
|
||||||
pie: { useMaxWidth: true },
|
|
||||||
journey: { useMaxWidth: false },
|
|
||||||
git: { useMaxWidth: false }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -5,10 +5,9 @@ import type NoteContext from "../components/note_context.js";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This widget allows for changing and updating depending on the active note.
|
* This widget allows for changing and updating depending on the active note.
|
||||||
* @extends {BasicWidget}
|
|
||||||
*/
|
*/
|
||||||
class NoteContextAwareWidget extends BasicWidget {
|
class NoteContextAwareWidget extends BasicWidget {
|
||||||
protected noteContext?: NoteContext;
|
noteContext?: NoteContext;
|
||||||
|
|
||||||
isNoteContext(ntxId: string | string[] | null | undefined) {
|
isNoteContext(ntxId: string | string[] | null | undefined) {
|
||||||
if (Array.isArray(ntxId)) {
|
if (Array.isArray(ntxId)) {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import utils from "../services/utils.js";
|
|||||||
import type { NoteType } from "../entities/fnote.js";
|
import type { NoteType } from "../entities/fnote.js";
|
||||||
import type TypeWidget from "./type_widgets/type_widget.js";
|
import type TypeWidget from "./type_widgets/type_widget.js";
|
||||||
import LlmChatTypeWidget from "./type_widgets/llm_chat.js";
|
import LlmChatTypeWidget from "./type_widgets/llm_chat.js";
|
||||||
|
import { MermaidTypeWidget } from "./type_widgets/mermaid.js";
|
||||||
|
|
||||||
const TPL = `
|
const TPL = `
|
||||||
<div class="note-detail">
|
<div class="note-detail">
|
||||||
@@ -74,7 +75,10 @@ const typeWidgetClasses = {
|
|||||||
attachmentList: AttachmentListTypeWidget,
|
attachmentList: AttachmentListTypeWidget,
|
||||||
mindMap: MindMapWidget,
|
mindMap: MindMapWidget,
|
||||||
geoMap: GeoMapTypeWidget,
|
geoMap: GeoMapTypeWidget,
|
||||||
llmChat: LlmChatTypeWidget
|
llmChat: LlmChatTypeWidget,
|
||||||
|
|
||||||
|
// Split type editors
|
||||||
|
mermaid: MermaidTypeWidget
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,7 +86,7 @@ const typeWidgetClasses = {
|
|||||||
* for protected session or attachment information.
|
* for protected session or attachment information.
|
||||||
*/
|
*/
|
||||||
type ExtendedNoteType =
|
type ExtendedNoteType =
|
||||||
| Exclude<NoteType, "mermaid" | "launcher" | "text" | "code">
|
| Exclude<NoteType, "launcher" | "text" | "code">
|
||||||
| "empty"
|
| "empty"
|
||||||
| "readOnlyCode"
|
| "readOnlyCode"
|
||||||
| "readOnlyText"
|
| "readOnlyText"
|
||||||
@@ -196,7 +200,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
|||||||
// https://github.com/zadam/trilium/issues/2522
|
// https://github.com/zadam/trilium/issues/2522
|
||||||
const isBackendNote = this.noteContext?.noteId === "_backendLog";
|
const isBackendNote = this.noteContext?.noteId === "_backendLog";
|
||||||
const isSqlNote = this.mime === "text/x-sqlite;schema=trilium";
|
const isSqlNote = this.mime === "text/x-sqlite;schema=trilium";
|
||||||
const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "geoMap"].includes(this.type ?? "");
|
const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "geoMap", "mermaid"].includes(this.type ?? "");
|
||||||
const isFullHeight = (!this.noteContext?.hasNoteList() && isFullHeightNoteType && !isSqlNote)
|
const isFullHeight = (!this.noteContext?.hasNoteList() && isFullHeightNoteType && !isSqlNote)
|
||||||
|| this.noteContext?.viewScope?.viewMode === "attachments"
|
|| this.noteContext?.viewScope?.viewMode === "attachments"
|
||||||
|| isBackendNote;
|
|| isBackendNote;
|
||||||
@@ -235,7 +239,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
|||||||
resultingType = "readOnlyCode";
|
resultingType = "readOnlyCode";
|
||||||
} else if (type === "text") {
|
} else if (type === "text") {
|
||||||
resultingType = "editableText";
|
resultingType = "editableText";
|
||||||
} else if (type === "code" || type === "mermaid") {
|
} else if (type === "code") {
|
||||||
resultingType = "editableCode";
|
resultingType = "editableCode";
|
||||||
} else if (type === "launcher") {
|
} else if (type === "launcher") {
|
||||||
resultingType = "doc";
|
resultingType = "doc";
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import server from "../services/server.js";
|
|||||||
import type FNote from "../entities/fnote.js";
|
import type FNote from "../entities/fnote.js";
|
||||||
import type { EventData } from "../components/app_context.js";
|
import type { EventData } from "../components/app_context.js";
|
||||||
import type { Icon } from "./icon_list.js";
|
import type { Icon } from "./icon_list.js";
|
||||||
|
import { Dropdown } from "bootstrap";
|
||||||
|
|
||||||
const TPL = `
|
const TPL = `
|
||||||
<div class="note-icon-widget dropdown">
|
<div class="note-icon-widget dropdown">
|
||||||
@@ -88,6 +89,7 @@ interface IconToCountCache {
|
|||||||
|
|
||||||
export default class NoteIconWidget extends NoteContextAwareWidget {
|
export default class NoteIconWidget extends NoteContextAwareWidget {
|
||||||
|
|
||||||
|
private dropdown!: bootstrap.Dropdown;
|
||||||
private $icon!: JQuery<HTMLElement>;
|
private $icon!: JQuery<HTMLElement>;
|
||||||
private $iconList!: JQuery<HTMLElement>;
|
private $iconList!: JQuery<HTMLElement>;
|
||||||
private $iconCategory!: JQuery<HTMLElement>;
|
private $iconCategory!: JQuery<HTMLElement>;
|
||||||
@@ -96,6 +98,8 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
doRender() {
|
doRender() {
|
||||||
this.$widget = $(TPL);
|
this.$widget = $(TPL);
|
||||||
|
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]);
|
||||||
|
|
||||||
this.$icon = this.$widget.find("button.note-icon");
|
this.$icon = this.$widget.find("button.note-icon");
|
||||||
this.$iconList = this.$widget.find(".icon-list");
|
this.$iconList = this.$widget.find(".icon-list");
|
||||||
this.$iconList.on("click", "span", async (e) => {
|
this.$iconList.on("click", "span", async (e) => {
|
||||||
@@ -130,6 +134,8 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
async refreshWithNote(note: FNote) {
|
async refreshWithNote(note: FNote) {
|
||||||
this.$icon.removeClass().addClass(`${note.getIcon()} note-icon`);
|
this.$icon.removeClass().addClass(`${note.getIcon()} note-icon`);
|
||||||
|
this.$icon.prop("disabled", !!(this.noteContext?.viewScope?.viewMode !== "default"));
|
||||||
|
this.dropdown.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||||
|
|||||||
@@ -19,25 +19,10 @@ const TPL = `<div class="note-map-widget">
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-type-switcher {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
left: 10px;
|
|
||||||
z-index: 10; /* should be below dropdown (note actions) */
|
|
||||||
}
|
|
||||||
|
|
||||||
.map-type-switcher button.bx {
|
|
||||||
font-size: 130%;
|
|
||||||
padding: 1px 10px 1px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Style Ui Element to Drag Nodes */
|
/* Style Ui Element to Drag Nodes */
|
||||||
.fixnodes-type-switcher {
|
.fixnodes-type-switcher {
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
bottom: 10px;
|
|
||||||
left: 10px;
|
|
||||||
z-index: 10; /* should be below dropdown (note actions) */
|
z-index: 10; /* should be below dropdown (note actions) */
|
||||||
border-radius: .2rem;
|
border-radius: .2rem;
|
||||||
}
|
}
|
||||||
@@ -94,14 +79,14 @@ const TPL = `<div class="note-map-widget">
|
|||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="btn-group btn-group-sm map-type-switcher" role="group">
|
<div class="btn-group btn-group-sm map-type-switcher content-floating-buttons top-left" role="group">
|
||||||
<button type="button" class="btn bx bx-network-chart tn-tool-button" title="${t("note-map.button-link-map")}" data-type="link"></button>
|
<button type="button" class="btn bx bx-network-chart tn-tool-button" title="${t("note-map.button-link-map")}" data-type="link"></button>
|
||||||
<button type="button" class="btn bx bx-sitemap tn-tool-button" title="${t("note-map.button-tree-map")}" data-type="tree"></button>
|
<button type="button" class="btn bx bx-sitemap tn-tool-button" title="${t("note-map.button-tree-map")}" data-type="tree"></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<! UI for dragging Notes and link force >
|
<! UI for dragging Notes and link force >
|
||||||
|
|
||||||
<div class=" btn-group-sm fixnodes-type-switcher" role="group">
|
<div class="btn-group-sm fixnodes-type-switcher content-floating-buttons bottom-left" role="group">
|
||||||
<button type="button" data-toggle="button" class="btn bx bx-lock-alt tn-tool-button" title="${t("note_map.fix-nodes")}" data-type="moveable"></button>
|
<button type="button" data-toggle="button" class="btn bx bx-lock-alt tn-tool-button" title="${t("note_map.fix-nodes")}" data-type="moveable"></button>
|
||||||
<input type="range" class="slider" min="1" title="${t("note_map.link-distance")}" max="100" value="40" >
|
<input type="range" class="slider" min="1" title="${t("note_map.link-distance")}" max="100" value="40" >
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export default class BasicPropertiesWidget extends NoteContextAwareWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$widget.find(".editability-select-container").toggle(this.note && ["text", "code"].includes(this.note.type));
|
this.$widget.find(".editability-select-container").toggle(this.note && ["text", "code", "mermaid"].includes(this.note.type));
|
||||||
this.$widget.find(".note-language-container").toggle(this.note && ["text"].includes(this.note.type));
|
this.$widget.find(".note-language-container").toggle(this.note && ["text"].includes(this.note.type));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import openService from "../../services/open.js";
|
|||||||
import utils from "../../services/utils.js";
|
import utils from "../../services/utils.js";
|
||||||
import protectedSessionHolder from "../../services/protected_session_holder.js";
|
import protectedSessionHolder from "../../services/protected_session_holder.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
|
import type FNote from "../../entities/fnote.js";
|
||||||
|
|
||||||
const TPL = `
|
const TPL = `
|
||||||
<div class="file-properties-widget">
|
<div class="file-properties-widget">
|
||||||
@@ -66,6 +67,16 @@ const TPL = `
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
export default class FilePropertiesWidget extends NoteContextAwareWidget {
|
export default class FilePropertiesWidget extends NoteContextAwareWidget {
|
||||||
|
|
||||||
|
private $fileNoteId!: JQuery<HTMLElement>;
|
||||||
|
private $fileName!: JQuery<HTMLElement>;
|
||||||
|
private $fileType!: JQuery<HTMLElement>;
|
||||||
|
private $fileSize!: JQuery<HTMLElement>;
|
||||||
|
private $downloadButton!: JQuery<HTMLElement>;
|
||||||
|
private $openButton!: JQuery<HTMLElement>;
|
||||||
|
private $uploadNewRevisionButton!: JQuery<HTMLElement>;
|
||||||
|
private $uploadNewRevisionInput!: JQuery<HTMLFormElement>;
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return "fileProperties";
|
return "fileProperties";
|
||||||
}
|
}
|
||||||
@@ -99,8 +110,8 @@ export default class FilePropertiesWidget extends NoteContextAwareWidget {
|
|||||||
this.$uploadNewRevisionButton = this.$widget.find(".file-upload-new-revision");
|
this.$uploadNewRevisionButton = this.$widget.find(".file-upload-new-revision");
|
||||||
this.$uploadNewRevisionInput = this.$widget.find(".file-upload-new-revision-input");
|
this.$uploadNewRevisionInput = this.$widget.find(".file-upload-new-revision-input");
|
||||||
|
|
||||||
this.$downloadButton.on("click", () => openService.downloadFileNote(this.noteId));
|
this.$downloadButton.on("click", () => this.noteId && openService.downloadFileNote(this.noteId));
|
||||||
this.$openButton.on("click", () => openService.openNoteExternally(this.noteId, this.note.mime));
|
this.$openButton.on("click", () => this.noteId && this.note && openService.openNoteExternally(this.noteId, this.note.mime));
|
||||||
|
|
||||||
this.$uploadNewRevisionButton.on("click", () => {
|
this.$uploadNewRevisionButton.on("click", () => {
|
||||||
this.$uploadNewRevisionInput.trigger("click");
|
this.$uploadNewRevisionInput.trigger("click");
|
||||||
@@ -122,16 +133,20 @@ export default class FilePropertiesWidget extends NoteContextAwareWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshWithNote(note) {
|
async refreshWithNote(note: FNote) {
|
||||||
this.$widget.show();
|
this.$widget.show();
|
||||||
|
|
||||||
|
if (!this.note) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.$fileNoteId.text(note.noteId);
|
this.$fileNoteId.text(note.noteId);
|
||||||
this.$fileName.text(note.getLabelValue("originalFileName") || "?");
|
this.$fileName.text(note.getLabelValue("originalFileName") || "?");
|
||||||
this.$fileType.text(note.mime);
|
this.$fileType.text(note.mime);
|
||||||
|
|
||||||
const blob = await this.note.getBlob();
|
const blob = await this.note.getBlob();
|
||||||
|
|
||||||
this.$fileSize.text(utils.formatSize(blob.contentLength));
|
this.$fileSize.text(utils.formatSize(blob?.contentLength ?? 0));
|
||||||
|
|
||||||
// open doesn't work for protected notes since it works through a browser which isn't in protected session
|
// open doesn't work for protected notes since it works through a browser which isn't in protected session
|
||||||
this.$openButton.toggle(!note.isProtected);
|
this.$openButton.toggle(!note.isProtected);
|
||||||
@@ -4,6 +4,7 @@ import toastService from "../../services/toast.js";
|
|||||||
import openService from "../../services/open.js";
|
import openService from "../../services/open.js";
|
||||||
import utils from "../../services/utils.js";
|
import utils from "../../services/utils.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
|
import type FNote from "../../entities/fnote.js";
|
||||||
|
|
||||||
const TPL = `
|
const TPL = `
|
||||||
<div class="image-properties">
|
<div class="image-properties">
|
||||||
@@ -50,6 +51,16 @@ const TPL = `
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
export default class ImagePropertiesWidget extends NoteContextAwareWidget {
|
export default class ImagePropertiesWidget extends NoteContextAwareWidget {
|
||||||
|
|
||||||
|
private $copyReferenceToClipboardButton!: JQuery<HTMLElement>;
|
||||||
|
private $uploadNewRevisionButton!: JQuery<HTMLElement>;
|
||||||
|
private $uploadNewRevisionInput!: JQuery<HTMLFormElement>;
|
||||||
|
private $fileName!: JQuery<HTMLElement>;
|
||||||
|
private $fileType!: JQuery<HTMLElement>;
|
||||||
|
private $fileSize!: JQuery<HTMLElement>;
|
||||||
|
private $openButton!: JQuery<HTMLElement>;
|
||||||
|
private $imageDownloadButton!: JQuery<HTMLElement>;
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return "imageProperties";
|
return "imageProperties";
|
||||||
}
|
}
|
||||||
@@ -76,7 +87,7 @@ export default class ImagePropertiesWidget extends NoteContextAwareWidget {
|
|||||||
this.contentSized();
|
this.contentSized();
|
||||||
|
|
||||||
this.$copyReferenceToClipboardButton = this.$widget.find(".image-copy-reference-to-clipboard");
|
this.$copyReferenceToClipboardButton = this.$widget.find(".image-copy-reference-to-clipboard");
|
||||||
this.$copyReferenceToClipboardButton.on("click", () => this.triggerEvent(`copyImageReferenceToClipboard`, { ntxId: this.noteContext.ntxId }));
|
this.$copyReferenceToClipboardButton.on("click", () => this.triggerEvent(`copyImageReferenceToClipboard`, { ntxId: this.noteContext?.ntxId }));
|
||||||
|
|
||||||
this.$uploadNewRevisionButton = this.$widget.find(".image-upload-new-revision");
|
this.$uploadNewRevisionButton = this.$widget.find(".image-upload-new-revision");
|
||||||
this.$uploadNewRevisionInput = this.$widget.find(".image-upload-new-revision-input");
|
this.$uploadNewRevisionInput = this.$widget.find(".image-upload-new-revision-input");
|
||||||
@@ -86,10 +97,10 @@ export default class ImagePropertiesWidget extends NoteContextAwareWidget {
|
|||||||
this.$fileSize = this.$widget.find(".image-filesize");
|
this.$fileSize = this.$widget.find(".image-filesize");
|
||||||
|
|
||||||
this.$openButton = this.$widget.find(".image-open");
|
this.$openButton = this.$widget.find(".image-open");
|
||||||
this.$openButton.on("click", () => openService.openNoteExternally(this.noteId, this.note.mime));
|
this.$openButton.on("click", () => this.noteId && this.note && openService.openNoteExternally(this.noteId, this.note.mime));
|
||||||
|
|
||||||
this.$imageDownloadButton = this.$widget.find(".image-download");
|
this.$imageDownloadButton = this.$widget.find(".image-download");
|
||||||
this.$imageDownloadButton.on("click", () => openService.downloadFileNote(this.noteId));
|
this.$imageDownloadButton.on("click", () => this.noteId && openService.downloadFileNote(this.noteId));
|
||||||
|
|
||||||
this.$uploadNewRevisionButton.on("click", () => {
|
this.$uploadNewRevisionButton.on("click", () => {
|
||||||
this.$uploadNewRevisionInput.trigger("click");
|
this.$uploadNewRevisionInput.trigger("click");
|
||||||
@@ -113,13 +124,13 @@ export default class ImagePropertiesWidget extends NoteContextAwareWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshWithNote(note) {
|
async refreshWithNote(note: FNote) {
|
||||||
this.$widget.show();
|
this.$widget.show();
|
||||||
|
|
||||||
const blob = await this.note.getBlob();
|
const blob = await this.note?.getBlob();
|
||||||
|
|
||||||
this.$fileName.text(note.getLabelValue("originalFileName") || "?");
|
this.$fileName.text(note.getLabelValue("originalFileName") || "?");
|
||||||
this.$fileSize.text(utils.formatSize(blob.contentLength));
|
this.$fileSize.text(utils.formatSize(blob?.contentLength ?? 0));
|
||||||
this.$fileType.text(note.mime);
|
this.$fileType.text(note.mime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,8 @@ import AttributeDetailWidget from "../attribute_widgets/attribute_detail.js";
|
|||||||
import attributeRenderer from "../../services/attribute_renderer.js";
|
import attributeRenderer from "../../services/attribute_renderer.js";
|
||||||
import attributeService from "../../services/attributes.js";
|
import attributeService from "../../services/attributes.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
|
import type FNote from "../../entities/fnote.js";
|
||||||
|
import type { EventData } from "../../components/app_context.js";
|
||||||
|
|
||||||
const TPL = `
|
const TPL = `
|
||||||
<div class="inherited-attributes-widget">
|
<div class="inherited-attributes-widget">
|
||||||
@@ -10,7 +12,7 @@ const TPL = `
|
|||||||
.inherited-attributes-widget {
|
.inherited-attributes-widget {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inherited-attributes-container {
|
.inherited-attributes-container {
|
||||||
color: var(--muted-text-color);
|
color: var(--muted-text-color);
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
@@ -23,6 +25,11 @@ const TPL = `
|
|||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
export default class InheritedAttributesWidget extends NoteContextAwareWidget {
|
export default class InheritedAttributesWidget extends NoteContextAwareWidget {
|
||||||
|
|
||||||
|
private attributeDetailWidget: AttributeDetailWidget;
|
||||||
|
|
||||||
|
private $container!: JQuery<HTMLElement>;
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return "inheritedAttributes";
|
return "inheritedAttributes";
|
||||||
}
|
}
|
||||||
@@ -34,7 +41,6 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
/** @type {AttributeDetailWidget} */
|
|
||||||
this.attributeDetailWidget = new AttributeDetailWidget().contentSized().setParent(this);
|
this.attributeDetailWidget = new AttributeDetailWidget().contentSized().setParent(this);
|
||||||
|
|
||||||
this.child(this.attributeDetailWidget);
|
this.child(this.attributeDetailWidget);
|
||||||
@@ -42,7 +48,7 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
getTitle() {
|
getTitle() {
|
||||||
return {
|
return {
|
||||||
show: !this.note.isLaunchBarConfig(),
|
show: !this.note?.isLaunchBarConfig(),
|
||||||
title: t("inherited_attribute_list.title"),
|
title: t("inherited_attribute_list.title"),
|
||||||
icon: "bx bx-list-plus"
|
icon: "bx bx-list-plus"
|
||||||
};
|
};
|
||||||
@@ -56,7 +62,7 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
|
|||||||
this.$widget.append(this.attributeDetailWidget.render());
|
this.$widget.append(this.attributeDetailWidget.render());
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshWithNote(note) {
|
async refreshWithNote(note: FNote) {
|
||||||
this.$container.empty();
|
this.$container.empty();
|
||||||
|
|
||||||
const inheritedAttributes = this.getInheritedAttributes(note);
|
const inheritedAttributes = this.getInheritedAttributes(note);
|
||||||
@@ -90,7 +96,7 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getInheritedAttributes(note) {
|
getInheritedAttributes(note: FNote) {
|
||||||
const attrs = note.getAttributes().filter((attr) => attr.noteId !== this.noteId);
|
const attrs = note.getAttributes().filter((attr) => attr.noteId !== this.noteId);
|
||||||
|
|
||||||
attrs.sort((a, b) => {
|
attrs.sort((a, b) => {
|
||||||
@@ -105,7 +111,7 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
|
|||||||
return attrs;
|
return attrs;
|
||||||
}
|
}
|
||||||
|
|
||||||
entitiesReloadedEvent({ loadResults }) {
|
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||||
if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) {
|
if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,10 @@ import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
|||||||
import attributeService from "../../services/attributes.js";
|
import attributeService from "../../services/attributes.js";
|
||||||
import options from "../../services/options.js";
|
import options from "../../services/options.js";
|
||||||
import utils from "../../services/utils.js";
|
import utils from "../../services/utils.js";
|
||||||
|
import type FNote from "../../entities/fnote.js";
|
||||||
|
import type { Attribute } from "../../services/attribute_parser.js";
|
||||||
|
import type FAttribute from "../../entities/fattribute.js";
|
||||||
|
import type { EventData } from "../../components/app_context.js";
|
||||||
|
|
||||||
const TPL = `
|
const TPL = `
|
||||||
<div class="promoted-attributes-widget">
|
<div class="promoted-attributes-widget">
|
||||||
@@ -60,12 +64,20 @@ const TPL = `
|
|||||||
<div class="promoted-attributes-container"></div>
|
<div class="promoted-attributes-container"></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
|
// TODO: Deduplicate
|
||||||
|
interface AttributeResult {
|
||||||
|
attributeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This widget is quite special because it's used in the desktop ribbon, but in mobile outside of ribbon.
|
* This widget is quite special because it's used in the desktop ribbon, but in mobile outside of ribbon.
|
||||||
* This works without many issues (apart from autocomplete), but it should be kept in mind when changing things
|
* This works without many issues (apart from autocomplete), but it should be kept in mind when changing things
|
||||||
* and testing.
|
* and testing.
|
||||||
*/
|
*/
|
||||||
export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||||
|
|
||||||
|
private $container!: JQuery<HTMLElement>;
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return "promotedAttributes";
|
return "promotedAttributes";
|
||||||
}
|
}
|
||||||
@@ -80,7 +92,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
|||||||
this.$container = this.$widget.find(".promoted-attributes-container");
|
this.$container = this.$widget.find(".promoted-attributes-container");
|
||||||
}
|
}
|
||||||
|
|
||||||
getTitle(note) {
|
getTitle(note: FNote) {
|
||||||
const promotedDefAttrs = note.getPromotedDefinitionAttributes();
|
const promotedDefAttrs = note.getPromotedDefinitionAttributes();
|
||||||
|
|
||||||
if (promotedDefAttrs.length === 0) {
|
if (promotedDefAttrs.length === 0) {
|
||||||
@@ -95,7 +107,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshWithNote(note) {
|
async refreshWithNote(note: FNote) {
|
||||||
this.$container.empty();
|
this.$container.empty();
|
||||||
|
|
||||||
const promotedDefAttrs = note.getPromotedDefinitionAttributes();
|
const promotedDefAttrs = note.getPromotedDefinitionAttributes();
|
||||||
@@ -116,7 +128,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
|||||||
const valueType = definitionAttr.name.startsWith("label:") ? "label" : "relation";
|
const valueType = definitionAttr.name.startsWith("label:") ? "label" : "relation";
|
||||||
const valueName = definitionAttr.name.substr(valueType.length + 1);
|
const valueName = definitionAttr.name.substr(valueType.length + 1);
|
||||||
|
|
||||||
let valueAttrs = ownedAttributes.filter((el) => el.name === valueName && el.type === valueType);
|
let valueAttrs = ownedAttributes.filter((el) => el.name === valueName && el.type === valueType) as Attribute[];
|
||||||
|
|
||||||
if (valueAttrs.length === 0) {
|
if (valueAttrs.length === 0) {
|
||||||
valueAttrs.push({
|
valueAttrs.push({
|
||||||
@@ -134,7 +146,9 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
|||||||
for (const valueAttr of valueAttrs) {
|
for (const valueAttr of valueAttrs) {
|
||||||
const $cell = await this.createPromotedAttributeCell(definitionAttr, valueAttr, valueName);
|
const $cell = await this.createPromotedAttributeCell(definitionAttr, valueAttr, valueName);
|
||||||
|
|
||||||
$cells.push($cell);
|
if ($cell) {
|
||||||
|
$cells.push($cell);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,14 +158,14 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
|||||||
this.toggleInt(true);
|
this.toggleInt(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createPromotedAttributeCell(definitionAttr, valueAttr, valueName) {
|
async createPromotedAttributeCell(definitionAttr: FAttribute, valueAttr: Attribute, valueName: string) {
|
||||||
const definition = definitionAttr.getDefinition();
|
const definition = definitionAttr.getDefinition();
|
||||||
const id = `value-${valueAttr.attributeId}`;
|
const id = `value-${valueAttr.attributeId}`;
|
||||||
|
|
||||||
const $input = $("<input>")
|
const $input = $("<input>")
|
||||||
.prop("tabindex", 200 + definitionAttr.position)
|
.prop("tabindex", 200 + definitionAttr.position)
|
||||||
.prop("id", id)
|
.prop("id", id)
|
||||||
.attr("data-attribute-id", valueAttr.noteId === this.noteId ? valueAttr.attributeId : "") // if not owned, we'll force creation of a new attribute instead of updating the inherited one
|
.attr("data-attribute-id", valueAttr.noteId === this.noteId ? valueAttr.attributeId ?? "" : "") // if not owned, we'll force creation of a new attribute instead of updating the inherited one
|
||||||
.attr("data-attribute-type", valueAttr.type)
|
.attr("data-attribute-type", valueAttr.type)
|
||||||
.attr("data-attribute-name", valueAttr.name)
|
.attr("data-attribute-name", valueAttr.name)
|
||||||
.prop("value", valueAttr.value)
|
.prop("value", valueAttr.value)
|
||||||
@@ -161,7 +175,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
|||||||
.on("change", (event) => this.promotedAttributeChanged(event));
|
.on("change", (event) => this.promotedAttributeChanged(event));
|
||||||
|
|
||||||
const $actionCell = $("<div>");
|
const $actionCell = $("<div>");
|
||||||
const $multiplicityCell = $("<td>").addClass("multiplicity").attr("nowrap", true);
|
const $multiplicityCell = $("<td>").addClass("multiplicity").attr("nowrap", "true");
|
||||||
|
|
||||||
const $wrapper = $('<div class="promoted-attribute-cell">')
|
const $wrapper = $('<div class="promoted-attribute-cell">')
|
||||||
.append(
|
.append(
|
||||||
@@ -180,12 +194,12 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
|||||||
// autocomplete for label values is just nice to have, mobile can keep labels editable without autocomplete
|
// autocomplete for label values is just nice to have, mobile can keep labels editable without autocomplete
|
||||||
if (utils.isDesktop()) {
|
if (utils.isDesktop()) {
|
||||||
// no need to await for this, can be done asynchronously
|
// no need to await for this, can be done asynchronously
|
||||||
server.get(`attribute-values/${encodeURIComponent(valueAttr.name)}`).then((attributeValues) => {
|
server.get<string[]>(`attribute-values/${encodeURIComponent(valueAttr.name)}`).then((_attributeValues) => {
|
||||||
if (attributeValues.length === 0) {
|
if (_attributeValues.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
attributeValues = attributeValues.map((attribute) => ({ value: attribute }));
|
const attributeValues = _attributeValues.map((attribute) => ({ value: attribute }));
|
||||||
|
|
||||||
$input.autocomplete(
|
$input.autocomplete(
|
||||||
{
|
{
|
||||||
@@ -245,11 +259,11 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
|||||||
const $openButton = $("<span>")
|
const $openButton = $("<span>")
|
||||||
.addClass("input-group-text open-external-link-button bx bx-window-open")
|
.addClass("input-group-text open-external-link-button bx bx-window-open")
|
||||||
.prop("title", t("promoted_attributes.open_external_link"))
|
.prop("title", t("promoted_attributes.open_external_link"))
|
||||||
.on("click", () => window.open($input.val(), "_blank"));
|
.on("click", () => window.open($input.val() as string, "_blank"));
|
||||||
|
|
||||||
$input.after($openButton);
|
$input.after($openButton);
|
||||||
} else {
|
} else {
|
||||||
ws.logError(t("promoted_attributes.unknown_label_type", { type: definitionAttr.labelType }));
|
ws.logError(t("promoted_attributes.unknown_label_type", { type: definition.labelType }));
|
||||||
}
|
}
|
||||||
} else if (valueAttr.type === "relation") {
|
} else if (valueAttr.type === "relation") {
|
||||||
if (valueAttr.value) {
|
if (valueAttr.value) {
|
||||||
@@ -290,9 +304,11 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
|||||||
valueName
|
valueName
|
||||||
);
|
);
|
||||||
|
|
||||||
$wrapper.after($new);
|
if ($new) {
|
||||||
|
$wrapper.after($new);
|
||||||
|
|
||||||
$new.find("input").trigger("focus");
|
$new.find("input").trigger("focus");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const $removeButton = $("<span>")
|
const $removeButton = $("<span>")
|
||||||
@@ -320,7 +336,9 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
|||||||
valueName
|
valueName
|
||||||
);
|
);
|
||||||
|
|
||||||
$wrapper.after($new);
|
if ($new) {
|
||||||
|
$wrapper.after($new);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$wrapper.remove();
|
$wrapper.remove();
|
||||||
@@ -332,7 +350,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
|||||||
return $wrapper;
|
return $wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
async promotedAttributeChanged(event) {
|
async promotedAttributeChanged(event: JQuery.TriggeredEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) {
|
||||||
const $attr = $(event.target);
|
const $attr = $(event.target);
|
||||||
|
|
||||||
let value;
|
let value;
|
||||||
@@ -347,7 +365,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
|||||||
value = $attr.val();
|
value = $attr.val();
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await server.put(
|
const result = await server.put<AttributeResult>(
|
||||||
`notes/${this.noteId}/attribute`,
|
`notes/${this.noteId}/attribute`,
|
||||||
{
|
{
|
||||||
attributeId: $attr.attr("data-attribute-id"),
|
attributeId: $attr.attr("data-attribute-id"),
|
||||||
@@ -365,7 +383,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
|||||||
this.$widget.find(".promoted-attribute-input:first").focus();
|
this.$widget.find(".promoted-attribute-input:first").focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
entitiesReloadedEvent({ loadResults }) {
|
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||||
if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) {
|
if (loadResults.getAttributeRows(this.componentId).find((attr) => attributeService.isAffecting(attr, this.note))) {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ export default class ScriptExecutorWidget extends NoteContextAwareWidget {
|
|||||||
show: this.isEnabled(),
|
show: this.isEnabled(),
|
||||||
activate: true,
|
activate: true,
|
||||||
title: this.isTriliumSqlite() ? t("script_executor.query") : t("script_executor.script"),
|
title: this.isTriliumSqlite() ? t("script_executor.query") : t("script_executor.script"),
|
||||||
icon: "bx bx-run"
|
icon: "bx bx-play"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { t } from "../services/i18n.js";
|
import { t } from "../services/i18n.js";
|
||||||
import BasicWidget from "./basic_widget.js";
|
import BasicWidget from "./basic_widget.js";
|
||||||
import contextMenu from "../menus/context_menu.js";
|
import contextMenu from "../menus/context_menu.js";
|
||||||
import appContext from "../components/app_context.js";
|
import appContext, { type CommandNames } from "../components/app_context.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
|
|
||||||
const TPL = `<div class="spacer"></div>`;
|
const TPL = `<div class="spacer"></div>`;
|
||||||
@@ -26,7 +26,7 @@ export default class SpacerWidget extends BasicWidget {
|
|||||||
this.$widget.on("contextmenu", (e) => {
|
this.$widget.on("contextmenu", (e) => {
|
||||||
this.$widget.tooltip("hide");
|
this.$widget.tooltip("hide");
|
||||||
|
|
||||||
contextMenu.show({
|
contextMenu.show<CommandNames>({
|
||||||
x: e.pageX,
|
x: e.pageX,
|
||||||
y: e.pageY,
|
y: e.pageY,
|
||||||
items: [{ title: t("spacer.configure_launchbar"), command: "showLaunchBarSubtree", uiIcon: "bx " + (utils.isMobile() ? "bx-mobile" : "bx-sidebar") }],
|
items: [{ title: t("spacer.configure_launchbar"), command: "showLaunchBarSubtree", uiIcon: "bx " + (utils.isMobile() ? "bx-mobile" : "bx-sidebar") }],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import BasicWidget from "./basic_widget.js";
|
|||||||
import contextMenu from "../menus/context_menu.js";
|
import contextMenu from "../menus/context_menu.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
import keyboardActionService from "../services/keyboard_actions.js";
|
import keyboardActionService from "../services/keyboard_actions.js";
|
||||||
import appContext, { type CommandListenerData, type EventData } from "../components/app_context.js";
|
import appContext, { type CommandNames, type CommandListenerData, type EventData } from "../components/app_context.js";
|
||||||
import froca from "../services/froca.js";
|
import froca from "../services/froca.js";
|
||||||
import attributeService from "../services/attributes.js";
|
import attributeService from "../services/attributes.js";
|
||||||
import type NoteContext from "../components/note_context.js";
|
import type NoteContext from "../components/note_context.js";
|
||||||
@@ -268,7 +268,7 @@ export default class TabRowWidget extends BasicWidget {
|
|||||||
|
|
||||||
const ntxId = $(e.target).closest(".note-tab").attr("data-ntx-id");
|
const ntxId = $(e.target).closest(".note-tab").attr("data-ntx-id");
|
||||||
|
|
||||||
contextMenu.show({
|
contextMenu.show<CommandNames>({
|
||||||
x: e.pageX,
|
x: e.pageX,
|
||||||
y: e.pageY,
|
y: e.pageY,
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export default class AbstractCodeTypeWidget extends TypeWidget {
|
|||||||
*
|
*
|
||||||
* @returns the extra options to be passed to the CodeMirror constructor.
|
* @returns the extra options to be passed to the CodeMirror constructor.
|
||||||
*/
|
*/
|
||||||
getExtraOpts() {
|
getExtraOpts(): Partial<CodeMirrorOpts> {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
import type FNote from "../../entities/fnote.js";
|
||||||
|
import utils from "../../services/utils.js";
|
||||||
|
import EditableCodeTypeWidget from "./editable_code.js";
|
||||||
|
import TypeWidget from "./type_widget.js";
|
||||||
|
import Split from "split.js";
|
||||||
|
import { DEFAULT_GUTTER_SIZE } from "../../services/resizer.js";
|
||||||
|
import options from "../../services/options.js";
|
||||||
|
import type SwitchSplitOrientationButton from "../floating_buttons/switch_layout_button.js";
|
||||||
|
import type { EventData } from "../../components/app_context.js";
|
||||||
|
import type OnClickButtonWidget from "../buttons/onclick_button.js";
|
||||||
|
|
||||||
|
const TPL = `\
|
||||||
|
<div class="note-detail-split note-detail-printable">
|
||||||
|
<div class="note-detail-split-editor-col">
|
||||||
|
<div class="note-detail-split-editor"></div>
|
||||||
|
<div class="admonition caution note-detail-error-container hidden-ext"></div>
|
||||||
|
</div>
|
||||||
|
<div class="note-detail-split-preview-col">
|
||||||
|
<div class="note-detail-split-preview"></div>
|
||||||
|
<div class="btn-group btn-group-sm map-type-switcher content-floating-buttons preview-buttons bottom-right" role="group"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.note-detail-split {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-split-editor-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-split-preview-col {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-split .note-detail-split-editor {
|
||||||
|
width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-split .note-detail-split-editor .note-detail-code {
|
||||||
|
contain: size !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-split .note-detail-error-container {
|
||||||
|
font-family: var(--monospace-font-family);
|
||||||
|
margin: 5px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-split .note-detail-split-preview {
|
||||||
|
transition: opacity 250ms ease-in-out;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-split .note-detail-split-preview.on-error {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal layout */
|
||||||
|
|
||||||
|
.note-detail-split.split-horizontal > .note-detail-split-preview-col {
|
||||||
|
border-left: 1px solid var(--main-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-split.split-horizontal > .note-detail-split-editor-col,
|
||||||
|
.note-detail-split.split-horizontal > .note-detail-split-preview-col {
|
||||||
|
height: 100%;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-split.split-horizontal .note-detail-split-preview {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vertical layout */
|
||||||
|
|
||||||
|
.note-detail-split.split-vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-split.split-vertical > .note-detail-split-editor-col,
|
||||||
|
.note-detail-split.split-vertical > .note-detail-split-preview-col {
|
||||||
|
width: 100%;
|
||||||
|
height: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-split.split-vertical > .note-detail-split-editor-col {
|
||||||
|
border-top: 1px solid var(--main-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-split.split-vertical .note-detail-split-preview-col {
|
||||||
|
order: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read-only view */
|
||||||
|
|
||||||
|
.note-detail-split.split-read-only .note-detail-split-preview-col {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract `TypeWidget` which contains a preview and editor pane, each displayed on half of the available screen.
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
*
|
||||||
|
* - The two panes are resizeable via a split, on desktop. The split can be optionally customized via {@link buildSplitExtraOptions}.
|
||||||
|
* - Can display errors to the user via {@link setError}.
|
||||||
|
* - Horizontal or vertical orientation for the editor/preview split, adjustable via {@link SwitchSplitOrientationButton}.
|
||||||
|
*/
|
||||||
|
export default abstract class AbstractSplitTypeWidget extends TypeWidget {
|
||||||
|
|
||||||
|
private splitInstance?: Split.Instance;
|
||||||
|
|
||||||
|
protected $preview!: JQuery<HTMLElement>;
|
||||||
|
private $editorCol!: JQuery<HTMLElement>;
|
||||||
|
private $previewCol!: JQuery<HTMLElement>;
|
||||||
|
private $editor!: JQuery<HTMLElement>;
|
||||||
|
private $errorContainer!: JQuery<HTMLElement>;
|
||||||
|
private editorTypeWidget: EditableCodeTypeWidget;
|
||||||
|
private layoutOrientation?: "horizontal" | "vertical";
|
||||||
|
private isReadOnly?: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.editorTypeWidget = new EditableCodeTypeWidget();
|
||||||
|
this.editorTypeWidget.isEnabled = () => true;
|
||||||
|
this.editorTypeWidget.getExtraOpts = this.buildEditorExtraOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
doRender(): void {
|
||||||
|
this.$widget = $(TPL);
|
||||||
|
|
||||||
|
// Preview pane
|
||||||
|
this.$previewCol = this.$widget.find(".note-detail-split-preview-col");
|
||||||
|
this.$preview = this.$widget.find(".note-detail-split-preview");
|
||||||
|
|
||||||
|
// Editor pane
|
||||||
|
this.$editorCol = this.$widget.find(".note-detail-split-editor-col");
|
||||||
|
this.$editor = this.$widget.find(".note-detail-split-editor");
|
||||||
|
this.$editor.append(this.editorTypeWidget.render());
|
||||||
|
this.$errorContainer = this.$widget.find(".note-detail-error-container");
|
||||||
|
this.#adjustLayoutOrientation();
|
||||||
|
|
||||||
|
// Preview pane buttons
|
||||||
|
const $previewButtons = this.$previewCol.find(".preview-buttons");
|
||||||
|
const previewButtons = this.buildPreviewButtons();
|
||||||
|
$previewButtons.toggle(previewButtons.length > 0);
|
||||||
|
for (const previewButton of previewButtons) {
|
||||||
|
const $button = previewButton.render();
|
||||||
|
$button.removeClass("button-widget")
|
||||||
|
.addClass("btn")
|
||||||
|
.addClass("tn-tool-button");
|
||||||
|
$previewButtons.append($button);
|
||||||
|
previewButton.refreshIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
super.doRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup(): void {
|
||||||
|
this.#destroyResizer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async doRefresh(note: FNote | null | undefined) {
|
||||||
|
this.#adjustLayoutOrientation();
|
||||||
|
|
||||||
|
if (note && !this.isReadOnly) {
|
||||||
|
await this.editorTypeWidget.initialized;
|
||||||
|
this.editorTypeWidget.noteContext = this.noteContext;
|
||||||
|
this.editorTypeWidget.spacedUpdate = this.spacedUpdate;
|
||||||
|
this.editorTypeWidget.doRefresh(note);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#adjustLayoutOrientation() {
|
||||||
|
// Read-only
|
||||||
|
const isReadOnly = this.note?.hasLabel("readOnly");
|
||||||
|
if (this.isReadOnly !== isReadOnly) {
|
||||||
|
this.$editorCol.toggle(!isReadOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertical vs horizontal layout
|
||||||
|
const layoutOrientation = (!utils.isMobile() ? options.get("splitEditorOrientation") ?? "horizontal" : "vertical");
|
||||||
|
if (this.layoutOrientation !== layoutOrientation || this.isReadOnly !== isReadOnly) {
|
||||||
|
this.$widget
|
||||||
|
.toggleClass("split-horizontal", !isReadOnly && layoutOrientation === "horizontal")
|
||||||
|
.toggleClass("split-vertical", !isReadOnly && layoutOrientation === "vertical")
|
||||||
|
.toggleClass("split-read-only", isReadOnly);
|
||||||
|
this.layoutOrientation = layoutOrientation as ("horizontal" | "vertical");
|
||||||
|
this.isReadOnly = isReadOnly;
|
||||||
|
this.#destroyResizer();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.splitInstance) {
|
||||||
|
this.#setupResizer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#setupResizer() {
|
||||||
|
if (!utils.isDesktop()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let elements = [ this.$editorCol[0], this.$previewCol[0] ];
|
||||||
|
if (this.layoutOrientation === "vertical") {
|
||||||
|
elements.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.splitInstance?.destroy();
|
||||||
|
|
||||||
|
if (!this.isReadOnly) {
|
||||||
|
this.splitInstance = Split(elements, {
|
||||||
|
sizes: [ 50, 50 ],
|
||||||
|
direction: this.layoutOrientation,
|
||||||
|
gutterSize: DEFAULT_GUTTER_SIZE,
|
||||||
|
...this.buildSplitExtraOptions()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.splitInstance = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#destroyResizer() {
|
||||||
|
this.splitInstance?.destroy();
|
||||||
|
this.splitInstance = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called upon when the split between the preview and content pane is initialized. Can be used to add additional listeners if needed.
|
||||||
|
*/
|
||||||
|
buildSplitExtraOptions(): Split.Options {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called upon when the code editor is being initialized. Can be used to add additional options to the editor.
|
||||||
|
*/
|
||||||
|
buildEditorExtraOptions(): Partial<CodeMirrorOpts> {
|
||||||
|
return {
|
||||||
|
lineWrapping: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPreviewButtons(): OnClickButtonWidget[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(message: string | null | undefined) {
|
||||||
|
this.$errorContainer.toggleClass("hidden-ext", !message);
|
||||||
|
this.$preview.toggleClass("on-error", !!message);
|
||||||
|
this.$errorContainer.text(message ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
getData() {
|
||||||
|
return this.editorTypeWidget.getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||||
|
if (loadResults.isOptionReloaded("splitEditorOrientation")) {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import type { EventData } from "../../components/app_context.js";
|
||||||
|
import type FNote from "../../entities/fnote.js";
|
||||||
|
import { t } from "../../services/i18n.js";
|
||||||
|
import server from "../../services/server.js";
|
||||||
|
import utils from "../../services/utils.js";
|
||||||
|
import OnClickButtonWidget from "../buttons/onclick_button.js";
|
||||||
|
import AbstractSplitTypeWidget from "./abstract_split_type_widget.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A specialization of `SplitTypeWidget` meant for note types that have a SVG preview.
|
||||||
|
*
|
||||||
|
* This adds the following functionality:
|
||||||
|
*
|
||||||
|
* - Automatic handling of the preview when content or the note changes via {@link renderSvg}.
|
||||||
|
* - Built-in pan and zoom functionality with automatic re-centering.
|
||||||
|
* - Automatically displays errors to the user if {@link renderSvg} failed.
|
||||||
|
* - Automatically saves the SVG attachment.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export default abstract class AbstractSvgSplitTypeWidget extends AbstractSplitTypeWidget {
|
||||||
|
|
||||||
|
private $renderContainer!: JQuery<HTMLElement>;
|
||||||
|
private zoomHandler: () => void;
|
||||||
|
private zoomInstance?: SvgPanZoom.Instance;
|
||||||
|
private svg?: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.zoomHandler = () => {
|
||||||
|
if (this.zoomInstance) {
|
||||||
|
this.zoomInstance.resize();
|
||||||
|
this.zoomInstance.fit();
|
||||||
|
this.zoomInstance.center();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doRender(): void {
|
||||||
|
super.doRender();
|
||||||
|
this.$renderContainer = $(`<div>`)
|
||||||
|
.addClass("render-container")
|
||||||
|
.css("height", "100%");
|
||||||
|
this.$preview.append(this.$renderContainer);
|
||||||
|
$(window).on("resize", this.zoomHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
async doRefresh(note: FNote | null | undefined) {
|
||||||
|
super.doRefresh(note);
|
||||||
|
|
||||||
|
const blob = await note?.getBlob();
|
||||||
|
const content = blob?.content || "";
|
||||||
|
this.onContentChanged(content, true);
|
||||||
|
|
||||||
|
// Save the SVG when entering a note only when it does not have an attachment.
|
||||||
|
this.note?.getAttachments().then((attachments) => {
|
||||||
|
const attachmentName = `${this.attachmentName}.svg`;
|
||||||
|
if (!attachments.find((a) => a.title === attachmentName)) {
|
||||||
|
this.#saveSvg();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getData(): { content: string; } {
|
||||||
|
const data = super.getData();
|
||||||
|
this.onContentChanged(data.content, false);
|
||||||
|
this.#saveSvg();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers an update of the preview pane with the provided content.
|
||||||
|
*
|
||||||
|
* @param content the content that will be passed to `renderSvg` for rendering. It is not the SVG content.
|
||||||
|
* @param recenter `true` to reposition the pan/zoom to fit the image and to center it.
|
||||||
|
*/
|
||||||
|
async onContentChanged(content: string, recenter: boolean) {
|
||||||
|
if (!this.note) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let svg: string = "";
|
||||||
|
try {
|
||||||
|
svg = await this.renderSvg(content);
|
||||||
|
|
||||||
|
// Rendering was succesful.
|
||||||
|
this.setError(null);
|
||||||
|
|
||||||
|
if (svg === this.svg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.svg = svg;
|
||||||
|
this.$renderContainer.html(svg);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
// Rendering failed.
|
||||||
|
this.setError((e as Error)?.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.#setupPanZoom(!recenter);
|
||||||
|
}
|
||||||
|
|
||||||
|
#saveSvg() {
|
||||||
|
const payload = {
|
||||||
|
role: "image",
|
||||||
|
title: `${this.attachmentName}.svg`,
|
||||||
|
mime: "image/svg+xml",
|
||||||
|
content: this.svg,
|
||||||
|
position: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
server.post(`notes/${this.noteId}/attachments?matchBy=title`, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup(): void {
|
||||||
|
this.#cleanUpZoom();
|
||||||
|
$(window).off("resize", this.zoomHandler);
|
||||||
|
super.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called upon when the SVG preview needs refreshing, such as when the editor has switched to a new note or the content has switched.
|
||||||
|
*
|
||||||
|
* The method must return a valid SVG string that will be automatically displayed in the preview.
|
||||||
|
*
|
||||||
|
* @param content the content of the note, in plain text.
|
||||||
|
*/
|
||||||
|
abstract renderSvg(content: string): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to obtain the name of the note attachment (without .svg extension) that will be used for storing the preview.
|
||||||
|
*/
|
||||||
|
abstract get attachmentName(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param preservePanZoom `true` to keep the pan/zoom settings of the previous image, or `false` to re-center it.
|
||||||
|
*/
|
||||||
|
async #setupPanZoom(preservePanZoom: boolean) {
|
||||||
|
// Clean up
|
||||||
|
let pan = null;
|
||||||
|
let zoom = null;
|
||||||
|
if (preservePanZoom && this.zoomInstance) {
|
||||||
|
// Store pan and zoom for same note, when the user is editing the note.
|
||||||
|
pan = this.zoomInstance.getPan();
|
||||||
|
zoom = this.zoomInstance.getZoom();
|
||||||
|
this.#cleanUpZoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
const $svgEl = this.$renderContainer.find("svg");
|
||||||
|
|
||||||
|
// Fit the image to bounds
|
||||||
|
$svgEl.attr("width", "100%")
|
||||||
|
.attr("height", "100%")
|
||||||
|
.css("max-width", "100%");
|
||||||
|
|
||||||
|
if (!$svgEl.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const svgPanZoom = (await import("svg-pan-zoom")).default;
|
||||||
|
const zoomInstance = svgPanZoom($svgEl[0], {
|
||||||
|
zoomEnabled: true,
|
||||||
|
controlIconsEnabled: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (preservePanZoom && pan && zoom) {
|
||||||
|
// Restore the pan and zoom.
|
||||||
|
zoomInstance.zoom(zoom);
|
||||||
|
zoomInstance.pan(pan);
|
||||||
|
} else {
|
||||||
|
// New instance, reposition properly.
|
||||||
|
zoomInstance.resize();
|
||||||
|
zoomInstance.center();
|
||||||
|
zoomInstance.fit();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.zoomInstance = zoomInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildSplitExtraOptions(): Split.Options {
|
||||||
|
return {
|
||||||
|
onDrag: () => this.zoomHandler?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPreviewButtons(): OnClickButtonWidget[] {
|
||||||
|
return [
|
||||||
|
new OnClickButtonWidget()
|
||||||
|
.icon("bx-zoom-in")
|
||||||
|
.title(t("relation_map_buttons.zoom_in_title"))
|
||||||
|
.titlePlacement("top")
|
||||||
|
.onClick(() => this.zoomInstance?.zoomIn())
|
||||||
|
, new OnClickButtonWidget()
|
||||||
|
.icon("bx-zoom-out")
|
||||||
|
.title(t("relation_map_buttons.zoom_out_title"))
|
||||||
|
.titlePlacement("top")
|
||||||
|
.onClick(() => this.zoomInstance?.zoomOut())
|
||||||
|
, new OnClickButtonWidget()
|
||||||
|
.icon("bx-crop")
|
||||||
|
.title(t("relation_map_buttons.reset_pan_zoom_title"))
|
||||||
|
.titlePlacement("top")
|
||||||
|
.onClick(() => this.zoomHandler())
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#cleanUpZoom() {
|
||||||
|
if (this.zoomInstance) {
|
||||||
|
this.zoomInstance.destroy();
|
||||||
|
this.zoomInstance = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportSvgEvent({ ntxId }: EventData<"exportSvg">) {
|
||||||
|
if (!this.isNoteContext(ntxId) || this.note?.type !== "mermaid" || !this.svg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.downloadSvg(this.note.title, this.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportPngEvent({ ntxId }: EventData<"exportPng">) {
|
||||||
|
if (!this.isNoteContext(ntxId) || this.note?.type !== "mermaid" || !this.svg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.downloadSvgAsPng(this.note.title, this.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import TypeWidget from "./type_widget.js";
|
import TypeWidget from "./type_widget.js";
|
||||||
import appContext from "../../components/app_context.js";
|
import appContext, { type EventData } from "../../components/app_context.js";
|
||||||
import froca from "../../services/froca.js";
|
import froca from "../../services/froca.js";
|
||||||
import linkService from "../../services/link.js";
|
import linkService from "../../services/link.js";
|
||||||
import contentRenderer from "../../services/content_renderer.js";
|
import contentRenderer from "../../services/content_renderer.js";
|
||||||
@@ -13,7 +13,7 @@ export default class AbstractTextTypeWidget extends TypeWidget {
|
|||||||
this.refreshCodeBlockOptions();
|
this.refreshCodeBlockOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
setupImageOpening(singleClickOpens) {
|
setupImageOpening(singleClickOpens: boolean) {
|
||||||
this.$widget.on("dblclick", "img", (e) => this.openImageInCurrentTab($(e.target)));
|
this.$widget.on("dblclick", "img", (e) => this.openImageInCurrentTab($(e.target)));
|
||||||
|
|
||||||
this.$widget.on("click", "img", (e) => {
|
this.$widget.on("click", "img", (e) => {
|
||||||
@@ -29,27 +29,27 @@ export default class AbstractTextTypeWidget extends TypeWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async openImageInCurrentTab($img) {
|
async openImageInCurrentTab($img: JQuery<HTMLElement>) {
|
||||||
const { noteId, viewScope } = await this.parseFromImage($img);
|
const parsedImage = await this.parseFromImage($img);
|
||||||
|
|
||||||
if (noteId) {
|
if (parsedImage) {
|
||||||
appContext.tabManager.getActiveContext().setNote(noteId, { viewScope });
|
appContext.tabManager.getActiveContext()?.setNote(parsedImage.noteId, { viewScope: parsedImage.viewScope });
|
||||||
} else {
|
} else {
|
||||||
window.open($img.prop("src"), "_blank");
|
window.open($img.prop("src"), "_blank");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async openImageInNewTab($img) {
|
async openImageInNewTab($img: JQuery<HTMLElement>) {
|
||||||
const { noteId, viewScope } = await this.parseFromImage($img);
|
const parsedImage = await this.parseFromImage($img);
|
||||||
|
|
||||||
if (noteId) {
|
if (parsedImage) {
|
||||||
appContext.tabManager.openTabWithNoteWithHoisting(noteId, { viewScope });
|
appContext.tabManager.openTabWithNoteWithHoisting(parsedImage.noteId, { viewScope: parsedImage.viewScope });
|
||||||
} else {
|
} else {
|
||||||
window.open($img.prop("src"), "_blank");
|
window.open($img.prop("src"), "_blank");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseFromImage($img) {
|
async parseFromImage($img: JQuery<HTMLElement>) {
|
||||||
const imgSrc = $img.prop("src");
|
const imgSrc = $img.prop("src");
|
||||||
|
|
||||||
const imageNoteMatch = imgSrc.match(/\/api\/images\/([A-Za-z0-9_]+)\//);
|
const imageNoteMatch = imgSrc.match(/\/api\/images\/([A-Za-z0-9_]+)\//);
|
||||||
@@ -66,7 +66,7 @@ export default class AbstractTextTypeWidget extends TypeWidget {
|
|||||||
const attachment = await froca.getAttachment(attachmentId);
|
const attachment = await froca.getAttachment(attachmentId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
noteId: attachment.ownerId,
|
noteId: attachment?.ownerId,
|
||||||
viewScope: {
|
viewScope: {
|
||||||
viewMode: "attachments",
|
viewMode: "attachments",
|
||||||
attachmentId: attachmentId
|
attachmentId: attachmentId
|
||||||
@@ -77,7 +77,7 @@ export default class AbstractTextTypeWidget extends TypeWidget {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadIncludedNote(noteId, $el) {
|
async loadIncludedNote(noteId: string, $el: JQuery<HTMLElement>) {
|
||||||
const note = await froca.getNote(noteId);
|
const note = await froca.getNote(noteId);
|
||||||
|
|
||||||
if (note) {
|
if (note) {
|
||||||
@@ -97,11 +97,11 @@ export default class AbstractTextTypeWidget extends TypeWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadReferenceLinkTitle($el, href = null) {
|
async loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | null = null) {
|
||||||
await linkService.loadReferenceLinkTitle($el, href);
|
await linkService.loadReferenceLinkTitle($el, href);
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshIncludedNote($container, noteId) {
|
refreshIncludedNote($container: JQuery<HTMLElement>, noteId: string) {
|
||||||
if ($container) {
|
if ($container) {
|
||||||
$container.find(`section[data-note-id="${noteId}"]`).each((_, el) => {
|
$container.find(`section[data-note-id="${noteId}"]`).each((_, el) => {
|
||||||
this.loadIncludedNote(noteId, $(el));
|
this.loadIncludedNote(noteId, $(el));
|
||||||
@@ -114,7 +114,7 @@ export default class AbstractTextTypeWidget extends TypeWidget {
|
|||||||
this.$widget.toggleClass("word-wrap", wordWrap);
|
this.$widget.toggleClass("word-wrap", wordWrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
async entitiesReloadedEvent({ loadResults }) {
|
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||||
if (loadResults.isOptionReloaded("codeBlockWordWrap")) {
|
if (loadResults.isOptionReloaded("codeBlockWordWrap")) {
|
||||||
this.refreshCodeBlockOptions();
|
this.refreshCodeBlockOptions();
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ import library_loader from "../../../services/library_loader.js";
|
|||||||
import mime_types from "../../../services/mime_types.js";
|
import mime_types from "../../../services/mime_types.js";
|
||||||
import { isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
|
import { isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
|
||||||
|
|
||||||
export async function initSyntaxHighlighting(editor) {
|
export async function initSyntaxHighlighting(editor: TextEditor) {
|
||||||
if (!isSyntaxHighlightEnabled) {
|
if (!isSyntaxHighlightEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -25,39 +25,38 @@ const HIGHLIGHT_MAX_BLOCK_COUNT = 500;
|
|||||||
|
|
||||||
const tag = "SyntaxHighlightWidget";
|
const tag = "SyntaxHighlightWidget";
|
||||||
const debugLevels = ["error", "warn", "info", "log", "debug"];
|
const debugLevels = ["error", "warn", "info", "log", "debug"];
|
||||||
const debugLevel = "debug";
|
const debugLevel = debugLevels.indexOf("warn");
|
||||||
|
|
||||||
let warn = function () {};
|
let warn = function (...args: unknown[]) {};
|
||||||
if (debugLevel >= debugLevels.indexOf("warn")) {
|
if (debugLevel >= debugLevels.indexOf("warn")) {
|
||||||
warn = console.warn.bind(console, tag + ": ");
|
warn = console.warn.bind(console, tag + ": ");
|
||||||
}
|
}
|
||||||
|
|
||||||
let info = function () {};
|
let info = function (...args: unknown[]) {};
|
||||||
if (debugLevel >= debugLevels.indexOf("info")) {
|
if (debugLevel >= debugLevels.indexOf("info")) {
|
||||||
info = console.info.bind(console, tag + ": ");
|
info = console.info.bind(console, tag + ": ");
|
||||||
}
|
}
|
||||||
|
|
||||||
let log = function () {};
|
let log = function (...args: unknown[]) {};
|
||||||
if (debugLevel >= debugLevels.indexOf("log")) {
|
if (debugLevel >= debugLevels.indexOf("log")) {
|
||||||
log = console.log.bind(console, tag + ": ");
|
log = console.log.bind(console, tag + ": ");
|
||||||
}
|
}
|
||||||
|
|
||||||
let dbg = function () {};
|
let dbg = function (...args: unknown[]) {};
|
||||||
if (debugLevel >= debugLevels.indexOf("debug")) {
|
if (debugLevel >= debugLevels.indexOf("debug")) {
|
||||||
dbg = console.debug.bind(console, tag + ": ");
|
dbg = console.debug.bind(console, tag + ": ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function assert(e, msg) {
|
function assert(e: boolean, msg?: string) {
|
||||||
console.assert(e, tag + ": " + msg);
|
console.assert(e, tag + ": " + msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Should this be scoped to note?
|
// TODO: Should this be scoped to note?
|
||||||
let markerCounter = 0;
|
let markerCounter = 0;
|
||||||
|
|
||||||
function initTextEditor(textEditor) {
|
function initTextEditor(textEditor: TextEditor) {
|
||||||
log("initTextEditor");
|
log("initTextEditor");
|
||||||
|
|
||||||
let widget = this;
|
|
||||||
const document = textEditor.model.document;
|
const document = textEditor.model.document;
|
||||||
|
|
||||||
// Create a conversion from model to view that converts
|
// Create a conversion from model to view that converts
|
||||||
@@ -100,7 +99,7 @@ function initTextEditor(textEditor) {
|
|||||||
// See
|
// See
|
||||||
// https://github.com/ckeditor/ckeditor5/blob/b53d2a4b49679b072f4ae781ac094e7e831cfb14/packages/ckeditor5-block-quote/src/blockquoteediting.js#L54
|
// https://github.com/ckeditor/ckeditor5/blob/b53d2a4b49679b072f4ae781ac094e7e831cfb14/packages/ckeditor5-block-quote/src/blockquoteediting.js#L54
|
||||||
const changes = document.differ.getChanges();
|
const changes = document.differ.getChanges();
|
||||||
let dirtyCodeBlocks = new Set();
|
let dirtyCodeBlocks = new Set<CKNode>();
|
||||||
|
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
dbg("change " + JSON.stringify(change));
|
dbg("change " + JSON.stringify(change));
|
||||||
@@ -151,7 +150,7 @@ function initTextEditor(textEditor) {
|
|||||||
* the formatting would be stored with the note and it would need a
|
* the formatting would be stored with the note and it would need a
|
||||||
* way to remove that formatting when editing back the note.
|
* way to remove that formatting when editing back the note.
|
||||||
*/
|
*/
|
||||||
function highlightCodeBlock(codeBlock, writer) {
|
function highlightCodeBlock(codeBlock: CKNode, writer: Writer) {
|
||||||
log("highlighting codeblock " + JSON.stringify(codeBlock.toJSON()));
|
log("highlighting codeblock " + JSON.stringify(codeBlock.toJSON()));
|
||||||
const model = codeBlock.root.document.model;
|
const model = codeBlock.root.document.model;
|
||||||
|
|
||||||
@@ -291,16 +290,16 @@ function highlightCodeBlock(codeBlock, writer) {
|
|||||||
iHtml = html.indexOf(">", iHtml) + 1;
|
iHtml = html.indexOf(">", iHtml) + 1;
|
||||||
|
|
||||||
// push the span
|
// push the span
|
||||||
let posStart = writer.createPositionAt(codeBlock, child.startOffset + iChildText);
|
let posStart = writer.createPositionAt(codeBlock, (child?.startOffset ?? 0) + iChildText);
|
||||||
spanStack.push({ className: className, posStart: posStart });
|
spanStack.push({ className: className, posStart: posStart });
|
||||||
} else if (html[iHtml] == "<" && html[iHtml + 1] == "/") {
|
} else if (html[iHtml] == "<" && html[iHtml + 1] == "/") {
|
||||||
// Done with this span, pop the span and mark the range
|
// Done with this span, pop the span and mark the range
|
||||||
iHtml = html.indexOf(">", iHtml + 1) + 1;
|
iHtml = html.indexOf(">", iHtml + 1) + 1;
|
||||||
|
|
||||||
let stackTop = spanStack.pop();
|
let stackTop = spanStack.pop();
|
||||||
let posStart = stackTop.posStart;
|
let posStart = stackTop?.posStart;
|
||||||
let className = stackTop.className;
|
let className = stackTop?.className;
|
||||||
let posEnd = writer.createPositionAt(codeBlock, child.startOffset + iChildText);
|
let posEnd = writer.createPositionAt(codeBlock, (child?.startOffset ?? 0) + iChildText);
|
||||||
let range = writer.createRange(posStart, posEnd);
|
let range = writer.createRange(posStart, posEnd);
|
||||||
let markerName = "hljs:" + className + ":" + markerCounter;
|
let markerName = "hljs:" + className + ":" + markerCounter;
|
||||||
// Use an incrementing number for the uniqueId, random of
|
// Use an incrementing number for the uniqueId, random of
|
||||||
@@ -38,7 +38,7 @@ export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget {
|
|||||||
super.doRender();
|
super.doRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
getExtraOpts() {
|
getExtraOpts(): Partial<CodeMirrorOpts> {
|
||||||
return {
|
return {
|
||||||
keyMap: options.is("vimKeymapEnabled") ? "vim" : "default",
|
keyMap: options.is("vimKeymapEnabled") ? "vim" : "default",
|
||||||
lint: true,
|
lint: true,
|
||||||
|
|||||||
@@ -8,22 +8,23 @@ import froca from "../../services/froca.js";
|
|||||||
import noteCreateService from "../../services/note_create.js";
|
import noteCreateService from "../../services/note_create.js";
|
||||||
import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
|
import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
|
||||||
import link from "../../services/link.js";
|
import link from "../../services/link.js";
|
||||||
import appContext from "../../components/app_context.js";
|
import appContext, { type EventData } from "../../components/app_context.js";
|
||||||
import dialogService from "../../services/dialog.js";
|
import dialogService from "../../services/dialog.js";
|
||||||
import { initSyntaxHighlighting } from "./ckeditor/syntax_highlight.js";
|
import { initSyntaxHighlighting } from "./ckeditor/syntax_highlight.js";
|
||||||
import options from "../../services/options.js";
|
import options from "../../services/options.js";
|
||||||
import toast from "../../services/toast.js";
|
import toast from "../../services/toast.js";
|
||||||
import { getMermaidConfig } from "../mermaid.js";
|
|
||||||
import { normalizeMimeTypeForCKEditor } from "../../services/mime_type_definitions.js";
|
import { normalizeMimeTypeForCKEditor } from "../../services/mime_type_definitions.js";
|
||||||
import { buildConfig, buildToolbarConfig } from "./ckeditor/config.js";
|
import { buildConfig, buildToolbarConfig } from "./ckeditor/config.js";
|
||||||
|
import type FNote from "../../entities/fnote.js";
|
||||||
|
import { getMermaidConfig } from "../../services/mermaid.js";
|
||||||
|
|
||||||
const ENABLE_INSPECTOR = false;
|
const ENABLE_INSPECTOR = false;
|
||||||
|
|
||||||
const mentionSetup = {
|
const mentionSetup: MentionConfig = {
|
||||||
feeds: [
|
feeds: [
|
||||||
{
|
{
|
||||||
marker: "@",
|
marker: "@",
|
||||||
feed: (queryText) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
|
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
|
||||||
itemRenderer: (item) => {
|
itemRenderer: (item) => {
|
||||||
const itemElement = document.createElement("button");
|
const itemElement = document.createElement("button");
|
||||||
|
|
||||||
@@ -118,6 +119,12 @@ function buildListOfLanguages() {
|
|||||||
* - Decoupled mode, in which the editing toolbar is actually added on the client side (in {@link ClassicEditorToolbar}), see https://ckeditor.com/docs/ckeditor5/latest/examples/framework/bottom-toolbar-editor.html for an example on how the decoupled editor works.
|
* - Decoupled mode, in which the editing toolbar is actually added on the client side (in {@link ClassicEditorToolbar}), see https://ckeditor.com/docs/ckeditor5/latest/examples/framework/bottom-toolbar-editor.html for an example on how the decoupled editor works.
|
||||||
*/
|
*/
|
||||||
export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||||
|
|
||||||
|
private contentLanguage?: string | null;
|
||||||
|
private watchdog!: CKWatchdog;
|
||||||
|
|
||||||
|
private $editor!: JQuery<HTMLElement>;
|
||||||
|
|
||||||
static getType() {
|
static getType() {
|
||||||
return "editableText";
|
return "editableText";
|
||||||
}
|
}
|
||||||
@@ -195,7 +202,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const contentLanguage = this.note.getLabelValue("language");
|
const contentLanguage = this.note?.getLabelValue("language");
|
||||||
if (contentLanguage) {
|
if (contentLanguage) {
|
||||||
finalConfig.language = {
|
finalConfig.language = {
|
||||||
ui: (typeof finalConfig.language === "string" ? finalConfig.language : "en"),
|
ui: (typeof finalConfig.language === "string" ? finalConfig.language : "en"),
|
||||||
@@ -209,7 +216,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
const editor = await editorClass.create(elementOrData, finalConfig);
|
const editor = await editorClass.create(elementOrData, finalConfig);
|
||||||
|
|
||||||
const notificationsPlugin = editor.plugins.get("Notification");
|
const notificationsPlugin = editor.plugins.get("Notification");
|
||||||
notificationsPlugin.on("show:warning", (evt, data) => {
|
notificationsPlugin.on("show:warning", (evt: CKEvent, data: PluginEventData) => {
|
||||||
const title = data.title;
|
const title = data.title;
|
||||||
const message = data.message.message;
|
const message = data.message.message;
|
||||||
|
|
||||||
@@ -246,6 +253,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
editor.model.document.on("change:data", () => this.spacedUpdate.scheduleUpdate());
|
editor.model.document.on("change:data", () => this.spacedUpdate.scheduleUpdate());
|
||||||
|
|
||||||
if (glob.isDev && ENABLE_INSPECTOR) {
|
if (glob.isDev && ENABLE_INSPECTOR) {
|
||||||
|
//@ts-expect-error TODO: Check if this still works.
|
||||||
await import(/* webpackIgnore: true */ "../../../libraries/ckeditor/inspector.js");
|
await import(/* webpackIgnore: true */ "../../../libraries/ckeditor/inspector.js");
|
||||||
CKEditorInspector.attach(editor);
|
CKEditorInspector.attach(editor);
|
||||||
}
|
}
|
||||||
@@ -271,18 +279,18 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
enablePreview: true // Enable preview view
|
enablePreview: true // Enable preview view
|
||||||
},
|
},
|
||||||
mermaid: {
|
mermaid: {
|
||||||
lazyLoad: async () => await libraryLoader.requireLibrary(libraryLoader.MERMAID),
|
lazyLoad: async () => (await import("mermaid")).default, // FIXME
|
||||||
config: getMermaidConfig()
|
config: getMermaidConfig()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async doRefresh(note) {
|
async doRefresh(note: FNote) {
|
||||||
const blob = await note.getBlob();
|
const blob = await note.getBlob();
|
||||||
|
|
||||||
await this.spacedUpdate.allowUpdateWithoutChange(async () => {
|
await this.spacedUpdate.allowUpdateWithoutChange(async () => {
|
||||||
const data = blob.content || "";
|
const data = blob?.content || "";
|
||||||
const newContentLanguage = this.note.getLabelValue("language");
|
const newContentLanguage = this.note?.getLabelValue("language");
|
||||||
if (this.contentLanguage !== newContentLanguage) {
|
if (this.contentLanguage !== newContentLanguage) {
|
||||||
await this.reinitialize(data);
|
await this.reinitialize(data);
|
||||||
} else {
|
} else {
|
||||||
@@ -334,7 +342,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
this.addTextToEditor(dateString);
|
this.addTextToEditor(dateString);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addLinkToEditor(linkHref, linkTitle) {
|
async addLinkToEditor(linkHref: string, linkTitle: string) {
|
||||||
await this.initialized;
|
await this.initialized;
|
||||||
|
|
||||||
this.watchdog.editor.model.change((writer) => {
|
this.watchdog.editor.model.change((writer) => {
|
||||||
@@ -343,7 +351,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async addTextToEditor(text) {
|
async addTextToEditor(text: string) {
|
||||||
await this.initialized;
|
await this.initialized;
|
||||||
|
|
||||||
this.watchdog.editor.model.change((writer) => {
|
this.watchdog.editor.model.change((writer) => {
|
||||||
@@ -352,7 +360,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addTextToActiveEditorEvent({ text }) {
|
addTextToActiveEditorEvent({ text }: EventData<"addTextToActiveEditor">) {
|
||||||
if (!this.isActive()) {
|
if (!this.isActive()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -360,7 +368,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
this.addTextToEditor(text);
|
this.addTextToEditor(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addLink(notePath, linkTitle, externalLink = false) {
|
async addLink(notePath: string, linkTitle: string | null, externalLink: boolean = false) {
|
||||||
await this.initialized;
|
await this.initialized;
|
||||||
|
|
||||||
if (linkTitle) {
|
if (linkTitle) {
|
||||||
@@ -384,7 +392,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
return !selection.isCollapsed;
|
return !selection.isCollapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeWithTextEditorEvent({ callback, resolve, ntxId }) {
|
async executeWithTextEditorEvent({ callback, resolve, ntxId }: EventData<"executeWithTextEditor">) {
|
||||||
if (!this.isNoteContext(ntxId)) {
|
if (!this.isNoteContext(ntxId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -428,7 +436,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
const notePath = selectedElement.getAttribute("notePath");
|
const notePath = selectedElement.getAttribute("notePath");
|
||||||
|
|
||||||
if (notePath) {
|
if (notePath) {
|
||||||
await appContext.tabManager.getActiveContext().setNote(notePath);
|
await appContext.tabManager.getActiveContext()?.setNote(notePath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -441,7 +449,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
const notePath = link.getNotePathFromUrl(selectedLinkUrl);
|
const notePath = link.getNotePathFromUrl(selectedLinkUrl);
|
||||||
|
|
||||||
if (notePath) {
|
if (notePath) {
|
||||||
await appContext.tabManager.getActiveContext().setNote(notePath);
|
await appContext.tabManager.getActiveContext()?.setNote(notePath);
|
||||||
} else {
|
} else {
|
||||||
window.open(selectedLinkUrl, "_blank");
|
window.open(selectedLinkUrl, "_blank");
|
||||||
}
|
}
|
||||||
@@ -451,7 +459,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
this.triggerCommand("showIncludeNoteDialog", { textTypeWidget: this });
|
this.triggerCommand("showIncludeNoteDialog", { textTypeWidget: this });
|
||||||
}
|
}
|
||||||
|
|
||||||
addIncludeNote(noteId, boxSize) {
|
addIncludeNote(noteId: string, boxSize?: string) {
|
||||||
this.watchdog.editor.model.change((writer) => {
|
this.watchdog.editor.model.change((writer) => {
|
||||||
// Insert <includeNote>*</includeNote> at the current selection position
|
// Insert <includeNote>*</includeNote> at the current selection position
|
||||||
// in a way that will result in creating a valid model structure
|
// in a way that will result in creating a valid model structure
|
||||||
@@ -464,8 +472,11 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async addImage(noteId) {
|
async addImage(noteId: string) {
|
||||||
const note = await froca.getNote(noteId);
|
const note = await froca.getNote(noteId);
|
||||||
|
if (!note) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.watchdog.editor.model.change((writer) => {
|
this.watchdog.editor.model.change((writer) => {
|
||||||
const encodedTitle = encodeURIComponent(note.title);
|
const encodedTitle = encodeURIComponent(note.title);
|
||||||
@@ -475,24 +486,28 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async createNoteForReferenceLink(title) {
|
async createNoteForReferenceLink(title: string) {
|
||||||
|
if (!this.notePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const resp = await noteCreateService.createNoteWithTypePrompt(this.notePath, {
|
const resp = await noteCreateService.createNoteWithTypePrompt(this.notePath, {
|
||||||
activate: false,
|
activate: false,
|
||||||
title: title
|
title: title
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!resp) {
|
if (!resp || !resp.note) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp.note.getBestNotePathString();
|
return resp.note.getBestNotePathString();
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshIncludedNoteEvent({ noteId }) {
|
async refreshIncludedNoteEvent({ noteId }: EventData<"refreshIncludedNote">) {
|
||||||
this.refreshIncludedNote(this.$editor, noteId);
|
this.refreshIncludedNote(this.$editor, noteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async reinitialize(data) {
|
async reinitialize(data: string) {
|
||||||
if (!this.watchdog) {
|
if (!this.watchdog) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
43
src/public/app/widgets/type_widgets/linters/mermaid.spec.ts
Normal file
43
src/public/app/widgets/type_widgets/linters/mermaid.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { trimIndentation } from "../../../../../../spec/support/utils.js";
|
||||||
|
import { validateMermaid } from "./mermaid.js";
|
||||||
|
|
||||||
|
describe("Mermaid linter", () => {
|
||||||
|
|
||||||
|
(global as any).CodeMirror = {
|
||||||
|
Pos(line: number, col: number) {
|
||||||
|
return { line, col };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
it("reports correctly bad diagram type", async () => {
|
||||||
|
const input = trimIndentation`\
|
||||||
|
stateDiagram-v23
|
||||||
|
[*] -> Still
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await validateMermaid(input);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
message: "Expecting 'SPACE', 'NL', 'SD', got 'ID'",
|
||||||
|
from: { line: 0, col: 0 },
|
||||||
|
to: { line: 0, col: 1 }
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports correctly basic arrow missing in diagram", async () => {
|
||||||
|
const input = trimIndentation`\
|
||||||
|
xychart-beta horizontal
|
||||||
|
title "Percentage usge"
|
||||||
|
x-axis [data, sys, usr, var]
|
||||||
|
y-axis 0--->100
|
||||||
|
bar [20, 70, 0, 0]
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await validateMermaid(input);
|
||||||
|
expect(result).toMatchObject([{
|
||||||
|
message: "Expecting 'ARROW_DELIMITER', got 'MINUS'",
|
||||||
|
from: { line: 3, col: 8 },
|
||||||
|
to: { line: 3, col: 9 }
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
});
|
||||||
58
src/public/app/widgets/type_widgets/linters/mermaid.ts
Normal file
58
src/public/app/widgets/type_widgets/linters/mermaid.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import mermaid from "mermaid";
|
||||||
|
|
||||||
|
interface MermaidParseError extends Error {
|
||||||
|
hash: {
|
||||||
|
text: string;
|
||||||
|
token: string;
|
||||||
|
line: number;
|
||||||
|
loc: {
|
||||||
|
first_line: number;
|
||||||
|
first_column: number;
|
||||||
|
last_line: number;
|
||||||
|
last_column: number;
|
||||||
|
};
|
||||||
|
expected: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function registerErrorReporter() {
|
||||||
|
CodeMirror.registerHelper("lint", null, validateMermaid);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function validateMermaid(text: string) {
|
||||||
|
if (!text.trim()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mermaid.parse(text);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
console.warn("Got validation error", JSON.stringify(e));
|
||||||
|
|
||||||
|
const mermaidError = (e as MermaidParseError);
|
||||||
|
const loc = mermaidError.hash.loc;
|
||||||
|
|
||||||
|
let firstCol = loc.first_column + 1;
|
||||||
|
let lastCol = loc.last_column + 1;
|
||||||
|
|
||||||
|
if (firstCol === 1 && lastCol === 1) {
|
||||||
|
firstCol = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let messageLines = mermaidError.message.split("\n");
|
||||||
|
if (messageLines.length >= 4) {
|
||||||
|
messageLines = messageLines.slice(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
message: messageLines.join("\n"),
|
||||||
|
severity: "error",
|
||||||
|
from: CodeMirror.Pos(loc.first_line - 1, firstCol),
|
||||||
|
to: CodeMirror.Pos(loc.last_line - 1, lastCol)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
35
src/public/app/widgets/type_widgets/mermaid.ts
Normal file
35
src/public/app/widgets/type_widgets/mermaid.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { getMermaidConfig, loadElkIfNeeded, postprocessMermaidSvg } from "../../services/mermaid.js";
|
||||||
|
import AbstractSvgSplitTypeWidget from "./abstract_svg_split_type_widget.js";
|
||||||
|
|
||||||
|
let idCounter = 1;
|
||||||
|
let registeredErrorReporter = false;
|
||||||
|
|
||||||
|
export class MermaidTypeWidget extends AbstractSvgSplitTypeWidget {
|
||||||
|
|
||||||
|
static getType() {
|
||||||
|
return "mermaid";
|
||||||
|
}
|
||||||
|
|
||||||
|
get attachmentName(): string {
|
||||||
|
return "mermaid-export";
|
||||||
|
}
|
||||||
|
|
||||||
|
async renderSvg(content: string) {
|
||||||
|
const mermaid = (await import("mermaid")).default;
|
||||||
|
await loadElkIfNeeded(mermaid, content);
|
||||||
|
if (!registeredErrorReporter) {
|
||||||
|
// (await import("./linters/mermaid.js")).default();
|
||||||
|
registeredErrorReporter = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
|
...(getMermaidConfig() as any),
|
||||||
|
});
|
||||||
|
|
||||||
|
idCounter++;
|
||||||
|
const { svg } = await mermaid.render(`mermaid-graph-${idCounter}`, content);
|
||||||
|
return postprocessMermaidSvg(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -276,4 +276,14 @@ export default class MindMapWidget extends TypeWidget {
|
|||||||
const svg = await this.renderSvg();
|
const svg = await this.renderSvg();
|
||||||
utils.downloadSvg(this.note.title, svg);
|
utils.downloadSvg(this.note.title, svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async exportPngEvent({ ntxId }: EventData<"exportPng">) {
|
||||||
|
if (!this.isNoteContext(ntxId) || this.note?.type !== "mindMap") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const svg = await this.renderSvg();
|
||||||
|
utils.downloadSvgAsPng(this.note.title, svg);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
|
import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
|
||||||
import libraryLoader from "../../services/library_loader.js";
|
import libraryLoader from "../../services/library_loader.js";
|
||||||
import { applySyntaxHighlight } from "../../services/syntax_highlight.js";
|
import { applySyntaxHighlight } from "../../services/syntax_highlight.js";
|
||||||
import { getMermaidConfig } from "../mermaid.js";
|
|
||||||
import type FNote from "../../entities/fnote.js";
|
import type FNote from "../../entities/fnote.js";
|
||||||
import type { EventData } from "../../components/app_context.js";
|
import type { EventData } from "../../components/app_context.js";
|
||||||
import { getLocaleById } from "../../services/i18n.js";
|
import { getLocaleById } from "../../services/i18n.js";
|
||||||
|
import { getMermaidConfig } from "../../services/mermaid.js";
|
||||||
|
|
||||||
const TPL = `
|
const TPL = `
|
||||||
<div class="note-detail-readonly-text note-detail-printable">
|
<div class="note-detail-readonly-text note-detail-printable">
|
||||||
@@ -114,7 +114,9 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
this.$content.find("section").each((_, el) => {
|
this.$content.find("section").each((_, el) => {
|
||||||
const noteId = $(el).attr("data-note-id");
|
const noteId = $(el).attr("data-note-id");
|
||||||
|
|
||||||
this.loadIncludedNote(noteId, $(el));
|
if (noteId) {
|
||||||
|
this.loadIncludedNote(noteId, $(el));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.$content.find("span.math-tex").length > 0) {
|
if (this.$content.find("span.math-tex").length > 0) {
|
||||||
@@ -139,8 +141,11 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Initialize mermaid
|
// Initialize mermaid
|
||||||
await libraryLoader.requireLibrary(libraryLoader.MERMAID);
|
const mermaid = (await import("mermaid")).default;
|
||||||
mermaid.init(getMermaidConfig(), this.$content.find(".mermaid-diagram"));
|
mermaid.initialize(getMermaidConfig());
|
||||||
|
mermaid.run({
|
||||||
|
nodes: this.$content.find(".mermaid-diagram")
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshIncludedNoteEvent({ noteId }: EventData<"refreshIncludedNote">) {
|
async refreshIncludedNoteEvent({ noteId }: EventData<"refreshIncludedNote">) {
|
||||||
|
|||||||
@@ -1,17 +1,36 @@
|
|||||||
import server from "../../services/server.js";
|
import server from "../../services/server.js";
|
||||||
import linkService from "../../services/link.js";
|
import linkService from "../../services/link.js";
|
||||||
import libraryLoader from "../../services/library_loader.js";
|
|
||||||
import contextMenu from "../../menus/context_menu.js";
|
import contextMenu from "../../menus/context_menu.js";
|
||||||
import toastService from "../../services/toast.js";
|
import toastService from "../../services/toast.js";
|
||||||
import attributeAutocompleteService from "../../services/attribute_autocomplete.js";
|
import attributeAutocompleteService from "../../services/attribute_autocomplete.js";
|
||||||
import TypeWidget from "./type_widget.js";
|
import TypeWidget from "./type_widget.js";
|
||||||
import appContext from "../../components/app_context.js";
|
import appContext, { type EventData } from "../../components/app_context.js";
|
||||||
import utils from "../../services/utils.js";
|
import utils from "../../services/utils.js";
|
||||||
import froca from "../../services/froca.js";
|
import froca from "../../services/froca.js";
|
||||||
import dialogService from "../../services/dialog.js";
|
import dialogService from "../../services/dialog.js";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
|
import type FNote from "../../entities/fnote.js";
|
||||||
|
import type { ConnectionMadeEventInfo, jsPlumbInstance, OverlaySpec } from "jsplumb";
|
||||||
|
import "../../../stylesheets/relation_map.css";
|
||||||
|
|
||||||
const uniDirectionalOverlays = [
|
declare module "jsplumb" {
|
||||||
|
|
||||||
|
interface Connection {
|
||||||
|
canvas: HTMLCanvasElement;
|
||||||
|
getType(): string;
|
||||||
|
bind(event: string, callback: (obj: unknown, event: MouseEvent) => void): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Overlay {
|
||||||
|
setLabel(label: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConnectParams {
|
||||||
|
type: RelationType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniDirectionalOverlays: OverlaySpec[] = [
|
||||||
[
|
[
|
||||||
"Arrow",
|
"Arrow",
|
||||||
{
|
{
|
||||||
@@ -92,7 +111,62 @@ const TPL = `
|
|||||||
|
|
||||||
let containerCounter = 1;
|
let containerCounter = 1;
|
||||||
|
|
||||||
|
interface Clipboard {
|
||||||
|
noteId: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MapData {
|
||||||
|
notes: {
|
||||||
|
noteId: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}[];
|
||||||
|
transform: {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
scale: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RelationType = "uniDirectional" | "biDirectional" | "inverse";
|
||||||
|
|
||||||
|
interface Relation {
|
||||||
|
name: string;
|
||||||
|
attributeId: string;
|
||||||
|
sourceNoteId: string;
|
||||||
|
targetNoteId: string;
|
||||||
|
type: RelationType;
|
||||||
|
render: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Deduplicate.
|
||||||
|
interface PostNoteResponse {
|
||||||
|
note: {
|
||||||
|
noteId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Deduplicate.
|
||||||
|
interface RelationMapPostResponse {
|
||||||
|
relations: Relation[];
|
||||||
|
inverseRelations: Record<string, string>;
|
||||||
|
noteTitles: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MenuCommands = "openInNewTab" | "remove" | "editTitle";
|
||||||
|
|
||||||
export default class RelationMapTypeWidget extends TypeWidget {
|
export default class RelationMapTypeWidget extends TypeWidget {
|
||||||
|
|
||||||
|
private clipboard?: Clipboard | null;
|
||||||
|
private jsPlumbInstance?: import("jsplumb").jsPlumbInstance | null;
|
||||||
|
private pzInstance?: PanZoom | null;
|
||||||
|
private mapData?: MapData | null;
|
||||||
|
private relations?: Relation[] | null;
|
||||||
|
|
||||||
|
private $relationMapContainer!: JQuery<HTMLElement>;
|
||||||
|
private $relationMapWrapper!: JQuery<HTMLElement>;
|
||||||
|
|
||||||
static getType() {
|
static getType() {
|
||||||
return "relationMap";
|
return "relationMap";
|
||||||
}
|
}
|
||||||
@@ -109,7 +183,7 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
|
|
||||||
this.$relationMapWrapper = this.$widget.find(".relation-map-wrapper");
|
this.$relationMapWrapper = this.$widget.find(".relation-map-wrapper");
|
||||||
this.$relationMapWrapper.on("click", (event) => {
|
this.$relationMapWrapper.on("click", (event) => {
|
||||||
if (this.clipboard) {
|
if (this.clipboard && this.mapData) {
|
||||||
let { x, y } = this.getMousePosition(event);
|
let { x, y } = this.getMousePosition(event);
|
||||||
|
|
||||||
// modifying position so that the cursor is on the top-center of the box
|
// modifying position so that the cursor is on the top-center of the box
|
||||||
@@ -130,7 +204,7 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
|
|
||||||
this.$relationMapContainer.attr("id", "relation-map-container-" + containerCounter++);
|
this.$relationMapContainer.attr("id", "relation-map-container-" + containerCounter++);
|
||||||
this.$relationMapContainer.on("contextmenu", ".note-box", (e) => {
|
this.$relationMapContainer.on("contextmenu", ".note-box", (e) => {
|
||||||
contextMenu.show({
|
contextMenu.show<MenuCommands>({
|
||||||
x: e.pageX,
|
x: e.pageX,
|
||||||
y: e.pageY,
|
y: e.pageY,
|
||||||
items: [
|
items: [
|
||||||
@@ -150,15 +224,15 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
this.$widget.on("dragover", (ev) => ev.preventDefault());
|
this.$widget.on("dragover", (ev) => ev.preventDefault());
|
||||||
|
|
||||||
this.initialized = new Promise(async (res) => {
|
this.initialized = new Promise(async (res) => {
|
||||||
await libraryLoader.requireLibrary(libraryLoader.RELATION_MAP);
|
// Weird typecast is needed probably due to bad typings in the module itself.
|
||||||
|
const jsPlumb = (await import("jsplumb")).default.jsPlumb as unknown as jsPlumbInstance;
|
||||||
jsPlumb.ready(res);
|
jsPlumb.ready(res);
|
||||||
});
|
});
|
||||||
|
|
||||||
super.doRender();
|
super.doRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
async contextMenuHandler(command, originalTarget) {
|
async contextMenuHandler(command: MenuCommands | undefined, originalTarget: HTMLElement) {
|
||||||
const $noteBox = $(originalTarget).closest(".note-box");
|
const $noteBox = $(originalTarget).closest(".note-box");
|
||||||
const $title = $noteBox.find(".title a");
|
const $title = $noteBox.find(".title a");
|
||||||
const noteId = this.idToNoteId($noteBox.prop("id"));
|
const noteId = this.idToNoteId($noteBox.prop("id"));
|
||||||
@@ -168,11 +242,11 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
} else if (command === "remove") {
|
} else if (command === "remove") {
|
||||||
const result = await dialogService.confirmDeleteNoteBoxWithNote($title.text());
|
const result = await dialogService.confirmDeleteNoteBoxWithNote($title.text());
|
||||||
|
|
||||||
if (!result.confirmed) {
|
if (typeof result !== "object" || !result.confirmed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.jsPlumbInstance.remove(this.noteIdToId(noteId));
|
this.jsPlumbInstance?.remove(this.noteIdToId(noteId));
|
||||||
|
|
||||||
if (result.isDeleteNoteChecked) {
|
if (result.isDeleteNoteChecked) {
|
||||||
const taskId = utils.randomString(10);
|
const taskId = utils.randomString(10);
|
||||||
@@ -180,9 +254,13 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
await server.remove(`notes/${noteId}?taskId=${taskId}&last=true`);
|
await server.remove(`notes/${noteId}?taskId=${taskId}&last=true`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mapData.notes = this.mapData.notes.filter((note) => note.noteId !== noteId);
|
if (this.mapData) {
|
||||||
|
this.mapData.notes = this.mapData.notes.filter((note) => note.noteId !== noteId);
|
||||||
|
}
|
||||||
|
|
||||||
this.relations = this.relations.filter((relation) => relation.sourceNoteId !== noteId && relation.targetNoteId !== noteId);
|
if (this.relations) {
|
||||||
|
this.relations = this.relations.filter((relation) => relation.sourceNoteId !== noteId && relation.targetNoteId !== noteId);
|
||||||
|
}
|
||||||
|
|
||||||
this.saveData();
|
this.saveData();
|
||||||
} else if (command === "editTitle") {
|
} else if (command === "editTitle") {
|
||||||
@@ -216,9 +294,9 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const blob = await this.note.getBlob();
|
const blob = await this.note?.getBlob();
|
||||||
|
|
||||||
if (blob.content) {
|
if (blob?.content) {
|
||||||
try {
|
try {
|
||||||
this.mapData = JSON.parse(blob.content);
|
this.mapData = JSON.parse(blob.content);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -227,20 +305,20 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
noteIdToId(noteId) {
|
noteIdToId(noteId: string) {
|
||||||
return `rel-map-note-${noteId}`;
|
return `rel-map-note-${noteId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
idToNoteId(id) {
|
idToNoteId(id: string) {
|
||||||
return id.substr(13);
|
return id.substr(13);
|
||||||
}
|
}
|
||||||
|
|
||||||
async doRefresh(note) {
|
async doRefresh(note: FNote) {
|
||||||
await this.loadMapData();
|
await this.loadMapData();
|
||||||
|
|
||||||
this.initJsPlumbInstance();
|
await this.initJsPlumbInstance();
|
||||||
|
|
||||||
this.initPanZoom();
|
await this.initPanZoom();
|
||||||
|
|
||||||
this.loadNotesAndRelations();
|
this.loadNotesAndRelations();
|
||||||
}
|
}
|
||||||
@@ -248,15 +326,19 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
clearMap() {
|
clearMap() {
|
||||||
// delete all endpoints and connections
|
// delete all endpoints and connections
|
||||||
// this is done at this point (after async operations) to reduce flicker to the minimum
|
// this is done at this point (after async operations) to reduce flicker to the minimum
|
||||||
this.jsPlumbInstance.deleteEveryEndpoint();
|
this.jsPlumbInstance?.deleteEveryEndpoint();
|
||||||
|
|
||||||
// without this, we still end up with note boxes remaining in the canvas
|
// without this, we still end up with note boxes remaining in the canvas
|
||||||
this.$relationMapContainer.empty();
|
this.$relationMapContainer.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadNotesAndRelations() {
|
async loadNotesAndRelations() {
|
||||||
|
if (!this.mapData || !this.jsPlumbInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const noteIds = this.mapData.notes.map((note) => note.noteId);
|
const noteIds = this.mapData.notes.map((note) => note.noteId);
|
||||||
const data = await server.post("relation-map", { noteIds, relationMapNoteId: this.noteId });
|
const data = await server.post<RelationMapPostResponse>("relation-map", { noteIds, relationMapNoteId: this.noteId });
|
||||||
|
|
||||||
this.relations = [];
|
this.relations = [];
|
||||||
|
|
||||||
@@ -282,6 +364,10 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
this.mapData.notes = this.mapData.notes.filter((note) => note.noteId in data.noteTitles);
|
this.mapData.notes = this.mapData.notes.filter((note) => note.noteId in data.noteTitles);
|
||||||
|
|
||||||
this.jsPlumbInstance.batch(async () => {
|
this.jsPlumbInstance.batch(async () => {
|
||||||
|
if (!this.jsPlumbInstance || !this.mapData || !this.relations) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.clearMap();
|
this.clearMap();
|
||||||
|
|
||||||
for (const note of this.mapData.notes) {
|
for (const note of this.mapData.notes) {
|
||||||
@@ -301,6 +387,8 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
type: relation.type
|
type: relation.type
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: Does this actually do anything.
|
||||||
|
//@ts-expect-error
|
||||||
connection.id = relation.attributeId;
|
connection.id = relation.attributeId;
|
||||||
|
|
||||||
if (relation.type === "inverse") {
|
if (relation.type === "inverse") {
|
||||||
@@ -315,30 +403,37 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
initPanZoom() {
|
async initPanZoom() {
|
||||||
if (this.pzInstance) {
|
if (this.pzInstance) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const panzoom = (await import("panzoom")).default;
|
||||||
this.pzInstance = panzoom(this.$relationMapContainer[0], {
|
this.pzInstance = panzoom(this.$relationMapContainer[0], {
|
||||||
maxZoom: 2,
|
maxZoom: 2,
|
||||||
minZoom: 0.3,
|
minZoom: 0.3,
|
||||||
smoothScroll: false,
|
smoothScroll: false,
|
||||||
filterKey: function (e, dx, dy, dz) {
|
|
||||||
|
//@ts-expect-error Upstream incorrectly mentions no arguments.
|
||||||
|
filterKey: function (e: KeyboardEvent) {
|
||||||
// if ALT is pressed, then panzoom should bubble the event up
|
// if ALT is pressed, then panzoom should bubble the event up
|
||||||
// this is to preserve ALT-LEFT, ALT-RIGHT navigation working
|
// this is to preserve ALT-LEFT, ALT-RIGHT navigation working
|
||||||
return e.altKey;
|
return e.altKey;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!this.pzInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.pzInstance.on("transform", () => {
|
this.pzInstance.on("transform", () => {
|
||||||
// gets triggered on any transform change
|
// gets triggered on any transform change
|
||||||
this.jsPlumbInstance.setZoom(this.getZoom());
|
this.jsPlumbInstance?.setZoom(this.getZoom());
|
||||||
|
|
||||||
this.saveCurrentTransform();
|
this.saveCurrentTransform();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.mapData.transform) {
|
if (this.mapData?.transform) {
|
||||||
this.pzInstance.zoomTo(0, 0, this.mapData.transform.scale);
|
this.pzInstance.zoomTo(0, 0, this.mapData.transform.scale);
|
||||||
|
|
||||||
this.pzInstance.moveTo(this.mapData.transform.x, this.mapData.transform.y);
|
this.pzInstance.moveTo(this.mapData.transform.x, this.mapData.transform.y);
|
||||||
@@ -349,9 +444,13 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveCurrentTransform() {
|
saveCurrentTransform() {
|
||||||
|
if (!this.pzInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newTransform = this.pzInstance.getTransform();
|
const newTransform = this.pzInstance.getTransform();
|
||||||
|
|
||||||
if (JSON.stringify(newTransform) !== JSON.stringify(this.mapData.transform)) {
|
if (this.mapData && JSON.stringify(newTransform) !== JSON.stringify(this.mapData.transform)) {
|
||||||
// clone transform object
|
// clone transform object
|
||||||
this.mapData.transform = JSON.parse(JSON.stringify(newTransform));
|
this.mapData.transform = JSON.parse(JSON.stringify(newTransform));
|
||||||
|
|
||||||
@@ -370,13 +469,14 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initJsPlumbInstance() {
|
async initJsPlumbInstance() {
|
||||||
if (this.jsPlumbInstance) {
|
if (this.jsPlumbInstance) {
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const jsPlumb = (await import("jsplumb")).default.jsPlumb;
|
||||||
this.jsPlumbInstance = jsPlumb.getInstance({
|
this.jsPlumbInstance = jsPlumb.getInstance({
|
||||||
Endpoint: ["Dot", { radius: 2 }],
|
Endpoint: ["Dot", { radius: 2 }],
|
||||||
Connector: "StateMachine",
|
Connector: "StateMachine",
|
||||||
@@ -385,6 +485,10 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
Container: this.$relationMapContainer.attr("id")
|
Container: this.$relationMapContainer.attr("id")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!this.jsPlumbInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.jsPlumbInstance.registerConnectionType("uniDirectional", { anchor: "Continuous", connector: "StateMachine", overlays: uniDirectionalOverlays });
|
this.jsPlumbInstance.registerConnectionType("uniDirectional", { anchor: "Continuous", connector: "StateMachine", overlays: uniDirectionalOverlays });
|
||||||
|
|
||||||
this.jsPlumbInstance.registerConnectionType("biDirectional", { anchor: "Continuous", connector: "StateMachine", overlays: biDirectionalOverlays });
|
this.jsPlumbInstance.registerConnectionType("biDirectional", { anchor: "Continuous", connector: "StateMachine", overlays: biDirectionalOverlays });
|
||||||
@@ -396,10 +500,10 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
this.jsPlumbInstance.bind("connection", (info, originalEvent) => this.connectionCreatedHandler(info, originalEvent));
|
this.jsPlumbInstance.bind("connection", (info, originalEvent) => this.connectionCreatedHandler(info, originalEvent));
|
||||||
}
|
}
|
||||||
|
|
||||||
async connectionCreatedHandler(info, originalEvent) {
|
async connectionCreatedHandler(info: ConnectionMadeEventInfo, originalEvent: Event) {
|
||||||
const connection = info.connection;
|
const connection = info.connection;
|
||||||
|
|
||||||
connection.bind("contextmenu", (obj, event) => {
|
connection.bind("contextmenu", (obj: unknown, event: MouseEvent) => {
|
||||||
if (connection.getType().includes("link")) {
|
if (connection.getType().includes("link")) {
|
||||||
// don't create context menu if it's a link since there's nothing to do with link from relation map
|
// don't create context menu if it's a link since there's nothing to do with link from relation map
|
||||||
// (don't open browser menu either)
|
// (don't open browser menu either)
|
||||||
@@ -414,15 +518,17 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
items: [{ title: t("relation_map.remove_relation"), command: "remove", uiIcon: "bx bx-trash" }],
|
items: [{ title: t("relation_map.remove_relation"), command: "remove", uiIcon: "bx bx-trash" }],
|
||||||
selectMenuItemHandler: async ({ command }) => {
|
selectMenuItemHandler: async ({ command }) => {
|
||||||
if (command === "remove") {
|
if (command === "remove") {
|
||||||
if (!(await dialogService.confirm(t("relation_map.confirm_remove_relation")))) {
|
if (!(await dialogService.confirm(t("relation_map.confirm_remove_relation"))) || !this.relations) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const relation = this.relations.find((rel) => rel.attributeId === connection.id);
|
const relation = this.relations.find((rel) => rel.attributeId === connection.id);
|
||||||
|
|
||||||
await server.remove(`notes/${relation.sourceNoteId}/relations/${relation.name}/to/${relation.targetNoteId}`);
|
if (relation) {
|
||||||
|
await server.remove(`notes/${relation.sourceNoteId}/relations/${relation.name}/to/${relation.targetNoteId}`);
|
||||||
|
}
|
||||||
|
|
||||||
this.jsPlumbInstance.deleteConnection(connection);
|
this.jsPlumbInstance?.deleteConnection(connection);
|
||||||
|
|
||||||
this.relations = this.relations.filter((relation) => relation.attributeId !== connection.id);
|
this.relations = this.relations.filter((relation) => relation.attributeId !== connection.id);
|
||||||
}
|
}
|
||||||
@@ -432,16 +538,20 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// if there's no event, then this has been triggered programmatically
|
// if there's no event, then this has been triggered programmatically
|
||||||
if (!originalEvent) {
|
if (!originalEvent || !this.jsPlumbInstance) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = await dialogService.prompt({
|
let name = await dialogService.prompt({
|
||||||
message: t("relation_map.specify_new_relation_name"),
|
message: t("relation_map.specify_new_relation_name"),
|
||||||
shown: ({ $answer }) => {
|
shown: ({ $answer }) => {
|
||||||
|
if (!$answer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$answer.on("keyup", () => {
|
$answer.on("keyup", () => {
|
||||||
// invalid characters are simply ignored (from user perspective they are not even entered)
|
// invalid characters are simply ignored (from user perspective they are not even entered)
|
||||||
const attrName = utils.filterAttributeName($answer.val());
|
const attrName = utils.filterAttributeName($answer.val() as string);
|
||||||
|
|
||||||
$answer.val(attrName);
|
$answer.val(attrName);
|
||||||
});
|
});
|
||||||
@@ -465,7 +575,7 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
const targetNoteId = this.idToNoteId(connection.target.id);
|
const targetNoteId = this.idToNoteId(connection.target.id);
|
||||||
const sourceNoteId = this.idToNoteId(connection.source.id);
|
const sourceNoteId = this.idToNoteId(connection.source.id);
|
||||||
|
|
||||||
const relationExists = this.relations.some((rel) => rel.targetNoteId === targetNoteId && rel.sourceNoteId === sourceNoteId && rel.name === name);
|
const relationExists = this.relations?.some((rel) => rel.targetNoteId === targetNoteId && rel.sourceNoteId === sourceNoteId && rel.name === name);
|
||||||
|
|
||||||
if (relationExists) {
|
if (relationExists) {
|
||||||
await dialogService.info(t("relation_map.connection_exists", { name }));
|
await dialogService.info(t("relation_map.connection_exists", { name }));
|
||||||
@@ -484,11 +594,18 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
this.spacedUpdate.scheduleUpdate();
|
this.spacedUpdate.scheduleUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
async createNoteBox(noteId, title, x, y) {
|
async createNoteBox(noteId: string, title: string, x: number, y: number) {
|
||||||
|
if (!this.jsPlumbInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const $link = await linkService.createLink(noteId, { title });
|
const $link = await linkService.createLink(noteId, { title });
|
||||||
$link.mousedown((e) => linkService.goToLink(e));
|
$link.mousedown((e) => linkService.goToLink(e));
|
||||||
|
|
||||||
const note = await froca.getNote(noteId);
|
const note = await froca.getNote(noteId);
|
||||||
|
if (!note) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const $noteBox = $("<div>")
|
const $noteBox = $("<div>")
|
||||||
.addClass("note-box")
|
.addClass("note-box")
|
||||||
@@ -507,13 +624,14 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
stop: (params) => {
|
stop: (params) => {
|
||||||
const noteId = this.idToNoteId(params.el.id);
|
const noteId = this.idToNoteId(params.el.id);
|
||||||
|
|
||||||
const note = this.mapData.notes.find((note) => note.noteId === noteId);
|
const note = this.mapData?.notes.find((note) => note.noteId === noteId);
|
||||||
|
|
||||||
if (!note) {
|
if (!note) {
|
||||||
logError(t("relation_map.note_not_found", { noteId }));
|
logError(t("relation_map.note_not_found", { noteId }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//@ts-expect-error TODO: Check if this is still valid.
|
||||||
[note.x, note.y] = params.finalPos;
|
[note.x, note.y] = params.finalPos;
|
||||||
|
|
||||||
this.saveData();
|
this.saveData();
|
||||||
@@ -552,25 +670,29 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
throw new Error(t("relation_map.cannot_match_transform", { transform }));
|
throw new Error(t("relation_map.cannot_match_transform", { transform }));
|
||||||
}
|
}
|
||||||
|
|
||||||
return matches[1];
|
return parseFloat(matches[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async dropNoteOntoRelationMapHandler(ev) {
|
async dropNoteOntoRelationMapHandler(ev: JQuery.DropEvent) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
const notes = JSON.parse(ev.originalEvent.dataTransfer.getData("text"));
|
const dragData = ev.originalEvent?.dataTransfer?.getData("text");
|
||||||
|
if (!dragData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const notes = JSON.parse(dragData);
|
||||||
|
|
||||||
let { x, y } = this.getMousePosition(ev);
|
let { x, y } = this.getMousePosition(ev);
|
||||||
|
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
const exists = this.mapData.notes.some((n) => n.noteId === note.noteId);
|
const exists = this.mapData?.notes.some((n) => n.noteId === note.noteId);
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
toastService.showError(t("relation_map.note_already_in_diagram", { title: note.title }));
|
toastService.showError(t("relation_map.note_already_in_diagram", { title: note.title }));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.mapData.notes.push({ noteId: note.noteId, x, y });
|
this.mapData?.notes.push({ noteId: note.noteId, x, y });
|
||||||
|
|
||||||
if (x > 1000) {
|
if (x > 1000) {
|
||||||
y += 100;
|
y += 100;
|
||||||
@@ -585,14 +707,14 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
this.loadNotesAndRelations();
|
this.loadNotesAndRelations();
|
||||||
}
|
}
|
||||||
|
|
||||||
getMousePosition(evt) {
|
getMousePosition(evt: JQuery.ClickEvent | JQuery.DropEvent) {
|
||||||
const rect = this.$relationMapContainer[0].getBoundingClientRect();
|
const rect = this.$relationMapContainer[0].getBoundingClientRect();
|
||||||
|
|
||||||
const zoom = this.getZoom();
|
const zoom = this.getZoom();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: (evt.clientX - rect.left) / zoom,
|
x: ((evt.clientX ?? 0) - rect.left) / zoom,
|
||||||
y: (evt.clientY - rect.top) / zoom
|
y: ((evt.clientY ?? 0) - rect.top) / zoom
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,18 +724,18 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async relationMapCreateChildNoteEvent({ ntxId }) {
|
async relationMapCreateChildNoteEvent({ ntxId }: EventData<"relationMapCreateChildNote">) {
|
||||||
if (!this.isNoteContext(ntxId)) {
|
if (!this.isNoteContext(ntxId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = await dialogService.prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") });
|
const title = await dialogService.prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") });
|
||||||
|
|
||||||
if (!title.trim()) {
|
if (!title?.trim()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { note } = await server.post(`notes/${this.noteId}/children?target=into`, {
|
const { note } = await server.post<PostNoteResponse>(`notes/${this.noteId}/children?target=into`, {
|
||||||
title,
|
title,
|
||||||
content: "",
|
content: "",
|
||||||
type: "text"
|
type: "text"
|
||||||
@@ -624,29 +746,29 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
this.clipboard = { noteId: note.noteId, title };
|
this.clipboard = { noteId: note.noteId, title };
|
||||||
}
|
}
|
||||||
|
|
||||||
relationMapResetPanZoomEvent({ ntxId }) {
|
relationMapResetPanZoomEvent({ ntxId }: EventData<"relationMapResetPanZoom">) {
|
||||||
if (!this.isNoteContext(ntxId)) {
|
if (!this.isNoteContext(ntxId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset to initial pan & zoom state
|
// reset to initial pan & zoom state
|
||||||
this.pzInstance.zoomTo(0, 0, 1 / this.getZoom());
|
this.pzInstance?.zoomTo(0, 0, 1 / this.getZoom());
|
||||||
this.pzInstance.moveTo(0, 0);
|
this.pzInstance?.moveTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
relationMapResetZoomInEvent({ ntxId }) {
|
relationMapResetZoomInEvent({ ntxId }: EventData<"relationMapResetZoomIn">) {
|
||||||
if (!this.isNoteContext(ntxId)) {
|
if (!this.isNoteContext(ntxId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pzInstance.zoomTo(0, 0, 1.2);
|
this.pzInstance?.zoomTo(0, 0, 1.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
relationMapResetZoomOutEvent({ ntxId }) {
|
relationMapResetZoomOutEvent({ ntxId }: EventData<"relationMapResetZoomOut">) {
|
||||||
if (!this.isNoteContext(ntxId)) {
|
if (!this.isNoteContext(ntxId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pzInstance.zoomTo(0, 0, 0.8);
|
this.pzInstance?.zoomTo(0, 0, 0.8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -579,11 +579,7 @@ table.promoted-attributes-in-tooltip th {
|
|||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
font-size: var(--main-font-size) !important;
|
font-size: var(--main-font-size) !important;
|
||||||
/*
|
z-index: calc(var(--ck-z-panel) - 1) !important;
|
||||||
TODO: Investigate the purpose of this
|
|
||||||
z-index: calc(var(--ck-z-panel) - 1) !important;
|
|
||||||
*/
|
|
||||||
z-index: 3000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-trigger {
|
.tooltip-trigger {
|
||||||
@@ -1315,6 +1311,12 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile, phone mode */
|
/* Mobile, phone mode */
|
||||||
|
|
||||||
|
#detail-container {
|
||||||
|
max-height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 991px) {
|
@media (max-width: 991px) {
|
||||||
body.mobile #launcher-pane .dropdown.global-menu > .dropdown-menu.show,
|
body.mobile #launcher-pane .dropdown.global-menu > .dropdown-menu.show,
|
||||||
body.mobile #launcher-container .dropdown > .dropdown-menu.show {
|
body.mobile #launcher-container .dropdown > .dropdown-menu.show {
|
||||||
@@ -1354,6 +1356,10 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
|||||||
z-index: 2000;
|
z-index: 2000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#detail-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
#mobile-sidebar-container.show #mobile-sidebar-wrapper {
|
#mobile-sidebar-container.show #mobile-sidebar-wrapper {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
@@ -1903,7 +1909,7 @@ footer.file-footer button {
|
|||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ck-content .admonition {
|
.admonition {
|
||||||
--accent-color: var(--card-border-color);
|
--accent-color: var(--card-border-color);
|
||||||
border: 1px solid var(--accent-color);
|
border: 1px solid var(--accent-color);
|
||||||
box-shadow: var(--card-box-shadow);
|
box-shadow: var(--card-box-shadow);
|
||||||
@@ -1911,17 +1917,11 @@ footer.file-footer button {
|
|||||||
border-radius: 0.5em;
|
border-radius: 0.5em;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
margin: 1.25em 0;
|
.admonition p:last-child {
|
||||||
position: relative;
|
|
||||||
padding-left: 2.5em;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ck-content .admonition p:last-child {
|
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ck-content .admonition::before {
|
.admonition::before {
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
font-family: boxicons !important;
|
font-family: boxicons !important;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -1929,11 +1929,11 @@ footer.file-footer button {
|
|||||||
left: 1em;
|
left: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ck-content .admonition.note { --accent-color: #69c7ff; }
|
.admonition.note { --accent-color: #69c7ff; }
|
||||||
.ck-content .admonition.tip { --accent-color: #40c025; }
|
.admonition.tip { --accent-color: #40c025; }
|
||||||
.ck-content .admonition.important { --accent-color: #9839f7; }
|
.admonition.important { --accent-color: #9839f7; }
|
||||||
.ck-content .admonition.caution { --accent-color: #ff2e2e; }
|
.admonition.caution { --accent-color: #ff2e2e; }
|
||||||
.ck-content .admonition.warning { --accent-color: #e2aa03; }
|
.admonition.warning { --accent-color: #e2aa03; }
|
||||||
|
|
||||||
.ck-content .admonition.note::before { content: "\eb21"; }
|
.ck-content .admonition.note::before { content: "\eb21"; }
|
||||||
.ck-content .admonition.tip::before { content: "\ea0d"; }
|
.ck-content .admonition.tip::before { content: "\ea0d"; }
|
||||||
@@ -1990,3 +1990,44 @@ footer.file-footer button {
|
|||||||
border-left: 2px solid #e2aa03;
|
border-left: 2px solid #e2aa03;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
.admonition.note::before { content: "\eb21"; }
|
||||||
|
.admonition.tip::before { content: "\ea0d"; }
|
||||||
|
.admonition.important::before { content: "\ea7c"; }
|
||||||
|
.admonition.caution::before { content: "\eac7"; }
|
||||||
|
.admonition.warning::before { content: "\eac5"; }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* In-content floating buttons
|
||||||
|
*/
|
||||||
|
|
||||||
|
.content-floating-buttons {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10; /* should be below dropdown (note actions) */
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-floating-buttons.top-left {
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-floating-buttons.bottom-left {
|
||||||
|
bottom: 10px;
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-floating-buttons.bottom-right {
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-floating-buttons button.bx {
|
||||||
|
font-size: 130%;
|
||||||
|
padding: 1px 10px 1px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Customized icons */
|
||||||
|
|
||||||
|
.bx-tn-toc::before {
|
||||||
|
content: "\ec24";
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|||||||
@@ -174,6 +174,19 @@
|
|||||||
--promoted-attribute-card-background-color: var(--card-background-color);
|
--promoted-attribute-card-background-color: var(--card-background-color);
|
||||||
--promoted-attribute-card-shadow-color: #000000b3;
|
--promoted-attribute-card-shadow-color: #000000b3;
|
||||||
|
|
||||||
|
--floating-button-shadow-color: #00000080;
|
||||||
|
--floating-button-background-color: #494949d2;
|
||||||
|
--floating-button-color: var(--button-text-color);
|
||||||
|
--floating-button-hover-background: #ffffff20;
|
||||||
|
--floating-button-hover-color: white;
|
||||||
|
--floating-button-show-button-background: var(--left-pane-item-action-button-background);
|
||||||
|
--floating-button-show-button-color: var(--left-pane-item-action-button-color);
|
||||||
|
--floating-button-show-button-shadow: none;
|
||||||
|
--floating-button-show-button-hover-background: var(--left-pane-item-action-button-hover-background);
|
||||||
|
--floating-button-show-button-hover-shadow: 0 2px 4px #0000007b;
|
||||||
|
--floating-button-hide-button-background: #00000029;
|
||||||
|
--floating-button-hide-button-color: #ffffff63;
|
||||||
|
|
||||||
--right-pane-item-hover-background: #ffffff26;
|
--right-pane-item-hover-background: #ffffff26;
|
||||||
--right-pane-item-hover-color: white;
|
--right-pane-item-hover-color: white;
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,19 @@
|
|||||||
--promoted-attribute-card-background-color: var(--card-background-color);
|
--promoted-attribute-card-background-color: var(--card-background-color);
|
||||||
--promoted-attribute-card-shadow-color: #00000033;
|
--promoted-attribute-card-shadow-color: #00000033;
|
||||||
|
|
||||||
|
--floating-button-shadow-color: #00000042;
|
||||||
|
--floating-button-background-color: #eaeaeacc;
|
||||||
|
--floating-button-color: #454545;
|
||||||
|
--floating-button-hover-background: #00000017;
|
||||||
|
--floating-button-hover-color: black;
|
||||||
|
--floating-button-show-button-background: var(--left-pane-item-action-button-background);
|
||||||
|
--floating-button-show-button-color: var(--left-pane-item-action-button-color);
|
||||||
|
--floating-button-show-button-shadow: none;
|
||||||
|
--floating-button-show-button-hover-background: var(--left-pane-item-action-button-hover-background);
|
||||||
|
--floating-button-show-button-hover-shadow: 0 2px 4px #00000068;
|
||||||
|
--floating-button-hide-button-background: #0000000d;
|
||||||
|
--floating-button-hide-button-color: gray;
|
||||||
|
|
||||||
--new-tab-button-background: #d8d8d8;
|
--new-tab-button-background: #d8d8d8;
|
||||||
--new-tab-button-color: #3a3a3a;
|
--new-tab-button-color: #3a3a3a;
|
||||||
--new-tab-button-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
--new-tab-button-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
|||||||
@@ -53,6 +53,11 @@
|
|||||||
|
|
||||||
--center-pane-border-radius: 10px;
|
--center-pane-border-radius: 10px;
|
||||||
|
|
||||||
|
--floating-button-height: 34px;
|
||||||
|
--floating-button-width: 40px;
|
||||||
|
--floating-button-icon-size: 20px;
|
||||||
|
--floating-button-show-hide-button-size: 26px;
|
||||||
|
|
||||||
--menu-padding-size: 8px;
|
--menu-padding-size: 8px;
|
||||||
--menu-item-icon-vert-offset: -2px;
|
--menu-item-icon-vert-offset: -2px;
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,11 @@ div.note-detail-empty {
|
|||||||
height: 1em;
|
height: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Add a gap between consecutive buttons */
|
||||||
|
.note-detail-content-widget-content.options button.btn + button.btn {
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.note-detail-content-widget-content.options:has(.shortcuts-options-section)::after {
|
.note-detail-content-widget-content.options:has(.shortcuts-options-section)::after {
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1298,6 +1298,201 @@ div.promoted-attribute-cell .multiplicity:has(span) {
|
|||||||
font-size: 0; /* Prevent whitespaces creating a gap between buttons */
|
font-size: 0; /* Prevent whitespaces creating a gap between buttons */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Floating buttons
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Floating buttons container */
|
||||||
|
div#center-pane .floating-buttons-children {
|
||||||
|
opacity: 1;
|
||||||
|
min-height: var(--floating-button-height);
|
||||||
|
transform-origin: right;
|
||||||
|
box-shadow: 1px 1px 1px var(--floating-button-shadow-color);
|
||||||
|
background: var(--floating-button-background-color);
|
||||||
|
backdrop-filter: blur(10px) saturate(6);
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: transform 250ms ease-out,
|
||||||
|
opacity 250ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Floating buttons container (collapsed) */
|
||||||
|
div#center-pane .floating-buttons-children.temporarily-hidden {
|
||||||
|
display: flex !important;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scaleX(0);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Floating buttons */
|
||||||
|
|
||||||
|
.floating-buttons-children {
|
||||||
|
--border-radius-size: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-buttons-children > * {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.floating-buttons-children > button,
|
||||||
|
div.floating-buttons-children .floating-button {
|
||||||
|
width: var(--floating-button-width);
|
||||||
|
height: var(--floating-button-height);
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
font-size: var(--floating-button-icon-size);
|
||||||
|
align-items: center;
|
||||||
|
color: var(--floating-button-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.floating-buttons-children > button:hover,
|
||||||
|
div.floating-buttons-children .floating-button:hover {
|
||||||
|
background: var(--floating-button-hover-background);
|
||||||
|
color: var(--floating-button-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.floating-buttons-children > button:active,
|
||||||
|
div.floating-buttons-children .floating-button:active {
|
||||||
|
font-size: calc(var(--floating-button-icon-size) * 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The first visible floating button */
|
||||||
|
div.floating-buttons-children > *:nth-child(1 of .visible) {
|
||||||
|
--border-radius: var(--border-radius-size) 0 0 var(--border-radius-size);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "Show / hide floating buttons" buttons */
|
||||||
|
|
||||||
|
@keyframes floating-buttons-show-hide-button-animation {
|
||||||
|
from {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
} to {
|
||||||
|
transform: rotate(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.floating-buttons-children .close-floating-buttons-button,
|
||||||
|
div.floating-buttons .show-floating-buttons-button {
|
||||||
|
margin-top: calc((var(--floating-button-height) - var(--floating-button-show-hide-button-size)) / 2);
|
||||||
|
width: var(--floating-button-show-hide-button-size);
|
||||||
|
height: var(--floating-button-show-hide-button-size);
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
font-size: calc(var(--floating-button-show-hide-button-size) * .8);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.floating-buttons-children .close-floating-buttons-button:active,
|
||||||
|
div.floating-buttons .show-floating-buttons-button:active {
|
||||||
|
font-size: calc(var(--floating-button-show-hide-button-size) * .65);
|
||||||
|
background: transparent!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.floating-buttons-children .close-floating-buttons-button::before,
|
||||||
|
div.floating-buttons .show-floating-buttons-button::before {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "Show buttons" button */
|
||||||
|
|
||||||
|
div.floating-buttons .show-floating-buttons-button {
|
||||||
|
box-shadow: var(--floating-button-show-button-shadow);
|
||||||
|
background: var(--floating-button-show-button-background);
|
||||||
|
color: var(--floating-button-show-button-color);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.floating-buttons .show-floating-buttons-button::before {
|
||||||
|
animation: floating-buttons-show-hide-button-animation 400ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.floating-buttons .show-floating-buttons-button:hover,
|
||||||
|
div.floating-buttons .show-floating-buttons-button:active {
|
||||||
|
box-shadow: var(--floating-button-show-button-hover-shadow);
|
||||||
|
background: var(--floating-button-show-button-hover-background) !important;
|
||||||
|
color: var(--floating-button-show-button-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "Hide buttons" button */
|
||||||
|
|
||||||
|
div.floating-buttons-children:not(.temporarily-hidden) .close-floating-buttons-button::before {
|
||||||
|
animation: floating-buttons-show-hide-button-animation 400ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.floating-buttons-children .close-floating-buttons {
|
||||||
|
border-radius: 0 var(--border-radius-size) var(--border-radius-size) 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.floating-buttons-children .close-floating-buttons {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
background: var(--floating-button-hide-button-background);
|
||||||
|
color: var(--floating-button-hide-button-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.floating-buttons-children .close-floating-buttons:has(.close-floating-buttons-button:hover) {
|
||||||
|
background: var(--floating-button-hover-background);
|
||||||
|
color: var(--floating-button-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Backlink count */
|
||||||
|
|
||||||
|
.backlinks-widget .backlinks-ticker {
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background: transparent;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backlinks-widget .backlinks-ticker:hover {
|
||||||
|
background: var(--floating-button-hover-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backlinks-widget .backlinks-items {
|
||||||
|
--menu-padding-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TODO: Restyle the path */
|
||||||
|
.backlinks-widget .backlinks-items .note-path {
|
||||||
|
padding-inline-start: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Copy image reference */
|
||||||
|
|
||||||
|
.floating-buttons .copy-image-reference-button .hidden-image-copy {
|
||||||
|
/* Take out of the the hidden image from flexbox to prevent the layout being affected */
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code, relation map buttons */
|
||||||
|
|
||||||
|
.floating-buttons .code-buttons-widget,
|
||||||
|
.floating-buttons .relation-map-buttons {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The highlight animation */
|
||||||
|
|
||||||
|
@keyframes floating-button-highlight {
|
||||||
|
from {
|
||||||
|
background: var(--floating-button-background);
|
||||||
|
color: var(--floating-button-color);
|
||||||
|
} to {
|
||||||
|
background: var(--floating-button-hover-background);
|
||||||
|
color: var(--floating-button-hover-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-buttons .bx-tada {
|
||||||
|
/* The class is applied for 1700ms */
|
||||||
|
animation: floating-button-highlight 425ms ease-in-out alternate infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-buttons .bx-tada::before {
|
||||||
|
font-size: var(--floating-button-icon-size);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Find and replace bar
|
* Find and replace bar
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1455,9 +1455,6 @@
|
|||||||
"title": "高亮列表",
|
"title": "高亮列表",
|
||||||
"options": "选项"
|
"options": "选项"
|
||||||
},
|
},
|
||||||
"mermaid": {
|
|
||||||
"diagram_error": "图表无法显示。 请参考 <a href=\"https://mermaid-js.github.io/mermaid/#/flowchart?id=graph\">帮助文档和示例</a>。"
|
|
||||||
},
|
|
||||||
"quick-search": {
|
"quick-search": {
|
||||||
"placeholder": "快速搜索",
|
"placeholder": "快速搜索",
|
||||||
"searching": "正在搜索...",
|
"searching": "正在搜索...",
|
||||||
|
|||||||
@@ -1449,9 +1449,6 @@
|
|||||||
"title": "Hervorhebungs-Liste",
|
"title": "Hervorhebungs-Liste",
|
||||||
"options": "Optionen"
|
"options": "Optionen"
|
||||||
},
|
},
|
||||||
"mermaid": {
|
|
||||||
"diagram_error": "Das Diagramm konnte nicht angezeigt werden. Siehe <a href=\"https://mermaid-js.github.io/mermaid/#/flowchart?id=graph\">Hilfe und Beispiele</a>."
|
|
||||||
},
|
|
||||||
"quick-search": {
|
"quick-search": {
|
||||||
"placeholder": "Schnellsuche",
|
"placeholder": "Schnellsuche",
|
||||||
"searching": "Suche läuft…",
|
"searching": "Suche läuft…",
|
||||||
|
|||||||
@@ -1582,9 +1582,6 @@
|
|||||||
"title": "Highlights List",
|
"title": "Highlights List",
|
||||||
"options": "Options"
|
"options": "Options"
|
||||||
},
|
},
|
||||||
"mermaid": {
|
|
||||||
"diagram_error": "The diagram could not be displayed. See <a href=\"https://mermaid-js.github.io/mermaid/#/flowchart?id=graph\">help and examples</a>."
|
|
||||||
},
|
|
||||||
"quick-search": {
|
"quick-search": {
|
||||||
"placeholder": "Quick search",
|
"placeholder": "Quick search",
|
||||||
"searching": "Searching...",
|
"searching": "Searching...",
|
||||||
@@ -1853,5 +1850,16 @@
|
|||||||
"processing": {
|
"processing": {
|
||||||
"common": "Processing..."
|
"common": "Processing..."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"switch_layout_button": {
|
||||||
|
"title_vertical": "Move editing pane to the bottom",
|
||||||
|
"title_horizontal": "Move editing pane to the left"
|
||||||
|
},
|
||||||
|
"toggle_read_only_button": {
|
||||||
|
"unlock-editing": "Unlock editing",
|
||||||
|
"lock-editing": "Lock editing"
|
||||||
|
},
|
||||||
|
"png_export_button": {
|
||||||
|
"button_title": "Export diagram as PNG"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -744,7 +744,8 @@
|
|||||||
"basic_properties": {
|
"basic_properties": {
|
||||||
"note_type": "Tipo de nota",
|
"note_type": "Tipo de nota",
|
||||||
"editable": "Editable",
|
"editable": "Editable",
|
||||||
"basic_properties": "Propiedades básicas"
|
"basic_properties": "Propiedades básicas",
|
||||||
|
"language": "Idioma"
|
||||||
},
|
},
|
||||||
"book_properties": {
|
"book_properties": {
|
||||||
"view_type": "Tipo de vista",
|
"view_type": "Tipo de vista",
|
||||||
@@ -1090,6 +1091,7 @@
|
|||||||
"title": "Ancho del contenido",
|
"title": "Ancho del contenido",
|
||||||
"default_description": "Trilium limita de forma predeterminada el ancho máximo del contenido para mejorar la legibilidad de ventanas maximizadas en pantallas anchas.",
|
"default_description": "Trilium limita de forma predeterminada el ancho máximo del contenido para mejorar la legibilidad de ventanas maximizadas en pantallas anchas.",
|
||||||
"max_width_label": "Ancho máximo del contenido en píxeles",
|
"max_width_label": "Ancho máximo del contenido en píxeles",
|
||||||
|
"max_width_unit": "píxeles",
|
||||||
"apply_changes_description": "Para aplicar cambios en el ancho del contenido, haga clic en",
|
"apply_changes_description": "Para aplicar cambios en el ancho del contenido, haga clic en",
|
||||||
"reload_button": "recargar la interfaz",
|
"reload_button": "recargar la interfaz",
|
||||||
"reload_description": "cambios desde las opciones de apariencia"
|
"reload_description": "cambios desde las opciones de apariencia"
|
||||||
@@ -1127,7 +1129,11 @@
|
|||||||
"code_auto_read_only_size": {
|
"code_auto_read_only_size": {
|
||||||
"title": "Tamaño automático de solo lectura",
|
"title": "Tamaño automático de solo lectura",
|
||||||
"description": "El tamaño de nota de solo lectura automático es el tamaño después del cual las notas se mostrarán en modo de solo lectura (por razones de rendimiento).",
|
"description": "El tamaño de nota de solo lectura automático es el tamaño después del cual las notas se mostrarán en modo de solo lectura (por razones de rendimiento).",
|
||||||
"label": "Tamaño automático de solo lectura (notas de código)"
|
"label": "Tamaño automático de solo lectura (notas de código)",
|
||||||
|
"unit": "caracteres"
|
||||||
|
},
|
||||||
|
"code-editor-options": {
|
||||||
|
"title": "Editor"
|
||||||
},
|
},
|
||||||
"code_mime_types": {
|
"code_mime_types": {
|
||||||
"title": "Tipos MIME disponibles en el menú desplegable"
|
"title": "Tipos MIME disponibles en el menú desplegable"
|
||||||
@@ -1146,6 +1152,7 @@
|
|||||||
"download_images_description": "El HTML pegado puede contener referencias a imágenes en línea; Trilium encontrará esas referencias y descargará las imágenes para que estén disponibles sin conexión.",
|
"download_images_description": "El HTML pegado puede contener referencias a imágenes en línea; Trilium encontrará esas referencias y descargará las imágenes para que estén disponibles sin conexión.",
|
||||||
"enable_image_compression": "Habilitar la compresión de imágenes",
|
"enable_image_compression": "Habilitar la compresión de imágenes",
|
||||||
"max_image_dimensions": "Ancho/alto máximo de una imagen en píxeles (la imagen cambiará de tamaño si excede esta configuración).",
|
"max_image_dimensions": "Ancho/alto máximo de una imagen en píxeles (la imagen cambiará de tamaño si excede esta configuración).",
|
||||||
|
"max_image_dimensions_unit": "píxeles",
|
||||||
"jpeg_quality_description": "Calidad JPEG (10 - peor calidad, 100 - mejor calidad, se recomienda 50 - 85)"
|
"jpeg_quality_description": "Calidad JPEG (10 - peor calidad, 100 - mejor calidad, se recomienda 50 - 85)"
|
||||||
},
|
},
|
||||||
"attachment_erasure_timeout": {
|
"attachment_erasure_timeout": {
|
||||||
@@ -1177,6 +1184,7 @@
|
|||||||
"note_revisions_snapshot_limit_title": "Límite de respaldos de revisiones de nota",
|
"note_revisions_snapshot_limit_title": "Límite de respaldos de revisiones de nota",
|
||||||
"note_revisions_snapshot_limit_description": "El límite de número de respaldos de revisiones de notas se refiere al número máximo de revisiones que pueden guardarse para cada nota. Donde -1 significa sin límite, 0 significa borrar todas las revisiones. Puede establecer el máximo de revisiones para una sola nota a través de la etiqueta #versioningLimit.",
|
"note_revisions_snapshot_limit_description": "El límite de número de respaldos de revisiones de notas se refiere al número máximo de revisiones que pueden guardarse para cada nota. Donde -1 significa sin límite, 0 significa borrar todas las revisiones. Puede establecer el máximo de revisiones para una sola nota a través de la etiqueta #versioningLimit.",
|
||||||
"snapshot_number_limit_label": "Número límite de respaldos de revisiones de nota:",
|
"snapshot_number_limit_label": "Número límite de respaldos de revisiones de nota:",
|
||||||
|
"snapshot_number_limit_unit": "respaldos",
|
||||||
"erase_excess_revision_snapshots": "Eliminar el exceso de respaldos de revisiones ahora",
|
"erase_excess_revision_snapshots": "Eliminar el exceso de respaldos de revisiones ahora",
|
||||||
"erase_excess_revision_snapshots_prompt": "El exceso de respaldos de revisiones han sido eliminadas."
|
"erase_excess_revision_snapshots_prompt": "El exceso de respaldos de revisiones han sido eliminadas."
|
||||||
},
|
},
|
||||||
@@ -1219,13 +1227,15 @@
|
|||||||
"table_of_contents": {
|
"table_of_contents": {
|
||||||
"title": "Tabla de contenido",
|
"title": "Tabla de contenido",
|
||||||
"description": "La tabla de contenido aparecerá en las notas de texto cuando la nota tenga más de un número definido de títulos. Puede personalizar este número:",
|
"description": "La tabla de contenido aparecerá en las notas de texto cuando la nota tenga más de un número definido de títulos. Puede personalizar este número:",
|
||||||
|
"unit": "títulos",
|
||||||
"disable_info": "También puede utilizar esta opción para desactivar la TDC (TOC) de forma efectiva estableciendo un número muy alto.",
|
"disable_info": "También puede utilizar esta opción para desactivar la TDC (TOC) de forma efectiva estableciendo un número muy alto.",
|
||||||
"shortcut_info": "Puede configurar un atajo de teclado para alternar rápidamente el panel derecho (incluido el TDC) en Opciones -> Atajos (nombre 'toggleRightPane')."
|
"shortcut_info": "Puede configurar un atajo de teclado para alternar rápidamente el panel derecho (incluido el TDC) en Opciones -> Atajos (nombre 'toggleRightPane')."
|
||||||
},
|
},
|
||||||
"text_auto_read_only_size": {
|
"text_auto_read_only_size": {
|
||||||
"title": "Tamaño para modo de solo lectura automático",
|
"title": "Tamaño para modo de solo lectura automático",
|
||||||
"description": "El tamaño de nota de solo lectura automático es el tamaño después del cual las notas se mostrarán en modo de solo lectura (por razones de rendimiento).",
|
"description": "El tamaño de nota de solo lectura automático es el tamaño después del cual las notas se mostrarán en modo de solo lectura (por razones de rendimiento).",
|
||||||
"label": "Tamaño para modo de solo lectura automático (notas de texto)"
|
"label": "Tamaño para modo de solo lectura automático (notas de texto)",
|
||||||
|
"unit": "caracteres"
|
||||||
},
|
},
|
||||||
"i18n": {
|
"i18n": {
|
||||||
"title": "Localización",
|
"title": "Localización",
|
||||||
@@ -1302,7 +1312,7 @@
|
|||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"keyboard_shortcuts": "Atajos de teclado",
|
"keyboard_shortcuts": "Atajos de teclado",
|
||||||
"multiple_shortcuts": "Varios atajos para la misma acción se pueden separar mediante comas.",
|
"multiple_shortcuts": "Varios atajos para la misma acción se pueden separar mediante comas.",
|
||||||
"electron_documentation": "Véa <a href=\"https://www.electronjs.org/docs/latest/api/accelerator\">documentation de Electron </a> para los modificadores y códigos de tecla disponibles.",
|
"electron_documentation": "Véa la <a href=\"https://www.electronjs.org/docs/latest/api/accelerator\">documentación de Electron </a> para los modificadores y códigos de tecla disponibles.",
|
||||||
"type_text_to_filter": "Escriba texto para filtrar los accesos directos...",
|
"type_text_to_filter": "Escriba texto para filtrar los accesos directos...",
|
||||||
"action_name": "Nombre de la acción",
|
"action_name": "Nombre de la acción",
|
||||||
"shortcuts": "Atajos",
|
"shortcuts": "Atajos",
|
||||||
@@ -1326,6 +1336,7 @@
|
|||||||
"config_title": "Configuración de sincronización",
|
"config_title": "Configuración de sincronización",
|
||||||
"server_address": "Dirección de la instancia del servidor",
|
"server_address": "Dirección de la instancia del servidor",
|
||||||
"timeout": "Tiempo de espera de sincronización (milisegundos)",
|
"timeout": "Tiempo de espera de sincronización (milisegundos)",
|
||||||
|
"timeout_unit": "milisegundos",
|
||||||
"proxy_label": "Sincronizar servidor proxy (opcional)",
|
"proxy_label": "Sincronizar servidor proxy (opcional)",
|
||||||
"note": "Nota",
|
"note": "Nota",
|
||||||
"note_description": "Si deja la configuración del proxy en blanco, se utilizará el proxy del sistema (se aplica únicamente a la compilación de escritorio/electron).",
|
"note_description": "Si deja la configuración del proxy en blanco, se utilizará el proxy del sistema (se aplica únicamente a la compilación de escritorio/electron).",
|
||||||
@@ -1421,8 +1432,7 @@
|
|||||||
"widget": "Widget",
|
"widget": "Widget",
|
||||||
"confirm-change": "No es recomendado cambiar el tipo de nota cuando el contenido de la nota no está vacío. ¿Desea continuar de cualquier manera?",
|
"confirm-change": "No es recomendado cambiar el tipo de nota cuando el contenido de la nota no está vacío. ¿Desea continuar de cualquier manera?",
|
||||||
"geo-map": "Mapa Geo",
|
"geo-map": "Mapa Geo",
|
||||||
"beta-feature": "Beta",
|
"beta-feature": "Beta"
|
||||||
"task-list": "Lista de tareas"
|
|
||||||
},
|
},
|
||||||
"protect_note": {
|
"protect_note": {
|
||||||
"toggle-on": "Proteger la nota",
|
"toggle-on": "Proteger la nota",
|
||||||
@@ -1455,9 +1465,6 @@
|
|||||||
"title": "Lista de destacados",
|
"title": "Lista de destacados",
|
||||||
"options": "Opciones"
|
"options": "Opciones"
|
||||||
},
|
},
|
||||||
"mermaid": {
|
|
||||||
"diagram_error": "El diagrama no pudo ser mostrado. Vea <a href=\"https://mermaid-js.github.io/mermaid/#/flowchart?id=graph\">ayuda y ejemplos</a>."
|
|
||||||
},
|
|
||||||
"quick-search": {
|
"quick-search": {
|
||||||
"placeholder": "Búsqueda rápida",
|
"placeholder": "Búsqueda rápida",
|
||||||
"searching": "Buscando...",
|
"searching": "Buscando...",
|
||||||
@@ -1682,5 +1689,27 @@
|
|||||||
"tomorrow": "Mañana",
|
"tomorrow": "Mañana",
|
||||||
"yesterday": "Ayer"
|
"yesterday": "Ayer"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"content_widget": {
|
||||||
|
"unknown_widget": "Widget desconocido para \"{{id}}\"."
|
||||||
|
},
|
||||||
|
"note_language": {
|
||||||
|
"not_set": "No establecido",
|
||||||
|
"configure-languages": "Configurar idiomas..."
|
||||||
|
},
|
||||||
|
"content_language": {
|
||||||
|
"title": "Contenido de idiomas",
|
||||||
|
"description": "Seleccione uno o más idiomas que deben aparecer en la selección del idioma en la sección Propiedades Básicas de una nota de texto de solo lectura o editable. Esto permitirá características tales como corrección de ortografía o soporte de derecha a izquierda."
|
||||||
|
},
|
||||||
|
"switch_layout_button": {
|
||||||
|
"title_vertical": "Mover el panel de edición hacia abajo",
|
||||||
|
"title_horizontal": "Mover el panel de edición a la izquierda"
|
||||||
|
},
|
||||||
|
"toggle_read_only_button": {
|
||||||
|
"unlock-editing": "Desbloquear la edición",
|
||||||
|
"lock-editing": "Bloquear la edición"
|
||||||
|
},
|
||||||
|
"png_export_button": {
|
||||||
|
"button_title": "Exportar diagrama como PNG"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1455,9 +1455,6 @@
|
|||||||
"title": "Accentuations",
|
"title": "Accentuations",
|
||||||
"options": "Options"
|
"options": "Options"
|
||||||
},
|
},
|
||||||
"mermaid": {
|
|
||||||
"diagram_error": "Le diagramme n'a pas pu être affiché. Consultez l'<a href=\"https://mermaid-js.github.io/mermaid/#/flowchart?id=graph\">aide et exemples</a>."
|
|
||||||
},
|
|
||||||
"quick-search": {
|
"quick-search": {
|
||||||
"placeholder": "Recherche rapide",
|
"placeholder": "Recherche rapide",
|
||||||
"searching": "Recherche...",
|
"searching": "Recherche...",
|
||||||
|
|||||||
@@ -1428,9 +1428,6 @@
|
|||||||
"options": "Setări",
|
"options": "Setări",
|
||||||
"title": "Listă de evidențieri"
|
"title": "Listă de evidențieri"
|
||||||
},
|
},
|
||||||
"mermaid": {
|
|
||||||
"diagram_error": "Diagrama nu a putut fi afișată. Vedeți <a href=\"https://mermaid-js.github.io/mermaid/#/flowchart?id=graph\">informații și exemple pe site-ul oficial</a>."
|
|
||||||
},
|
|
||||||
"note_icon": {
|
"note_icon": {
|
||||||
"change_note_icon": "Schimbă iconița notiței",
|
"change_note_icon": "Schimbă iconița notiței",
|
||||||
"category": "Categorie:",
|
"category": "Categorie:",
|
||||||
|
|||||||
@@ -1399,9 +1399,6 @@
|
|||||||
"title": "高亮列表",
|
"title": "高亮列表",
|
||||||
"options": "選項"
|
"options": "選項"
|
||||||
},
|
},
|
||||||
"mermaid": {
|
|
||||||
"diagram_error": "圖表無法顯示。 請參考 <a href=\"https://mermaid-js.github.io/mermaid/#/flowchart?id=graph\">幫助文檔和示例</a>。"
|
|
||||||
},
|
|
||||||
"quick-search": {
|
"quick-search": {
|
||||||
"placeholder": "快速搜尋",
|
"placeholder": "快速搜尋",
|
||||||
"searching": "正在搜尋...",
|
"searching": "正在搜尋...",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { listSyntaxHighlightingThemes } from "../../services/code_block_theme.js
|
|||||||
import type { OptionNames } from "../../services/options_interface.js";
|
import type { OptionNames } from "../../services/options_interface.js";
|
||||||
|
|
||||||
// options allowed to be updated directly in the Options dialog
|
// options allowed to be updated directly in the Options dialog
|
||||||
const ALLOWED_OPTIONS = new Set([
|
const ALLOWED_OPTIONS = new Set<OptionNames>([
|
||||||
"eraseEntitiesAfterTimeInSeconds",
|
"eraseEntitiesAfterTimeInSeconds",
|
||||||
"eraseEntitiesAfterTimeScale",
|
"eraseEntitiesAfterTimeScale",
|
||||||
"protectedSessionTimeout",
|
"protectedSessionTimeout",
|
||||||
@@ -108,7 +108,8 @@ const ALLOWED_OPTIONS = new Set([
|
|||||||
"maxNotesPerLlmQuery",
|
"maxNotesPerLlmQuery",
|
||||||
"enableAutomaticIndexing",
|
"enableAutomaticIndexing",
|
||||||
"embeddingGenerationLocation",
|
"embeddingGenerationLocation",
|
||||||
"embeddingDimensionStrategy"
|
"embeddingDimensionStrategy",
|
||||||
|
"splitEditorOrientation"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function getOptions() {
|
function getOptions() {
|
||||||
@@ -193,7 +194,10 @@ function getSupportedLocales() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isAllowed(name: string) {
|
function isAllowed(name: string) {
|
||||||
return ALLOWED_OPTIONS.has(name) || name.startsWith("keyboardShortcuts") || name.endsWith("Collapsed") || name.startsWith("hideArchivedNotes");
|
return (ALLOWED_OPTIONS as Set<string>).has(name)
|
||||||
|
|| name.startsWith("keyboardShortcuts")
|
||||||
|
|| name.endsWith("Collapsed")
|
||||||
|
|| name.startsWith("hideArchivedNotes");
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user