620 lines
16 KiB
JavaScript
Raw Normal View History

2021-09-17 22:34:23 +02:00
function reloadFrontendApp(reason) {
if (reason) {
logInfo(`Frontend app reload: ${reason}`);
2021-09-17 22:34:23 +02:00
}
window.location.reload();
}
function parseDate(str) {
try {
return new Date(Date.parse(str));
2018-03-24 23:37:55 -04:00
}
catch (e) {
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-24 23:37:55 -04:00
function padNum(num) {
return `${num <= 9 ? "0" : ""}${num}`;
}
2018-03-24 23:37:55 -04:00
function formatTime(date) {
return `${padNum(date.getHours())}:${padNum(date.getMinutes())}`;
}
2018-03-24 23:37:55 -04:00
function formatTimeWithSeconds(date) {
return `${padNum(date.getHours())}:${padNum(date.getMinutes())}:${padNum(date.getSeconds())}`;
}
2023-04-21 00:19:17 +02:00
function formatTimeInterval(ms) {
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 segments = [];
if (days > 0) {
segments.push(plural(days, 'day'));
}
if (days < 2) {
if (hours % 24 > 0) {
segments.push(plural(hours % 24, 'hour'));
}
if (hours < 4) {
if (minutes % 60 > 0) {
segments.push(plural(minutes % 60, 'minute'));
}
if (minutes < 5) {
if (seconds % 60 > 0) {
segments.push(plural(seconds % 60, 'second'));
}
}
}
}
return segments.join(", ");
}
// this is producing local time!
function formatDate(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) {
return `${date.getFullYear()}-${padNum(date.getMonth() + 1)}-${padNum(date.getDate())}`;
}
function formatDateTime(date) {
return `${formatDate(date)} ${formatTime(date)}`;
}
function localNowDateTime() {
return dayjs().format('YYYY-MM-DD HH:mm:ss.SSSZZ');
}
function now() {
return formatTimeWithSeconds(new Date());
}
/**
* Returns `true` if the client is currently running under Electron, or `false` if running in a web browser.
*/
function isElectron() {
2019-02-09 19:25:55 +01:00
return !!(window && window.process && window.process.type);
}
function isMac() {
return navigator.platform.indexOf('Mac') > -1;
}
function isCtrlKey(evt) {
return (!isMac() && evt.ctrlKey)
|| (isMac() && evt.metaKey);
}
function assertArguments() {
for (const i in arguments) {
if (!arguments[i]) {
console.trace(`Argument idx#${i} should not be falsy: ${arguments[i]}`);
2018-03-24 23:37:55 -04:00
}
}
}
const entityMap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;'
};
function escapeHtml(str) {
return str.replace(/[&<>"'`=\/]/g, s => entityMap[s]);
}
2018-03-24 23:37:55 -04:00
2023-03-24 10:57:32 +01:00
function formatSize(size) {
size = Math.max(Math.round(size / 1024), 1);
if (size < 1024) {
return `${size} KiB`;
}
else {
return `${Math.round(size / 102.4) / 10} MiB`;
}
}
function toObject(array, fn) {
const obj = {};
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;
}
return obj;
}
function randomString(len) {
let text = "";
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < len; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
function isMobile() {
2023-05-03 22:49:24 +02:00
return window.glob?.device === "mobile"
// window.glob.device is not available in setup
|| (!window.glob?.device && /Mobi/.test(navigator.userAgent));
}
function isDesktop() {
2023-05-03 22:49:24 +02:00
return window.glob?.device === "desktop"
// window.glob.device is not available in setup
|| (!window.glob?.device && !/Mobi/.test(navigator.userAgent));
}
2023-06-30 11:18:34 +02: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
2018-12-29 00:09:16 +01:00
function setCookie(name, value) {
const date = new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1000);
const expires = `; expires=${date.toUTCString()}`;
2018-12-29 00:09:16 +01:00
document.cookie = `${name}=${value || ""}${expires};`;
}
function getNoteTypeClass(type) {
return `type-${type}`;
}
function getMimeTypeClass(mime) {
2021-02-22 21:59:37 +01:00
if (!mime) {
return "";
}
const semicolonIdx = mime.indexOf(';');
if (semicolonIdx !== -1) {
// stripping everything following the semicolon
mime = mime.substr(0, semicolonIdx);
}
return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`;
}
function closeActiveDialog() {
if (glob.activeDialog) {
2024-09-03 18:15:10 +02:00
bootstrap.Modal.getOrCreateInstance(glob.activeDialog).hide();
glob.activeDialog = null;
}
}
let $lastFocusedElement = null;
// perhaps there should be saved focused element per tab?
function saveFocusedElement() {
$lastFocusedElement = $(":focus");
}
function focusSavedElement() {
if (!$lastFocusedElement) {
return;
}
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
const editor = $lastFocusedElement
.closest('.ck-editor__editable')
.prop('ckeditorInstance');
if (editor) {
editor.editing.view.focus();
} else {
console.log("Could not find CKEditor instance to focus last element");
}
} else {
$lastFocusedElement.focus();
}
$lastFocusedElement = null;
}
2022-01-10 17:09:20 +01:00
async function openDialog($dialog, closeActDialog = true) {
if (closeActDialog) {
closeActiveDialog();
glob.activeDialog = $dialog;
}
saveFocusedElement();
2024-09-03 18:15:10 +02:00
bootstrap.Modal.getOrCreateInstance($dialog).show();
$dialog.on('hidden.bs.modal', () => {
$(".aa-input").autocomplete("close");
if (!glob.activeDialog || glob.activeDialog === $dialog) {
focusSavedElement();
}
});
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
keyboardActionsService.updateDisplayedShortcuts($dialog);
}
function isHtmlEmpty(html) {
2020-03-26 16:59:40 +01:00
if (!html) {
return true;
} else if (typeof html !== 'string') {
logError(`Got object of type '${typeof html}' where string was expected.`);
return false;
2020-03-26 16:59:40 +01:00
}
html = html.toLowerCase();
return !html.includes('<img')
&& !html.includes('<section')
// the line below will actually attempt to load images so better to check for images first
&& $("<div>").html(html).text().trim().length === 0;
}
2019-11-08 23:09:57 +01:00
async function clearBrowserCache() {
if (isElectron()) {
2022-06-13 22:38:59 +02:00
const win = dynamicRequire('@electron/remote').getCurrentWindow();
2019-11-08 23:09:57 +01:00
await win.webContents.session.clearCache();
}
}
function copySelectionToClipboard() {
2020-01-19 09:25:35 +01:00
const text = window.getSelection().toString();
if (navigator.clipboard) {
2020-01-19 09:25:35 +01:00
navigator.clipboard.writeText(text);
}
}
2020-04-12 14:22:51 +02:00
function dynamicRequire(moduleName) {
if (typeof __non_webpack_require__ !== 'undefined') {
return __non_webpack_require__(moduleName);
}
else {
return require(moduleName);
}
}
function timeLimit(promise, limitMs, errorMessage) {
if (!promise || !promise.then) { // it's not actually a promise
return promise;
}
2020-07-29 23:34:49 +02:00
// better stack trace if created outside of promise
const error = new Error(errorMessage || `Process exceeded time limit ${limitMs}`);
2020-07-29 23:34:49 +02:00
2020-06-11 00:13:56 +02:00
return new Promise((res, rej) => {
let resolved = false;
2020-06-13 22:34:15 +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);
});
}
function initHelpDropdown($el) {
// stop inside clicks from closing the menu
const $dropdownMenu = $el.find('.help-dropdown .dropdown-menu');
$dropdownMenu.on('click', e => e.stopPropagation());
// previous propagation stop will also block help buttons from being opened, so we need to re-init for this element
initHelpButtons($dropdownMenu);
}
const wikiBaseUrl = "https://triliumnext.github.io/Docs/Wiki/";
2023-07-14 21:59:43 +02:00
function openHelp($button) {
const helpPage = $button.attr("data-help-page");
if (helpPage) {
const url = wikiBaseUrl + helpPage;
window.open(url, '_blank');
}
2021-12-20 17:30:47 +01:00
}
function initHelpButtons($el) {
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
$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
});
}
function filterAttributeName(name) {
return name.replace(/[^\p{L}\p{N}_:]/ug, "");
}
const ATTR_NAME_MATCHER = new RegExp("^[\\p{L}\\p{N}_:]+$", "u");
function isValidAttributeName(name) {
return ATTR_NAME_MATCHER.test(name);
}
2022-05-09 16:50:06 +02:00
function sleep(time_ms) {
return new Promise((resolve) => {
setTimeout(resolve, time_ms);
});
}
2022-05-09 16:50:06 +02:00
2022-05-25 23:38:06 +02:00
function escapeRegExp(str) {
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}
function areObjectsEqual() {
let i, l, leftChain, rightChain;
2023-04-11 17:45:51 +02:00
function compare2Objects(x, y) {
let p;
2023-04-11 17:45:51 +02:00
// remember that NaN === NaN returns false
// and isNaN(undefined) returns true
if (isNaN(x) && isNaN(y) && typeof x === 'number' && typeof y === 'number') {
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
if ((typeof x === 'function' && typeof y === 'function') ||
(x instanceof Date && y instanceof Date) ||
(x instanceof RegExp && y instanceof RegExp) ||
(x instanceof String && y instanceof String) ||
(x instanceof Number && y instanceof Number)) {
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;
}
if (x.prototype !== y.prototype) {
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;
}
else if (typeof y[p] !== typeof x[p]) {
return false;
}
}
for (p in x) {
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
}
else if (typeof y[p] !== typeof x[p]) {
return false;
}
switch (typeof (x[p])) {
case 'object':
case 'function':
leftChain.push(x);
rightChain.push(y);
if (!compare2Objects(x[p], y[p])) {
2023-04-11 17:45:51 +02:00
return false;
}
leftChain.pop();
rightChain.pop();
break;
default:
if (x[p] !== y[p]) {
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;
}
2023-05-29 00:19:54 +02:00
function copyHtmlToClipboard(content) {
2024-03-27 20:42:36 +01:00
function listener(e) {
e.clipboardData.setData("text/html", content);
e.clipboardData.setData("text/plain", content);
e.preventDefault();
}
document.addEventListener("copy", listener);
document.execCommand("copy");
document.removeEventListener("copy", listener);
2023-05-29 00:19:54 +02:00
}
/**
* @param {FNote} note
* @return {string}
*/
function createImageSrcUrl(note) {
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.
*
* @param {string} nameWithoutExtension the name of the file. The .svg suffix is automatically added to it.
* @param {string} svgContent the content of the SVG file download.
*/
function downloadSvg(nameWithoutExtension, svgContent) {
const filename = `${nameWithoutExtension}.svg`;
const element = document.createElement('a');
element.setAttribute('href', `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`);
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
/**
* 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
*
* @param {string} v1 First version string
* @param {string} v2 Second version string
* @returns {number}
*/
function compareVersions(v1, v2) {
// Remove 'v' prefix and everything after dash if present
v1 = v1.replace(/^v/, '').split('-')[0];
v2 = v2.replace(/^v/, '').split('-')[0];
const v1parts = v1.split('.').map(Number);
const v2parts = v2.split('.').map(Number);
// Pad shorter version with zeros
while (v1parts.length < 3) v1parts.push(0);
while (v2parts.length < 3) v2parts.push(0);
// Compare major version
if (v1parts[0] !== v2parts[0]) {
return v1parts[0] > v2parts[0] ? 1 : -1;
}
// Compare minor version
if (v1parts[1] !== v2parts[1]) {
return v1parts[1] > v2parts[1] ? 1 : -1;
}
// Compare patch version
if (v1parts[2] !== v2parts[2]) {
return v1parts[2] > v2parts[2] ? 1 : -1;
}
return 0;
}
function isUpdateAvailable(latestVersion, currentVersion) {
return compareVersions(latestVersion, currentVersion) > 0;
}
export default {
2021-08-24 22:59:51 +02:00
reloadFrontendApp,
parseDate,
formatDateISO,
formatDateTime,
2023-04-21 00:19:17 +02:00
formatTimeInterval,
2023-03-24 10:57:32 +01:00
formatSize,
localNowDateTime,
now,
isElectron,
isMac,
isCtrlKey,
assertArguments,
escapeHtml,
toObject,
randomString,
isMobile,
2018-12-29 00:09:16 +01:00
isDesktop,
setCookie,
getNoteTypeClass,
getMimeTypeClass,
closeActiveDialog,
openDialog,
saveFocusedElement,
focusSavedElement,
2019-11-08 23:09:57 +01:00
isHtmlEmpty,
clearBrowserCache,
copySelectionToClipboard,
2020-06-11 00:13:56 +02:00
dynamicRequire,
timeLimit,
initHelpDropdown,
initHelpButtons,
2021-12-20 17:30:47 +01:00
openHelp,
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,
copyHtmlToClipboard,
2024-09-01 23:20:41 +03:00
createImageSrcUrl,
downloadSvg,
compareVersions,
isUpdateAvailable
2020-05-20 00:03:33 +02:00
};