2024-07-25 00:18:57 +03:00
|
|
|
import dayjs from "dayjs";
|
|
|
|
|
2024-07-25 20:47:33 +03:00
|
|
|
function reloadFrontendApp(reason?: string) {
|
2021-09-17 22:34:23 +02:00
|
|
|
if (reason) {
|
2022-12-21 15:19:05 +01:00
|
|
|
logInfo(`Frontend app reload: ${reason}`);
|
2021-09-17 22:34:23 +02:00
|
|
|
}
|
|
|
|
|
2023-01-15 21:04:17 +01:00
|
|
|
window.location.reload();
|
2018-03-25 11:09:17 -04:00
|
|
|
}
|
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function parseDate(str: string) {
|
2018-03-25 11:09:17 -04:00
|
|
|
try {
|
|
|
|
return new Date(Date.parse(str));
|
2025-01-09 18:07:02 +02:00
|
|
|
} catch (e: any) {
|
2023-04-20 00:11:09 +02:00
|
|
|
throw new Error(`Can't parse date from '${str}': ${e.message} ${e.stack}`);
|
2018-03-24 23:37:55 -04:00
|
|
|
}
|
2018-03-25 11:09:17 -04:00
|
|
|
}
|
2018-03-24 23:37:55 -04:00
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function padNum(num: number) {
|
2022-12-21 15:19:05 +01:00
|
|
|
return `${num <= 9 ? "0" : ""}${num}`;
|
2018-03-25 11:09:17 -04:00
|
|
|
}
|
2018-03-24 23:37:55 -04:00
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function formatTime(date: Date) {
|
2022-12-21 15:19:05 +01:00
|
|
|
return `${padNum(date.getHours())}:${padNum(date.getMinutes())}`;
|
2018-03-25 11:09:17 -04:00
|
|
|
}
|
2018-03-24 23:37:55 -04:00
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function formatTimeWithSeconds(date: Date) {
|
2022-12-21 15:19:05 +01:00
|
|
|
return `${padNum(date.getHours())}:${padNum(date.getMinutes())}:${padNum(date.getSeconds())}`;
|
2018-03-25 11:09:17 -04:00
|
|
|
}
|
2017-12-23 11:02:38 -05:00
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function formatTimeInterval(ms: number) {
|
2023-04-21 00:19:17 +02:00
|
|
|
const seconds = Math.round(ms / 1000);
|
|
|
|
const minutes = Math.floor(seconds / 60);
|
|
|
|
const hours = Math.floor(minutes / 60);
|
|
|
|
const days = Math.floor(hours / 24);
|
2025-01-09 18:07:02 +02:00
|
|
|
const plural = (count: number, name: string) => `${count} ${name}${count > 1 ? "s" : ""}`;
|
2023-04-21 00:19:17 +02:00
|
|
|
const segments = [];
|
|
|
|
|
|
|
|
if (days > 0) {
|
2025-01-09 18:07:02 +02:00
|
|
|
segments.push(plural(days, "day"));
|
2023-04-21 00:19:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (days < 2) {
|
|
|
|
if (hours % 24 > 0) {
|
2025-01-09 18:07:02 +02:00
|
|
|
segments.push(plural(hours % 24, "hour"));
|
2023-04-21 00:19:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (hours < 4) {
|
|
|
|
if (minutes % 60 > 0) {
|
2025-01-09 18:07:02 +02:00
|
|
|
segments.push(plural(minutes % 60, "minute"));
|
2023-04-21 00:19:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (minutes < 5) {
|
|
|
|
if (seconds % 60 > 0) {
|
2025-01-09 18:07:02 +02:00
|
|
|
segments.push(plural(seconds % 60, "second"));
|
2023-04-21 00:19:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return segments.join(", ");
|
|
|
|
}
|
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
/** this is producing local time! **/
|
|
|
|
function formatDate(date: Date) {
|
2023-04-08 21:07:48 +08:00
|
|
|
// return padNum(date.getDate()) + ". " + padNum(date.getMonth() + 1) + ". " + date.getFullYear();
|
2018-08-15 08:48:16 +02:00
|
|
|
// instead of european format we'll just use ISO as that's pretty unambiguous
|
|
|
|
|
|
|
|
return formatDateISO(date);
|
2018-03-25 11:09:17 -04:00
|
|
|
}
|
2017-12-23 12:19:15 -05:00
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
/** this is producing local time! **/
|
|
|
|
function formatDateISO(date: Date) {
|
2022-12-21 15:19:05 +01:00
|
|
|
return `${date.getFullYear()}-${padNum(date.getMonth() + 1)}-${padNum(date.getDate())}`;
|
2018-03-25 11:09:17 -04:00
|
|
|
}
|
2017-12-23 12:19:15 -05:00
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function formatDateTime(date: Date) {
|
2022-12-21 15:19:05 +01:00
|
|
|
return `${formatDate(date)} ${formatTime(date)}`;
|
2018-03-25 11:09:17 -04:00
|
|
|
}
|
2017-12-28 19:00:31 -05:00
|
|
|
|
2020-04-26 14:26:57 +02:00
|
|
|
function localNowDateTime() {
|
2025-01-09 18:07:02 +02:00
|
|
|
return dayjs().format("YYYY-MM-DD HH:mm:ss.SSSZZ");
|
2020-04-26 14:26:57 +02:00
|
|
|
}
|
|
|
|
|
2018-03-25 11:09:17 -04:00
|
|
|
function now() {
|
|
|
|
return formatTimeWithSeconds(new Date());
|
|
|
|
}
|
2018-01-22 23:18:08 -05:00
|
|
|
|
2024-08-16 21:41:15 +03:00
|
|
|
/**
|
|
|
|
* Returns `true` if the client is currently running under Electron, or `false` if running in a web browser.
|
|
|
|
*/
|
2018-03-25 11:09:17 -04:00
|
|
|
function isElectron() {
|
2019-02-09 19:25:55 +01:00
|
|
|
return !!(window && window.process && window.process.type);
|
2018-03-25 11:09:17 -04:00
|
|
|
}
|
2018-01-25 23:49:03 -05:00
|
|
|
|
2018-12-02 14:04:53 +01:00
|
|
|
function isMac() {
|
2025-01-09 18:07:02 +02:00
|
|
|
return navigator.platform.indexOf("Mac") > -1;
|
2018-12-02 14:04:53 +01:00
|
|
|
}
|
|
|
|
|
2025-01-18 11:09:57 +02:00
|
|
|
function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement>) {
|
2025-01-09 18:07:02 +02:00
|
|
|
return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey);
|
2022-12-09 16:48:00 +01:00
|
|
|
}
|
|
|
|
|
2024-07-25 20:55:04 +03:00
|
|
|
function assertArguments(...args: string[]) {
|
|
|
|
for (const i in args) {
|
|
|
|
if (!args[i]) {
|
|
|
|
console.trace(`Argument idx#${i} should not be falsy: ${args[i]}`);
|
2018-03-24 23:37:55 -04:00
|
|
|
}
|
|
|
|
}
|
2018-03-25 11:09:17 -04:00
|
|
|
}
|
2018-03-06 23:04:35 -05:00
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
const entityMap: Record<string, string> = {
|
2025-01-09 18:07:02 +02:00
|
|
|
"&": "&",
|
|
|
|
"<": "<",
|
|
|
|
">": ">",
|
|
|
|
'"': """,
|
|
|
|
"'": "'",
|
|
|
|
"/": "/",
|
|
|
|
"`": "`",
|
|
|
|
"=": "="
|
2020-06-24 21:07:55 +02:00
|
|
|
};
|
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function escapeHtml(str: string) {
|
2025-01-09 18:07:02 +02:00
|
|
|
return str.replace(/[&<>"'`=\/]/g, (s) => entityMap[s]);
|
2018-03-25 11:09:17 -04:00
|
|
|
}
|
2018-03-24 23:37:55 -04:00
|
|
|
|
2025-01-28 21:03:39 +02:00
|
|
|
export function escapeQuotes(value: string) {
|
|
|
|
return value.replaceAll("\"", """);
|
|
|
|
}
|
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function formatSize(size: number) {
|
2023-03-24 10:57:32 +01:00
|
|
|
size = Math.max(Math.round(size / 1024), 1);
|
|
|
|
|
|
|
|
if (size < 1024) {
|
|
|
|
return `${size} KiB`;
|
2025-01-09 18:07:02 +02:00
|
|
|
} else {
|
2023-03-24 10:57:32 +01:00
|
|
|
return `${Math.round(size / 102.4) / 10} MiB`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-19 22:16:03 +02:00
|
|
|
function toObject<T, R>(array: T[], fn: (arg0: T) => [key: string, value: R]) {
|
|
|
|
const obj: Record<string, R> = {};
|
2018-03-04 23:28:26 -05:00
|
|
|
|
2018-03-25 11:09:17 -04:00
|
|
|
for (const item of array) {
|
2018-03-25 23:25:17 -04:00
|
|
|
const [key, value] = fn(item);
|
2018-03-24 23:37:55 -04:00
|
|
|
|
2018-03-25 23:25:17 -04:00
|
|
|
obj[key] = value;
|
2018-03-04 23:28:26 -05:00
|
|
|
}
|
|
|
|
|
2018-03-25 11:09:17 -04:00
|
|
|
return obj;
|
|
|
|
}
|
2018-03-12 23:27:21 -04:00
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function randomString(len: number) {
|
2018-03-25 11:09:17 -04:00
|
|
|
let text = "";
|
|
|
|
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
2018-03-12 23:27:21 -04:00
|
|
|
|
2018-03-25 11:09:17 -04:00
|
|
|
for (let i = 0; i < len; i++) {
|
|
|
|
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
2018-03-12 23:27:21 -04:00
|
|
|
}
|
|
|
|
|
2018-03-25 11:09:17 -04:00
|
|
|
return text;
|
|
|
|
}
|
|
|
|
|
2018-12-24 10:10:36 +01:00
|
|
|
function isMobile() {
|
2025-01-09 18:07:02 +02:00
|
|
|
return (
|
|
|
|
window.glob?.device === "mobile" ||
|
2023-05-03 22:49:24 +02:00
|
|
|
// window.glob.device is not available in setup
|
2025-01-09 18:07:02 +02:00
|
|
|
(!window.glob?.device && /Mobi/.test(navigator.userAgent))
|
|
|
|
);
|
2018-12-24 10:10:36 +01:00
|
|
|
}
|
|
|
|
|
2024-12-23 11:00:10 +02:00
|
|
|
function isDesktop() {
|
2025-01-09 18:07:02 +02:00
|
|
|
return (
|
|
|
|
window.glob?.device === "desktop" ||
|
2023-05-03 22:49:24 +02:00
|
|
|
// window.glob.device is not available in setup
|
2025-01-09 18:07:02 +02:00
|
|
|
(!window.glob?.device && !/Mobi/.test(navigator.userAgent))
|
|
|
|
);
|
2018-12-24 10:10:36 +01:00
|
|
|
}
|
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
/**
|
|
|
|
* 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) {
|
2018-12-29 00:09:16 +01:00
|
|
|
const date = new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1000);
|
2022-12-21 15:19:05 +01:00
|
|
|
const expires = `; expires=${date.toUTCString()}`;
|
2018-12-29 00:09:16 +01:00
|
|
|
|
2022-12-21 15:19:05 +01:00
|
|
|
document.cookie = `${name}=${value || ""}${expires};`;
|
2019-03-13 21:53:09 +01:00
|
|
|
}
|
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function getNoteTypeClass(type: string) {
|
2022-12-21 15:19:05 +01:00
|
|
|
return `type-${type}`;
|
2019-01-28 21:42:37 +01:00
|
|
|
}
|
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function getMimeTypeClass(mime: string) {
|
2021-02-22 21:59:37 +01:00
|
|
|
if (!mime) {
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
const semicolonIdx = mime.indexOf(";");
|
2019-01-28 21:42:37 +01:00
|
|
|
|
|
|
|
if (semicolonIdx !== -1) {
|
|
|
|
// stripping everything following the semicolon
|
|
|
|
mime = mime.substr(0, semicolonIdx);
|
|
|
|
}
|
|
|
|
|
2022-12-21 15:19:05 +01:00
|
|
|
return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`;
|
2019-01-28 21:42:37 +01:00
|
|
|
}
|
|
|
|
|
2019-06-10 22:45:03 +02:00
|
|
|
function closeActiveDialog() {
|
|
|
|
if (glob.activeDialog) {
|
2024-12-22 00:10:02 +02:00
|
|
|
// TODO: Fix once we use proper ES imports.
|
|
|
|
//@ts-ignore
|
2024-12-22 00:34:25 +02:00
|
|
|
bootstrap.Modal.getOrCreateInstance(glob.activeDialog[0]).hide();
|
2020-02-09 10:00:13 +01:00
|
|
|
glob.activeDialog = null;
|
2019-06-10 22:45:03 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
let $lastFocusedElement: JQuery<HTMLElement> | null;
|
2020-02-09 10:00:13 +01:00
|
|
|
|
2020-09-04 23:35:10 +02:00
|
|
|
// perhaps there should be saved focused element per tab?
|
2020-02-09 10:00:13 +01:00
|
|
|
function saveFocusedElement() {
|
|
|
|
$lastFocusedElement = $(":focus");
|
|
|
|
}
|
|
|
|
|
|
|
|
function focusSavedElement() {
|
|
|
|
if (!$lastFocusedElement) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-11-23 22:47:20 +01:00
|
|
|
if ($lastFocusedElement.hasClass("ck")) {
|
|
|
|
// must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607
|
|
|
|
// the bug manifests itself in resetting the cursor position to the first character - jumping above
|
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
const editor = $lastFocusedElement.closest(".ck-editor__editable").prop("ckeditorInstance");
|
2022-11-23 22:47:20 +01:00
|
|
|
|
2022-12-03 13:26:33 +01:00
|
|
|
if (editor) {
|
|
|
|
editor.editing.view.focus();
|
|
|
|
} else {
|
|
|
|
console.log("Could not find CKEditor instance to focus last element");
|
|
|
|
}
|
2022-11-23 22:47:20 +01:00
|
|
|
} else {
|
|
|
|
$lastFocusedElement.focus();
|
|
|
|
}
|
|
|
|
|
2020-02-09 10:00:13 +01:00
|
|
|
$lastFocusedElement = null;
|
|
|
|
}
|
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true) {
|
2022-01-10 17:09:20 +01:00
|
|
|
if (closeActDialog) {
|
|
|
|
closeActiveDialog();
|
|
|
|
glob.activeDialog = $dialog;
|
|
|
|
}
|
2020-02-09 10:00:13 +01:00
|
|
|
|
|
|
|
saveFocusedElement();
|
2024-12-22 00:10:02 +02:00
|
|
|
// TODO: Fix once we use proper ES imports.
|
|
|
|
//@ts-ignore
|
2024-12-22 00:34:25 +02:00
|
|
|
bootstrap.Modal.getOrCreateInstance($dialog[0]).show();
|
2020-02-09 10:00:13 +01:00
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
$dialog.on("hidden.bs.modal", () => {
|
2025-01-04 19:15:24 +02:00
|
|
|
const $autocompleteEl = $(".aa-input");
|
|
|
|
if ("autocomplete" in $autocompleteEl) {
|
|
|
|
$autocompleteEl.autocomplete("close");
|
|
|
|
}
|
2022-10-26 16:52:44 +02:00
|
|
|
|
2020-02-09 10:00:13 +01:00
|
|
|
if (!glob.activeDialog || glob.activeDialog === $dialog) {
|
|
|
|
focusSavedElement();
|
|
|
|
}
|
|
|
|
});
|
2020-04-18 11:14:09 +02:00
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
// TODO: Fix once keyboard_actions is ported.
|
|
|
|
// @ts-ignore
|
2020-04-18 11:14:09 +02:00
|
|
|
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
|
|
|
|
keyboardActionsService.updateDisplayedShortcuts($dialog);
|
2025-01-05 00:24:25 +02:00
|
|
|
|
|
|
|
return $dialog;
|
2020-02-09 10:00:13 +01:00
|
|
|
}
|
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function isHtmlEmpty(html: string) {
|
2020-03-26 16:59:40 +01:00
|
|
|
if (!html) {
|
|
|
|
return true;
|
2025-01-09 18:07:02 +02:00
|
|
|
} else if (typeof html !== "string") {
|
2023-06-14 22:21:22 +02:00
|
|
|
logError(`Got object of type '${typeof html}' where string was expected.`);
|
|
|
|
return false;
|
2020-03-26 16:59:40 +01:00
|
|
|
}
|
|
|
|
|
2020-01-04 13:22:07 +01:00
|
|
|
html = html.toLowerCase();
|
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
return (
|
|
|
|
!html.includes("<img") &&
|
|
|
|
!html.includes("<section") &&
|
2023-06-14 22:21:22 +02:00
|
|
|
// the line below will actually attempt to load images so better to check for images first
|
2025-01-09 18:07:02 +02:00
|
|
|
$("<div>").html(html).text().trim().length === 0
|
|
|
|
);
|
2019-10-05 20:27:30 +02:00
|
|
|
}
|
|
|
|
|
2019-11-08 23:09:57 +01:00
|
|
|
async function clearBrowserCache() {
|
|
|
|
if (isElectron()) {
|
2025-01-09 18:07:02 +02:00
|
|
|
const win = dynamicRequire("@electron/remote").getCurrentWindow();
|
2019-11-08 23:09:57 +01:00
|
|
|
await win.webContents.session.clearCache();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-19 11:16:36 +03:00
|
|
|
function copySelectionToClipboard() {
|
2024-07-25 00:18:57 +03:00
|
|
|
const text = window?.getSelection()?.toString();
|
|
|
|
if (text && navigator.clipboard) {
|
2020-01-19 09:25:35 +01:00
|
|
|
navigator.clipboard.writeText(text);
|
2020-01-19 11:16:36 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function dynamicRequire(moduleName: string) {
|
2025-01-09 18:07:02 +02:00
|
|
|
if (typeof __non_webpack_require__ !== "undefined") {
|
2020-04-12 14:22:51 +02:00
|
|
|
return __non_webpack_require__(moduleName);
|
2025-01-09 18:07:02 +02:00
|
|
|
} else {
|
2025-01-19 12:27:43 +01:00
|
|
|
// explicitly pass as string and not as expression to suppress webpack warning
|
|
|
|
// 'Critical dependency: the request of a dependency is an expression'
|
|
|
|
return require(`${moduleName}`);
|
2020-04-12 14:22:51 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-25 20:47:33 +03:00
|
|
|
function timeLimit<T>(promise: Promise<T>, limitMs: number, errorMessage?: string) {
|
2025-01-09 18:07:02 +02:00
|
|
|
if (!promise || !promise.then) {
|
|
|
|
// it's not actually a promise
|
2021-02-20 23:17:29 +01:00
|
|
|
return promise;
|
|
|
|
}
|
|
|
|
|
2020-07-29 23:34:49 +02:00
|
|
|
// better stack trace if created outside of promise
|
2021-02-20 23:17:29 +01:00
|
|
|
const error = new Error(errorMessage || `Process exceeded time limit ${limitMs}`);
|
2020-07-29 23:34:49 +02:00
|
|
|
|
2024-07-25 19:21:24 +03:00
|
|
|
return new Promise<T>((res, rej) => {
|
2020-06-11 00:13:56 +02:00
|
|
|
let resolved = false;
|
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
promise.then((result) => {
|
2020-06-11 00:13:56 +02:00
|
|
|
resolved = true;
|
|
|
|
|
2020-06-13 22:34:15 +02:00
|
|
|
res(result);
|
2020-06-11 00:13:56 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
if (!resolved) {
|
2020-07-29 23:34:49 +02:00
|
|
|
rej(error);
|
2020-06-11 00:13:56 +02:00
|
|
|
}
|
|
|
|
}, limitMs);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function initHelpDropdown($el: JQuery<HTMLElement>) {
|
2021-01-26 14:44:53 +01:00
|
|
|
// stop inside clicks from closing the menu
|
2025-01-09 18:07:02 +02:00
|
|
|
const $dropdownMenu = $el.find(".help-dropdown .dropdown-menu");
|
|
|
|
$dropdownMenu.on("click", (e) => e.stopPropagation());
|
2021-01-26 14:44:53 +01:00
|
|
|
|
2023-01-15 21:04:17 +01:00
|
|
|
// previous propagation stop will also block help buttons from being opened, so we need to re-init for this element
|
2021-01-26 14:44:53 +01:00
|
|
|
initHelpButtons($dropdownMenu);
|
|
|
|
}
|
|
|
|
|
2024-08-10 00:26:39 +03:00
|
|
|
const wikiBaseUrl = "https://triliumnext.github.io/Docs/Wiki/";
|
2021-01-26 14:44:53 +01:00
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function openHelp($button: JQuery<HTMLElement>) {
|
2023-07-14 21:59:43 +02:00
|
|
|
const helpPage = $button.attr("data-help-page");
|
|
|
|
|
|
|
|
if (helpPage) {
|
|
|
|
const url = wikiBaseUrl + helpPage;
|
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
window.open(url, "_blank");
|
2023-07-14 21:59:43 +02:00
|
|
|
}
|
2021-12-20 17:30:47 +01:00
|
|
|
}
|
|
|
|
|
2024-12-21 17:47:09 +02:00
|
|
|
function initHelpButtons($el: JQuery<HTMLElement> | JQuery<Window>) {
|
2023-06-29 23:32:19 +02:00
|
|
|
// for some reason, the .on(event, listener, handler) does not work here (e.g. Options -> Sync -> Help button)
|
2022-12-08 15:18:41 +01:00
|
|
|
// so we do it manually
|
2025-01-09 18:07:02 +02:00
|
|
|
$el.on("click", (e) => {
|
2023-07-14 21:59:43 +02:00
|
|
|
const $helpButton = $(e.target).closest("[data-help-page]");
|
|
|
|
openHelp($helpButton);
|
2022-12-08 15:18:41 +01:00
|
|
|
});
|
2021-01-26 14:44:53 +01:00
|
|
|
}
|
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function filterAttributeName(name: string) {
|
2025-01-09 18:07:02 +02:00
|
|
|
return name.replace(/[^\p{L}\p{N}_:]/gu, "");
|
2021-02-17 23:22:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const ATTR_NAME_MATCHER = new RegExp("^[\\p{L}\\p{N}_:]+$", "u");
|
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function isValidAttributeName(name: string) {
|
2021-02-17 23:22:14 +01:00
|
|
|
return ATTR_NAME_MATCHER.test(name);
|
|
|
|
}
|
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function sleep(time_ms: number) {
|
2022-05-09 16:50:06 +02:00
|
|
|
return new Promise((resolve) => {
|
|
|
|
setTimeout(resolve, time_ms);
|
|
|
|
});
|
2022-05-13 23:20:56 +02:00
|
|
|
}
|
2022-05-09 16:50:06 +02:00
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function escapeRegExp(str: string) {
|
2022-05-25 23:38:06 +02:00
|
|
|
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
|
|
|
|
}
|
|
|
|
|
2024-12-23 15:16:41 +02:00
|
|
|
function areObjectsEqual(...args: unknown[]) {
|
2024-07-25 00:18:57 +03:00
|
|
|
let i;
|
|
|
|
let l;
|
|
|
|
let leftChain: Object[];
|
|
|
|
let rightChain: Object[];
|
2023-04-11 17:45:51 +02:00
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
function compare2Objects(x: unknown, y: unknown) {
|
2023-05-05 23:17:23 +02:00
|
|
|
let p;
|
2023-04-11 17:45:51 +02:00
|
|
|
|
|
|
|
// remember that NaN === NaN returns false
|
|
|
|
// and isNaN(undefined) returns true
|
2025-01-09 18:07:02 +02:00
|
|
|
if (typeof x === "number" && typeof y === "number" && isNaN(x) && isNaN(y)) {
|
2023-04-11 17:45:51 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Compare primitives and functions.
|
|
|
|
// Check if both arguments link to the same object.
|
|
|
|
// Especially useful on the step where we compare prototypes
|
|
|
|
if (x === y) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Works in case when functions are created in constructor.
|
|
|
|
// Comparing dates is a common scenario. Another built-ins?
|
|
|
|
// We can even handle functions passed across iframes
|
2025-01-09 18:07:02 +02:00
|
|
|
if (
|
|
|
|
(typeof x === "function" && typeof y === "function") ||
|
2023-04-11 17:45:51 +02:00
|
|
|
(x instanceof Date && y instanceof Date) ||
|
|
|
|
(x instanceof RegExp && y instanceof RegExp) ||
|
|
|
|
(x instanceof String && y instanceof String) ||
|
2025-01-09 18:07:02 +02:00
|
|
|
(x instanceof Number && y instanceof Number)
|
|
|
|
) {
|
2023-04-11 17:45:51 +02:00
|
|
|
return x.toString() === y.toString();
|
|
|
|
}
|
|
|
|
|
2023-06-30 11:18:34 +02:00
|
|
|
// At last, checking prototypes as good as we can
|
2023-04-11 17:45:51 +02:00
|
|
|
if (!(x instanceof Object && y instanceof Object)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (x.isPrototypeOf(y) || y.isPrototypeOf(x)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (x.constructor !== y.constructor) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
if ((x as any).prototype !== (y as any).prototype) {
|
2023-04-11 17:45:51 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for infinitive linking loops
|
|
|
|
if (leftChain.indexOf(x) > -1 || rightChain.indexOf(y) > -1) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Quick checking of one object being a subset of another.
|
|
|
|
// todo: cache the structure of arguments[0] for performance
|
|
|
|
for (p in y) {
|
|
|
|
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
|
|
|
|
return false;
|
2025-01-09 18:07:02 +02:00
|
|
|
} else if (typeof (y as any)[p] !== typeof (x as any)[p]) {
|
2023-04-11 17:45:51 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (p in x) {
|
|
|
|
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
|
|
|
|
return false;
|
2025-01-09 18:07:02 +02:00
|
|
|
} else if (typeof (y as any)[p] !== typeof (x as any)[p]) {
|
2023-04-11 17:45:51 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
switch (typeof (x as any)[p]) {
|
|
|
|
case "object":
|
|
|
|
case "function":
|
2023-04-11 17:45:51 +02:00
|
|
|
leftChain.push(x);
|
|
|
|
rightChain.push(y);
|
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
if (!compare2Objects((x as any)[p], (y as any)[p])) {
|
2023-04-11 17:45:51 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
leftChain.pop();
|
|
|
|
rightChain.pop();
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
2024-07-25 00:18:57 +03:00
|
|
|
if ((x as any)[p] !== (y as any)[p]) {
|
2023-04-11 17:45:51 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (arguments.length < 1) {
|
|
|
|
return true; //Die silently? Don't know how to handle such case, please help...
|
|
|
|
// throw "Need two or more arguments to compare";
|
|
|
|
}
|
|
|
|
|
|
|
|
for (i = 1, l = arguments.length; i < l; i++) {
|
|
|
|
leftChain = []; //Todo: this can be cached
|
|
|
|
rightChain = [];
|
|
|
|
|
|
|
|
if (!compare2Objects(arguments[0], arguments[i])) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
function copyHtmlToClipboard(content: string) {
|
|
|
|
function listener(e: ClipboardEvent) {
|
|
|
|
if (e.clipboardData) {
|
|
|
|
e.clipboardData.setData("text/html", content);
|
|
|
|
e.clipboardData.setData("text/plain", content);
|
|
|
|
}
|
2024-03-27 20:42:36 +01:00
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
document.addEventListener("copy", listener);
|
|
|
|
document.execCommand("copy");
|
|
|
|
document.removeEventListener("copy", listener);
|
2023-05-29 00:19:54 +02:00
|
|
|
}
|
|
|
|
|
2024-07-25 00:18:57 +03:00
|
|
|
// TODO: Set to FNote once the file is ported.
|
|
|
|
function createImageSrcUrl(note: { noteId: string; title: string }) {
|
2023-10-19 09:20:23 +02:00
|
|
|
return `api/images/${note.noteId}/${encodeURIComponent(note.title)}?timestamp=${Date.now()}`;
|
|
|
|
}
|
|
|
|
|
2024-09-01 23:20:41 +03:00
|
|
|
/**
|
|
|
|
* Given a string representation of an SVG, triggers a download of the file on the client device.
|
2024-12-23 11:00:10 +02:00
|
|
|
*
|
2024-10-26 10:29:15 +03:00
|
|
|
* @param nameWithoutExtension the name of the file. The .svg suffix is automatically added to it.
|
|
|
|
* @param svgContent the content of the SVG file download.
|
2024-09-01 23:20:41 +03:00
|
|
|
*/
|
2024-10-26 10:29:15 +03:00
|
|
|
function downloadSvg(nameWithoutExtension: string, svgContent: string) {
|
2024-09-01 23:20:41 +03:00
|
|
|
const filename = `${nameWithoutExtension}.svg`;
|
2025-01-09 18:07:02 +02:00
|
|
|
const element = document.createElement("a");
|
|
|
|
element.setAttribute("href", `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`);
|
|
|
|
element.setAttribute("download", filename);
|
2024-09-01 23:20:41 +03:00
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
element.style.display = "none";
|
2024-09-01 23:20:41 +03:00
|
|
|
document.body.appendChild(element);
|
|
|
|
|
|
|
|
element.click();
|
|
|
|
|
|
|
|
document.body.removeChild(element);
|
|
|
|
}
|
|
|
|
|
2024-11-09 22:16:00 +00:00
|
|
|
/**
|
|
|
|
* Compares two semantic version strings.
|
|
|
|
* Returns:
|
|
|
|
* 1 if v1 is greater than v2
|
|
|
|
* 0 if v1 is equal to v2
|
|
|
|
* -1 if v1 is less than v2
|
2024-12-23 11:00:10 +02:00
|
|
|
*
|
2024-12-14 09:43:01 +02:00
|
|
|
* @param v1 First version string
|
|
|
|
* @param v2 Second version string
|
2024-12-23 11:00:10 +02:00
|
|
|
* @returns
|
2024-11-09 22:16:00 +00:00
|
|
|
*/
|
2024-12-14 09:43:01 +02:00
|
|
|
function compareVersions(v1: string, v2: string): number {
|
2024-11-09 22:16:00 +00:00
|
|
|
// Remove 'v' prefix and everything after dash if present
|
2025-01-09 18:07:02 +02:00
|
|
|
v1 = v1.replace(/^v/, "").split("-")[0];
|
|
|
|
v2 = v2.replace(/^v/, "").split("-")[0];
|
2024-12-23 11:00:10 +02:00
|
|
|
|
2025-01-09 18:07:02 +02:00
|
|
|
const v1parts = v1.split(".").map(Number);
|
|
|
|
const v2parts = v2.split(".").map(Number);
|
2024-12-23 11:00:10 +02:00
|
|
|
|
2024-11-09 22:16:00 +00:00
|
|
|
// Pad shorter version with zeros
|
|
|
|
while (v1parts.length < 3) v1parts.push(0);
|
|
|
|
while (v2parts.length < 3) v2parts.push(0);
|
2024-12-23 11:00:10 +02:00
|
|
|
|
2024-11-09 22:16:00 +00:00
|
|
|
// Compare major version
|
|
|
|
if (v1parts[0] !== v2parts[0]) {
|
|
|
|
return v1parts[0] > v2parts[0] ? 1 : -1;
|
|
|
|
}
|
2024-12-23 11:00:10 +02:00
|
|
|
|
2024-11-09 22:16:00 +00:00
|
|
|
// Compare minor version
|
|
|
|
if (v1parts[1] !== v2parts[1]) {
|
|
|
|
return v1parts[1] > v2parts[1] ? 1 : -1;
|
|
|
|
}
|
2024-12-23 11:00:10 +02:00
|
|
|
|
2024-11-09 22:16:00 +00:00
|
|
|
// Compare patch version
|
|
|
|
if (v1parts[2] !== v2parts[2]) {
|
|
|
|
return v1parts[2] > v2parts[2] ? 1 : -1;
|
|
|
|
}
|
2024-12-23 11:00:10 +02:00
|
|
|
|
2024-11-09 22:16:00 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2024-11-09 22:23:02 +00:00
|
|
|
/**
|
|
|
|
* Compares two semantic version strings and returns `true` if the latest version is greater than the current version.
|
|
|
|
*/
|
2024-12-14 09:43:01 +02:00
|
|
|
function isUpdateAvailable(latestVersion: string, currentVersion: string): boolean {
|
2024-11-09 22:16:00 +00:00
|
|
|
return compareVersions(latestVersion, currentVersion) > 0;
|
|
|
|
}
|
|
|
|
|
2025-01-04 21:52:41 +02:00
|
|
|
function isLaunchBarConfig(noteId: string) {
|
2025-01-09 18:07:02 +02:00
|
|
|
return ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers", "_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(noteId);
|
2025-01-04 21:52:41 +02:00
|
|
|
}
|
|
|
|
|
2024-07-16 18:33:39 +03:00
|
|
|
export default {
|
2021-08-24 22:59:51 +02:00
|
|
|
reloadFrontendApp,
|
2018-03-25 11:09:17 -04:00
|
|
|
parseDate,
|
|
|
|
formatDateISO,
|
|
|
|
formatDateTime,
|
2023-04-21 00:19:17 +02:00
|
|
|
formatTimeInterval,
|
2023-03-24 10:57:32 +01:00
|
|
|
formatSize,
|
2020-04-26 14:26:57 +02:00
|
|
|
localNowDateTime,
|
2018-03-25 11:09:17 -04:00
|
|
|
now,
|
|
|
|
isElectron,
|
2018-12-02 14:04:53 +01:00
|
|
|
isMac,
|
2022-12-09 16:48:00 +01:00
|
|
|
isCtrlKey,
|
2018-03-25 11:09:17 -04:00
|
|
|
assertArguments,
|
|
|
|
escapeHtml,
|
|
|
|
toObject,
|
2018-03-25 19:49:33 -04:00
|
|
|
randomString,
|
2018-12-24 10:10:36 +01:00
|
|
|
isMobile,
|
2018-12-29 00:09:16 +01:00
|
|
|
isDesktop,
|
2019-01-28 21:42:37 +01:00
|
|
|
setCookie,
|
|
|
|
getNoteTypeClass,
|
2019-06-10 22:45:03 +02:00
|
|
|
getMimeTypeClass,
|
2019-10-05 20:27:30 +02:00
|
|
|
closeActiveDialog,
|
2020-02-09 10:00:13 +01:00
|
|
|
openDialog,
|
|
|
|
saveFocusedElement,
|
|
|
|
focusSavedElement,
|
2019-11-08 23:09:57 +01:00
|
|
|
isHtmlEmpty,
|
2019-11-22 20:35:17 +01:00
|
|
|
clearBrowserCache,
|
2020-02-02 16:28:19 +08:00
|
|
|
copySelectionToClipboard,
|
2020-06-11 00:13:56 +02:00
|
|
|
dynamicRequire,
|
2021-01-26 14:44:53 +01:00
|
|
|
timeLimit,
|
|
|
|
initHelpDropdown,
|
2021-02-17 23:22:14 +01:00
|
|
|
initHelpButtons,
|
2021-12-20 17:30:47 +01:00
|
|
|
openHelp,
|
2021-02-17 23:22:14 +01:00
|
|
|
filterAttributeName,
|
2022-05-09 16:50:06 +02:00
|
|
|
isValidAttributeName,
|
|
|
|
sleep,
|
2023-04-11 17:45:51 +02:00
|
|
|
escapeRegExp,
|
2023-05-29 00:19:54 +02:00
|
|
|
areObjectsEqual,
|
2023-10-19 09:20:23 +02:00
|
|
|
copyHtmlToClipboard,
|
2024-09-01 23:20:41 +03:00
|
|
|
createImageSrcUrl,
|
2024-11-09 22:16:00 +00:00
|
|
|
downloadSvg,
|
|
|
|
compareVersions,
|
2025-01-04 21:52:41 +02:00
|
|
|
isUpdateAvailable,
|
|
|
|
isLaunchBarConfig
|
2020-05-20 00:03:33 +02:00
|
|
|
};
|