mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-11 18:24:43 +02:00
All: Resolves #134: Allow linking to a note from another note
This commit is contained in:
parent
ff1ee1249b
commit
a419bc7253
@ -96,6 +96,16 @@ class NoteListComponent extends React.Component {
|
||||
}
|
||||
}}));
|
||||
|
||||
menu.append(new MenuItem({label: _('Copy Markdown link'), click: async () => {
|
||||
const { clipboard } = require('electron');
|
||||
const links = [];
|
||||
for (let i = 0; i < noteIds.length; i++) {
|
||||
const note = await Note.load(noteIds[i]);
|
||||
links.push(Note.markdownTag(note));
|
||||
}
|
||||
clipboard.writeText(links.join(' '));
|
||||
}}));
|
||||
|
||||
const exportMenu = new Menu();
|
||||
|
||||
const ioService = new InteropService();
|
||||
|
@ -1,5 +1,6 @@
|
||||
const React = require('react');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Search = require('lib/models/Search.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
@ -76,7 +77,6 @@ class NoteTextComponent extends React.Component {
|
||||
mdToHtml() {
|
||||
if (this.mdToHtml_) return this.mdToHtml_;
|
||||
this.mdToHtml_ = new MdToHtml({
|
||||
supportsResourceLinks: true,
|
||||
resourceBaseUrl: 'file://' + Setting.value('resourceDir') + '/',
|
||||
});
|
||||
return this.mdToHtml_;
|
||||
@ -334,11 +334,29 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
menu.popup(bridge().window());
|
||||
} else if (msg.indexOf('joplin://') === 0) {
|
||||
const resourceId = msg.substr('joplin://'.length);
|
||||
Resource.load(resourceId).then((resource) => {
|
||||
const filePath = Resource.fullPath(resource);
|
||||
const itemId = msg.substr('joplin://'.length);
|
||||
const item = await BaseItem.loadItemById(itemId);
|
||||
|
||||
if (!item) throw new Error('No item with ID ' + itemId);
|
||||
|
||||
if (item.type_ === BaseModel.TYPE_RESOURCE) {
|
||||
const filePath = Resource.fullPath(item);
|
||||
bridge().openItem(filePath);
|
||||
});
|
||||
} else if (item.type_ === BaseModel.TYPE_NOTE) {
|
||||
this.props.dispatch({
|
||||
type: "FOLDER_SELECT",
|
||||
id: item.parent_id,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.props.dispatch({
|
||||
type: 'NOTE_SELECT',
|
||||
id: item.id,
|
||||
});
|
||||
}, 10);
|
||||
} else {
|
||||
throw new Error('Unsupported item type: ' + item.type_);
|
||||
}
|
||||
} else {
|
||||
bridge().showErrorMessageBox(_('Unsupported link or message: %s', msg));
|
||||
}
|
||||
|
@ -203,6 +203,14 @@ If for any reason the notifications do not work, please [open an issue](https://
|
||||
|
||||
Joplin uses and renders [Github-flavoured Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) with a few variations and additions. In particular:
|
||||
|
||||
## Links to other notes
|
||||
|
||||
You can create a link to a note by specifying its ID in the URL. For example:
|
||||
|
||||
[Link to my note](:/0b0d62d15e60409dac34f354b6e9e839)
|
||||
|
||||
Since getting the ID of a note is not straightforward, each app provides a way to create such link. In the **desktop app**, right click on a note an select "Copy Markdown link". In the **mobile app**, open a note and, in the top right menu, select "Copy Markdown link". You can then paste this link anywhere in another note.
|
||||
|
||||
## Math notation
|
||||
|
||||
Math expressions can be added using the [Katex notation](https://khan.github.io/KaTeX/). To add an inline equation, wrap the expression in `$EXPRESSION$`, eg. `$\sqrt{3x-1}+(1+x)^2$`. To create an expression block, wrap it as follow:
|
||||
|
@ -14,7 +14,6 @@ class MdToHtml {
|
||||
constructor(options = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
this.supportsResourceLinks_ = !!options.supportsResourceLinks;
|
||||
this.loadedResources_ = {};
|
||||
this.cachedContent_ = null;
|
||||
this.cachedContentKey_ = null;
|
||||
@ -132,40 +131,27 @@ class MdToHtml {
|
||||
const isResourceUrl = Resource.isResourceUrl(href);
|
||||
const title = isResourceUrl ? this.getAttr_(attrs, 'title') : href;
|
||||
|
||||
if (isResourceUrl && !this.supportsResourceLinks_) {
|
||||
// In mobile, links to local resources, such as PDF, etc. currently aren't supported.
|
||||
// Ideally they should be opened in the user's browser.
|
||||
return '<span style="opacity: 0.5">(Resource not yet supported: ';
|
||||
let resourceIdAttr = "";
|
||||
let icon = "";
|
||||
let hrefAttr = '#';
|
||||
if (isResourceUrl) {
|
||||
const resourceId = Resource.pathToId(href);
|
||||
href = "joplin://" + resourceId;
|
||||
resourceIdAttr = "data-resource-id='" + resourceId + "'";
|
||||
icon = '<span class="resource-icon"></span>';
|
||||
} else {
|
||||
let resourceIdAttr = "";
|
||||
let icon = "";
|
||||
let hrefAttr = '#';
|
||||
if (isResourceUrl) {
|
||||
const resourceId = Resource.pathToId(href);
|
||||
href = "joplin://" + resourceId;
|
||||
resourceIdAttr = "data-resource-id='" + resourceId + "'";
|
||||
icon = '<span class="resource-icon"></span>';
|
||||
} else {
|
||||
// If the link is a plain URL (as opposed to a resource link), set the href to the actual
|
||||
// link. This allows the link to be exported too when exporting to PDF.
|
||||
hrefAttr = href;
|
||||
}
|
||||
|
||||
const js = options.postMessageSyntax + "(" + JSON.stringify(href) + "); return false;";
|
||||
let output = "<a " + resourceIdAttr + " title='" + htmlentities(title) + "' href='" + hrefAttr + "' onclick='" + js + "'>" + icon;
|
||||
return output;
|
||||
// If the link is a plain URL (as opposed to a resource link), set the href to the actual
|
||||
// link. This allows the link to be exported too when exporting to PDF.
|
||||
hrefAttr = href;
|
||||
}
|
||||
|
||||
const js = options.postMessageSyntax + "(" + JSON.stringify(href) + "); return false;";
|
||||
let output = "<a " + resourceIdAttr + " title='" + htmlentities(title) + "' href='" + hrefAttr + "' onclick='" + js + "'>" + icon;
|
||||
return output;
|
||||
}
|
||||
|
||||
renderCloseLink_(attrs, options) {
|
||||
const href = this.getAttr_(attrs, 'href');
|
||||
const isResourceUrl = Resource.isResourceUrl(href);
|
||||
|
||||
if (isResourceUrl && !this.supportsResourceLinks_) {
|
||||
return ')</span>';
|
||||
} else {
|
||||
return '</a>';
|
||||
}
|
||||
return '</a>';
|
||||
}
|
||||
|
||||
rendererPlugin_(language) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
const React = require('react'); const Component = React.Component;
|
||||
const { Platform, WebView, View, Linking } = require('react-native');
|
||||
const { Platform, WebView, View } = require('react-native');
|
||||
const { globalStyle } = require('lib/components/global-style.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
@ -19,7 +19,7 @@ class NoteBodyViewer extends Component {
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.mdToHtml_ = new MdToHtml({ supportsResourceLinks: false });
|
||||
this.mdToHtml_ = new MdToHtml();
|
||||
this.isMounted_ = true;
|
||||
}
|
||||
|
||||
@ -115,7 +115,7 @@ class NoteBodyViewer extends Component {
|
||||
//msg = msg.split(':');
|
||||
//this.bodyScrollTop_ = Number(msg[1]);
|
||||
} else {
|
||||
Linking.openURL(msg);
|
||||
this.props.onJoplinLinkClick(msg);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -1,9 +1,10 @@
|
||||
const React = require('react'); const Component = React.Component;
|
||||
const { Platform, Keyboard, BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking, Image } = require('react-native');
|
||||
const { Platform, Clipboard, Keyboard, BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking, Image } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const { uuid } = require('lib/uuid.js');
|
||||
const RNFS = require('react-native-fs');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
@ -22,8 +23,8 @@ const { _ } = require('lib/locale.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||
const { dialogs } = require('lib/dialogs.js');
|
||||
const { globalStyle, themeStyle } = require('lib/components/global-style.js');
|
||||
const { dialogs } = require('lib/dialogs.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
const { NoteBodyViewer } = require('lib/components/note-body-viewer.js');
|
||||
const RNFetchBlob = require('react-native-fetch-blob').default;
|
||||
@ -107,6 +108,39 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.noteTagDialog_closeRequested = () => {
|
||||
this.setState({ noteTagDialogShown: false });
|
||||
}
|
||||
|
||||
this.onJoplinLinkClick_ = async (msg) => {
|
||||
try {
|
||||
if (msg.indexOf('joplin://') === 0) {
|
||||
const itemId = msg.substr('joplin://'.length);
|
||||
const item = await BaseItem.loadItemById(itemId);
|
||||
if (!item) throw new Error(_('No item with ID %s', itemId));
|
||||
|
||||
if (item.type_ === BaseModel.TYPE_NOTE) {
|
||||
// Easier to just go back, then go to the note since
|
||||
// the Note screen doesn't handle reloading a different note
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'NAV_BACK',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Note',
|
||||
noteId: item.id,
|
||||
});
|
||||
}, 5);
|
||||
} else {
|
||||
throw new Error(_('The Joplin mobile app does not currently support this type of link: %s', BaseModel.modelTypeToName(item.type_)));
|
||||
}
|
||||
} else {
|
||||
Linking.openURL(msg);
|
||||
}
|
||||
} catch (error) {
|
||||
dialogs.error(this, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
styles() {
|
||||
@ -402,6 +436,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
}
|
||||
|
||||
copyMarkdownLink_onPress() {
|
||||
const note = this.state.note;
|
||||
Clipboard.setString(Note.markdownTag(note));
|
||||
}
|
||||
|
||||
menuOptions() {
|
||||
const note = this.state.note;
|
||||
const isTodo = note && !!note.is_todo;
|
||||
@ -425,6 +464,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
|
||||
if (isSaved) output.push({ title: _('Tags'), onPress: () => { this.tags_onPress(); } });
|
||||
output.push({ title: isTodo ? _('Convert to note') : _('Convert to todo'), onPress: () => { this.toggleIsTodo_onPress(); } });
|
||||
if (isSaved) output.push({ title: _('Copy Markdown link'), onPress: () => { this.copyMarkdownLink_onPress(); } });
|
||||
output.push({ isDivider: true });
|
||||
if (this.props.showAdvancedOptions) output.push({ title: this.state.showNoteMetadata ? _('Hide metadata') : _('Show metadata'), onPress: () => { this.showMetadata_onPress(); } });
|
||||
output.push({ title: _('View on map'), onPress: () => { this.showOnMap_onPress(); } });
|
||||
@ -466,7 +506,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.saveOneProperty('body', newBody);
|
||||
};
|
||||
|
||||
bodyComponent = <NoteBodyViewer style={this.styles().noteBodyViewer} webViewStyle={theme} note={note} onCheckboxChange={(newBody) => { onCheckboxChange(newBody) }}/>
|
||||
bodyComponent = <NoteBodyViewer onJoplinLinkClick={this.onJoplinLinkClick_} style={this.styles().noteBodyViewer} webViewStyle={theme} note={note} onCheckboxChange={(newBody) => { onCheckboxChange(newBody) }}/>
|
||||
} else {
|
||||
const focusBody = !isNew && !!note.title;
|
||||
|
||||
|
@ -6,6 +6,7 @@ const { time } = require('lib/time-utils.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const moment = require('moment');
|
||||
const { markdownUtils } = require('lib/markdown-utils.js');
|
||||
|
||||
class BaseItem extends BaseModel {
|
||||
|
||||
@ -649,6 +650,15 @@ class BaseItem extends BaseModel {
|
||||
return super.save(o, options);
|
||||
}
|
||||
|
||||
static markdownTag(item) {
|
||||
const output = [];
|
||||
output.push('[');
|
||||
output.push(markdownUtils.escapeLinkText(item.title));
|
||||
output.push(']');
|
||||
output.push('(:/' + item.id + ')');
|
||||
return output.join('');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BaseItem.encryptionService_ = null;
|
||||
|
@ -45,7 +45,7 @@ function createChangeLog(releases) {
|
||||
let s = [];
|
||||
s.push('## ' + r.tag_name + ' - ' + r.published_at);
|
||||
s.push('');
|
||||
let body = r.body.replace(/(#\d+)/g, '[$1](https://github.com/laurent22/joplin/issues/$1)');
|
||||
let body = r.body.replace(/#(\d+)/g, '[$1](https://github.com/laurent22/joplin/issues/$1)');
|
||||
s.push(body);
|
||||
output.push(s.join('\n'));
|
||||
}
|
||||
|
@ -422,6 +422,10 @@
|
||||
<p>If for any reason the notifications do not work, please <a href="https://github.com/laurent22/joplin/issues">open an issue</a>.</p>
|
||||
<h1 id="markdown">Markdown</h1>
|
||||
<p>Joplin uses and renders <a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet">Github-flavoured Markdown</a> with a few variations and additions. In particular:</p>
|
||||
<h2 id="links-to-other-notes">Links to other notes</h2>
|
||||
<p>You can create a link to a note by specifying its ID in the URL. For example:</p>
|
||||
<pre><code>[Link to my note](:/0b0d62d15e60409dac34f354b6e9e839)
|
||||
</code></pre><p>Since getting the ID of a note is not straightforward, each app provides a way to create such link. In the <strong>desktop app</strong>, right click on a note an select "Copy Markdown link". In the <strong>mobile app</strong>, open a note and, in the top right menu, select "Copy Markdown link". You can then paste this link anywhere in another note.</p>
|
||||
<h2 id="math-notation">Math notation</h2>
|
||||
<p>Math expressions can be added using the <a href="https://khan.github.io/KaTeX/">Katex notation</a>. To add an inline equation, wrap the expression in <code>$EXPRESSION$</code>, eg. <code>$\sqrt{3x-1}+(1+x)^2$</code>. To create an expression block, wrap it as follow:</p>
|
||||
<pre><code>$$
|
||||
@ -486,14 +490,14 @@ $$
|
||||
<td><img src="https://joplin.cozic.net/images/flags/country-4x3/hr.png" alt=""></td>
|
||||
<td>Croatian</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/hr_HR.po">hr_HR</a></td>
|
||||
<td>Hrvoje Mandić <a href="mailto:trbuhom@net.hr">trbuhom@net.hr</a></td>
|
||||
<td>Hrvoje Mandić <a href="mailto:trbuhom@net.hr">trbuhom@net.hr</a></td>
|
||||
<td>62%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplin.cozic.net/images/flags/country-4x3/cz.png" alt=""></td>
|
||||
<td>Czech</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/cs_CZ.po">cs_CZ</a></td>
|
||||
<td>Lukas Helebrandt <a href="mailto:lukas@aiya.cz">lukas@aiya.cz</a></td>
|
||||
<td>Lukas Helebrandt <a href="mailto:lukas@aiya.cz">lukas@aiya.cz</a></td>
|
||||
<td>96%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -507,7 +511,7 @@ $$
|
||||
<td><img src="https://joplin.cozic.net/images/flags/country-4x3/de.png" alt=""></td>
|
||||
<td>Deutsch</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po">de_DE</a></td>
|
||||
<td>Tobias Grasse <a href="mailto:mail@tobias-grasse.net">mail@tobias-grasse.net</a></td>
|
||||
<td>Tobias Grasse <a href="mailto:mail@tobias-grasse.net">mail@tobias-grasse.net</a></td>
|
||||
<td>95%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -521,7 +525,7 @@ $$
|
||||
<td><img src="https://joplin.cozic.net/images/flags/country-4x3/es.png" alt=""></td>
|
||||
<td>Español</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po">es_ES</a></td>
|
||||
<td>Fernando Martín <a href="mailto:f@mrtn.es">f@mrtn.es</a></td>
|
||||
<td>Fernando Martín <a href="mailto:f@mrtn.es">f@mrtn.es</a></td>
|
||||
<td>99%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -535,7 +539,7 @@ $$
|
||||
<td><img src="https://joplin.cozic.net/images/flags/country-4x3/es.png" alt=""></td>
|
||||
<td>Galician</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/gl_ES.po">gl_ES</a></td>
|
||||
<td>Marcos Lans <a href="mailto:marcoslansgarza@gmail.com">marcoslansgarza@gmail.com</a></td>
|
||||
<td>Marcos Lans <a href="mailto:marcoslansgarza@gmail.com">marcoslansgarza@gmail.com</a></td>
|
||||
<td>97%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -556,14 +560,14 @@ $$
|
||||
<td><img src="https://joplin.cozic.net/images/flags/country-4x3/br.png" alt=""></td>
|
||||
<td>Português (Brasil)</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/pt_BR.po">pt_BR</a></td>
|
||||
<td>Renato Nunes Bastos <a href="mailto:rnbastos@gmail.com">rnbastos@gmail.com</a></td>
|
||||
<td>Renato Nunes Bastos <a href="mailto:rnbastos@gmail.com">rnbastos@gmail.com</a></td>
|
||||
<td>99%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplin.cozic.net/images/flags/country-4x3/ru.png" alt=""></td>
|
||||
<td>Русский</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po">ru_RU</a></td>
|
||||
<td>Artyom Karlov <a href="mailto:artyom.karlov@gmail.com">artyom.karlov@gmail.com</a></td>
|
||||
<td>Artyom Karlov <a href="mailto:artyom.karlov@gmail.com">artyom.karlov@gmail.com</a></td>
|
||||
<td>95%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
Loading…
Reference in New Issue
Block a user