mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
529 lines
13 KiB
JavaScript
529 lines
13 KiB
JavaScript
function reloadFrontendApp(reason) {
|
|
if (reason) {
|
|
logInfo(`Frontend app reload: ${reason}`);
|
|
}
|
|
|
|
window.location.reload();
|
|
}
|
|
|
|
function parseDate(str) {
|
|
try {
|
|
return new Date(Date.parse(str));
|
|
}
|
|
catch (e) {
|
|
throw new Error(`Can't parse date from '${str}': ${e.message} ${e.stack}`);
|
|
}
|
|
}
|
|
|
|
function padNum(num) {
|
|
return `${num <= 9 ? "0" : ""}${num}`;
|
|
}
|
|
|
|
function formatTime(date) {
|
|
return `${padNum(date.getHours())}:${padNum(date.getMinutes())}`;
|
|
}
|
|
|
|
function formatTimeWithSeconds(date) {
|
|
return `${padNum(date.getHours())}:${padNum(date.getMinutes())}:${padNum(date.getSeconds())}`;
|
|
}
|
|
|
|
// 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());
|
|
}
|
|
|
|
function isElectron() {
|
|
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]}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const entityMap = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": ''',
|
|
'/': '/',
|
|
'`': '`',
|
|
'=': '='
|
|
};
|
|
|
|
function escapeHtml(str) {
|
|
return str.replace(/[&<>"'`=\/]/g, s => entityMap[s]);
|
|
}
|
|
|
|
async function stopWatch(what, func) {
|
|
const start = new Date();
|
|
|
|
const ret = await func();
|
|
|
|
const tookMs = Date.now() - start.getTime();
|
|
|
|
console.log(`${what} took ${tookMs}ms`);
|
|
|
|
return ret;
|
|
}
|
|
|
|
function formatValueWithWhitespace(val) {
|
|
return /[^\w-]/.test(val) ? `"${val}"` : val;
|
|
}
|
|
|
|
function formatLabel(label) {
|
|
let str = `#${formatValueWithWhitespace(label.name)}`;
|
|
|
|
if (label.value !== "") {
|
|
str += `=${formatValueWithWhitespace(label.value)}`;
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
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) {
|
|
const [key, value] = fn(item);
|
|
|
|
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() {
|
|
return window.device === "mobile"
|
|
// window.device is not available in setup
|
|
|| (!window.device && /Mobi/.test(navigator.userAgent));
|
|
}
|
|
|
|
function isDesktop() {
|
|
return window.device === "desktop"
|
|
// window.device is not available in setup
|
|
|| (!window.device && !/Mobi/.test(navigator.userAgent));
|
|
}
|
|
|
|
// cookie code below works for simple use cases only - ASCII only
|
|
// not setting path so that cookies do not leak into other websites if multiplexed with reverse proxy
|
|
|
|
function setCookie(name, value) {
|
|
const date = new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1000);
|
|
const expires = `; expires=${date.toUTCString()}`;
|
|
|
|
document.cookie = `${name}=${value || ""}${expires};`;
|
|
}
|
|
|
|
function setSessionCookie(name, value) {
|
|
document.cookie = `${name}=${value || ""}; SameSite=Strict`;
|
|
}
|
|
|
|
function getCookie(name) {
|
|
const valueMatch = document.cookie.match(`(^|;) ?${name}=([^;]*)(;|$)`);
|
|
return valueMatch ? valueMatch[2] : null;
|
|
}
|
|
|
|
function getNoteTypeClass(type) {
|
|
return `type-${type}`;
|
|
}
|
|
|
|
function getMimeTypeClass(mime) {
|
|
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) {
|
|
glob.activeDialog.modal('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;
|
|
}
|
|
|
|
async function openDialog($dialog, closeActDialog = true) {
|
|
if (closeActDialog) {
|
|
closeActiveDialog();
|
|
glob.activeDialog = $dialog;
|
|
}
|
|
|
|
saveFocusedElement();
|
|
|
|
$dialog.modal();
|
|
|
|
$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) {
|
|
if (!html) {
|
|
return true;
|
|
}
|
|
|
|
html = html.toLowerCase();
|
|
|
|
return !html.includes('<img')
|
|
&& !html.includes('<section')
|
|
// line below will actually attempt to load images so better to check for images first
|
|
&& $("<div>").html(html).text().trim().length === 0;
|
|
}
|
|
|
|
async function clearBrowserCache() {
|
|
if (isElectron()) {
|
|
const win = dynamicRequire('@electron/remote').getCurrentWindow();
|
|
await win.webContents.session.clearCache();
|
|
}
|
|
}
|
|
|
|
function copySelectionToClipboard() {
|
|
const text = window.getSelection().toString();
|
|
if (navigator.clipboard) {
|
|
navigator.clipboard.writeText(text);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// better stack trace if created outside of promise
|
|
const error = new Error(errorMessage || `Process exceeded time limit ${limitMs}`);
|
|
|
|
return new Promise((res, rej) => {
|
|
let resolved = false;
|
|
|
|
promise.then(result => {
|
|
resolved = true;
|
|
|
|
res(result);
|
|
});
|
|
|
|
setTimeout(() => {
|
|
if (!resolved) {
|
|
rej(error);
|
|
}
|
|
}, 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://github.com/zadam/trilium/wiki/";
|
|
|
|
function openHelp(e) {
|
|
window.open(wikiBaseUrl + $(e.target).attr("data-help-page"), '_blank');
|
|
}
|
|
|
|
function initHelpButtons($el) {
|
|
// 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 => {
|
|
if ($(e.target).attr("data-help-page")) {
|
|
openHelp(e);
|
|
}
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
function sleep(time_ms) {
|
|
return new Promise((resolve) => {
|
|
setTimeout(resolve, time_ms);
|
|
});
|
|
}
|
|
|
|
function escapeRegExp(str) {
|
|
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
|
|
}
|
|
|
|
function areObjectsEqual () {
|
|
var i, l, leftChain, rightChain;
|
|
|
|
function compare2Objects (x, y) {
|
|
var p;
|
|
|
|
// 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();
|
|
}
|
|
|
|
// At last checking prototypes as good as we can
|
|
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])) {
|
|
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;
|
|
}
|
|
|
|
export default {
|
|
reloadFrontendApp,
|
|
parseDate,
|
|
padNum,
|
|
formatTime,
|
|
formatTimeWithSeconds,
|
|
formatDate,
|
|
formatDateISO,
|
|
formatDateTime,
|
|
formatSize,
|
|
localNowDateTime,
|
|
now,
|
|
isElectron,
|
|
isMac,
|
|
isCtrlKey,
|
|
assertArguments,
|
|
escapeHtml,
|
|
stopWatch,
|
|
formatLabel,
|
|
toObject,
|
|
randomString,
|
|
isMobile,
|
|
isDesktop,
|
|
setCookie,
|
|
setSessionCookie,
|
|
getCookie,
|
|
getNoteTypeClass,
|
|
getMimeTypeClass,
|
|
closeActiveDialog,
|
|
openDialog,
|
|
saveFocusedElement,
|
|
focusSavedElement,
|
|
isHtmlEmpty,
|
|
clearBrowserCache,
|
|
copySelectionToClipboard,
|
|
dynamicRequire,
|
|
timeLimit,
|
|
initHelpDropdown,
|
|
initHelpButtons,
|
|
openHelp,
|
|
filterAttributeName,
|
|
isValidAttributeName,
|
|
sleep,
|
|
escapeRegExp,
|
|
areObjectsEqual
|
|
};
|