diff --git a/package-lock.json b/package-lock.json index 7d0e67515..451c22720 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,6 +94,7 @@ "@electron-forge/plugin-auto-unpack-natives": "^6.4.2", "@types/archiver": "^6.0.2", "@types/better-sqlite3": "^7.6.9", + "@types/bootstrap": "^5.2.10", "@types/cls-hooked": "^4.3.8", "@types/compression": "^1.7.5", "@types/cookie-parser": "^1.4.7", @@ -2060,6 +2061,16 @@ "node": ">=14" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -2153,6 +2164,15 @@ "@types/node": "*" } }, + "node_modules/@types/bootstrap": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.10.tgz", + "integrity": "sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==", + "dev": true, + "dependencies": { + "@popperjs/core": "^2.9.2" + } + }, "node_modules/@types/cacheable-request": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz", diff --git a/package.json b/package.json index cdf387bd3..b6a619c0d 100644 --- a/package.json +++ b/package.json @@ -128,6 +128,7 @@ "@electron-forge/plugin-auto-unpack-natives": "^6.4.2", "@types/archiver": "^6.0.2", "@types/better-sqlite3": "^7.6.9", + "@types/bootstrap": "^5.2.10", "@types/cls-hooked": "^4.3.8", "@types/compression": "^1.7.5", "@types/cookie-parser": "^1.4.7", diff --git a/src/public/app/services/utils.js b/src/public/app/services/utils.ts similarity index 80% rename from src/public/app/services/utils.js rename to src/public/app/services/utils.ts index 9e17f1c38..ef58b4a32 100644 --- a/src/public/app/services/utils.js +++ b/src/public/app/services/utils.ts @@ -1,4 +1,6 @@ -function reloadFrontendApp(reason) { +import dayjs from "dayjs"; + +function reloadFrontendApp(reason: string) { if (reason) { logInfo(`Frontend app reload: ${reason}`); } @@ -6,33 +8,33 @@ function reloadFrontendApp(reason) { window.location.reload(); } -function parseDate(str) { +function parseDate(str: string) { try { return new Date(Date.parse(str)); } - catch (e) { + catch (e: any) { throw new Error(`Can't parse date from '${str}': ${e.message} ${e.stack}`); } } -function padNum(num) { +function padNum(num: number) { return `${num <= 9 ? "0" : ""}${num}`; } -function formatTime(date) { +function formatTime(date: Date) { return `${padNum(date.getHours())}:${padNum(date.getMinutes())}`; } -function formatTimeWithSeconds(date) { +function formatTimeWithSeconds(date: Date) { return `${padNum(date.getHours())}:${padNum(date.getMinutes())}:${padNum(date.getSeconds())}`; } -function formatTimeInterval(ms) { +function formatTimeInterval(ms: number) { const seconds = Math.round(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); - const plural = (count, name) => `${count} ${name}${count > 1 ? 's' : ''}`; + const plural = (count: number, name: string) => `${count} ${name}${count > 1 ? 's' : ''}`; const segments = []; if (days > 0) { @@ -60,20 +62,20 @@ function formatTimeInterval(ms) { return segments.join(", "); } -// this is producing local time! -function formatDate(date) { +/** this is producing local time! **/ +function formatDate(date: Date) { // return padNum(date.getDate()) + ". " + padNum(date.getMonth() + 1) + ". " + date.getFullYear(); // instead of european format we'll just use ISO as that's pretty unambiguous return formatDateISO(date); } -// this is producing local time! -function formatDateISO(date) { +/** this is producing local time! **/ +function formatDateISO(date: Date) { return `${date.getFullYear()}-${padNum(date.getMonth() + 1)}-${padNum(date.getDate())}`; } -function formatDateTime(date) { +function formatDateTime(date: Date) { return `${formatDate(date)} ${formatTime(date)}`; } @@ -93,7 +95,7 @@ function isMac() { return navigator.platform.indexOf('Mac') > -1; } -function isCtrlKey(evt) { +function isCtrlKey(evt: KeyboardEvent) { return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey); } @@ -106,7 +108,7 @@ function assertArguments() { } } -const entityMap = { +const entityMap: Record = { '&': '&', '<': '<', '>': '>', @@ -117,11 +119,11 @@ const entityMap = { '=': '=' }; -function escapeHtml(str) { +function escapeHtml(str: string) { return str.replace(/[&<>"'`=\/]/g, s => entityMap[s]); } -function formatSize(size) { +function formatSize(size: number) { size = Math.max(Math.round(size / 1024), 1); if (size < 1024) { @@ -132,8 +134,8 @@ function formatSize(size) { } } -function toObject(array, fn) { - const obj = {}; +function toObject(array: T[], fn: (arg0: T) => [key: string, value: T]) { + const obj: Record = {}; for (const item of array) { const [key, value] = fn(item); @@ -144,7 +146,7 @@ function toObject(array, fn) { return obj; } -function randomString(len) { +function randomString(len: number) { let text = ""; const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; @@ -161,27 +163,28 @@ function isMobile() { || (!window.glob?.device && /Mobi/.test(navigator.userAgent)); } -function isDesktop() { +function isDesktop() { return window.glob?.device === "desktop" // window.glob.device is not available in setup || (!window.glob?.device && !/Mobi/.test(navigator.userAgent)); } -// the cookie code below works for simple use cases only - ASCII only -// not setting a path so that cookies do not leak into other websites if multiplexed with reverse proxy - -function setCookie(name, value) { +/** + * the cookie code below works for simple use cases only - ASCII only + * not setting a path so that cookies do not leak into other websites if multiplexed with reverse proxy + */ +function setCookie(name: string, value: string) { const date = new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1000); const expires = `; expires=${date.toUTCString()}`; document.cookie = `${name}=${value || ""}${expires};`; } -function getNoteTypeClass(type) { +function getNoteTypeClass(type: string) { return `type-${type}`; } -function getMimeTypeClass(mime) { +function getMimeTypeClass(mime: string) { if (!mime) { return ""; } @@ -203,7 +206,7 @@ function closeActiveDialog() { } } -let $lastFocusedElement = null; +let $lastFocusedElement: JQuery | null; // perhaps there should be saved focused element per tab? function saveFocusedElement() { @@ -235,7 +238,7 @@ function focusSavedElement() { $lastFocusedElement = null; } -async function openDialog($dialog, closeActDialog = true) { +async function openDialog($dialog: JQuery, closeActDialog = true) { if (closeActDialog) { closeActiveDialog(); glob.activeDialog = $dialog; @@ -253,11 +256,13 @@ async function openDialog($dialog, closeActDialog = true) { } }); + // TODO: Fix once keyboard_actions is ported. + // @ts-ignore const keyboardActionsService = (await import("./keyboard_actions.js")).default; keyboardActionsService.updateDisplayedShortcuts($dialog); } -function isHtmlEmpty(html) { +function isHtmlEmpty(html: string) { if (!html) { return true; } else if (typeof html !== 'string') { @@ -281,13 +286,13 @@ async function clearBrowserCache() { } function copySelectionToClipboard() { - const text = window.getSelection().toString(); - if (navigator.clipboard) { + const text = window?.getSelection()?.toString(); + if (text && navigator.clipboard) { navigator.clipboard.writeText(text); } } -function dynamicRequire(moduleName) { +function dynamicRequire(moduleName: string) { if (typeof __non_webpack_require__ !== 'undefined') { return __non_webpack_require__(moduleName); } @@ -296,7 +301,7 @@ function dynamicRequire(moduleName) { } } -function timeLimit(promise, limitMs, errorMessage) { +function timeLimit(promise: Promise, limitMs: number, errorMessage: string) { if (!promise || !promise.then) { // it's not actually a promise return promise; } @@ -321,7 +326,7 @@ function timeLimit(promise, limitMs, errorMessage) { }); } -function initHelpDropdown($el) { +function initHelpDropdown($el: JQuery) { // stop inside clicks from closing the menu const $dropdownMenu = $el.find('.help-dropdown .dropdown-menu'); $dropdownMenu.on('click', e => e.stopPropagation()); @@ -332,7 +337,7 @@ function initHelpDropdown($el) { const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/"; -function openHelp($button) { +function openHelp($button: JQuery) { const helpPage = $button.attr("data-help-page"); if (helpPage) { @@ -342,7 +347,7 @@ function openHelp($button) { } } -function initHelpButtons($el) { +function initHelpButtons($el: JQuery) { // for some reason, the .on(event, listener, handler) does not work here (e.g. Options -> Sync -> Help button) // so we do it manually $el.on("click", e => { @@ -351,35 +356,38 @@ function initHelpButtons($el) { }); } -function filterAttributeName(name) { +function filterAttributeName(name: string) { return name.replace(/[^\p{L}\p{N}_:]/ug, ""); } const ATTR_NAME_MATCHER = new RegExp("^[\\p{L}\\p{N}_:]+$", "u"); -function isValidAttributeName(name) { +function isValidAttributeName(name: string) { return ATTR_NAME_MATCHER.test(name); } -function sleep(time_ms) { +function sleep(time_ms: number) { return new Promise((resolve) => { setTimeout(resolve, time_ms); }); } -function escapeRegExp(str) { +function escapeRegExp(str: string) { return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); } -function areObjectsEqual() { - let i, l, leftChain, rightChain; +function areObjectsEqual () { + let i; + let l; + let leftChain: Object[]; + let rightChain: Object[]; - function compare2Objects(x, y) { + function compare2Objects (x: unknown, y: unknown) { let p; // remember that NaN === NaN returns false // and isNaN(undefined) returns true - if (isNaN(x) && isNaN(y) && typeof x === 'number' && typeof y === 'number') { + if (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y)) { return true; } @@ -414,7 +422,7 @@ function areObjectsEqual() { return false; } - if (x.prototype !== y.prototype) { + if ((x as any).prototype !== (y as any).prototype) { return false; } @@ -429,7 +437,7 @@ function areObjectsEqual() { if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { return false; } - else if (typeof y[p] !== typeof x[p]) { + else if (typeof (y as any)[p] !== typeof (x as any)[p]) { return false; } } @@ -438,18 +446,18 @@ function areObjectsEqual() { if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) { return false; } - else if (typeof y[p] !== typeof x[p]) { + else if (typeof (y as any)[p] !== typeof (x as any)[p]) { return false; } - switch (typeof (x[p])) { + switch (typeof ((x as any)[p])) { case 'object': case 'function': leftChain.push(x); rightChain.push(y); - if (!compare2Objects(x[p], y[p])) { + if (!compare2Objects((x as any)[p], (y as any)[p])) { return false; } @@ -458,7 +466,7 @@ function areObjectsEqual() { break; default: - if (x[p] !== y[p]) { + if ((x as any)[p] !== (y as any)[p]) { return false; } break; @@ -486,10 +494,12 @@ function areObjectsEqual() { return true; } -function copyHtmlToClipboard(content) { - function listener(e) { - e.clipboardData.setData("text/html", content); - e.clipboardData.setData("text/plain", content); +function copyHtmlToClipboard(content: string) { + function listener(e: ClipboardEvent) { + if (e.clipboardData) { + e.clipboardData.setData("text/html", content); + e.clipboardData.setData("text/plain", content); + } e.preventDefault(); } document.addEventListener("copy", listener); @@ -497,11 +507,8 @@ function copyHtmlToClipboard(content) { document.removeEventListener("copy", listener); } -/** - * @param {FNote} note - * @return {string} - */ -function createImageSrcUrl(note) { +// TODO: Set to FNote once the file is ported. +function createImageSrcUrl(note: { noteId: string; title: string }) { return `api/images/${note.noteId}/${encodeURIComponent(note.title)}?timestamp=${Date.now()}`; } diff --git a/src/public/app/types.d.ts b/src/public/app/types.d.ts new file mode 100644 index 000000000..daf31e929 --- /dev/null +++ b/src/public/app/types.d.ts @@ -0,0 +1,46 @@ +import FNote from "./entities/fnote"; + +interface ElectronProcess { + type: string; +} + +interface CustomGlobals { + isDesktop: boolean; + isMobile: boolean; + device: "mobile" | "desktop"; + getComponentsByEl: (el: unknown) => unknown; + getHeaders: Promise>; + getReferenceLinkTitle: (href: string) => Promise; + getReferenceLinkTitleSync: (href: string) => string; + getActiveContextNote: FNote; + requireLibrary: (library: string) => Promise; + ESLINT: { js: string[]; }; + appContext: AppContext; + froca: Froca; + treeCache: Froca; + importMarkdownInline: () => Promise; + SEARCH_HELP_TEXT: string; + activeDialog: JQuery | null; +} + +type RequireMethod = (moduleName: string) => any; + +declare global { + interface Window { + logError(message: string); + logInfo(message: string); + + process?: ElectronProcess; + glob?: CustomGlobals; + } + + interface JQuery { + autocomplete: (action: "close") => void; + } + + declare var logError: (message: string) => void; + declare var logInfo: (message: string) => void; + declare var glob: CustomGlobals; + declare var require: RequireMethod; + declare var __non_webpack_require__: RequireMethod | undefined; +} diff --git a/tsconfig.json b/tsconfig.json index 0d206e511..1bdd21ef5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,7 @@ ], "exclude": ["./node_modules/**/*"], "files": [ - "src/types.d.ts" + "src/types.d.ts", + "src/public/app/types.d.ts" ] }