Merge branch 'develop' of https://github.com/TriliumNext/Notes into develop

This commit is contained in:
Adorian Doran 2025-02-12 01:17:02 +02:00
commit 2f00839f52
74 changed files with 945 additions and 514 deletions

View File

@ -28,6 +28,15 @@ keyPath=
# expressjs shortcuts are supported: loopback(127.0.0.1/8, ::1/128), linklocal(169.254.0.0/16, fe80::/10), uniquelocal(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7)
trustedReverseProxy=false
[Session]
# Use this setting to constrain the current instance's "Path" value for the set cookies
# This can be useful, when you have several instances running on the same domain, under different paths (e.g. by using a reverse proxy).
# It prevents your instances from overwriting each others' cookies.
# e.g. if you have https://your-domain.com/triliumNext/instanceA and https://your-domain.com/triliumNext/instanceB
# you would want to set the cookiePath value to "/triliumNext/instanceA" for your first and "/triliumNext/instanceB" for your second instance
cookiePath=/
[Sync]
#syncServerHost=
#syncServerTimeout=

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

201
package-lock.json generated
View File

@ -13,11 +13,14 @@
"@electron/remote": "2.1.2",
"@excalidraw/excalidraw": "0.17.6",
"@highlightjs/cdn-assets": "11.11.1",
"@joplin/turndown-plugin-gfm": "1.0.61",
"@mermaid-js/layout-elk": "0.1.7",
"@mind-elixir/node-menu": "1.0.4",
"@triliumnext/express-partial-content": "1.0.1",
"@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.16",
"@types/react-dom": "18.3.5",
"@types/swagger-ui-express": "4.1.7",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"autocomplete.js": "0.38.1",
@ -29,7 +32,7 @@
"chokidar": "4.0.3",
"cls-hooked": "4.2.2",
"codemirror": "5.65.18",
"compression": "1.7.5",
"compression": "1.8.0",
"cookie-parser": "1.4.7",
"csrf-csrf": "3.1.0",
"dayjs": "1.11.13",
@ -60,10 +63,10 @@
"is-animated": "2.0.2",
"is-svg": "5.1.0",
"jimp": "1.6.0",
"joplin-turndown-plugin-gfm": "1.0.12",
"jquery": "3.7.1",
"jquery-hotkeys": "0.2.2",
"jquery.fancytree": "2.38.4",
"js-yaml": "4.1.0",
"jsdom": "26.0.0",
"jsplumb": "2.15.6",
"katex": "0.16.21",
@ -71,10 +74,10 @@
"leaflet": "1.9.4",
"leaflet-gpx": "2.1.2",
"mark.js": "8.11.1",
"marked": "15.0.6",
"marked": "15.0.7",
"mermaid": "11.4.1",
"mime-types": "2.1.35",
"mind-elixir": "4.3.6",
"mind-elixir": "4.3.7",
"multer": "1.4.5-lts.1",
"normalize-strings": "1.1.1",
"normalize.css": "8.0.1",
@ -92,6 +95,7 @@
"split.js": "1.6.5",
"stream-throttle": "0.1.3",
"striptags": "3.2.0",
"swagger-ui-express": "5.0.1",
"tmp": "0.2.3",
"ts-loader": "9.5.2",
"turndown": "7.2.0",
@ -155,16 +159,15 @@
"cross-env": "7.0.3",
"electron": "34.1.1",
"esm": "3.2.25",
"jasmine": "5.5.0",
"jsdoc": "4.0.4",
"lorem-ipsum": "2.0.8",
"nodemon": "3.1.9",
"prettier": "3.4.2",
"prettier": "3.5.0",
"rcedit": "4.0.1",
"rimraf": "6.0.1",
"tslib": "2.8.1",
"tsx": "4.19.2",
"typedoc": "0.27.6",
"typedoc": "0.27.7",
"typescript": "5.7.3",
"vitest": "3.0.5",
"webpack": "5.97.1",
@ -2638,6 +2641,12 @@
"node": ">=18"
}
},
"node_modules/@joplin/turndown-plugin-gfm": {
"version": "1.0.61",
"resolved": "https://registry.npmjs.org/@joplin/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.61.tgz",
"integrity": "sha512-m5PNP1OkktlGgmFI7r/HWON/vQA56GCiM1oTWYkY2JFc28Uc8yHj0nT46pahDyU8uRYPj4TXnxLjQzDDJ11i7w==",
"license": "MIT"
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
@ -3431,6 +3440,12 @@
"win32"
]
},
"node_modules/@scarf/scarf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
"hasInstallScript": true
},
"node_modules/@shikijs/engine-oniguruma": {
"version": "1.24.2",
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.24.2.tgz",
@ -3550,7 +3565,6 @@
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/connect": "*",
@ -3613,7 +3627,6 @@
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
@ -3940,7 +3953,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz",
"integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
@ -3953,7 +3965,6 @@
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz",
"integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@ -4028,7 +4039,6 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/ini": {
@ -4055,6 +4065,11 @@
"@types/sizzle": "*"
}
},
"node_modules/@types/js-yaml": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="
},
"node_modules/@types/jsdom": {
"version": "21.1.7",
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz",
@ -4140,7 +4155,6 @@
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/mime-types": {
@ -4187,14 +4201,12 @@
"version": "6.9.17",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz",
"integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
@ -4266,7 +4278,6 @@
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/mime": "^1",
@ -4287,7 +4298,6 @@
"version": "1.15.7",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
@ -4333,6 +4343,15 @@
"@types/node": "*"
}
},
"node_modules/@types/swagger-ui-express": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.7.tgz",
"integrity": "sha512-ovLM9dNincXkzH4YwyYpll75vhzPBlWx6La89wwvYH7mHjVpf0X0K/vR/aUM7SRxmr5tt9z7E5XJcjQ46q+S3g==",
"dependencies": {
"@types/express": "*",
"@types/serve-static": "*"
}
},
"node_modules/@types/tmp": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz",
@ -5167,7 +5186,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/array-flatten": {
@ -6529,9 +6547,9 @@
}
},
"node_modules/compression": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz",
"integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==",
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz",
"integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
@ -11356,84 +11374,6 @@
"node": ">=10"
}
},
"node_modules/jasmine": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.5.0.tgz",
"integrity": "sha512-JKlEVCVD5QBPYLsg/VE+IUtjyseDCrW8rMBu8la+9ysYashDgavMLM9Kotls1FhI6dCJLJ40dBCIfQjGLPZI1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"glob": "^10.2.2",
"jasmine-core": "~5.5.0"
},
"bin": {
"jasmine": "bin/jasmine.js"
}
},
"node_modules/jasmine-core": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.5.0.tgz",
"integrity": "sha512-NHOvoPO6o9gVR6pwqEACTEpbgcH+JJ6QDypyymGbSUIFIFsMMbBJ/xsFNud8MSClfnWclXd7RQlAZBz7yVo5TQ==",
"dev": true,
"license": "MIT"
},
"node_modules/jasmine/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/jasmine/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/jasmine/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/jasmine/node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/jest-worker": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
@ -11501,12 +11441,6 @@
"node": ">=18"
}
},
"node_modules/joplin-turndown-plugin-gfm": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/joplin-turndown-plugin-gfm/-/joplin-turndown-plugin-gfm-1.0.12.tgz",
"integrity": "sha512-qL4+1iycQjZ1fs8zk3jSRk7cg3ROBUHk7GKtiLAQLFzLPKErnILUvz5DLszSQvz3s1sTjPbywLDISVUtBY6HaA==",
"license": "MIT"
},
"node_modules/jpeg-js": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
@ -11540,6 +11474,17 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/js2xmlparser": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz",
@ -12298,9 +12243,9 @@
}
},
"node_modules/marked": {
"version": "15.0.6",
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.6.tgz",
"integrity": "sha512-Y07CUOE+HQXbVDCGl3LXggqJDbXDP2pArc2C1N1RRMN0ONiShoSsIInMd5Gsxupe7fKLpgimTV+HOJ9r7bA+pg==",
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.7.tgz",
"integrity": "sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
@ -12531,9 +12476,9 @@
}
},
"node_modules/mind-elixir": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/mind-elixir/-/mind-elixir-4.3.6.tgz",
"integrity": "sha512-6E9DT5vOYJ7DMDFXJlAnKU3Q6ekwBkR48Tjo6PchEcxJjPURJsiIASxtIeZCfvp8V39N4WyIa3Yt7Q/SFQkVfw==",
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/mind-elixir/-/mind-elixir-4.3.7.tgz",
"integrity": "sha512-Ja8zcKAwjYG180ZGB8WKygmbqs4RFEQZ0JTE1T49/6nCHWXVcA0WNW8Syl0sMbMyYZvQ5kOZWzLylgCvplqhRw==",
"license": "MIT"
},
"node_modules/minimalistic-assert": {
@ -13946,9 +13891,9 @@
}
},
"node_modules/prettier": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.0.tgz",
"integrity": "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==",
"dev": true,
"license": "MIT",
"bin": {
@ -16143,6 +16088,28 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/swagger-ui-dist": {
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.3.tgz",
"integrity": "sha512-G33HFW0iFNStfY2x6QXO2JYVMrFruc8AZRX0U/L71aA7WeWfX2E5Nm8E/tsipSZJeIZZbSjUDeynLK/wcuNWIw==",
"dependencies": {
"@scarf/scarf": "=1.4.0"
}
},
"node_modules/swagger-ui-express": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
"dependencies": {
"swagger-ui-dist": ">=5.0.0"
},
"engines": {
"node": ">= v0.10.32"
},
"peerDependencies": {
"express": ">=4.0.0 || >=5.0.0-beta"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@ -16886,9 +16853,9 @@
}
},
"node_modules/typedoc": {
"version": "0.27.6",
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.27.6.tgz",
"integrity": "sha512-oBFRoh2Px6jFx366db0lLlihcalq/JzyCVp7Vaq1yphL/tbgx2e+bkpkCgJPunaPvPwoTOXSwasfklWHm7GfAw==",
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.27.7.tgz",
"integrity": "sha512-K/JaUPX18+61W3VXek1cWC5gwmuLvYTOXJzBvD9W7jFvbPnefRnCHQCEPw7MSNrP/Hj7JJrhZtDDLKdcYm6ucg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {

View File

@ -25,12 +25,10 @@
"start-server-no-dir": "cross-env TRILIUM_ENV=dev TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 nodemon src/main.ts",
"start-test-server": "npm run switch-server && rimraf ./data-test && cross-env TRILIUM_DATA_DIR=./data-test TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev TRILIUM_PORT=9999 nodemon src/main.ts",
"qstart-server": "npm run switch-server && npm run start-server",
"start-electron": "cross-env NODE_OPTIONS=\"--import tsx\" TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev electron ./electron-main.ts --inspect=5858 .",
"start-electron-nix": "electron-rebuild --version 33.3.1 && cross-env NODE_OPTIONS=\"--import tsx\" TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./electron-main.ts --inspect=5858 .\"",
"start-electron-no-dir": "cross-env NODE_OPTIONS=\"--import tsx\" TRILIUM_ENV=dev electron --inspect=5858 .",
"start-electron-no-dir-nix": "electron-rebuild --version 33.3.1 && cross-env NODE_OPTIONS=\"--import tsx\" TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./electron-main.ts --inspect=5858 .\"",
"start-electron-prod": "npm run prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev electron ./dist/electron-main.js --inspect=5858 .",
"start-electron-prod-nix": "electron-rebuild --version 33.3.1 && npm run prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"",
"start-electron-prod-no-dir": "npm run prepare-dist && cross-env TRILIUM_ENV=dev electron --inspect=5858 .",
@ -64,11 +62,14 @@
"@electron/remote": "2.1.2",
"@excalidraw/excalidraw": "0.17.6",
"@highlightjs/cdn-assets": "11.11.1",
"@joplin/turndown-plugin-gfm": "1.0.61",
"@mermaid-js/layout-elk": "0.1.7",
"@mind-elixir/node-menu": "1.0.4",
"@triliumnext/express-partial-content": "1.0.1",
"@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.16",
"@types/react-dom": "18.3.5",
"@types/swagger-ui-express": "4.1.7",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"autocomplete.js": "0.38.1",
@ -80,7 +81,7 @@
"chokidar": "4.0.3",
"cls-hooked": "4.2.2",
"codemirror": "5.65.18",
"compression": "1.7.5",
"compression": "1.8.0",
"cookie-parser": "1.4.7",
"csrf-csrf": "3.1.0",
"dayjs": "1.11.13",
@ -111,10 +112,10 @@
"is-animated": "2.0.2",
"is-svg": "5.1.0",
"jimp": "1.6.0",
"joplin-turndown-plugin-gfm": "1.0.12",
"jquery": "3.7.1",
"jquery-hotkeys": "0.2.2",
"jquery.fancytree": "2.38.4",
"js-yaml": "4.1.0",
"jsdom": "26.0.0",
"jsplumb": "2.15.6",
"katex": "0.16.21",
@ -122,10 +123,10 @@
"leaflet": "1.9.4",
"leaflet-gpx": "2.1.2",
"mark.js": "8.11.1",
"marked": "15.0.6",
"marked": "15.0.7",
"mermaid": "11.4.1",
"mime-types": "2.1.35",
"mind-elixir": "4.3.6",
"mind-elixir": "4.3.7",
"multer": "1.4.5-lts.1",
"normalize-strings": "1.1.1",
"normalize.css": "8.0.1",
@ -143,6 +144,7 @@
"split.js": "1.6.5",
"stream-throttle": "0.1.3",
"striptags": "3.2.0",
"swagger-ui-express": "5.0.1",
"tmp": "0.2.3",
"ts-loader": "9.5.2",
"turndown": "7.2.0",
@ -203,16 +205,15 @@
"cross-env": "7.0.3",
"electron": "34.1.1",
"esm": "3.2.25",
"jasmine": "5.5.0",
"jsdoc": "4.0.4",
"lorem-ipsum": "2.0.8",
"nodemon": "3.1.9",
"prettier": "3.4.2",
"prettier": "3.5.0",
"rcedit": "4.0.1",
"rimraf": "6.0.1",
"tslib": "2.8.1",
"tsx": "4.19.2",
"typedoc": "0.27.6",
"typedoc": "0.27.7",
"typescript": "5.7.3",
"vitest": "3.0.5",
"webpack": "5.97.1",

View File

@ -80,6 +80,7 @@ export type CommandMappings = {
};
closeTocCommand: CommandData;
showLaunchBarSubtree: CommandData;
showRevisions: CommandData;
showOptions: CommandData & {
section: string;
};
@ -112,6 +113,8 @@ export type CommandMappings = {
openNoteInNewWindow: CommandData;
hideLeftPane: CommandData;
showLeftPane: CommandData;
leaveProtectedSession: CommandData;
enterProtectedSession: CommandData;
openInTab: ContextMenuCommandData;
openNoteInSplit: ContextMenuCommandData;
@ -210,6 +213,12 @@ export type CommandMappings = {
reEvaluateRightPaneVisibility: CommandData;
runActiveNote: CommandData;
scrollContainerToCommand: CommandData & {
position: number;
};
moveThisNoteSplit: CommandData & {
isMovingLeft: boolean;
};
// Geomap
deleteFromMap: { noteId: string },
@ -291,6 +300,7 @@ type EventMappings = {
noteContextReorderEvent: {
oldMainNtxId: string;
newMainNtxId: string;
ntxIdsInOrder: string[];
};
newNoteContextCreated: {
noteContext: NoteContext;
@ -299,7 +309,7 @@ type EventMappings = {
ntxIds: string[];
};
exportSvg: {
ntxId: string;
ntxId: string | null | undefined;
};
geoMapCreateChildNote: {
ntxId: string | null | undefined; // TODO: deduplicate ntxId

View File

@ -30,6 +30,7 @@ import HelpDialog from "../widgets/dialogs/help.js";
import type AppContext from "../components/app_context.js";
import TabRowWidget from "../widgets/tab_row.js";
import JumpToNoteDialog from "../widgets/dialogs/jump_to_note.js";
import RecentChangesDialog from "../widgets/dialogs/recent_changes.js";
const MOBILE_CSS = `
<style>
@ -45,7 +46,7 @@ kbd {
background: none;
border: none;
cursor: pointer;
font-size: 1.5em;
font-size: 1.25em;
padding-left: 0.5em;
padding-right: 0.5em;
color: var(--main-text-color);
@ -151,7 +152,7 @@ export default class MobileLayout {
.css("font-size", "larger")
.css("align-items", "center")
.child(new ToggleSidebarButtonWidget().contentSized())
.child(new NoteTitleWidget().contentSized().css("position", "relative").css("top", "5px").css("padding-left", "0.5em"))
.child(new NoteTitleWidget().contentSized().css("position", "relative").css("padding-left", "0.5em"))
.child(new MobileDetailMenuWidget(true).contentSized())
)
.child(new SharedInfoWidget())
@ -169,7 +170,7 @@ export default class MobileLayout {
new ScrollingContainer()
.filling()
.contentSized()
.child(new NoteDetailWidget().css("padding", "5px 0 10px 0"))
.child(new NoteDetailWidget())
.child(new NoteListWidget())
.child(new FilePropertiesWidget().css("font-size", "smaller"))
)
@ -187,6 +188,7 @@ export default class MobileLayout {
.child(new ClassicEditorToolbar())
.child(new AboutDialog())
.child(new HelpDialog())
.child(new RecentChangesDialog())
.child(new JumpToNoteDialog());
}
}

View File

@ -4,6 +4,16 @@ import appContext, { type NoteCommandData } from "../components/app_context.js";
import froca from "./froca.js";
import utils from "./utils.js";
// Be consistent with `allowedSchemes` in `src\services\html_sanitizer.ts`
// TODO: Deduplicate with server once we can.
export const ALLOWED_PROTOCOLS = [
'http', 'https', 'ftp', 'ftps', 'mailto', 'data', 'evernote', 'file', 'facetime', 'gemini', 'git',
'gopher', 'imap', 'irc', 'irc6', 'jabber', 'jar', 'lastfm', 'ldap', 'ldaps', 'magnet', 'message',
'mumble', 'nfs', 'onenote', 'pop', 'rmi', 's3', 'sftp', 'skype', 'sms', 'spotify', 'steam', 'svn', 'udp',
'view-source', 'vlc', 'vnc', 'ws', 'wss', 'xmpp', 'jdbc', 'slack', 'tel', 'smb', 'zotero', 'geo',
'mid'
];
function getNotePathFromUrl(url: string) {
const notePathMatch = /#(root[A-Za-z0-9_/]*)$/.exec(url);
@ -296,58 +306,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
electron.shell.openPath(hrefLink);
} else {
// Enable protocols supported by CKEditor 5 to be clickable.
// Refer to `allowedProtocols` in https://github.com/TriliumNext/trilium-ckeditor5/blob/main/packages/ckeditor5-build-balloon-block/src/ckeditor.ts.
// And be consistent with `allowedSchemes` in `src\services\html_sanitizer.ts`
const allowedSchemes = [
"http",
"https",
"ftp",
"ftps",
"mailto",
"data",
"evernote",
"file",
"facetime",
"gemini",
"git",
"gopher",
"imap",
"irc",
"irc6",
"jabber",
"jar",
"lastfm",
"ldap",
"ldaps",
"magnet",
"message",
"mumble",
"nfs",
"onenote",
"pop",
"rmi",
"s3",
"sftp",
"skype",
"sms",
"spotify",
"steam",
"svn",
"udp",
"view-source",
"vlc",
"vnc",
"ws",
"wss",
"xmpp",
"jdbc",
"slack",
"tel",
"smb",
"zotero",
"geo"
];
if (allowedSchemes.some((protocol) => hrefLink.toLowerCase().startsWith(protocol + ":"))) {
if (ALLOWED_PROTOCOLS.some((protocol) => hrefLink.toLowerCase().startsWith(protocol + ":"))) {
window.open(hrefLink, "_blank");
}
}

View File

@ -158,7 +158,7 @@ export default class LoadResults {
return Object.keys(this.noteIdToComponentId);
}
isNoteReloaded(noteId: string | undefined, componentId: string | null = null) {
isNoteReloaded(noteId: string | undefined | null, componentId: string | null = null) {
if (!noteId) {
return false;
}

View File

@ -606,7 +606,10 @@ function compareVersions(v1: string, v2: string): number {
/**
* Compares two semantic version strings and returns `true` if the latest version is greater than the current version.
*/
function isUpdateAvailable(latestVersion: string, currentVersion: string): boolean {
function isUpdateAvailable(latestVersion: string | null | undefined, currentVersion: string): boolean {
if (!latestVersion) {
return false;
}
return compareVersions(latestVersion, currentVersion) > 0;
}

View File

@ -15,6 +15,7 @@ import type { CommandData, EventData, EventListener, FilteredCommandNames } from
import type { default as FAttribute, AttributeType } from "../../entities/fattribute.js";
import type FNote from "../../entities/fnote.js";
import { escapeQuotes } from "../../services/utils.js";
import { buildConfig } from "../type_widgets/ckeditor/toolbars.js";
const HELP_TEXT = `
<p>${t("attribute_editor.help_text_body1")}</p>
@ -130,6 +131,7 @@ const mentionSetup: MentionConfig = {
};
const editorConfig = {
...buildConfig(),
removePlugins: [
"Heading",
"Link",

View File

@ -2,13 +2,23 @@ import SwitchWidget from "./switch.js";
import server from "../services/server.js";
import toastService from "../services/toast.js";
import { t } from "../services/i18n.js";
import type FNote from "../entities/fnote.js";
import type { EventData } from "../components/app_context.js";
// TODO: Deduplicate
type Response = {
success: true;
} | {
success: false;
message: string;
}
export default class BookmarkSwitchWidget extends SwitchWidget {
isEnabled() {
return (
super.isEnabled() &&
// it's not possible to bookmark root because that would clone it under bookmarks and thus create a cycle
!["root", "_hidden"].includes(this.noteId)
!["root", "_hidden"].includes(this.noteId ?? "")
);
}
@ -22,21 +32,21 @@ export default class BookmarkSwitchWidget extends SwitchWidget {
this.switchOffTooltip = t("bookmark_switch.remove_bookmark");
}
async toggle(state) {
const resp = await server.put(`notes/${this.noteId}/toggle-in-parent/_lbBookmarks/${!!state}`);
async toggle(state: boolean | null | undefined) {
const resp = await server.put<Response>(`notes/${this.noteId}/toggle-in-parent/_lbBookmarks/${!!state}`);
if (!resp.success) {
toastService.showError(resp.message);
}
}
async refreshWithNote(note) {
async refreshWithNote(note: FNote) {
const isBookmarked = !!note.getParentBranches().find((b) => b.parentNoteId === "_lbBookmarks");
this.isToggled = isBookmarked;
}
entitiesReloadedEvent({ loadResults }) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.getBranchRows().find((b) => b.noteId === this.noteId)) {
this.refresh();
}

View File

@ -19,7 +19,7 @@ export default class AbstractButtonWidget<SettingsT extends AbstractButtonWidget
protected settings!: SettingsT;
protected tooltip!: bootstrap.Tooltip;
isEnabled() {
isEnabled(): boolean | null | undefined {
return true;
}

View File

@ -1,15 +1,19 @@
import froca from "../../services/froca.js";
import attributeService from "../../services/attributes.js";
import CommandButtonWidget from "./command_button.js";
import type { EventData } from "../../components/app_context.js";
export type ButtonNoteIdProvider = () => string;
export default class ButtonFromNoteWidget extends CommandButtonWidget {
constructor() {
super();
this.settings.buttonNoteIdProvider = null;
}
buttonNoteIdProvider(provider) {
buttonNoteIdProvider(provider: ButtonNoteIdProvider) {
this.settings.buttonNoteIdProvider = provider;
return this;
}
@ -21,6 +25,11 @@ export default class ButtonFromNoteWidget extends CommandButtonWidget {
}
updateIcon() {
if (!this.settings.buttonNoteIdProvider) {
console.error(`buttonNoteId for '${this.componentId}' is not defined.`);
return;
}
const buttonNoteId = this.settings.buttonNoteIdProvider();
if (!buttonNoteId) {
@ -29,13 +38,18 @@ export default class ButtonFromNoteWidget extends CommandButtonWidget {
}
froca.getNote(buttonNoteId).then((note) => {
this.settings.icon = note.getIcon();
const icon = note?.getIcon();
if (icon) {
this.settings.icon = icon;
}
this.refreshIcon();
});
}
entitiesReloadedEvent({ loadResults }) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
// TODO: this seems incorrect
//@ts-ignore
const buttonNote = froca.getNoteFromCache(this.buttonNoteIdProvider());
if (!buttonNote) {

View File

@ -1,3 +1,4 @@
import type { EventData } from "../../components/app_context.js";
import { t } from "../../services/i18n.js";
import OnClickButtonWidget from "./onclick_button.js";
@ -11,7 +12,7 @@ export default class ClosePaneButton extends OnClickButtonWidget {
);
}
async noteContextReorderEvent({ ntxIdsInOrder }) {
async noteContextReorderEvent({ ntxIdsInOrder }: EventData<"noteContextReorderEvent">) {
this.refresh();
}

View File

@ -1,6 +1,7 @@
import type { CommandNames } from "../../components/app_context.js";
import keyboardActionsService, { type Action } from "../../services/keyboard_actions.js";
import AbstractButtonWidget, { type AbstractButtonWidgetSettings } from "./abstract_button.js";
import type { ButtonNoteIdProvider } from "./button_from_note.js";
let actions: Action[];
@ -13,6 +14,7 @@ type CommandOrCallback = CommandNames | (() => CommandNames);
interface CommandButtonWidgetSettings extends AbstractButtonWidgetSettings {
command?: CommandOrCallback;
onClick?: ClickHandler;
buttonNoteIdProvider?: ButtonNoteIdProvider | null;
}
export default class CommandButtonWidget extends AbstractButtonWidget<CommandButtonWidgetSettings> {

View File

@ -88,16 +88,6 @@ const TPL = `
font-size: 120%;
margin-right: 6px;
}
body.mobile .global-menu .dropdown-submenu .dropdown-menu {
display: block;
font-size: 90%;
position: relative;
left: 0;
top: 5px;
--dropdown-shadow-opacity: 0;
--submenu-opening-delay: 0;
}
</style>
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true"
@ -344,6 +334,15 @@ export default class GlobalMenuWidget extends BasicWidget {
this.dropdown.toggle();
});
if (utils.isMobile()) {
this.$widget.on("click", ".dropdown-submenu .dropdown-toggle", (e) => {
const $submenu = $(e.target).closest(".dropdown-item");
$submenu.toggleClass("submenu-open");
$submenu.find("ul.dropdown-menu").toggleClass("show");
e.stopPropagation();
return;
});
}
this.$widget.on("click", ".dropdown-submenu", (e) => {
if ($(e.target).children(".dropdown-menu").length === 1 || $(e.target).hasClass("dropdown-toggle")) {
e.stopPropagation();

View File

@ -3,7 +3,10 @@ import appContext from "../../components/app_context.js";
import { t } from "../../services/i18n.js";
export default class MovePaneButton extends OnClickButtonWidget {
constructor(isMovingLeft) {
private isMovingLeft: boolean;
constructor(isMovingLeft: boolean) {
super();
this.isMovingLeft = isMovingLeft;

View File

@ -2,9 +2,13 @@ import OnClickButtonWidget from "./onclick_button.js";
import linkContextMenuService from "../../menus/link_context_menu.js";
import utils from "../../services/utils.js";
import appContext from "../../components/app_context.js";
import type FNote from "../../entities/fnote.js";
export default class OpenNoteButtonWidget extends OnClickButtonWidget {
constructor(noteToOpen) {
private noteToOpen: FNote;
constructor(noteToOpen: FNote) {
super();
this.noteToOpen = noteToOpen;
@ -13,10 +17,14 @@ export default class OpenNoteButtonWidget extends OnClickButtonWidget {
.icon(() => this.noteToOpen.getIcon())
.onClick((widget, evt) => this.launch(evt))
.onAuxClick((widget, evt) => this.launch(evt))
.onContextMenu((evt) => linkContextMenuService.openContextMenu(this.noteToOpen.noteId, evt));
.onContextMenu((evt) => {
if (evt) {
linkContextMenuService.openContextMenu(this.noteToOpen.noteId, evt);
}
});
}
async launch(evt) {
async launch(evt: JQuery.ClickEvent | JQuery.TriggeredEvent | JQuery.ContextMenuEvent) {
if (evt.which === 3) {
return;
}

View File

@ -9,6 +9,6 @@ export default class RevisionsButton extends CommandButtonWidget {
}
isEnabled() {
return super.isEnabled() && !["launcher", "doc"].includes(this.note?.type);
return super.isEnabled() && !["launcher", "doc"].includes(this.note?.type ?? "");
}
}

View File

@ -20,7 +20,7 @@ export default class RightPaneContainer extends FlexContainer<RightPanelWidget>
return super.isEnabled() && !this.rightPaneHidden && this.children.length > 0 && !!this.children.find((ch) => ch.isEnabled() && ch.canBeShown());
}
handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
async handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>) {
const promise = super.handleEventInChildren(name, data);
if (["activeContextChanged", "noteSwitchedAndActivated", "noteSwitched"].includes(name)) {

View File

@ -1,7 +1,8 @@
import type BasicWidget from "../basic_widget.js";
import FlexContainer from "./flex_container.js";
export default class RootContainer extends FlexContainer {
constructor(isHorizontalLayout) {
export default class RootContainer extends FlexContainer<BasicWidget> {
constructor(isHorizontalLayout: boolean) {
super(isHorizontalLayout ? "column" : "row");
this.id("root-widget");

View File

@ -1,50 +0,0 @@
import Container from "./container.js";
export default class ScrollingContainer extends Container {
constructor() {
super();
this.class("scrolling-container");
this.css("overflow", "auto");
this.css("scroll-behavior", "smooth");
this.css("position", "relative");
}
setNoteContextEvent({ noteContext }) {
/** @var {NoteContext} */
this.noteContext = noteContext;
}
async noteSwitchedEvent({ noteContext, notePath }) {
this.$widget.scrollTop(0);
}
async noteSwitchedAndActivatedEvent({ noteContext, notePath }) {
this.noteContext = noteContext;
this.$widget.scrollTop(0);
}
async activeContextChangedEvent({ noteContext }) {
this.noteContext = noteContext;
}
handleEventInChildren(name, data) {
if (name === "readOnlyTemporarilyDisabled" && this.noteContext && this.noteContext.ntxId === data.noteContext.ntxId) {
const scrollTop = this.$widget.scrollTop();
const promise = super.handleEventInChildren(name, data);
// there seems to be some asynchronicity, and we need to wait a bit before scrolling
promise.then(() => setTimeout(() => this.$widget.scrollTop(scrollTop), 500));
return promise;
} else {
return super.handleEventInChildren(name, data);
}
}
scrollContainerToCommand({ position }) {
this.$widget.scrollTop(position);
}
}

View File

@ -0,0 +1,57 @@
import type { CommandListenerData, EventData, EventNames } from "../../components/app_context.js";
import type NoteContext from "../../components/note_context.js";
import type BasicWidget from "../basic_widget.js";
import Container from "./container.js";
export default class ScrollingContainer extends Container<BasicWidget> {
private noteContext?: NoteContext;
constructor() {
super();
this.class("scrolling-container");
this.css("overflow", "auto");
this.css("scroll-behavior", "smooth");
this.css("position", "relative");
}
setNoteContextEvent({ noteContext }: EventData<"setNoteContext">) {
this.noteContext = noteContext;
}
async noteSwitchedEvent({ noteContext, notePath }: EventData<"noteSwitched">) {
this.$widget.scrollTop(0);
}
async noteSwitchedAndActivatedEvent({ noteContext, notePath }: EventData<"noteSwitchedAndActivatedEvent">) {
this.noteContext = noteContext;
this.$widget.scrollTop(0);
}
async activeContextChangedEvent({ noteContext }: EventData<"activeContextChanged">) {
this.noteContext = noteContext;
}
async handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>) {
if (name === "readOnlyTemporarilyDisabled" && this.noteContext && "noteContext" in data && this.noteContext.ntxId === data.noteContext?.ntxId) {
const scrollTop = this.$widget.scrollTop() ?? 0;
const promise = super.handleEventInChildren(name, data);
// there seems to be some asynchronicity, and we need to wait a bit before scrolling
if (promise) {
promise.then(() => setTimeout(() => this.$widget.scrollTop(scrollTop), 500));
}
return promise;
} else {
return super.handleEventInChildren(name, data);
}
}
scrollContainerToCommand({ position }: CommandListenerData<"scrollContainerToCommand">) {
this.$widget.scrollTop(position);
}
}

View File

@ -12,7 +12,7 @@ const TPL = `
</div>
<div class="modal-body">
${t("password_not_set.body1")}
${t("password_not_set.body2")}
</div>
</div>
@ -21,8 +21,13 @@ const TPL = `
`;
export default class PasswordNoteSetDialog extends BasicWidget {
private modal!: bootstrap.Modal;
private $openPasswordOptionsButton!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
//@ts-ignore fix once bootstrap is imported via JQuery.
this.modal = bootstrap.Modal.getOrCreateInstance(this.$widget);
this.$openPasswordOptionsButton = this.$widget.find(".open-password-options-button");
this.$openPasswordOptionsButton.on("click", () => {

View File

@ -8,13 +8,16 @@ const TPL = `
class="copy-image-reference-button"
title="${t("copy_image_reference_button.button_title")}">
<span class="bx bx-copy"></span>
<div class="hidden-image-copy"></div>
</button>`;
export default class CopyImageReferenceButton extends NoteContextAwareWidget {
private $hiddenImageCopy!: JQuery<HTMLElement>;
isEnabled() {
return super.isEnabled() && ["mermaid", "canvas", "mindMap"].includes(this.note?.type) && this.note.isContentAvailable() && this.noteContext?.viewScope.viewMode === "default";
return super.isEnabled() && ["mermaid", "canvas", "mindMap"].includes(this.note?.type ?? "") && this.note?.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
}
doRender() {
@ -24,6 +27,10 @@ export default class CopyImageReferenceButton extends NoteContextAwareWidget {
this.$hiddenImageCopy = this.$widget.find(".hidden-image-copy");
this.$widget.on("click", () => {
if (!this.note) {
return;
}
this.$hiddenImageCopy.empty().append($("<img>").attr("src", utils.createImageSrcUrl(this.note)));
imageService.copyImageReferenceToClipboard(this.$hiddenImageCopy);

View File

@ -11,7 +11,7 @@ const TPL = `
export default class SvgExportButton extends NoteContextAwareWidget {
isEnabled() {
return super.isEnabled() && ["mermaid", "mindMap"].includes(this.note?.type) && this.note.isContentAvailable() && this.noteContext?.viewScope.viewMode === "default";
return super.isEnabled() && ["mermaid", "mindMap"].includes(this.note?.type ?? "") && this.note?.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
}
doRender() {

View File

@ -11,6 +11,11 @@ const TPL = `\
height: 100%;
overflow: hidden;
}
.leaflet-top,
.leaflet-bottom {
z-index: 900;
}
</style>
<div class="geo-map-container"></div>
@ -49,7 +54,8 @@ export default class GeoMapWidget extends NoteContextAwareWidget {
}
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
detectRetina: true
}).addTo(map);
});
}

View File

@ -6,7 +6,7 @@ import branchService from "../../services/branches.js";
import treeService from "../../services/tree.js";
import { t } from "../../services/i18n.js";
const TPL = `<button type="button" class="action-button bx" style="padding-top: 10px;"></button>`;
const TPL = `<button type="button" class="action-button bx"></button>`;
class MobileDetailMenuWidget extends BasicWidget {
private isHorizontalLayout: boolean;

View File

@ -1,7 +1,7 @@
import BasicWidget from "../basic_widget.js";
const TPL = `
<button type="button" class="action-button bx bx-sidebar" style="padding-top: 10px;"></button>`;
<button type="button" class="action-button bx bx-sidebar"></button>`;
class ToggleSidebarButtonWidget extends BasicWidget {
doRender() {

View File

@ -167,6 +167,11 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
}
this.checkFullHeight();
if (utils.isMobile()) {
const hasFixedTree = this.noteContext?.hoistedNoteId === "_lbMobileRoot";
$("body").toggleClass("force-fixed-tree", hasFixedTree);
}
}
/**

View File

@ -18,13 +18,21 @@ const TPL = `
}
.note-title-widget input.note-title {
font-size: 180%;
font-size: 110%;
border: 0;
margin: 2px 0px;
min-width: 5em;
width: 100%;
}
body.mobile .note-title-widget input.note-title {
padding: 0;
}
body.desktop .note-title-widget input.note-title {
font-size: 180%;
}
.note-title-widget input.note-title.protected {
text-shadow: 4px 4px 4px var(--muted-text-color);
}

View File

@ -1,15 +1,22 @@
import FlexContainer from "./containers/flex_container.js";
import utils from "../services/utils.js";
import attributeService from "../services/attributes.js";
import type BasicWidget from "./basic_widget.js";
import type { EventData } from "../components/app_context.js";
import type NoteContext from "../components/note_context.js";
import type FNote from "../entities/fnote.js";
export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
private noteContext?: NoteContext;
export default class NoteWrapperWidget extends FlexContainer {
constructor() {
super("column");
this.css("flex-grow", "1").collapsible();
}
setNoteContextEvent({ noteContext }) {
setNoteContextEvent({ noteContext }: EventData<"setNoteContext">) {
this.noteContext = noteContext;
this.refresh();
@ -41,7 +48,7 @@ export default class NoteWrapperWidget extends FlexContainer {
return;
}
this.$widget.toggleClass("full-content-width", ["image", "mermaid", "book", "render", "canvas", "webView", "mindMap", "geoMap"].includes(note.type) || !!note?.isLabelTruthy("fullContentWidth"));
this.$widget.toggleClass("full-content-width", this.#isFullWidthNote(note));
this.$widget.addClass(note.getCssClass());
@ -51,7 +58,19 @@ export default class NoteWrapperWidget extends FlexContainer {
this.$widget.toggleClass("protected", note.isProtected);
}
async entitiesReloadedEvent({ loadResults }) {
#isFullWidthNote(note: FNote) {
if (["image", "mermaid", "book", "render", "canvas", "webView", "mindMap", "geoMap"].includes(note.type)) {
return true;
}
if (note.type === "file" && note.mime === "application/pdf") {
return true;
}
return !!note?.isLabelTruthy("fullContentWidth");
}
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
// listening on changes of note.type and CSS class
const noteId = this.noteContext?.noteId;

View File

@ -1,3 +1,5 @@
import type { EventData } from "../components/app_context.js";
import type FNote from "../entities/fnote.js";
import { t } from "../services/i18n.js";
import protectedSessionService from "../services/protected_session.js";
import SwitchWidget from "./switch.js";
@ -14,18 +16,22 @@ export default class ProtectedNoteSwitchWidget extends SwitchWidget {
}
switchOn() {
protectedSessionService.protectNote(this.noteId, true, false);
if (this.noteId) {
protectedSessionService.protectNote(this.noteId, true, false);
}
}
switchOff() {
protectedSessionService.protectNote(this.noteId, false, false);
if (this.noteId) {
protectedSessionService.protectNote(this.noteId, false, false);
}
}
async refreshWithNote(note) {
async refreshWithNote(note: FNote) {
this.isToggled = note.isProtected;
}
entitiesReloadedEvent({ loadResults }) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isNoteReloaded(this.noteId)) {
this.refresh();
}

View File

@ -10,7 +10,10 @@ import QuickSearchWidget from "./quick_search.js";
* - Hiding the widget on mobile.
*/
export default class QuickSearchLauncherWidget extends QuickSearchWidget {
constructor(isHorizontalLayout) {
private isHorizontalLayout: boolean;
constructor(isHorizontalLayout: boolean) {
super();
this.isHorizontalLayout = isHorizontalLayout;
}

View File

@ -38,6 +38,10 @@ const TPL = `\
user-select: none;
}
body.mobile .classic-toolbar-widget.visible::-webkit-scrollbar {
height: 3px;
}
@media (max-width: 991px) {
body.mobile .classic-toolbar-widget.visible {
bottom: calc(var(--tab-bar-height) + var(--launcher-pane-height) + var(--mobile-bottom-offset));

View File

@ -1,3 +1,4 @@
import type FNote from "../../entities/fnote.js";
import { t } from "../../services/i18n.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
@ -19,6 +20,9 @@ const TPL = `
* TODO: figure out better name or conceptualize better.
*/
export default class NotePropertiesWidget extends NoteContextAwareWidget {
private $pageUrl!: JQuery<HTMLElement>;
isEnabled() {
return this.note && !!this.note.getLabelValue("pageUrl");
}
@ -39,9 +43,9 @@ export default class NotePropertiesWidget extends NoteContextAwareWidget {
this.$pageUrl = this.$widget.find(".page-url");
}
async refreshWithNote(note) {
async refreshWithNote(note: FNote) {
const pageUrl = note.getLabelValue("pageUrl");
this.$pageUrl.attr("href", pageUrl).attr("title", pageUrl).text(pageUrl);
this.$pageUrl.attr("href", pageUrl).attr("title", pageUrl).text(pageUrl ?? "");
}
}

View File

@ -3,8 +3,11 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js";
const TPL = `<div class="scroll-padding-widget"></div>`;
export default class ScrollPaddingWidget extends NoteContextAwareWidget {
private $scrollingContainer!: JQuery<HTMLElement>;
isEnabled() {
return super.isEnabled() && ["text", "code"].includes(this.note?.type);
return super.isEnabled() && ["text", "code"].includes(this.note?.type ?? "");
}
doRender() {
@ -25,6 +28,6 @@ export default class ScrollPaddingWidget extends NoteContextAwareWidget {
refreshHeight() {
const containerHeight = this.$scrollingContainer.height();
this.$widget.css("height", Math.round(containerHeight / 2));
this.$widget.css("height", Math.round((containerHeight ?? 0) / 2));
}
}

View File

@ -27,7 +27,7 @@ export default class Debug extends AbstractSearchOption {
return "label";
}
static async create(noteId) {
static async create(noteId: string) {
await AbstractSearchOption.setAttribute(noteId, "label", "debug");
}

View File

@ -12,7 +12,7 @@ const TPL = `
<span class="bx bx-help-circle icon-action" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
<div class="dropdown-menu dropdown-menu-right p-4">
${t("fast_search.description")}
</div>
</div>
</div>
<span class="bx bx-x icon-action search-option-del"></span>
</td>
@ -26,7 +26,7 @@ export default class FastSearch extends AbstractSearchOption {
return "label";
}
static async create(noteId) {
static async create(noteId: string) {
await AbstractSearchOption.setAttribute(noteId, "label", "fastSearch");
}

View File

@ -20,7 +20,7 @@ export default class IncludeArchivedNotes extends AbstractSearchOption {
return "label";
}
static async create(noteId) {
static async create(noteId: string) {
await AbstractSearchOption.setAttribute(noteId, "label", "includeArchivedNotes");
}

View File

@ -123,13 +123,13 @@ export default class SwitchWidget extends NoteContextAwareWidget {
private $switchButton!: JQuery<HTMLElement>;
private $switchToggle!: JQuery<HTMLElement>;
private $switchName!: JQuery<HTMLElement>;
private $helpButton!: JQuery<HTMLElement>;
protected $helpButton!: JQuery<HTMLElement>;
private switchOnName = "";
private switchOnTooltip = "";
protected switchOnName = "";
protected switchOnTooltip = "";
private switchOffName = "";
private switchOffTooltip = "";
protected switchOffName = "";
protected switchOffTooltip = "";
private disabledTooltip = "";

View File

@ -9,11 +9,13 @@ import froca from "../services/froca.js";
import attributeService from "../services/attributes.js";
import type NoteContext from "../components/note_context.js";
const isDesktop = utils.isDesktop();
const TAB_CONTAINER_MIN_WIDTH = 24;
const TAB_CONTAINER_MAX_WIDTH = 240;
const TAB_CONTAINER_LEFT_PADDING = 5;
const NEW_TAB_WIDTH = 32;
const MIN_FILLER_WIDTH = 50;
const MIN_FILLER_WIDTH = (isDesktop ? 50 : 15);
const MARGIN_WIDTH = 5;
const TAB_SIZE_SMALL = 84;
@ -26,7 +28,7 @@ const TAB_TPL = `
<div class="note-tab-drag-handle"></div>
<div class="note-tab-icon"></div>
<div class="note-tab-title"></div>
<div class="note-tab-close bx bx-x" title="${t("tab_row.close_tab")}" data-trigger-command="closeActiveTab"></div>
<div class="note-tab-close bx bx-x" title="${t("tab_row.close_tab")}"></div>
</div>
</div>`;
@ -99,6 +101,10 @@ const TAB_ROW_TPL = `
height: 100%;
}
body.mobile .tab-row-filler {
display: none;
}
.tab-row-widget .note-tab[active] {
z-index: 5;
}
@ -186,7 +192,7 @@ const TAB_ROW_TPL = `
background-color: var(--tab-background-color, var(--active-tab-hover-background-color));
}
.tab-row-widget .note-tab .note-tab-close:hover {
body.desktop .tab-row-widget .note-tab .note-tab-close:hover {
background-color: var(--hover-item-background-color);
color: var(--hover-item-text-color);
}
@ -424,6 +430,11 @@ export default class TabRowWidget extends BasicWidget {
return true; // event has been handled
}
});
$tab.find(".note-tab-close").on("click", (e) => {
this.triggerCommand("closeActiveTab", { $el: $(e.target) });
return true;
});
}
get activeTabEl() {
@ -494,10 +505,6 @@ export default class TabRowWidget extends BasicWidget {
}
setupDraggabilly() {
if (utils.isMobile()) {
return;
}
const tabEls = this.tabEls;
const { tabPositions } = this.getTabPositions();

View File

@ -1,13 +1,16 @@
import SwitchWidget from "./switch.js";
import attributeService from "../services/attributes.js";
import { t } from "../services/i18n.js";
import type { EventData } from "../components/app_context.js";
import type FNote from "../entities/fnote.js";
/**
* Switch for the basic properties widget which allows the user to select whether the note is a template or not, which toggles the `#template` attribute.
*/
export default class TemplateSwitchWidget extends SwitchWidget {
isEnabled() {
return super.isEnabled() && !this.noteId.startsWith("_options");
return super.isEnabled() && !this.noteId?.startsWith("_options");
}
doRender() {
@ -23,21 +26,25 @@ export default class TemplateSwitchWidget extends SwitchWidget {
}
async switchOn() {
await attributeService.setLabel(this.noteId, "template");
}
async switchOff() {
for (const templateAttr of this.note.getOwnedLabels("template")) {
await attributeService.removeAttributeById(this.noteId, templateAttr.attributeId);
if (this.noteId) {
await attributeService.setLabel(this.noteId, "template");
}
}
async refreshWithNote(note) {
async switchOff() {
if (this.note && this.noteId) {
for (const templateAttr of this.note.getOwnedLabels("template")) {
await attributeService.removeAttributeById(this.noteId, templateAttr.attributeId);
}
}
}
async refreshWithNote(note: FNote) {
const isTemplate = note.hasLabel("template");
this.isToggled = isTemplate;
}
entitiesReloadedEvent({ loadResults }) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.getAttributeRows().find((attr) => attr.type === "label" && attr.name === "template" && attr.noteId === this.noteId)) {
this.refresh();
}

View File

@ -1,5 +1,6 @@
import TypeWidget from "./type_widget.js";
import { t } from "../../services/i18n.js";
import type FNote from "../../entities/fnote.js";
const TPL = `
<div class="note-detail-book note-detail-printable">
@ -19,6 +20,9 @@ const TPL = `
</div>`;
export default class BookTypeWidget extends TypeWidget {
private $helpNoChildren!: JQuery<HTMLElement>;
static getType() {
return "book";
}
@ -30,7 +34,7 @@ export default class BookTypeWidget extends TypeWidget {
super.doRender();
}
async doRefresh(note) {
this.$helpNoChildren.toggle(!this.note.hasChildren());
async doRefresh(note: FNote) {
this.$helpNoChildren.toggle(!this.note?.hasChildren());
}
}

View File

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

View File

@ -33,6 +33,19 @@ const TPL = `<div class="note-detail-doc note-detail-printable">
margin: 0;
padding-bottom: 0.25em;
}
img {
max-width: 90vw;
height: auto;
}
td img {
max-width: 40vw;
}
figure.table {
overflow: auto !important;
}
</style>
<div class="note-detail-doc-content"></div>
@ -79,6 +92,7 @@ export default class DocTypeWidget extends TypeWidget {
});
} else {
this.$content.empty();
resolve();
}
});
}

View File

@ -15,6 +15,7 @@ import options from "../../services/options.js";
import toast from "../../services/toast.js";
import { getMermaidConfig } from "../mermaid.js";
import { normalizeMimeTypeForCKEditor } from "../../services/mime_type_definitions.js";
import { buildConfig, buildToolbarConfig } from "./ckeditor/toolbars.js";
const ENABLE_INSPECTOR = false;
@ -183,16 +184,11 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.watchdog.setCreator(async (elementOrData, editorConfig) => {
logInfo("Creating new CKEditor");
const extraOpts = {};
if (isClassicEditor) {
extraOpts.toolbar = {
shouldNotGroupWhenFull: utils.isDesktop() && options.get("textNoteEditorMultilineToolbar") === "true"
};
}
const editor = await editorClass.create(elementOrData, {
...editorConfig,
...extraOpts,
...buildConfig(),
...buildToolbarConfig(isClassicEditor),
htmlSupport: {
allow: JSON.parse(options.get("allowedHtmlTags")),
styles: true,

View File

@ -1,6 +1,8 @@
import openService from "../../services/open.js";
import TypeWidget from "./type_widget.js";
import { t } from "../../services/i18n.js";
import type { EventData } from "../../components/app_context.js";
import type FNote from "../../entities/fnote.js";
const TPL = `
<div class="note-detail-file note-detail-printable">
@ -8,12 +10,16 @@ const TPL = `
.type-file .note-detail {
height: 100%;
}
.note-detail-file {
padding: 10px;
height: 100%;
}
.note-split.full-content-width .note-detail-file {
padding: 0;
}
.file-preview-content {
background-color: var(--accented-background-color);
padding: 15px;
@ -22,21 +28,28 @@ const TPL = `
margin: 10px;
}
</style>
<pre class="file-preview-content"></pre>
<div class="file-preview-not-available alert alert-info">
${t("file.file_preview_not_available")}
</div>
<iframe class="pdf-preview" style="width: 100%; height: 100%; flex-grow: 100;"></iframe>
<video class="video-preview" controls></video>
<audio class="audio-preview" controls></audio>
</div>`;
export default class FileTypeWidget extends TypeWidget {
private $previewContent!: JQuery<HTMLElement>;
private $previewNotAvailable!: JQuery<HTMLElement>;
private $pdfPreview!: JQuery<HTMLElement>;
private $videoPreview!: JQuery<HTMLElement>;
private $audioPreview!: JQuery<HTMLElement>;
static getType() {
return "file";
}
@ -52,10 +65,10 @@ export default class FileTypeWidget extends TypeWidget {
super.doRender();
}
async doRefresh(note) {
async doRefresh(note: FNote) {
this.$widget.show();
const blob = await this.note.getBlob();
const blob = await this.note?.getBlob();
this.$previewContent.empty().hide();
this.$pdfPreview.attr("src", "").empty().hide();
@ -63,7 +76,7 @@ export default class FileTypeWidget extends TypeWidget {
this.$videoPreview.hide();
this.$audioPreview.hide();
if (blob.content) {
if (blob?.content) {
this.$previewContent.show().scrollTop(0);
this.$previewContent.text(blob.content);
} else if (note.mime === "application/pdf") {
@ -72,20 +85,20 @@ export default class FileTypeWidget extends TypeWidget {
this.$videoPreview
.show()
.attr("src", openService.getUrlForDownload(`api/notes/${this.noteId}/open-partial`))
.attr("type", this.note.mime)
.css("width", this.$widget.width());
.attr("type", this.note?.mime ?? "")
.css("width", this.$widget.width() ?? 0);
} else if (note.mime.startsWith("audio/")) {
this.$audioPreview
.show()
.attr("src", openService.getUrlForDownload(`api/notes/${this.noteId}/open-partial`))
.attr("type", this.note.mime)
.css("width", this.$widget.width());
.attr("type", this.note?.mime ?? "")
.css("width", this.$widget.width() ?? 0);
} else {
this.$previewNotAvailable.show();
}
}
async entitiesReloadedEvent({ loadResults }) {
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isNoteReloaded(this.noteId)) {
this.refresh();
}

View File

@ -1,9 +1,13 @@
import TypeWidget from "./type_widget.js";
import NoteMapWidget from "../note_map.js";
import type FNote from "../../entities/fnote.js";
const TPL = `<div class="note-detail-note-map note-detail-printable"></div>`;
export default class NoteMapTypeWidget extends TypeWidget {
private noteMapWidget: NoteMapWidget;
static getType() {
return "noteMap";
}
@ -22,7 +26,7 @@ export default class NoteMapTypeWidget extends TypeWidget {
super.doRender();
}
async doRefresh(note) {
async doRefresh(note: FNote) {
await this.noteMapWidget.refresh();
}
}

View File

@ -20,7 +20,7 @@ const TPL = `
<form class="protected-session-password-form">
<div class="form-group">
<label for="protected-session-password-in-detail">${t("protected_session.enter_password_instruction")}</label>
<input class="protected-session-password-in-detail form-control protected-session-password" type="password">
<input id="protected-session-password-in-detail" class="form-control protected-session-password" type="password" autofocus>
</div>
<button class="btn btn-primary">${t("protected_session.start_session_button")}</button>
@ -28,6 +28,10 @@ const TPL = `
</div>`;
export default class ProtectedSessionTypeWidget extends TypeWidget {
private $passwordForm!: JQuery<HTMLElement>;
private $passwordInput!: JQuery<HTMLElement>;
static getType() {
return "protectedSession";
}
@ -38,7 +42,7 @@ export default class ProtectedSessionTypeWidget extends TypeWidget {
this.$passwordInput = this.$widget.find(".protected-session-password");
this.$passwordForm.on("submit", () => {
const password = this.$passwordInput.val();
const password = String(this.$passwordInput.val());
this.$passwordInput.val("");
protectedSessionService.setupProtectedSession(password);

View File

@ -17,7 +17,7 @@ export default abstract class TypeWidget extends NoteContextAwareWidget {
return super.doRender();
}
abstract doRefresh(note: FNote | null | undefined): Promise<void>;
doRefresh(note: FNote | null | undefined) {}
async refresh() {
const thisWidgetType = (this.constructor as any).getType();

View File

@ -61,6 +61,10 @@ body,
overflow: auto;
}
.ck.ck-editor__editable_inline {
overflow: hidden !important;
}
.note-title-widget input,
.note-detail-editable-text,
.note-detail-editable-text-editor {

View File

@ -294,6 +294,8 @@ button kbd {
color: var(--menu-text-color) !important;
font-size: inherit;
background-color: var(--menu-background-color) !important;
user-select: none;
-webkit-user-select: none;
}
body.desktop .dropdown-menu {
@ -357,8 +359,8 @@ body.desktop .dropdown-menu {
visibility: hidden;
}
.dropdown-menu:not(#context-menu-container) .dropdown-item,
#context-menu-container .dropdown-item > span {
body.desktop .dropdown-menu:not(#context-menu-container) .dropdown-item,
body.desktop #context-menu-container .dropdown-item > span {
display: flex;
align-items: center;
}
@ -1128,6 +1130,7 @@ body.mobile .dropdown-submenu > .dropdown-menu {
overflow: hidden !important;
top: unset !important;
margin-top: 0 !important;
width: 100%;
}
#context-menu-container,
@ -1301,17 +1304,18 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
background-color: var(--left-pane-background-color);
}
body.mobile #launcher-pane .dropdown-menu.show {
position: fixed !important;
bottom: calc(var(--mobile-bottom-offset) + var(--launcher-pane-size)) !important;
top: unset !important;
left: 0 !important;
right: 0 !important;
transform: unset !important;
}
/* Mobile, phone mode */
@media (max-width: 991px) {
body.mobile #launcher-pane .dropdown.global-menu > .dropdown-menu.show,
body.mobile #launcher-container .dropdown > .dropdown-menu.show {
position: fixed !important;
bottom: calc(var(--mobile-bottom-offset) + var(--launcher-pane-size)) !important;
top: unset !important;
left: 0 !important;
right: 0 !important;
transform: unset !important;
}
#mobile-sidebar-container {
position: fixed;
top: 0;
@ -1394,12 +1398,6 @@ body.mobile #launcher-pane .dropdown-menu.show {
background-color: var(--launcher-pane-background-color);
}
body.mobile #launcher-pane .dropdown-menu.show {
bottom: unset !important;
top: calc(env(safe-area-inset-top) + var(--launcher-pane-size)) !important;
left: unset !important;
}
#mobile-sidebar-wrapper {
transform: none !important;
background-color: var(--left-pane-background-color) !important;
@ -1414,6 +1412,40 @@ body.mobile #launcher-pane .dropdown-menu.show {
}
}
@media (max-width: 991px) {
body.mobile.force-fixed-tree #mobile-sidebar-wrapper {
padding-top: 0;
position: static;
height: 40vh;
width: 100vw;
transform: none !important;
background-color: var(--left-pane-background-color) !important;
border-bottom: 0.5px solid var(--main-border-color);
}
body.mobile.force-fixed-tree #mobile-sidebar-container {
display: none !important;
}
body.mobile.force-fixed-tree #mobile-sidebar-wrapper .quick-search {
display: none;
}
body.mobile.force-fixed-tree .component > button.bx-sidebar {
visibility: hidden;
padding: 0;
width: 6px;
}
body.mobile.force-fixed-tree #mobile-rest-container {
flex-direction: column !important;
}
body.mobile.force-fixed-tree #detail-container {
flex-grow: 1;
}
}
#launcher-pane {
color: var(--launcher-pane-text-color);
background-color: var(--launcher-pane-background-color);

View File

@ -462,7 +462,7 @@ body.mobile .fancytree-node > span {
background-color: rgba(0, 0, 0, 0.5);
}
body.mobile #mobile-sidebar-wrapper {
body.mobile:not(.force-fixed-tree) #mobile-sidebar-wrapper {
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
border-right: 1px solid var(--subtle-border-color);
@ -1020,10 +1020,6 @@ body.mobile .dropdown-item:not(:last-of-type) {
margin-bottom: 0.5em;
}
body.mobile #launcher-pane .dropdown-submenu > .dropdown-toggle {
display: none;
}
body.mobile .dropdown-submenu:hover {
background: transparent !important;
}
@ -1162,10 +1158,26 @@ body.mobile .dropdown-menu .dropdown-item.submenu-open .dropdown-toggle::after {
}
.calendar-dropdown-widget .calendar-header .calendar-month-selector .select-button {
--select-arrow-svg: ""; /* Disable the dropdown arrow */
--select-arrow-svg: ""; /* Disable the dropdown arrow */
}
min-width: 120px;
padding: 0 10px;
@media (max-width: 992px) {
.calendar-dropdown-widget .calendar-header button {
margin: 0 !important;
padding: 0;
}
.calendar-dropdown-widget .calendar-header .calendar-month-selector .select-button {
padding: 0 8px;
flex-grow: 1;
}
}
@media (min-width: 992px) {
.calendar-dropdown-widget .calendar-header .calendar-month-selector .select-button {
min-width: 120px;
padding: 0 10px;
}
}
.calendar-dropdown-widget .calendar-header .dropdown-toggle::after {

27
src/routes/api_docs.ts Normal file
View File

@ -0,0 +1,27 @@
import type { Router } from "express";
import swaggerUi from "swagger-ui-express";
import { readFile } from "fs/promises";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import yaml from "js-yaml";
import type { JsonObject } from "swagger-ui-express";
const __dirname = dirname(fileURLToPath(import.meta.url));
const swaggerDocument = yaml.load(
await readFile(join(__dirname, "../etapi/etapi.openapi.yaml"), "utf8")
) as JsonObject;
function register(router: Router) {
router.use(
"/etapi",
swaggerUi.serve,
swaggerUi.setup(swaggerDocument, {
explorer: true,
customSiteTitle: "TriliumNext ETAPI Documentation"
})
);
}
export default {
register
};

View File

@ -1,11 +1,12 @@
import { doubleCsrf } from "csrf-csrf";
import sessionSecret from "../services/session_secret.js";
import { isElectron } from "../services/utils.js";
import config from "../services/config.js";
const doubleCsrfUtilities = doubleCsrf({
getSecret: () => sessionSecret,
cookieOptions: {
path: "", // empty, so cookie is valid only for the current path
path: config.Session.cookiePath,
secure: false,
sameSite: "strict",
httpOnly: !isElectron // set to false for Electron, see https://github.com/TriliumNext/Notes/pull/966

View File

@ -57,30 +57,27 @@ function setPassword(req: Request, res: Response) {
}
function login(req: Request, res: Response) {
const guessedPassword = req.body.password;
const { password, rememberMe } = req.body;
if (verifyPassword(guessedPassword)) {
const rememberMe = req.body.rememberMe;
req.session.regenerate(() => {
if (rememberMe) {
req.session.cookie.maxAge = 21 * 24 * 3600000; // 3 weeks
} else {
req.session.cookie.expires = null;
}
req.session.loggedIn = true;
res.redirect(".");
});
} else {
if (!verifyPassword(password)) {
// note that logged IP address is usually meaningless since the traffic should come from a reverse proxy
log.info(`WARNING: Wrong password from ${req.ip}, rejecting.`);
res.status(401).render("login", {
return res.status(401).render("login", {
failedAuth: true,
assetPath: assetPath
});
}
req.session.regenerate(() => {
const sessionMaxAge = 21 * 24 * 3600000 // 3 weeks in Milliseconds
req.session.cookie.maxAge = (rememberMe) ? sessionMaxAge : undefined;
req.session.loggedIn = true;
res.redirect(".");
});
}
function verifyPassword(guessedPassword: string) {

View File

@ -71,7 +71,7 @@ import etapiSpecialNoteRoutes from "../etapi/special_notes.js";
import etapiSpecRoute from "../etapi/spec.js";
import etapiBackupRoute from "../etapi/backup.js";
import apiDocsRoute from "./api_docs.js";
const MAX_ALLOWED_FILE_SIZE_MB = 250;
const GET = "get",
@ -369,6 +369,9 @@ function register(app: express.Application) {
etapiSpecRoute.register(router);
etapiBackupRoute.register(router);
// API Documentation
apiDocsRoute.register(app);
app.use("", router);
}

View File

@ -2,6 +2,7 @@ import session from "express-session";
import sessionFileStore from "session-file-store";
import sessionSecret from "../services/session_secret.js";
import dataDir from "../services/data_dir.js";
import config from "../services/config.js";
const FileStore = sessionFileStore(session);
const sessionParser = session({
@ -9,7 +10,7 @@ const sessionParser = session({
resave: false, // true forces the session to be saved back to the session store, even if the session was never modified during the request.
saveUninitialized: false, // true forces a session that is "uninitialized" to be saved to the store. A session is uninitialized when it is new but not modified.
cookie: {
// path: "/",
path: config.Session.cookiePath,
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // in milliseconds
},

View File

@ -32,6 +32,9 @@ export interface TriliumConfig {
keyPath: string;
trustedReverseProxy: boolean | string;
};
Session: {
cookiePath: string;
}
Sync: {
syncServerHost: string;
syncServerTimeout: string;
@ -76,6 +79,11 @@ const config: TriliumConfig = {
process.env.TRILIUM_NETWORK_TRUSTEDREVERSEPROXY || iniConfig.Network.trustedReverseProxy || false
},
Session: {
cookiePath:
process.env.TRILIUM_SESSION_COOKIEPATH || iniConfig?.Session?.cookiePath || "/"
},
Sync: {
syncServerHost:
process.env.TRILIUM_SYNC_SERVER_HOST || iniConfig?.Sync?.syncServerHost || "",

View File

@ -68,4 +68,10 @@ describe("Markdown export", () => {
expect(markdownExportService.toMarkdown(html)).toBe(expected);
});
it("exports strikethrough text correctly", () => {
const html = "<s>hello</s>Hello <s>world</s>";
const expected = "~~hello~~Hello ~~world~~";
expect(markdownExportService.toMarkdown(html)).toBe(expected);
});
});

View File

@ -1,7 +1,7 @@
"use strict";
import TurndownService from "turndown";
import turndownPluginGfm from "joplin-turndown-plugin-gfm";
import turndownPluginGfm from "@joplin/turndown-plugin-gfm";
let instance: TurndownService | null = null;

View File

@ -6,8 +6,8 @@ import noteService from "./notes.js";
import log from "./log.js";
import migrationService from "./migration.js";
import { t } from "i18next";
import app_path from "./app_path.js";
import { getHelpHiddenSubtreeData } from "./in_app_help.js";
import buildLaunchBarConfig from "./hidden_subtree_launcherbar.js";
const LBTPL_ROOT = "_lbTplRoot";
const LBTPL_BASE = "_lbTplBase";
@ -59,6 +59,8 @@ enum Command {
let hiddenSubtreeDefinition: HiddenSubtreeItem;
function buildHiddenSubtreeDefinition(): HiddenSubtreeItem {
const launchbarConfig = buildLaunchBarConfig();
return {
id: "_hidden",
title: t("hidden-subtree.root-title"),
@ -215,25 +217,7 @@ function buildHiddenSubtreeDefinition(): HiddenSubtreeItem {
icon: "bx-hide",
isExpanded: true,
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: [
{
id: "_lbBackInHistory",
title: t("hidden-subtree.go-to-previous-note-title"),
type: "launcher",
builtinWidget: "backInHistoryButton",
icon: "bx bxs-chevron-left",
attributes: [{ type: "label", name: "docName", value: "launchbar_history_navigation" }]
},
{
id: "_lbForwardInHistory",
title: t("hidden-subtree.go-to-next-note-title"),
type: "launcher",
builtinWidget: "forwardInHistoryButton",
icon: "bx bxs-chevron-right",
attributes: [{ type: "label", name: "docName", value: "launchbar_history_navigation" }]
},
{ id: "_lbBackendLog", title: t("hidden-subtree.backend-log-title"), type: "launcher", targetNoteId: "_backendLog", icon: "bx bx-terminal" }
]
children: launchbarConfig.desktopAvailableLaunchers
},
{
id: "_lbVisibleLaunchers",
@ -242,43 +226,7 @@ function buildHiddenSubtreeDefinition(): HiddenSubtreeItem {
icon: "bx-show",
isExpanded: true,
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: [
{ id: "_lbNewNote", title: t("hidden-subtree.new-note-title"), type: "launcher", command: "createNoteIntoInbox", icon: "bx bx-file-blank" },
{
id: "_lbSearch",
title: t("hidden-subtree.search-notes-title"),
type: "launcher",
command: "searchNotes",
icon: "bx bx-search",
attributes: [{ type: "label", name: "desktopOnly" }]
},
{
id: "_lbJumpTo",
title: t("hidden-subtree.jump-to-note-title"),
type: "launcher",
command: "jumpToNote",
icon: "bx bx-send",
attributes: [{ type: "label", name: "desktopOnly" }]
},
{ id: "_lbNoteMap", title: t("hidden-subtree.note-map-title"), type: "launcher", targetNoteId: "_globalNoteMap", icon: "bx bxs-network-chart" },
{ id: "_lbCalendar", title: t("hidden-subtree.calendar-title"), type: "launcher", builtinWidget: "calendar", icon: "bx bx-calendar" },
{
id: "_lbRecentChanges",
title: t("hidden-subtree.recent-changes-title"),
type: "launcher",
command: "showRecentChanges",
icon: "bx bx-history",
attributes: [{ type: "label", name: "desktopOnly" }]
},
{ id: "_lbSpacer1", title: t("hidden-subtree.spacer-title"), type: "launcher", builtinWidget: "spacer", baseSize: "50", growthFactor: "0" },
{ id: "_lbBookmarks", title: t("hidden-subtree.bookmarks-title"), type: "launcher", builtinWidget: "bookmarks", icon: "bx bx-bookmark" },
{ id: "_lbToday", title: t("hidden-subtree.open-today-journal-note-title"), type: "launcher", builtinWidget: "todayInJournal", icon: "bx bx-calendar-star" },
{ id: "_lbSpacer2", title: t("hidden-subtree.spacer-title"), type: "launcher", builtinWidget: "spacer", baseSize: "0", growthFactor: "1" },
{ id: "_lbQuickSearch", title: t("hidden-subtree.quick-search-title"), type: "launcher", builtinWidget: "quickSearch", icon: "bx bx-rectangle" },
{ id: "_lbProtectedSession", title: t("hidden-subtree.protected-session-title"), type: "launcher", builtinWidget: "protectedSession", icon: "bx bx bx-shield-quarter" },
{ id: "_lbSyncStatus", title: t("hidden-subtree.sync-status-title"), type: "launcher", builtinWidget: "syncStatus", icon: "bx bx-wifi" },
{ id: "_lbSettings", title: t("hidden-subtree.settings-title"), type: "launcher", command: "showOptions", icon: "bx bx-cog" }
]
children: launchbarConfig.desktopVisibleLaunchers
}
]
},
@ -297,7 +245,7 @@ function buildHiddenSubtreeDefinition(): HiddenSubtreeItem {
icon: "bx-hide",
isExpanded: true,
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: []
children: launchbarConfig.mobileAvailableLaunchers
},
{
id: "_lbMobileVisibleLaunchers",
@ -306,25 +254,7 @@ function buildHiddenSubtreeDefinition(): HiddenSubtreeItem {
icon: "bx-show",
isExpanded: true,
attributes: [{ type: "label", name: "docName", value: "launchbar_intro" }],
children: [
{
id: "_lbMobileBackInHistory",
title: t("hidden-subtree.go-to-previous-note-title"),
type: "launcher",
builtinWidget: "backInHistoryButton",
icon: "bx bxs-chevron-left",
attributes: [{ type: "label", name: "docName", value: "launchbar_history_navigation" }]
},
{
id: "_lbMobileForwardInHistory",
title: t("hidden-subtree.go-to-next-note-title"),
type: "launcher",
builtinWidget: "forwardInHistoryButton",
icon: "bx bxs-chevron-right",
attributes: [{ type: "label", name: "docName", value: "launchbar_history_navigation" }]
},
{ id: "_lbMobileJumpTo", title: t("hidden-subtree.jump-to-note-title"), type: "launcher", command: "jumpToNote", icon: "bx bx-plus-circle" }
]
children: launchbarConfig.mobileVisibleLaunchers
}
]
},

View File

@ -0,0 +1,102 @@
import { t } from "i18next";
import type { HiddenSubtreeItem } from "./hidden_subtree.js";
export default function buildLaunchBarConfig() {
const sharedLaunchers: Record<string, Omit<HiddenSubtreeItem, "id">> = {
newNote: {
title: t("hidden-subtree.new-note-title"),
type: "launcher",
command: "createNoteIntoInbox",
icon: "bx bx-file-blank"
},
openToday: {
title: t("hidden-subtree.open-today-journal-note-title"),
type: "launcher",
builtinWidget: "todayInJournal",
icon: "bx bx-calendar-star"
},
backInHistory: {
title: t("hidden-subtree.go-to-previous-note-title"),
type: "launcher",
builtinWidget: "backInHistoryButton",
icon: "bx bxs-chevron-left",
attributes: [{ type: "label", name: "docName", value: "launchbar_history_navigation" }]
},
forwardInHistory: {
title: t("hidden-subtree.go-to-next-note-title"),
type: "launcher",
builtinWidget: "forwardInHistoryButton",
icon: "bx bxs-chevron-right",
attributes: [{ type: "label", name: "docName", value: "launchbar_history_navigation" }]
},
calendar: {
title: t("hidden-subtree.calendar-title"),
type: "launcher",
builtinWidget: "calendar",
icon: "bx bx-calendar"
},
recentChanges: {
title: t("hidden-subtree.recent-changes-title"),
type: "launcher",
command: "showRecentChanges",
icon: "bx bx-history"
}
};
const desktopAvailableLaunchers: HiddenSubtreeItem[] = [
{ id: "_lbBackInHistory", ...sharedLaunchers.backInHistory },
{ id: "_lbForwardInHistory", ...sharedLaunchers.forwardInHistory },
{ id: "_lbBackendLog", title: t("hidden-subtree.backend-log-title"), type: "launcher", targetNoteId: "_backendLog", icon: "bx bx-terminal" }
];
const desktopVisibleLaunchers: HiddenSubtreeItem[] = [
{ id: "_lbNewNote", ...sharedLaunchers.newNote },
{
id: "_lbSearch",
title: t("hidden-subtree.search-notes-title"),
type: "launcher",
command: "searchNotes",
icon: "bx bx-search",
attributes: [{ type: "label", name: "desktopOnly" }]
},
{
id: "_lbJumpTo",
title: t("hidden-subtree.jump-to-note-title"),
type: "launcher",
command: "jumpToNote",
icon: "bx bx-send",
attributes: [{ type: "label", name: "desktopOnly" }]
},
{ id: "_lbNoteMap", title: t("hidden-subtree.note-map-title"), type: "launcher", targetNoteId: "_globalNoteMap", icon: "bx bxs-network-chart" },
{ id: "_lbCalendar", ...sharedLaunchers.calendar },
{ id: "_lbRecentChanges", ...sharedLaunchers.recentChanges },
{ id: "_lbSpacer1", title: t("hidden-subtree.spacer-title"), type: "launcher", builtinWidget: "spacer", baseSize: "50", growthFactor: "0" },
{ id: "_lbBookmarks", title: t("hidden-subtree.bookmarks-title"), type: "launcher", builtinWidget: "bookmarks", icon: "bx bx-bookmark" },
{ id: "_lbToday", ...sharedLaunchers.openToday },
{ id: "_lbSpacer2", title: t("hidden-subtree.spacer-title"), type: "launcher", builtinWidget: "spacer", baseSize: "0", growthFactor: "1" },
{ id: "_lbQuickSearch", title: t("hidden-subtree.quick-search-title"), type: "launcher", builtinWidget: "quickSearch", icon: "bx bx-rectangle" },
{ id: "_lbProtectedSession", title: t("hidden-subtree.protected-session-title"), type: "launcher", builtinWidget: "protectedSession", icon: "bx bx bx-shield-quarter" },
{ id: "_lbSyncStatus", title: t("hidden-subtree.sync-status-title"), type: "launcher", builtinWidget: "syncStatus", icon: "bx bx-wifi" },
{ id: "_lbSettings", title: t("hidden-subtree.settings-title"), type: "launcher", command: "showOptions", icon: "bx bx-cog" }
]
const mobileAvailableLaunchers: HiddenSubtreeItem[] = [
{ id: "_lbMobileNewNote", ...sharedLaunchers.newNote },
{ id: "_lbMobileToday", ...sharedLaunchers.openToday }
];
const mobileVisibleLaunchers: HiddenSubtreeItem[] = [
{ id: "_lbMobileBackInHistory", ...sharedLaunchers.backInHistory },
{ id: "_lbMobileForwardInHistory", ...sharedLaunchers.forwardInHistory },
{ id: "_lbMobileJumpTo", title: t("hidden-subtree.jump-to-note-title"), type: "launcher", command: "jumpToNote", icon: "bx bx-plus-circle" },
{ id: "_lbMobileCalendar", ...sharedLaunchers.calendar },
{ id: "_lbMobileRecentChanges", ...sharedLaunchers.recentChanges }
];
return {
desktopAvailableLaunchers,
desktopVisibleLaunchers,
mobileAvailableLaunchers,
mobileVisibleLaunchers
}
}

View File

@ -2,6 +2,16 @@ import sanitizeHtml from "sanitize-html";
import sanitizeUrl from "@braintree/sanitize-url";
import optionService from "./options.js";
// Be consistent with `ALLOWED_PROTOCOLS` in `src\public\app\services\link.js`
// TODO: Deduplicate with client once we can.
export const ALLOWED_PROTOCOLS = [
'http', 'https', 'ftp', 'ftps', 'mailto', 'data', 'evernote', 'file', 'facetime', 'gemini', 'git',
'gopher', 'imap', 'irc', 'irc6', 'jabber', 'jar', 'lastfm', 'ldap', 'ldaps', 'magnet', 'message',
'mumble', 'nfs', 'onenote', 'pop', 'rmi', 's3', 'sftp', 'skype', 'sms', 'spotify', 'steam', 'svn', 'udp',
'view-source', 'vlc', 'vnc', 'ws', 'wss', 'xmpp', 'jdbc', 'slack', 'tel', 'smb', 'zotero', 'geo',
'mid'
];
// Default list of allowed HTML tags
export const DEFAULT_ALLOWED_TAGS = [
"h1",
@ -138,56 +148,7 @@ function sanitize(dirtyHtml: string) {
"*": ["class", "style", "title", "src", "href", "hash", "disabled", "align", "alt", "center", "data-*"],
input: ["type", "checked"]
},
// Be consistent with `allowedSchemes` in `src\public\app\services\link.js`
allowedSchemes: [
"http",
"https",
"ftp",
"ftps",
"mailto",
"data",
"evernote",
"file",
"facetime",
"gemini",
"git",
"gopher",
"imap",
"irc",
"irc6",
"jabber",
"jar",
"lastfm",
"ldap",
"ldaps",
"magnet",
"message",
"mumble",
"nfs",
"onenote",
"pop",
"rmi",
"s3",
"sftp",
"skype",
"sms",
"spotify",
"steam",
"svn",
"udp",
"view-source",
"vlc",
"vnc",
"ws",
"wss",
"xmpp",
"jdbc",
"slack",
"tel",
"smb",
"zotero",
"geo"
],
allowedSchemes: ALLOWED_PROTOCOLS,
nonTextTags: ["head"],
transformTags
});

View File

@ -12,12 +12,17 @@ describe("#getMime", () => {
],
[
"File extension that is defined in EXTENSION_TO_MIME",
"File extension ('.py') that is defined in EXTENSION_TO_MIME",
["test.py"], "text/x-python"
],
[
"File extension with inconsisten capitalization that is defined in EXTENSION_TO_MIME",
"File extension ('.ts') that is defined in EXTENSION_TO_MIME",
["test.ts"], "text/x-typescript"
],
[
"File extension with inconsistent capitalization that is defined in EXTENSION_TO_MIME",
["test.gRoOvY"], "text/x-groovy"
],

View File

@ -37,6 +37,7 @@ const CODE_MIME_TYPES = new Set([
"text/x-sql",
"text/x-stex",
"text/x-swift",
"text/x-typescript",
"text/x-yaml"
]);
@ -65,7 +66,8 @@ const EXTENSION_TO_MIME = new Map<string, string>([
[".py", "text/x-python"],
[".rb", "text/x-ruby"],
[".scala", "text/x-scala"],
[".swift", "text/x-swift"]
[".swift", "text/x-swift"],
[".ts", "text/x-typescript"]
]);
/** @returns false if MIME is not detected */

2
src/types.d.ts vendored
View File

@ -18,7 +18,7 @@ declare module "normalize-strings" {
export default normalizeString;
}
declare module "joplin-turndown-plugin-gfm" {
declare module "@joplin/turndown-plugin-gfm" {
import TurndownService from "turndown";
namespace gfm {
function gfm(service: TurndownService): void;

View File

@ -27,7 +27,7 @@
<div class="form-group">
<label for="password"><%= t("login.password") %></label>
<div class="controls">
<input id="password" name="password" placeholder="" class="form-control" type="password">
<input id="password" name="password" placeholder="" class="form-control" type="password" autofocus>
</div>
</div>
<div class="form-group">