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:
parent
5be98a46e3
commit
ae4cecc621
@ -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') {
|
||||
|
@ -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);
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user