mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	attachment improvements
This commit is contained in:
		
							
								
								
									
										1
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -5,6 +5,7 @@ | ||||
|   "requires": true, | ||||
|   "packages": { | ||||
|     "": { | ||||
|       "name": "trilium", | ||||
|       "version": "0.59.3", | ||||
|       "hasInstallScript": true, | ||||
|       "license": "AGPL-3.0-only", | ||||
|   | ||||
| @@ -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 | ||||
|         } | ||||
|   | ||||
| @@ -31,3 +31,5 @@ class FAttachment { | ||||
|         return this.froca.notes[this.parentId]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default FAttachment; | ||||
|   | ||||
| @@ -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'; | ||||
|   | ||||
| @@ -78,39 +78,39 @@ async function createNoteLink(notePath, options = {}) { | ||||
|     return $container; | ||||
| } | ||||
|  | ||||
| function getNotePathFromLink($link) { | ||||
|     const notePathAttr = $link.attr("data-note-path"); | ||||
|  | ||||
|     if (notePathAttr) { | ||||
|         return notePathAttr; | ||||
|     } | ||||
| function parseNotePathAndScope($link) { | ||||
|     let notePath = $link.attr("data-note-path"); | ||||
|  | ||||
|     if (!notePath) { | ||||
|         const url = $link.attr('href'); | ||||
|  | ||||
|     const notePath = url ? getNotePathFromUrl(url) : null; | ||||
|         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 (hrefLink) { | ||||
|         // this branch handles external links | ||||
|         const isWithinCKLinkDialog = $link.hasClass("ck-link-actions__preview"); | ||||
|         const isOutsideCKEditor = $link.closest("[contenteditable]").length === 0; | ||||
|  | ||||
|         if (openInNewTab || isWithinCKLinkDialog || isOutsideCKEditor) { | ||||
|             if (hrefLink.toLowerCase().startsWith('http')) { | ||||
|                 window.open(hrefLink, '_blank'); | ||||
|             } | ||||
|                 else if (address.toLowerCase().startsWith('file:') && utils.isElectron()) { | ||||
|             else if (hrefLink.toLowerCase().startsWith('file:') && utils.isElectron()) { | ||||
|                 const electron = utils.dynamicRequire('electron'); | ||||
|  | ||||
|                     electron.shell.openPath(address); | ||||
|                 } | ||||
|                 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 | ||||
| }; | ||||
|   | ||||
| @@ -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); | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| }; | ||||
|   | ||||
| @@ -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; | ||||
|         } | ||||
|     </style> | ||||
|  | ||||
|     <div class="attachment-detail-wrapper"> | ||||
|         <div class="attachment-title-line"> | ||||
|             <h4 class="attachment-title"><a href="javascript:" data-trigger-command="openAttachmentDetail"></a></h4>                 | ||||
|             <h4 class="attachment-title"></h4>                 | ||||
|             <div class="attachment-details"></div> | ||||
|             <div style="flex: 1 1;"></div> | ||||
|             <div class="attachment-actions-container"></div> | ||||
| @@ -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( | ||||
|                 $('<a href="javascript:">') | ||||
|                     .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()); | ||||
|   | ||||
| @@ -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("<strong>This attachment has been deleted.</strong>"); | ||||
|             this.$wrapper.html("<strong>This attachment has been deleted.</strong>"); | ||||
|  | ||||
|             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}) { | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user