diff --git a/package-lock.json b/package-lock.json index 7bb0b7ac6..1f02b40b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "trilium", "version": "0.59.3", "hasInstallScript": true, "license": "AGPL-3.0-only", diff --git a/src/public/app/components/note_context.js b/src/public/app/components/note_context.js index 2a6ec1cec..12ffa4ba4 100644 --- a/src/public/app/components/note_context.js +++ b/src/public/app/components/note_context.js @@ -39,6 +39,8 @@ class NoteContext extends Component { async setNote(inputNotePath, opts = {}) { opts.triggerSwitchEvent = opts.triggerSwitchEvent !== undefined ? opts.triggerSwitchEvent : true; + opts.viewScope = opts.viewScope || {}; + opts.viewScope.viewMode = opts.viewScope.viewMode || "default"; const resolvedNotePath = await this.getResolvedNotePath(inputNotePath); @@ -46,6 +48,10 @@ class NoteContext extends Component { return; } + if (this.notePath === resolvedNotePath && utils.areObjectsEqual(this.viewScope, opts.viewScope)) { + return; + } + await this.triggerEvent('beforeNoteSwitch', {noteContext: this}); utils.closeActiveDialog(); @@ -53,8 +59,7 @@ class NoteContext extends Component { this.notePath = resolvedNotePath; ({noteId: this.noteId, parentNoteId: this.parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(resolvedNotePath)); - this.viewScope = opts.viewScope || {}; - this.viewScope.viewMode = this.viewScope.viewMode || "default"; + this.viewScope = opts.viewScope; this.saveToRecentNotes(resolvedNotePath); @@ -137,10 +142,6 @@ class NoteContext extends Component { return; } - if (resolvedNotePath === this.notePath) { - return; - } - if (await hoistedNoteService.checkNoteAccess(resolvedNotePath, this) === false) { return; // note is outside of hoisted subtree and user chose not to unhoist } diff --git a/src/public/app/entities/fattachment.js b/src/public/app/entities/fattachment.js index 0846e2690..7b417aec2 100644 --- a/src/public/app/entities/fattachment.js +++ b/src/public/app/entities/fattachment.js @@ -31,3 +31,5 @@ class FAttachment { return this.froca.notes[this.parentId]; } } + +export default FAttachment; diff --git a/src/public/app/entities/fnote.js b/src/public/app/entities/fnote.js index 9b2e72447..fd3949d92 100644 --- a/src/public/app/entities/fnote.js +++ b/src/public/app/entities/fnote.js @@ -5,6 +5,7 @@ import options from "../services/options.js"; import froca from "../services/froca.js"; import protectedSessionHolder from "../services/protected_session_holder.js"; import cssClassManager from "../services/css_class_manager.js"; +import FAttachment from "./fattachment.js"; const LABEL = 'label'; const RELATION = 'relation'; diff --git a/src/public/app/services/link.js b/src/public/app/services/link.js index 4daef9eca..a2eff7b65 100644 --- a/src/public/app/services/link.js +++ b/src/public/app/services/link.js @@ -78,39 +78,39 @@ async function createNoteLink(notePath, options = {}) { return $container; } -function getNotePathFromLink($link) { - const notePathAttr = $link.attr("data-note-path"); +function parseNotePathAndScope($link) { + let notePath = $link.attr("data-note-path"); - if (notePathAttr) { - return notePathAttr; + if (!notePath) { + const url = $link.attr('href'); + + notePath = url ? getNotePathFromUrl(url) : null; } - const url = $link.attr('href'); - - const notePath = url ? getNotePathFromUrl(url) : null; const viewScope = { - viewMode: $link.attr('data-view-mode'), + viewMode: $link.attr('data-view-mode') || 'default', attachmentId: $link.attr('data-attachment-id'), }; return { notePath, + noteId: treeService.getNoteIdFromNotePath(notePath), viewScope }; } function goToLink(evt) { const $link = $(evt.target).closest("a,.block-link"); - const address = $link.attr('href'); + const hrefLink = $link.attr('href'); - if (address?.startsWith("data:")) { + if (hrefLink?.startsWith("data:")) { return true; } evt.preventDefault(); evt.stopPropagation(); - const {notePath, viewScope} = getNotePathFromLink($link); + const { notePath, viewScope } = parseNotePathAndScope($link); const ctrlKey = utils.isCtrlKey(evt); const isLeftClick = evt.which === 1; @@ -135,20 +135,19 @@ function goToLink(evt) { }); } } - else { - if (openInNewTab - || $link.hasClass("ck-link-actions__preview") // within edit link dialog single click suffices - || $link.closest("[contenteditable]").length === 0 // outside of CKEditor single click suffices - ) { - if (address) { - if (address.toLowerCase().startsWith('http')) { - window.open(address, '_blank'); - } - else if (address.toLowerCase().startsWith('file:') && utils.isElectron()) { - const electron = utils.dynamicRequire('electron'); + else if (hrefLink) { + // this branch handles external links + const isWithinCKLinkDialog = $link.hasClass("ck-link-actions__preview"); + const isOutsideCKEditor = $link.closest("[contenteditable]").length === 0; - electron.shell.openPath(address); - } + if (openInNewTab || isWithinCKLinkDialog || isOutsideCKEditor) { + if (hrefLink.toLowerCase().startsWith('http')) { + window.open(hrefLink, '_blank'); + } + else if (hrefLink.toLowerCase().startsWith('file:') && utils.isElectron()) { + const electron = utils.dynamicRequire('electron'); + + electron.shell.openPath(hrefLink); } } } @@ -159,7 +158,7 @@ function goToLink(evt) { function linkContextMenu(e) { const $link = $(e.target).closest("a"); - const {notePath, viewScope} = getNotePathFromLink($link); + const { notePath, viewScope } = parseNotePathAndScope($link); if (!notePath) { return; @@ -223,5 +222,6 @@ export default { getNotePathFromUrl, createNoteLink, goToLink, - loadReferenceLinkTitle + loadReferenceLinkTitle, + parseNotePathAndScope }; diff --git a/src/public/app/services/note_tooltip.js b/src/public/app/services/note_tooltip.js index b83296cd8..c16620c4c 100644 --- a/src/public/app/services/note_tooltip.js +++ b/src/public/app/services/note_tooltip.js @@ -31,18 +31,12 @@ async function mouseEnterHandler() { return; } - let notePath = linkService.getNotePathFromUrl($link.attr("href")); + const { notePath, noteId, viewScope } = linkService.parseNotePathAndScope($link); - if (!notePath) { - notePath = $link.attr("data-note-path"); - } - - if (!notePath) { + if (!notePath || viewScope.viewMode !== 'default') { return; } - const noteId = treeService.getNoteIdFromNotePath(notePath); - const note = await froca.getNote(noteId); const content = await renderTooltip(note); diff --git a/src/public/app/services/utils.js b/src/public/app/services/utils.js index d9f0d7b05..a8f758ae6 100644 --- a/src/public/app/services/utils.js +++ b/src/public/app/services/utils.js @@ -365,6 +365,121 @@ 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, @@ -408,5 +523,6 @@ export default { filterAttributeName, isValidAttributeName, sleep, - escapeRegExp + escapeRegExp, + areObjectsEqual }; diff --git a/src/public/app/widgets/attachment_detail.js b/src/public/app/widgets/attachment_detail.js index b853cdc88..e8a954fa7 100644 --- a/src/public/app/widgets/attachment_detail.js +++ b/src/public/app/widgets/attachment_detail.js @@ -20,24 +20,35 @@ const TPL = ` } .attachment-content pre { - max-height: 400px; background: var(--accented-background-color); padding: 10px; margin-top: 10px; margin-bottom: 10px; } + .attachment-detail-wrapper.list-view .attachment-content pre { + max-height: 400px; + } + .attachment-content img { margin: 10px; + } + + .attachment-detail-wrapper.list-view .attachment-content img { max-height: 300px; max-width: 90%; object-fit: contain; } + + .attachment-detail-wrapper.full-detail .attachment-content img { + max-width: 90%; + object-fit: contain; + }