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; + }
-

+

@@ -54,6 +65,7 @@ export default class AttachmentDetailWidget extends BasicWidget { this.contentSized(); this.attachment = attachment; this.attachmentActionsWidget = new AttachmentActionsWidget(attachment); + this.isFullDetail = true; this.child(this.attachmentActionsWidget); } @@ -73,7 +85,21 @@ export default class AttachmentDetailWidget extends BasicWidget { .html() ); this.$wrapper = this.$widget.find('.attachment-detail-wrapper'); - this.$wrapper.find('.attachment-title a').text(this.attachment.title); + this.$wrapper.addClass(this.isFullDetail ? "full-detail" : "list-view"); + + if (!this.isFullDetail) { + this.$wrapper.find('.attachment-title').append( + $('') + .attr("data-note-path", this.attachment.parentId) + .attr("data-view-mode", "attachments") + .attr("data-attachment-id", this.attachment.attachmentId) + .text(this.attachment.title) + ); + } else { + this.$wrapper.find('.attachment-title') + .text(this.attachment.title); + } + this.$wrapper.find('.attachment-details') .text(`Role: ${this.attachment.role}, Size: ${utils.formatSize(this.attachment.contentLength)}`); this.$wrapper.find('.attachment-actions-container').append(this.attachmentActionsWidget.render()); diff --git a/src/public/app/widgets/type_widgets/attachment_detail.js b/src/public/app/widgets/type_widgets/attachment_detail.js index 8a6e60047..128aca43c 100644 --- a/src/public/app/widgets/type_widgets/attachment_detail.js +++ b/src/public/app/widgets/type_widgets/attachment_detail.js @@ -30,18 +30,19 @@ export default class AttachmentDetailTypeWidget extends TypeWidget { this.children = []; this.renderedAttachmentIds = new Set(); - const attachment = await server.get(`notes/${this.noteId}/attachments/${this.noteContext.viewScope.attachment.attachmentId}/?includeContent=true`); + const attachment = await server.get(`notes/${this.noteId}/attachments/${this.noteContext.viewScope.attachmentId}/?includeContent=true`); if (!attachment) { - this.$list.html("This attachment has been deleted."); + this.$wrapper.html("This attachment has been deleted."); return; } const attachmentDetailWidget = new AttachmentDetailWidget(attachment); + attachmentDetailWidget.isFullDetail = true; this.child(attachmentDetailWidget); - this.$list.append(attachmentDetailWidget.render()); + this.$wrapper.append(attachmentDetailWidget.render()); } async entitiesReloadedEvent({loadResults}) { diff --git a/src/public/app/widgets/type_widgets/attachment_list.js b/src/public/app/widgets/type_widgets/attachment_list.js index 90861b4ba..0072a14be 100644 --- a/src/public/app/widgets/type_widgets/attachment_list.js +++ b/src/public/app/widgets/type_widgets/attachment_list.js @@ -40,6 +40,8 @@ export default class AttachmentListTypeWidget extends TypeWidget { for (const attachment of attachments) { const attachmentDetailWidget = new AttachmentDetailWidget(attachment); + attachmentDetailWidget.isFullDetail = false; + this.child(attachmentDetailWidget); this.renderedAttachmentIds.add(attachment.attachmentId);