mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Desktop: Resolves #1490: Add support for anchor hashes in note links
This commit is contained in:
parent
fa83107840
commit
7aea2cec69
@ -908,6 +908,9 @@ class Application extends BaseApplication {
|
||||
label: _('Website and documentation'),
|
||||
accelerator: 'F1',
|
||||
click () { bridge().openExternal('https://joplinapp.org'); },
|
||||
}, {
|
||||
label: _('Joplin Forum'),
|
||||
click () { bridge().openExternal('https://discourse.joplinapp.org'); },
|
||||
}, {
|
||||
label: _('Make a donation'),
|
||||
click () { bridge().openExternal('https://joplinapp.org/donate/'); },
|
||||
|
@ -574,8 +574,14 @@ class NoteTextComponent extends React.Component {
|
||||
this.editor_.editor.moveCursorTo(0, 0);
|
||||
|
||||
setTimeout(() => {
|
||||
this.setEditorPercentScroll(scrollPercent ? scrollPercent : 0);
|
||||
this.setViewerPercentScroll(scrollPercent ? scrollPercent : 0);
|
||||
// If we have an anchor hash, jump to that anchor
|
||||
if (this.props.selectedNoteHash) {
|
||||
this.webviewRef_.current.wrappedInstance.send('scrollToHash', this.props.selectedNoteHash);
|
||||
} else {
|
||||
// Otherwise restore the normal scroll position
|
||||
this.setEditorPercentScroll(scrollPercent ? scrollPercent : 0);
|
||||
this.setViewerPercentScroll(scrollPercent ? scrollPercent : 0);
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
@ -797,7 +803,8 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
menu.popup(bridge().window());
|
||||
} else if (msg.indexOf('joplin://') === 0) {
|
||||
const itemId = msg.substr('joplin://'.length);
|
||||
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);
|
||||
@ -815,6 +822,7 @@ class NoteTextComponent extends React.Component {
|
||||
type: 'FOLDER_AND_NOTE_SELECT',
|
||||
folderId: item.parent_id,
|
||||
noteId: item.id,
|
||||
hash: resourceUrlInfo.hash,
|
||||
historyNoteAction: {
|
||||
id: this.state.note.id,
|
||||
parent_id: this.state.note.parent_id,
|
||||
@ -2055,6 +2063,7 @@ const mapStateToProps = state => {
|
||||
noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
|
||||
notes: state.notes,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
selectedNoteHash: state.selectedNoteHash,
|
||||
noteTags: state.selectedNoteTags,
|
||||
folderId: state.selectedFolderId,
|
||||
itemType: state.selectedItemType,
|
||||
|
@ -105,6 +105,28 @@
|
||||
setPercentScroll(percentScroll_);
|
||||
}
|
||||
|
||||
ipc.scrollToHash = (event) => {
|
||||
if (window.scrollToHashIID_) clearInterval(window.scrollToHashIID_);
|
||||
window.scrollToHashIID_ = setInterval(() => {
|
||||
if (document.readyState !== 'complete') return;
|
||||
clearInterval(window.scrollToHashIID_);
|
||||
const hash = event.hash.toLowerCase();
|
||||
const e = document.getElementById(hash);
|
||||
if (!e) {
|
||||
console.warn('Cannot find hash', hash);
|
||||
return;
|
||||
}
|
||||
e.scrollIntoView();
|
||||
|
||||
// Make sure the editor pane is also scrolled
|
||||
setTimeout(() => {
|
||||
const percent = currentPercentScroll();
|
||||
setPercentScroll(percent);
|
||||
ipcProxySendToHost('percentScroll', percent);
|
||||
}, 10);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
ipc.setHtml = (event) => {
|
||||
const html = event.html;
|
||||
|
||||
@ -265,13 +287,17 @@
|
||||
document.getElementById('content').style.height = window.innerHeight + 'px';
|
||||
}
|
||||
|
||||
function currentPercentScroll() {
|
||||
const m = maxScrollTop();
|
||||
return m ? contentElement.scrollTop / m : 0;
|
||||
}
|
||||
|
||||
contentElement.addEventListener('scroll', webviewLib.logEnabledEventHandler(e => {
|
||||
if (ignoreNextScrollEvent) {
|
||||
ignoreNextScrollEvent = false;
|
||||
return;
|
||||
}
|
||||
const m = maxScrollTop();
|
||||
const percent = m ? contentElement.scrollTop / m : 0;
|
||||
const percent = currentPercentScroll();
|
||||
setPercentScroll(percent);
|
||||
|
||||
ipcProxySendToHost('percentScroll', percent);
|
||||
|
@ -9,6 +9,10 @@ ipcRenderer.on('setHtml', (event, html, options) => {
|
||||
window.postMessage({ target: 'webview', name: 'setHtml', data: { html: html, options: options } }, '*');
|
||||
});
|
||||
|
||||
ipcRenderer.on('scrollToHash', (event, hash) => {
|
||||
window.postMessage({ target: 'webview', name: 'scrollToHash', data: { hash: hash } }, '*');
|
||||
});
|
||||
|
||||
ipcRenderer.on('setPercentScroll', (event, percent) => {
|
||||
window.postMessage({ target: 'webview', name: 'setPercentScroll', data: { percent: percent } }, '*');
|
||||
});
|
||||
|
@ -196,7 +196,7 @@ class BaseApplication {
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
async refreshNotes(state, useSelectedNoteId = false) {
|
||||
async refreshNotes(state, useSelectedNoteId = false, noteHash = '') {
|
||||
let parentType = state.notesParentType;
|
||||
let parentId = null;
|
||||
|
||||
@ -248,6 +248,7 @@ class BaseApplication {
|
||||
this.store().dispatch({
|
||||
type: 'NOTE_SELECT',
|
||||
id: state.selectedNoteIds && state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
|
||||
hash: noteHash,
|
||||
});
|
||||
} else {
|
||||
const lastSelectedNoteIds = stateUtils.lastSelectedNoteIds(state);
|
||||
@ -388,6 +389,7 @@ class BaseApplication {
|
||||
let refreshFolders = false;
|
||||
// let refreshTags = false;
|
||||
let refreshNotesUseSelectedNoteId = false;
|
||||
let refreshNotesHash = '';
|
||||
|
||||
await reduxSharedMiddleware(store, next, action);
|
||||
|
||||
@ -407,7 +409,10 @@ class BaseApplication {
|
||||
this.currentFolder_ = newState.selectedFolderId ? await Folder.load(newState.selectedFolderId) : null;
|
||||
refreshNotes = true;
|
||||
|
||||
if (action.type === 'FOLDER_AND_NOTE_SELECT') refreshNotesUseSelectedNoteId = true;
|
||||
if (action.type === 'FOLDER_AND_NOTE_SELECT') {
|
||||
refreshNotesUseSelectedNoteId = true;
|
||||
refreshNotesHash = action.hash;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key == 'uncompletedTodosOnTop') || action.type == 'SETTING_UPDATE_ALL')) {
|
||||
@ -431,7 +436,7 @@ class BaseApplication {
|
||||
}
|
||||
|
||||
if (refreshNotes) {
|
||||
await this.refreshNotes(newState, refreshNotesUseSelectedNoteId);
|
||||
await this.refreshNotes(newState, refreshNotesUseSelectedNoteId, refreshNotesHash);
|
||||
}
|
||||
|
||||
if (action.type === 'NOTE_UPDATE_ONE') {
|
||||
|
@ -114,6 +114,20 @@ class NoteBodyViewer extends Component {
|
||||
if (document.readyState === "complete") {
|
||||
clearInterval(readyStateCheckInterval);
|
||||
if ("${resourceDownloadMode}" === "manual") webviewLib.setupResourceManualDownload();
|
||||
|
||||
const hash = "${this.props.noteHash}";
|
||||
// Gives it a bit of time before scrolling to the anchor
|
||||
// so that images are loaded.
|
||||
if (hash) {
|
||||
setTimeout(() => {
|
||||
const e = document.getElementById(hash);
|
||||
if (!e) {
|
||||
console.warn('Cannot find hash', hash);
|
||||
return;
|
||||
}
|
||||
e.scrollIntoView();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
}, 10);
|
||||
`);
|
||||
|
@ -36,6 +36,7 @@ const { SelectDateTimeDialog } = require('lib/components/select-date-time-dialog
|
||||
const ShareExtension = require('react-native-share-extension').default;
|
||||
const CameraView = require('lib/components/CameraView');
|
||||
const SearchEngine = require('lib/services/SearchEngine');
|
||||
const urlUtils = require('lib/urlUtils');
|
||||
|
||||
import FileViewer from 'react-native-file-viewer';
|
||||
|
||||
@ -123,7 +124,8 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.onJoplinLinkClick_ = async msg => {
|
||||
try {
|
||||
if (msg.indexOf('joplin://') === 0) {
|
||||
const itemId = msg.substr('joplin://'.length);
|
||||
const resourceUrlInfo = urlUtils.parseResourceUrl(msg);
|
||||
const itemId = resourceUrlInfo.itemId;
|
||||
const item = await BaseItem.loadItemById(itemId);
|
||||
if (!item) throw new Error(_('No item with ID %s', itemId));
|
||||
|
||||
@ -140,6 +142,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Note',
|
||||
noteId: item.id,
|
||||
noteHash: resourceUrlInfo.hash,
|
||||
});
|
||||
}, 5);
|
||||
} else if (item.type_ === BaseModel.TYPE_RESOURCE) {
|
||||
@ -823,6 +826,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
noteResources={this.state.noteResources}
|
||||
highlightedKeywords={keywords}
|
||||
theme={this.props.theme}
|
||||
noteHash={this.props.noteHash}
|
||||
onCheckboxChange={newBody => {
|
||||
onCheckboxChange(newBody);
|
||||
}}
|
||||
@ -906,6 +910,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
const NoteScreen = connect(state => {
|
||||
return {
|
||||
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
|
||||
noteHash: state.selectedNoteHash,
|
||||
folderId: state.selectedFolderId,
|
||||
itemType: state.selectedItemType,
|
||||
folders: state.folders,
|
||||
|
@ -12,6 +12,7 @@ const defaultState = {
|
||||
notLoadedMasterKeys: [],
|
||||
searches: [],
|
||||
selectedNoteIds: [],
|
||||
selectedNoteHash: '',
|
||||
selectedFolderId: null,
|
||||
selectedTagId: null,
|
||||
selectedSearchId: null,
|
||||
@ -267,6 +268,7 @@ function changeSelectedNotes(state, action, options = null) {
|
||||
if (JSON.stringify(newState.selectedNoteIds) === JSON.stringify(noteIds)) return state;
|
||||
newState.selectedNoteIds = noteIds;
|
||||
newState.newNote = null;
|
||||
newState.selectedNoteHash = action.hash ? action.hash : '';
|
||||
} else if (action.type === 'NOTE_SELECT_ADD') {
|
||||
if (!noteIds.length) return state;
|
||||
newState.selectedNoteIds = ArrayUtils.unique(newState.selectedNoteIds.concat(noteIds));
|
||||
|
@ -1,20 +1,21 @@
|
||||
const Entities = require('html-entities').AllHtmlEntities;
|
||||
const htmlentities = new Entities().encode;
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const utils = require('../../utils');
|
||||
const urlUtils = require('lib/urlUtils.js');
|
||||
|
||||
function installRule(markdownIt, mdOptions, ruleOptions) {
|
||||
markdownIt.renderer.rules.link_open = function(tokens, idx, options, env, self) {
|
||||
const token = tokens[idx];
|
||||
let href = utils.getAttr(token.attrs, 'href');
|
||||
const isResourceUrl = Resource.isResourceUrl(href);
|
||||
const resourceHrefInfo = urlUtils.parseResourceUrl(href);
|
||||
const isResourceUrl = !!resourceHrefInfo.itemId;
|
||||
const title = isResourceUrl ? utils.getAttr(token.attrs, 'title') : href;
|
||||
|
||||
let resourceIdAttr = '';
|
||||
let icon = '';
|
||||
let hrefAttr = '#';
|
||||
if (isResourceUrl) {
|
||||
const resourceId = Resource.pathToId(href);
|
||||
const resourceId = resourceHrefInfo.itemId;
|
||||
|
||||
const result = ruleOptions.resources[resourceId];
|
||||
const resourceStatus = utils.resourceStatus(result);
|
||||
@ -24,6 +25,7 @@ function installRule(markdownIt, mdOptions, ruleOptions) {
|
||||
return '<a class="not-loaded-resource resource-status-' + resourceStatus + '" data-resource-id="' + resourceId + '">' + '<img src="data:image/svg+xml;utf8,' + htmlentities(icon) + '"/>';
|
||||
} else {
|
||||
href = 'joplin://' + resourceId;
|
||||
if (resourceHrefInfo.hash) href += '#' + resourceHrefInfo.hash;
|
||||
resourceIdAttr = 'data-resource-id=\'' + resourceId + '\'';
|
||||
icon = '<span class="resource-icon"></span>';
|
||||
}
|
||||
|
@ -39,4 +39,19 @@ urlUtils.prependBaseUrl = function(url, baseUrl) {
|
||||
}
|
||||
};
|
||||
|
||||
urlUtils.parseResourceUrl = function(url) {
|
||||
const filename = url.split('/').pop();
|
||||
const splitted = filename.split('#');
|
||||
|
||||
const output = {
|
||||
itemId: '',
|
||||
hash: '',
|
||||
};
|
||||
|
||||
if (splitted.length) output.itemId = splitted[0];
|
||||
if (splitted.length >= 2) output.hash = splitted[1];
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
module.exports = urlUtils;
|
||||
|
@ -236,6 +236,8 @@ const appReducer = (state = appDefaultState, action) => {
|
||||
|
||||
newState = Object.assign({}, state);
|
||||
|
||||
newState.selectedNoteHash = '';
|
||||
|
||||
if ('noteId' in action) {
|
||||
newState.selectedNoteIds = action.noteId ? [action.noteId] : [];
|
||||
}
|
||||
@ -259,6 +261,10 @@ const appReducer = (state = appDefaultState, action) => {
|
||||
newState.selectedItemType = action.itemType;
|
||||
}
|
||||
|
||||
if ('noteHash' in action) {
|
||||
newState.selectedNoteHash = action.noteHash;
|
||||
}
|
||||
|
||||
if ('sharedData' in action) {
|
||||
newState.sharedData = action.sharedData;
|
||||
} else {
|
||||
|
Loading…
Reference in New Issue
Block a user