diff --git a/src/public/app/components/app_context.js b/src/public/app/components/app_context.js index eae71d199..5cf1b35dd 100644 --- a/src/public/app/components/app_context.js +++ b/src/public/app/components/app_context.js @@ -76,6 +76,10 @@ class AppContext extends Component { $("body").append($renderedWidget); $renderedWidget.on('click', "[data-trigger-command]", function() { + if ($(this).hasClass("disabled")) { + return; + } + const commandName = $(this).attr('data-trigger-command'); const $component = $(this).closest(".component"); const component = $component.prop("component"); diff --git a/src/public/app/desktop.js b/src/public/app/desktop.js index 7b0da9e35..eb0ac2c21 100644 --- a/src/public/app/desktop.js +++ b/src/public/app/desktop.js @@ -4,11 +4,9 @@ import noteTooltipService from './services/note_tooltip.js'; import bundleService from "./services/bundle.js"; import noteAutocompleteService from './services/note_autocomplete.js'; import macInit from './services/mac_init.js'; -import contextMenu from "./menus/context_menu.js"; +import electronContextMenu from "./menus/electron_context_menu.js"; import DesktopLayout from "./layouts/desktop_layout.js"; import glob from "./services/glob.js"; -import zoomService from './components/zoom.js'; -import options from "./services/options.js"; bundleService.getWidgetBundlesByParent().then(widgetBundles => { appContext.setLayout(new DesktopLayout(widgetBundles)); @@ -18,9 +16,8 @@ bundleService.getWidgetBundlesByParent().then(widgetBundles => { glob.setupGlobs(); if (utils.isElectron()) { - utils.dynamicRequire('electron').ipcRenderer.on('globalShortcut', async function(event, actionName) { - appContext.triggerCommand(actionName); - }); + utils.dynamicRequire('electron').ipcRenderer.on('globalShortcut', + async (event, actionName) => appContext.triggerCommand(actionName)); } macInit.init(); @@ -30,131 +27,5 @@ noteTooltipService.setupGlobalTooltip(); noteAutocompleteService.init(); if (utils.isElectron()) { - const electron = utils.dynamicRequire('electron'); - - const remote = utils.dynamicRequire('@electron/remote'); - const {webContents} = remote.getCurrentWindow(); - - webContents.on('context-menu', (event, params) => { - const {editFlags} = params; - const hasText = params.selectionText.trim().length > 0; - const isMac = process.platform === "darwin"; - const platformModifier = isMac ? 'Meta' : 'Ctrl'; - - const items = []; - - if (params.misspelledWord) { - for (const suggestion of params.dictionarySuggestions) { - items.push({ - title: suggestion, - command: "replaceMisspelling", - spellingSuggestion: suggestion, - uiIcon: "bx bx-empty" - }); - } - - items.push({ - title: `Add "${params.misspelledWord}" to dictionary`, - uiIcon: "bx bx-plus", - handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord) - }); - - items.push({ title: `----` }); - } - - if (params.isEditable) { - items.push({ - enabled: editFlags.canCut && hasText, - title: `Cut ${platformModifier}+X`, - uiIcon: "bx bx-cut", - handler: () => webContents.cut() - }); - } - - if (params.isEditable || hasText) { - items.push({ - enabled: editFlags.canCopy && hasText, - title: `Copy ${platformModifier}+C`, - uiIcon: "bx bx-copy", - handler: () => webContents.copy() - }); - } - - if (!["", "javascript:", "about:blank#blocked"].includes(params.linkURL) && params.mediaType === 'none') { - items.push({ - title: `Copy link`, - uiIcon: "bx bx-copy", - handler: () => { - electron.clipboard.write({ - bookmark: params.linkText, - text: params.linkURL - }); - } - }); - } - - if (params.isEditable) { - items.push({ - enabled: editFlags.canPaste, - title: `Paste ${platformModifier}+V`, - uiIcon: "bx bx-paste", - handler: () => webContents.paste() - }); - } - - if (params.isEditable) { - items.push({ - enabled: editFlags.canPaste, - title: `Paste as plain text ${platformModifier}+Shift+V`, - uiIcon: "bx bx-paste", - handler: () => webContents.pasteAndMatchStyle() - }); - } - - if (hasText) { - const shortenedSelection = params.selectionText.length > 15 - ? (`${params.selectionText.substr(0, 13)}…`) - : params.selectionText; - - // Read the search engine from the options and fallback to DuckDuckGo if the option is not set. - const customSearchEngineName = options.get("customSearchEngineName"); - const customSearchEngineUrl = options.get("customSearchEngineUrl"); - let searchEngineName; - let searchEngineUrl; - if (customSearchEngineName && customSearchEngineUrl) { - searchEngineName = customSearchEngineName; - searchEngineUrl = customSearchEngineUrl; - } else { - searchEngineName = "Duckduckgo"; - searchEngineUrl = "https://duckduckgo.com/?q={keyword}"; - } - - // Replace the placeholder with the real search keyword. - let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText)); - - items.push({ - enabled: editFlags.canPaste, - title: `Search for "${shortenedSelection}" with ${searchEngineName}`, - uiIcon: "bx bx-search-alt", - handler: () => electron.shell.openExternal(searchUrl) - }); - } - - if (items.length === 0) { - return; - } - - const zoomLevel = zoomService.getCurrentZoom(); - - contextMenu.show({ - x: params.x / zoomLevel, - y: params.y / zoomLevel, - items, - selectMenuItemHandler: ({command, spellingSuggestion}) => { - if (command === 'replaceMisspelling') { - webContents.insertText(spellingSuggestion); - } - } - }); - }); + electronContextMenu.setupContextMenu(); } diff --git a/src/public/app/menus/electron_context_menu.js b/src/public/app/menus/electron_context_menu.js new file mode 100644 index 000000000..6c455020b --- /dev/null +++ b/src/public/app/menus/electron_context_menu.js @@ -0,0 +1,138 @@ +import utils from "../services/utils.js"; +import options from "../services/options.js"; +import zoomService from "../components/zoom.js"; +import contextMenu from "./context_menu.js"; + +function setupContextMenu() { + const electron = utils.dynamicRequire('electron'); + + const remote = utils.dynamicRequire('@electron/remote'); + const {webContents} = remote.getCurrentWindow(); + + webContents.on('context-menu', (event, params) => { + const {editFlags} = params; + const hasText = params.selectionText.trim().length > 0; + const isMac = process.platform === "darwin"; + const platformModifier = isMac ? 'Meta' : 'Ctrl'; + + const items = []; + + if (params.misspelledWord) { + for (const suggestion of params.dictionarySuggestions) { + items.push({ + title: suggestion, + command: "replaceMisspelling", + spellingSuggestion: suggestion, + uiIcon: "bx bx-empty" + }); + } + + items.push({ + title: `Add "${params.misspelledWord}" to dictionary`, + uiIcon: "bx bx-plus", + handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord) + }); + + items.push({ title: `----` }); + } + + if (params.isEditable) { + items.push({ + enabled: editFlags.canCut && hasText, + title: `Cut ${platformModifier}+X`, + uiIcon: "bx bx-cut", + handler: () => webContents.cut() + }); + } + + if (params.isEditable || hasText) { + items.push({ + enabled: editFlags.canCopy && hasText, + title: `Copy ${platformModifier}+C`, + uiIcon: "bx bx-copy", + handler: () => webContents.copy() + }); + } + + if (!["", "javascript:", "about:blank#blocked"].includes(params.linkURL) && params.mediaType === 'none') { + items.push({ + title: `Copy link`, + uiIcon: "bx bx-copy", + handler: () => { + electron.clipboard.write({ + bookmark: params.linkText, + text: params.linkURL + }); + } + }); + } + + if (params.isEditable) { + items.push({ + enabled: editFlags.canPaste, + title: `Paste ${platformModifier}+V`, + uiIcon: "bx bx-paste", + handler: () => webContents.paste() + }); + } + + if (params.isEditable) { + items.push({ + enabled: editFlags.canPaste, + title: `Paste as plain text ${platformModifier}+Shift+V`, + uiIcon: "bx bx-paste", + handler: () => webContents.pasteAndMatchStyle() + }); + } + + if (hasText) { + const shortenedSelection = params.selectionText.length > 15 + ? (`${params.selectionText.substr(0, 13)}…`) + : params.selectionText; + + // Read the search engine from the options and fallback to DuckDuckGo if the option is not set. + const customSearchEngineName = options.get("customSearchEngineName"); + const customSearchEngineUrl = options.get("customSearchEngineUrl"); + let searchEngineName; + let searchEngineUrl; + if (customSearchEngineName && customSearchEngineUrl) { + searchEngineName = customSearchEngineName; + searchEngineUrl = customSearchEngineUrl; + } else { + searchEngineName = "Duckduckgo"; + searchEngineUrl = "https://duckduckgo.com/?q={keyword}"; + } + + // Replace the placeholder with the real search keyword. + let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText)); + + items.push({ + enabled: editFlags.canPaste, + title: `Search for "${shortenedSelection}" with ${searchEngineName}`, + uiIcon: "bx bx-search-alt", + handler: () => electron.shell.openExternal(searchUrl) + }); + } + + if (items.length === 0) { + return; + } + + const zoomLevel = zoomService.getCurrentZoom(); + + contextMenu.show({ + x: params.x / zoomLevel, + y: params.y / zoomLevel, + items, + selectMenuItemHandler: ({command, spellingSuggestion}) => { + if (command === 'replaceMisspelling') { + webContents.insertText(spellingSuggestion); + } + } + }); + }); +} + +export default { + setupContextMenu +}; diff --git a/src/public/app/services/note_tooltip.js b/src/public/app/services/note_tooltip.js index f0cf15651..70f4a1c2e 100644 --- a/src/public/app/services/note_tooltip.js +++ b/src/public/app/services/note_tooltip.js @@ -27,7 +27,7 @@ async function mouseEnterHandler() { return; } - // this is to avoid showing tooltip from inside CKEditor link editor dialog + // this is to avoid showing tooltip from inside the CKEditor link editor dialog if ($link.closest(".ck-link-actions").length) { return; } diff --git a/src/public/app/widgets/attachment_detail.js b/src/public/app/widgets/attachment_detail.js index 4030ba2c6..8f4b9e43d 100644 --- a/src/public/app/widgets/attachment_detail.js +++ b/src/public/app/widgets/attachment_detail.js @@ -67,13 +67,13 @@ const TPL = ` `; export default class AttachmentDetailWidget extends BasicWidget { - constructor(attachment) { + constructor(attachment, isFullDetail) { super(); this.contentSized(); this.attachment = attachment; - this.attachmentActionsWidget = new AttachmentActionsWidget(attachment); - this.isFullDetail = true; + this.attachmentActionsWidget = new AttachmentActionsWidget(attachment, isFullDetail); + this.isFullDetail = isFullDetail; this.child(this.attachmentActionsWidget); } diff --git a/src/public/app/widgets/buttons/attachments_actions.js b/src/public/app/widgets/buttons/attachments_actions.js index 1e8240e78..6a909e0ac 100644 --- a/src/public/app/widgets/buttons/attachments_actions.js +++ b/src/public/app/widgets/buttons/attachments_actions.js @@ -29,10 +29,8 @@ const TPL = ` aria-expanded="false" class="icon-action icon-action-always-border bx bx-dots-vertical-rounded"> `; export default class AttachmentActionsWidget extends BasicWidget { - constructor(attachment) { + constructor(attachment, isFullDetail) { super(); this.attachment = attachment; + this.isFullDetail = isFullDetail; } get attachmentId() { @@ -83,6 +82,17 @@ export default class AttachmentActionsWidget extends BasicWidget { toastService.showError("Upload of a new attachment revision failed."); } }); + + if (!this.isFullDetail) { + // we deactivate this button because the WatchedFileUpdateStatusWidget assumes only one visible attachment + // in a note context, so it doesn't work in a list + const $openAttachmentButton = this.$widget.find("[data-trigger-command='openAttachment']"); + $openAttachmentButton + .addClass("disabled") + .append($(' (?)') + .attr("title", "Opening attachment externally is available only from the detail page, please first click on the attachment detail first and repeat the action.") + ); + } } async openAttachmentCommand() { @@ -101,10 +111,6 @@ export default class AttachmentActionsWidget extends BasicWidget { this.parent.copyAttachmentReferenceToClipboard(); } - async openAttachmentExternallyCommand() { - await openService.openAttachmentExternally(this.attachmentId, this.attachment.mime); - } - async deleteAttachmentCommand() { if (!await dialogService.confirm(`Are you sure you want to delete attachment '${this.attachment.title}'?`)) { return; diff --git a/src/public/app/widgets/type_widgets/attachment_detail.js b/src/public/app/widgets/type_widgets/attachment_detail.js index 449ad9323..b74b2cfa8 100644 --- a/src/public/app/widgets/type_widgets/attachment_detail.js +++ b/src/public/app/widgets/type_widgets/attachment_detail.js @@ -37,8 +37,7 @@ export default class AttachmentDetailTypeWidget extends TypeWidget { return; } - const attachmentDetailWidget = new AttachmentDetailWidget(attachment); - attachmentDetailWidget.isFullDetail = true; + const attachmentDetailWidget = new AttachmentDetailWidget(attachment, true); this.child(attachmentDetailWidget); this.$wrapper.append(attachmentDetailWidget.render()); diff --git a/src/public/app/widgets/type_widgets/attachment_list.js b/src/public/app/widgets/type_widgets/attachment_list.js index e4876e995..1604ea313 100644 --- a/src/public/app/widgets/type_widgets/attachment_list.js +++ b/src/public/app/widgets/type_widgets/attachment_list.js @@ -39,8 +39,7 @@ export default class AttachmentListTypeWidget extends TypeWidget { } for (const attachment of attachments) { - const attachmentDetailWidget = new AttachmentDetailWidget(attachment); - attachmentDetailWidget.isFullDetail = false; + const attachmentDetailWidget = new AttachmentDetailWidget(attachment, false); this.child(attachmentDetailWidget); diff --git a/src/public/app/widgets/watched_file_update_status.js b/src/public/app/widgets/watched_file_update_status.js index 7e2818ac4..05e5567e7 100644 --- a/src/public/app/widgets/watched_file_update_status.js +++ b/src/public/app/widgets/watched_file_update_status.js @@ -12,7 +12,7 @@ const TPL = `

File has been last modified on .

- +
diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css index 02c3edc1a..f0d90056b 100644 --- a/src/public/stylesheets/style.css +++ b/src/public/stylesheets/style.css @@ -195,6 +195,12 @@ div.ui-tooltip { pointer-events: none; } +.dropdown-menu .disabled .disabled-tooltip { + pointer-events: all; + color: var(--menu-text-color); + cursor: help; +} + .dropdown-menu a:hover:not(.disabled), .dropdown-item:hover:not(.disabled) { color: var(--hover-item-text-color) !important; background-color: var(--hover-item-background-color) !important;