From ddf75cd5e5136f7e0e3a643af5632f3301211317 Mon Sep 17 00:00:00 2001 From: zadam Date: Tue, 25 Jul 2023 22:27:15 +0200 Subject: [PATCH] note tooltip displays the whole note with scrollbar, other behavior changes. closes #4120 --- src/public/app/components/entrypoints.js | 2 - src/public/app/entities/fattachment.js | 10 ++-- src/public/app/entities/fattribute.js | 1 + src/public/app/entities/fbranch.js | 1 + src/public/app/entities/fnote.js | 10 ++-- src/public/app/services/content_renderer.js | 26 ++++------ src/public/app/services/froca.js | 7 ++- src/public/app/services/note_tooltip.js | 53 ++++++++++++++------- src/public/stylesheets/style.css | 2 +- src/routes/api/notes.js | 4 +- src/services/blob.js | 8 +--- 11 files changed, 62 insertions(+), 62 deletions(-) diff --git a/src/public/app/components/entrypoints.js b/src/public/app/components/entrypoints.js index e4965a14f..6b281911f 100644 --- a/src/public/app/components/entrypoints.js +++ b/src/public/app/components/entrypoints.js @@ -182,8 +182,6 @@ export default class Entrypoints extends Component { } hideAllPopups() { - $(".tooltip").removeClass("show"); - if (utils.isDesktop()) { $(".aa-input").autocomplete("close"); } diff --git a/src/public/app/entities/fattachment.js b/src/public/app/entities/fattachment.js index ce8945076..85148abb3 100644 --- a/src/public/app/entities/fattachment.js +++ b/src/public/app/entities/fattachment.js @@ -1,5 +1,6 @@ class FAttachment { constructor(froca, row) { + /** @type {Froca} */ this.froca = froca; this.update(row); @@ -34,12 +35,9 @@ class FAttachment { return this.froca.notes[this.ownerId]; } - /** - * @param [opts.preview=false] - retrieve only first 10 000 characters for a preview - * @return {FBlob} - */ - async getBlob(opts = {}) { - return await this.froca.getBlob('attachments', this.attachmentId, opts); + /** @return {FBlob} */ + async getBlob() { + return await this.froca.getBlob('attachments', this.attachmentId); } } diff --git a/src/public/app/entities/fattribute.js b/src/public/app/entities/fattribute.js index ce166e038..cd8ccc4fa 100644 --- a/src/public/app/entities/fattribute.js +++ b/src/public/app/entities/fattribute.js @@ -6,6 +6,7 @@ import promotedAttributeDefinitionParser from '../services/promoted_attribute_de */ class FAttribute { constructor(froca, row) { + /** @type {Froca} */ this.froca = froca; this.update(row); diff --git a/src/public/app/entities/fbranch.js b/src/public/app/entities/fbranch.js index b01030c48..3fa20934b 100644 --- a/src/public/app/entities/fbranch.js +++ b/src/public/app/entities/fbranch.js @@ -4,6 +4,7 @@ */ class FBranch { constructor(froca, row) { + /** @type {Froca} */ this.froca = froca; this.update(row); diff --git a/src/public/app/entities/fnote.js b/src/public/app/entities/fnote.js index 81d37b8cc..848243fd6 100644 --- a/src/public/app/entities/fnote.js +++ b/src/public/app/entities/fnote.js @@ -31,6 +31,7 @@ class FNote { * @param {Object.} row */ constructor(froca, row) { + /** @type {Froca} */ this.froca = froca; /** @type {string[]} */ @@ -859,12 +860,9 @@ class FNote { return this.getBlob(); } - /** - * @param [opts.preview=false] - retrieve only first 10 000 characters for a preview - * @return {Promise} - */ - async getBlob(opts = {}) { - return await this.froca.getBlob('notes', this.noteId, opts); + /** @return {Promise} */ + async getBlob() { + return await this.froca.getBlob('notes', this.noteId); } toString() { diff --git a/src/public/app/services/content_renderer.js b/src/public/app/services/content_renderer.js index b51be52ee..4ea7d1ff7 100644 --- a/src/public/app/services/content_renderer.js +++ b/src/public/app/services/content_renderer.js @@ -19,7 +19,6 @@ let idCounter = 1; */ async function getRenderedContent(entity, options = {}) { options = Object.assign({ - trim: false, tooltip: false }, options); @@ -29,7 +28,7 @@ async function getRenderedContent(entity, options = {}) { const $renderedContent = $('
'); if (type === 'text') { - await renderText(entity, options, $renderedContent); + await renderText(entity, $renderedContent); } else if (type === 'code') { await renderCode(entity, options, $renderedContent); @@ -86,12 +85,13 @@ async function getRenderedContent(entity, options = {}) { }; } -async function renderText(note, options, $renderedContent) { +/** @param {FNote} note */ +async function renderText(note, $renderedContent) { // entity must be FNote - const blob = await note.getBlob({preview: options.trim}); + const blob = await note.getBlob(); if (!utils.isHtmlEmpty(blob.content)) { - $renderedContent.append($('
').html(trim(blob.content, options.trim))); + $renderedContent.append($('
').html(blob.content)); if ($renderedContent.find('span.math-tex').length > 0) { await libraryLoader.requireLibrary(libraryLoader.KATEX); @@ -112,10 +112,11 @@ async function renderText(note, options, $renderedContent) { } } -async function renderCode(note, options, $renderedContent) { - const blob = await note.getBlob({preview: options.trim}); +/** @param {FNote} note */ +async function renderCode(note, $renderedContent) { + const blob = await note.getBlob(); - $renderedContent.append($("
").text(trim(blob.content, options.trim)));
+    $renderedContent.append($("
").text(blob.content));
 }
 
 function renderImage(entity, $renderedContent, options = {}) {
@@ -285,15 +286,6 @@ async function renderChildrenList($renderedContent, note) {
     }
 }
 
-function trim(text, doTrim) {
-    if (!doTrim) {
-        return text;
-    }
-    else {
-        return text.substr(0, Math.min(text.length, 2000));
-    }
-}
-
 function getRenderingType(entity) {
     let type = entity.type || entity.role;
     const mime = entity.mime;
diff --git a/src/public/app/services/froca.js b/src/public/app/services/froca.js
index 9d45686fa..3b3e4158c 100644
--- a/src/public/app/services/froca.js
+++ b/src/public/app/services/froca.js
@@ -368,12 +368,11 @@ class Froca {
     }
 
     /** @returns {Promise} */
-    async getBlob(entityType, entityId, opts = {}) {
-        opts.preview = !!opts.preview;
-        const key = `${entityType}-${entityId}-${opts.preview}`;
+    async getBlob(entityType, entityId) {
+        const key = `${entityType}-${entityId}`;
 
         if (!this.blobPromises[key]) {
-            this.blobPromises[key] = server.get(`${entityType}/${entityId}/blob?preview=${opts.preview}`)
+            this.blobPromises[key] = server.get(`${entityType}/${entityId}/blob`)
                 .then(row => new FBlob(row))
                 .catch(e => console.error(`Cannot get blob for ${entityType} '${entityId}'`));
 
diff --git a/src/public/app/services/note_tooltip.js b/src/public/app/services/note_tooltip.js
index e8708f273..540d3e3b2 100644
--- a/src/public/app/services/note_tooltip.js
+++ b/src/public/app/services/note_tooltip.js
@@ -8,27 +8,32 @@ import appContext from "../components/app_context.js";
 
 function setupGlobalTooltip() {
     $(document).on("mouseenter", "a", mouseEnterHandler);
-    $(document).on("mouseleave", "a", mouseLeaveHandler);
 
     // close any note tooltip after click, this fixes the problem that sometimes tooltips remained on the screen
-    $(document).on("click", () => $('.note-tooltip').remove());
+    $(document).on("click", e => {
+        if ($(e.target).closest(".note-tooltip").length) {
+            // click within the tooltip shouldn't close it
+            return;
+        }
+
+        $('.note-tooltip').remove();
+    });
 }
 
 function setupElementTooltip($el) {
     $el.on('mouseenter', mouseEnterHandler);
-    $el.on('mouseleave', mouseLeaveHandler);
 }
 
 async function mouseEnterHandler() {
     const $link = $(this);
 
-    if ($link.hasClass("no-tooltip-preview")
-        || $link.hasClass("disabled")) {
+    if ($link.hasClass("no-tooltip-preview") || $link.hasClass("disabled")) {
         return;
-    }
-
-    // this is to avoid showing tooltip from inside the CKEditor link editor dialog
-    if ($link.closest(".ck-link-actions").length) {
+    } else if ($link.closest(".ck-link-actions").length) {
+        // this is to avoid showing tooltip from inside the CKEditor link editor dialog
+        return;
+    } else if ($link.closest(".note-tooltip").length) {
+        // don't show tooltip for links within tooltip
         return;
     }
 
@@ -39,8 +44,21 @@ async function mouseEnterHandler() {
         return;
     }
 
+    const linkId = $link.attr("data-link-id") || `link-${Math.floor(Math.random() * 1000000)}`;
+    $link.attr("data-link-id", linkId);
+
+    if ($(`.${linkId}`).is(":visible")) {
+        // tooltip is already open for this link
+        return;
+    }
+
     const note = await froca.getNote(noteId);
-    const content = await renderTooltip(note);
+
+    const [content] = await Promise.all([
+        renderTooltip(note),
+        // to reduce flicker due to accidental mouseover, cursor must stay for a bit over the link for tooltip to appear
+        new Promise(res => setTimeout(res, 500))
+    ]);
 
     if (utils.isHtmlEmpty(content)) {
         return;
@@ -53,7 +71,6 @@ async function mouseEnterHandler() {
     // we now create tooltip which won't close because it won't receive mouseleave event
     if ($(this).is(":hover")) {
         $(this).tooltip({
-            delay: {"show": 300, "hide": 100},
             container: 'body',
             // https://github.com/zadam/trilium/issues/2794 https://github.com/zadam/trilium/issues/2988
             // with bottom this flickering happens a bit less
@@ -63,15 +80,19 @@ async function mouseEnterHandler() {
             title: html,
             html: true,
             template: '',
-            sanitize: false
+            sanitize: false,
+            customClass: linkId
         });
 
         $(this).tooltip('show');
-    }
-}
 
-function mouseLeaveHandler() {
-    $(this).tooltip('dispose');
+        setTimeout(() => {
+            if (!$(this).is(":hover") && !$(`.${linkId}`).is(":hover")) {
+                // cursor is neither over the link nor over the tooltip, user likely is not interested
+                $(this).tooltip('dispose');
+            }
+        }, 1000);
+    }
 }
 
 async function renderTooltip(note) {
diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css
index aabc5f2b6..63f7863f2 100644
--- a/src/public/stylesheets/style.css
+++ b/src/public/stylesheets/style.css
@@ -471,7 +471,7 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
 .note-tooltip-content {
     /* height needs to stay small because tooltip has problem when it can't fit to either top or bottom of the cursor */
     max-height: 300px;
-    overflow: hidden;
+    overflow: auto;
 }
 
 .note-tooltip-content .note-title-with-path .note-path {
diff --git a/src/routes/api/notes.js b/src/routes/api/notes.js
index 3fb27446b..74f455be5 100644
--- a/src/routes/api/notes.js
+++ b/src/routes/api/notes.js
@@ -15,9 +15,7 @@ function getNote(req) {
 }
 
 function getNoteBlob(req) {
-    const preview = req.query.preview === 'true';
-
-    return blobService.getBlobPojo('notes', req.params.noteId, { preview });
+    return blobService.getBlobPojo('notes', req.params.noteId);
 }
 
 function getNoteMetadata(req) {
diff --git a/src/services/blob.js b/src/services/blob.js
index bef0954ca..6a4f20d1e 100644
--- a/src/services/blob.js
+++ b/src/services/blob.js
@@ -2,9 +2,7 @@ const becca = require('../becca/becca');
 const NotFoundError = require("../errors/not_found_error");
 const protectedSessionService = require("./protected_session");
 
-function getBlobPojo(entityName, entityId, opts = {}) {
-    opts.preview = !!opts.preview;
-
+function getBlobPojo(entityName, entityId) {
     const entity = becca.getEntity(entityName, entityId);
 
     if (!entity) {
@@ -19,10 +17,6 @@ function getBlobPojo(entityName, entityId, opts = {}) {
         pojo.content = null;
     } else {
         pojo.content = processContent(pojo.content, entity.isProtected, true);
-
-        if (opts.preview && pojo.content.length > 10000) {
-            pojo.content = `${pojo.content.substr(0, 10000)}\r\n\r\n... and ${pojo.content.length - 10000} more characters.`;
-        }
     }
 
     return pojo;