1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-24 08:12:24 +02:00

Desktop: WYSIWYG: Handle internal note links

This commit is contained in:
Laurent Cozic 2020-04-10 17:59:51 +00:00
parent 5be98a46e3
commit ae4cecc621
4 changed files with 90 additions and 52 deletions

View File

@ -16,6 +16,7 @@ const { time } = require('lib/time-utils.js');
const markupLanguageUtils = require('lib/markupLanguageUtils');
const HtmlToHtml = require('lib/joplin-renderer/HtmlToHtml');
const Setting = require('lib/models/Setting');
const BaseItem = require('lib/models/BaseItem');
const { MarkupToHtml } = require('lib/joplin-renderer');
const HtmlToMd = require('lib/HtmlToMd');
const { _ } = require('lib/locale');
@ -26,6 +27,7 @@ const { shim } = require('lib/shim');
const TemplateUtils = require('lib/TemplateUtils');
const { bridge } = require('electron').remote.require('./bridge');
const { urlDecode } = require('lib/string-utils');
const urlUtils = require('lib/urlUtils');
const ResourceFetcher = require('lib/services/ResourceFetcher.js');
const DecryptionWorker = require('lib/services/DecryptionWorker.js');
@ -614,7 +616,7 @@ function NoteText2(props:NoteTextProps) {
});
}, [formNote, handleProvisionalFlag]);
const onMessage = useCallback((event:any) => {
const onMessage = useCallback(async (event:any) => {
const msg = event.name;
const args = event.args;
@ -696,36 +698,36 @@ function NoteText2(props:NoteTextProps) {
// }
// 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);
} else if (msg === 'openInternal') {
const resourceUrlInfo = urlUtils.parseResourceUrl(args.url);
const itemId = resourceUrlInfo.itemId;
const item = await BaseItem.loadItemById(itemId);
// if (!item) throw new Error(`No item with ID ${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_}`);
// }
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) {
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') {

View File

@ -8,6 +8,7 @@ const { MarkupToHtml } = require('lib/joplin-renderer');
const taboverride = require('taboverride');
const { reg } = require('lib/registry.js');
const { _ } = require('lib/locale');
const BaseItem = require('lib/models/BaseItem');
const { themeStyle, buildStyle } = require('../../theme.js');
interface TinyMCEProps {
@ -24,6 +25,20 @@ interface TinyMCEProps {
disabled: boolean,
}
function markupRenderOptions(override:any = null) {
return {
plugins: {
checkbox: {
renderingType: 2,
},
link_open: {
linkRenderingType: 2,
},
},
...override,
};
}
function findBlockSource(node:any) {
const sources = node.getElementsByClassName('joplin-source');
if (!sources.length) throw new Error('No source for node');
@ -178,8 +193,16 @@ const TinyMCE = (props:TinyMCEProps, ref:any) => {
if (nodeName === 'A' && (event.ctrlKey || event.metaKey)) {
const href = event.target.getAttribute('href');
const joplinUrl = href.indexOf('joplin://') === 0 ? href : null;
if (href.indexOf('#') === 0) {
if (joplinUrl) {
props.onMessage({
name: 'openInternal',
args: {
url: joplinUrl,
},
});
} else if (href.indexOf('#') === 0) {
const anchorName = href.substr(1);
const anchor = editor.getDoc().getElementById(anchorName);
if (anchor) {
@ -609,16 +632,7 @@ const TinyMCE = (props:TinyMCEProps, ref:any) => {
let cancelled = false;
const loadContent = async () => {
const result = await props.markupToHtml(props.defaultEditorState.markupLanguage, props.defaultEditorState.value, {
plugins: {
checkbox: {
renderingType: 2,
},
link_open: {
linkRenderingType: 2,
},
},
});
const result = await props.markupToHtml(props.defaultEditorState.markupLanguage, props.defaultEditorState.value, markupRenderOptions());
if (cancelled) return;
editor.setContent(result.html);
@ -660,7 +674,7 @@ const TinyMCE = (props:TinyMCEProps, ref:any) => {
let onChangeHandlerIID:any = null;
const onChangeHandler = () => {
function onChangeHandler() {
const changeId = changeId_++;
props.onWillChange({ changeId: changeId });
@ -678,9 +692,9 @@ const TinyMCE = (props:TinyMCEProps, ref:any) => {
dispatchDidUpdate(editor);
}, 1000);
};
}
const onExecCommand = (event:any) => {
function onExecCommand(event:any) {
const c:string = event.command;
if (!c) return;
@ -699,13 +713,13 @@ const TinyMCE = (props:TinyMCEProps, ref:any) => {
if (changeCommands.includes(c) || c.indexOf('Insert') === 0 || c.indexOf('mceToggle') === 0 || c.indexOf('mceInsert') === 0) {
onChangeHandler();
}
};
}
// Keypress means that a printable key (letter, digit, etc.) has been
// pressed so we want to always trigger onChange in this case
const onKeypress = () => {
function onKeypress() {
onChangeHandler();
};
}
// KeyUp is triggered for any keypress, including Control, Shift, etc.
// so most of the time we don't want to trigger onChange. We trigger
@ -715,15 +729,26 @@ const TinyMCE = (props:TinyMCEProps, ref:any) => {
// onChange even though nothing is changed. The alternative would be to
// check the content before and after, but this is too slow, so let's
// keep it this way for now.
const onKeyUp = (event:any) => {
function onKeyUp(event:any) {
if (['Backspace', 'Delete', 'Enter', 'Tab'].includes(event.key)) {
onChangeHandler();
}
};
}
async function onPaste(event:any) {
const pastedText = event.clipboardData.getData('text');
if (BaseItem.isMarkdownTag(pastedText)) {
event.preventDefault();
const result = await markupToHtml.current(MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN, pastedText, markupRenderOptions({ bodyOnly: true }));
editor.insertContent(result.html);
} else {
onChangeHandler();
}
}
editor.on('keyup', onKeyUp);
editor.on('keypress', onKeypress);
editor.on('paste', onChangeHandler);
editor.on('paste', onPaste);
editor.on('cut', onChangeHandler);
editor.on('joplinChange', onChangeHandler);
editor.on('ExecCommand', onExecCommand);
@ -732,7 +757,7 @@ const TinyMCE = (props:TinyMCEProps, ref:any) => {
try {
editor.off('keyup', onKeyUp);
editor.off('keypress', onKeypress);
editor.off('paste', onChangeHandler);
editor.off('paste', onPaste);
editor.off('cut', onChangeHandler);
editor.off('joplinChange', onChangeHandler);
editor.off('ExecCommand', onExecCommand);

View File

@ -5,7 +5,12 @@ const urlUtils = require('../../urlUtils.js');
const { getClassNameForMimeType } = require('font-awesome-filetypes');
function installRule(markdownIt, mdOptions, ruleOptions) {
const pluginOptions = { linkRenderingType: 1, ...ruleOptions.plugins['link_open'] };
const pluginOptions = {
// linkRenderingType = 1 is the regular rendering and clicking on it is handled via embedded JS (in onclick attribute)
// linkRenderingType = 2 gives a plain link with no JS. Caller needs to handle clicking on the link.
linkRenderingType: 1,
...ruleOptions.plugins['link_open'],
};
markdownIt.renderer.rules.link_open = function(tokens, idx) {
const token = tokens[idx];
@ -61,7 +66,7 @@ function installRule(markdownIt, mdOptions, ruleOptions) {
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 || pluginOptions.linkRenderingType === 2) {
return `<a data-from-md ${resourceIdAttr} title='${htmlentities(title)}' href='${hrefAttr}' type='${htmlentities(mime)}'>`;
return `<a data-from-md ${resourceIdAttr} title='${htmlentities(title)}' href='${htmlentities(href)}' type='${htmlentities(mime)}'>`;
} else {
return `<a data-from-md ${resourceIdAttr} title='${htmlentities(title)}' href='${hrefAttr}' onclick='${js}' type='${htmlentities(mime)}'>${icon}`;
}

View File

@ -782,6 +782,12 @@ class BaseItem extends BaseModel {
output.push(`(:/${item.id})`);
return output.join('');
}
static isMarkdownTag(md) {
if (!md) return false;
return !!md.match(/^\[.*?\]\(:\/[0-9a-zA-Z]{32}\)$/);
}
}
BaseItem.encryptionService_ = null;