From 11d8466db18f74e0e726e5b217371cfcb5840e5e Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Fri, 27 Mar 2020 18:26:52 +0000 Subject: [PATCH] Desktop: WYSIWYG: Getting links to work --- ElectronClient/gui/NoteText2.tsx | 128 ++++++++++++++++++ ElectronClient/gui/editors/TinyMCE.tsx | 35 ++++- .../MdToHtml/rules/link_open.js | 4 +- .../lib/joplin-renderer/index.js | 1 + .../lib/joplin-renderer/noteStyle.js | 10 +- joplin.sublime-project | 3 +- 6 files changed, 174 insertions(+), 7 deletions(-) diff --git a/ElectronClient/gui/NoteText2.tsx b/ElectronClient/gui/NoteText2.tsx index f71b27771..9d292c7c7 100644 --- a/ElectronClient/gui/NoteText2.tsx +++ b/ElectronClient/gui/NoteText2.tsx @@ -24,6 +24,7 @@ const Resource = require('lib/models/Resource.js'); const { shim } = require('lib/shim'); const TemplateUtils = require('lib/TemplateUtils'); const { bridge } = require('electron').remote.require('./bridge'); +const { urlDecode } = require('lib/string-utils'); interface NoteTextProps { style: any, @@ -521,6 +522,132 @@ function NoteText2(props:NoteTextProps) { }); }, [formNote, handleProvisionalFlag]); + const onMessage = useCallback((event:any) => { + const msg = event.name; + const args = event.args; + + console.info('onMessage', msg, args); + + if (msg === 'setMarkerCount') { + // const ls = Object.assign({}, this.state.localSearch); + // ls.resultCount = arg0; + // ls.searching = false; + // this.setState({ localSearch: ls }); + } else if (msg.indexOf('markForDownload:') === 0) { + // const s = msg.split(':'); + // if (s.length < 2) throw new Error(`Invalid message: ${msg}`); + // ResourceFetcher.instance().markForDownload(s[1]); + } else if (msg === 'percentScroll') { + // this.ignoreNextEditorScroll_ = true; + // this.setEditorPercentScroll(arg0); + } else if (msg === 'contextMenu') { + // const itemType = arg0 && arg0.type; + + // const menu = new Menu(); + + // if (itemType === 'image' || itemType === 'resource') { + // const resource = await Resource.load(arg0.resourceId); + // const resourcePath = Resource.fullPath(resource); + + // menu.append( + // new MenuItem({ + // label: _('Open...'), + // click: async () => { + // const ok = bridge().openExternal(`file://${resourcePath}`); + // if (!ok) bridge().showErrorMessageBox(_('This file could not be opened: %s', resourcePath)); + // }, + // }) + // ); + + // menu.append( + // new MenuItem({ + // label: _('Save as...'), + // click: async () => { + // const filePath = bridge().showSaveDialog({ + // defaultPath: resource.filename ? resource.filename : resource.title, + // }); + // if (!filePath) return; + // await fs.copy(resourcePath, filePath); + // }, + // }) + // ); + + // menu.append( + // new MenuItem({ + // label: _('Copy path to clipboard'), + // click: async () => { + // clipboard.writeText(toSystemSlashes(resourcePath)); + // }, + // }) + // ); + // } else if (itemType === 'text') { + // menu.append( + // new MenuItem({ + // label: _('Copy'), + // click: async () => { + // clipboard.writeText(arg0.textToCopy); + // }, + // }) + // ); + // } else if (itemType === 'link') { + // menu.append( + // new MenuItem({ + // label: _('Copy Link Address'), + // click: async () => { + // clipboard.writeText(arg0.textToCopy); + // }, + // }) + // ); + // } else { + // reg.logger().error(`Unhandled item type: ${itemType}`); + // return; + // } + + // menu.popup(bridge().window()); + } else if (msg.indexOf('joplin://') === 0) { + // const resourceUrlInfo = urlUtils.parseResourceUrl(msg); + // const itemId = resourceUrlInfo.itemId; + // const item = await BaseItem.loadItemById(itemId); + + // if (!item) throw new Error(`No item with ID ${itemId}`); + + // if (item.type_ === BaseModel.TYPE_RESOURCE) { + // const localState = await Resource.localState(item); + // if (localState.fetch_status !== Resource.FETCH_STATUS_DONE || !!item.encryption_blob_encrypted) { + // if (localState.fetch_status === Resource.FETCH_STATUS_ERROR) { + // bridge().showErrorMessageBox(`${_('There was an error downloading this attachment:')}\n\n${localState.fetch_error}`); + // } else { + // bridge().showErrorMessageBox(_('This attachment is not downloaded or not decrypted yet')); + // } + // return; + // } + // const filePath = Resource.fullPath(item); + // bridge().openItem(filePath); + // } else if (item.type_ === BaseModel.TYPE_NOTE) { + // this.props.dispatch({ + // type: 'FOLDER_AND_NOTE_SELECT', + // folderId: item.parent_id, + // noteId: item.id, + // hash: resourceUrlInfo.hash, + // historyAction: 'goto', + // }); + // } else { + // throw new Error(`Unsupported item type: ${item.type_}`); + // } + } else if (msg.indexOf('#') === 0) { + // This is an internal anchor, which is handled by the WebView so skip this case + } else if (msg === 'openExternal') { + if (args.url.indexOf('file://') === 0) { + // When using the file:// protocol, openExternal doesn't work (does nothing) with URL-encoded paths + bridge().openExternal(urlDecode(args.url)); + } else { + bridge().openExternal(args.url); + } + } else { + bridge().showErrorMessageBox(_('Unsupported link or message: %s', msg)); + } + }, []); + const introductionPostLinkClick = useCallback(() => { bridge().openExternal('https://www.patreon.com/posts/34246624'); }, []); @@ -541,6 +668,7 @@ function NoteText2(props:NoteTextProps) { style: styles.tinyMCE, onChange: onBodyChange, onWillChange: onBodyWillChange, + onMessage: onMessage, defaultEditorState: defaultEditorState, markupToHtml: markupToHtml, allAssets: allAssets, diff --git a/ElectronClient/gui/editors/TinyMCE.tsx b/ElectronClient/gui/editors/TinyMCE.tsx index 7299769db..6225542a5 100644 --- a/ElectronClient/gui/editors/TinyMCE.tsx +++ b/ElectronClient/gui/editors/TinyMCE.tsx @@ -12,6 +12,7 @@ interface TinyMCEProps { style: any, onChange(event: OnChangeEvent): void, onWillChange(event:any): void, + onMessage(event:any): void, defaultEditorState: DefaultEditorState, markupToHtml: Function, allAssets: Function, @@ -122,11 +123,34 @@ const TinyMCE = (props:TinyMCEProps, ref:any) => { }; const onEditorContentClick = useCallback((event:any) => { - if (event.target && event.target.nodeName === 'INPUT' && event.target.getAttribute('type') === 'checkbox') { + const nodeName = event.target ? event.target.nodeName : ''; + + if (nodeName === 'INPUT' && event.target.getAttribute('type') === 'checkbox') { editor.fire('joplinChange'); dispatchDidUpdate(editor); } - }, [editor]); + + if (nodeName === 'A' && event.ctrlKey) { + const href = event.target.getAttribute('href'); + + if (href.indexOf('#') === 0) { + const anchorName = href.substr(1); + const anchor = editor.getDoc().getElementById(anchorName); + if (anchor) { + anchor.scrollIntoView(); + } else { + reg.logger().warn('TinyMce: could not find anchor with ID ', anchorName); + } + } else { + props.onMessage({ + name: 'openExternal', + args: { + url: href, + }, + }); + } + } + }, [editor, props.onMessage]); useImperativeHandle(ref, () => { return { @@ -368,9 +392,9 @@ const TinyMCE = (props:TinyMCEProps, ref:any) => { .filter((a:any) => a.mime === 'text/css' && !loadedAssetFiles_.includes(a.path)) .map((a:any) => a.path)); - const jsFiles = pluginAssets + const jsFiles = ['gui/editors/TinyMCE/content_script.js'].concat(pluginAssets .filter((a:any) => a.mime === 'application/javascript' && !loadedAssetFiles_.includes(a.path)) - .map((a:any) => a.path); + .map((a:any) => a.path)); for (const cssFile of cssFiles) loadedAssetFiles_.push(cssFile); for (const jsFile of jsFiles) loadedAssetFiles_.push(jsFile); @@ -405,6 +429,9 @@ const TinyMCE = (props:TinyMCEProps, ref:any) => { checkbox: { renderingType: 2, }, + link_open: { + linkRenderingType: 2, + }, }, }); if (cancelled) return; diff --git a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/link_open.js b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/link_open.js index ac9eafaef..181c8132c 100644 --- a/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/link_open.js +++ b/ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/link_open.js @@ -5,6 +5,8 @@ const urlUtils = require('../../urlUtils.js'); const { getClassNameForMimeType } = require('font-awesome-filetypes'); function installRule(markdownIt, mdOptions, ruleOptions) { + const pluginOptions = { linkRenderingType: 1, ...ruleOptions.plugins['link_open'] }; + markdownIt.renderer.rules.link_open = function(tokens, idx) { const token = tokens[idx]; let href = utils.getAttr(token.attrs, 'href'); @@ -58,7 +60,7 @@ function installRule(markdownIt, mdOptions, ruleOptions) { let js = `${ruleOptions.postMessageSyntax}(${JSON.stringify(href)}); return false;`; if (hrefAttr.indexOf('#') === 0 && href.indexOf('#') === 0) js = ''; // If it's an internal anchor, don't add any JS since the webview is going to handle navigating to the right place - if (ruleOptions.plainResourceRendering) { + if (ruleOptions.plainResourceRendering || pluginOptions.linkRenderingType === 2) { return ``; } else { return `${icon}`; diff --git a/ReactNativeClient/lib/joplin-renderer/index.js b/ReactNativeClient/lib/joplin-renderer/index.js index 036a2a5b9..c858d7eae 100644 --- a/ReactNativeClient/lib/joplin-renderer/index.js +++ b/ReactNativeClient/lib/joplin-renderer/index.js @@ -4,4 +4,5 @@ module.exports = { HtmlToHtml: require('./HtmlToHtml'), setupLinkify: require('./MdToHtml/setupLinkify'), assetsToHeaders: require('./assetsToHeaders'), + utils: require('./utils'), }; diff --git a/ReactNativeClient/lib/joplin-renderer/noteStyle.js b/ReactNativeClient/lib/joplin-renderer/noteStyle.js index ff4ba118b..e20f2ec3a 100644 --- a/ReactNativeClient/lib/joplin-renderer/noteStyle.js +++ b/ReactNativeClient/lib/joplin-renderer/noteStyle.js @@ -271,9 +271,13 @@ module.exports = function(theme) { display: none; } + /* =============================================== */ /* For TinyMCE */ + /* =============================================== */ + .mce-content-body { - padding: 5px 10px 10px 10px; + /* Note: we give a bit more padding at the bottom, to allow scrolling past the end of the document */ + padding: 5px 10px 10em 10px; } .mce-content-body code { @@ -292,6 +296,10 @@ module.exports = function(theme) { opacity: 0.5; } + /* =============================================== */ + /* For TinyMCE */ + /* =============================================== */ + @media print { body { height: auto !important; diff --git a/joplin.sublime-project b/joplin.sublime-project index bb32045c8..00460caa3 100644 --- a/joplin.sublime-project +++ b/joplin.sublime-project @@ -96,7 +96,8 @@ "ElectronClient/pluginAssets", "Modules/TinyMCE/JoplinLists/dist", "Modules/TinyMCE/JoplinLists/lib", - "Modules/TinyMCE/JoplinLists/scratch" + "Modules/TinyMCE/JoplinLists/scratch", + "CliClient/tests/tmp" ], "path": "." }