diff --git a/src/main/app/index.js b/src/main/app/index.js index 3424b6a2..b1e79497 100644 --- a/src/main/app/index.js +++ b/src/main/app/index.js @@ -460,17 +460,30 @@ class App { ipcMain.on('app-create-settings-window', category => { this._openSettingsWindow(category) }) - - ipcMain.on('app-open-file-by-id', (windowId, filePath) => { + ipcMain.on('scroll-to-header-by-name', (windowId, slug) => { + let editor = this._windowManager.get(windowId) + const win = editor.browserWindow + win.webContents.send('mt::scroll-to-header-by-name', slug) + }) + ipcMain.on('app-open-file-by-id', (windowId, filePath, slug) => { const openFilesInNewWindow = this._accessor.preferences.getItem('openFilesInNewWindow') + let editor = null if (openFilesInNewWindow) { this._createEditorWindow(null, [filePath]) } else { - const editor = this._windowManager.get(windowId) + editor = this._windowManager.get(windowId) if (editor) { editor.openTab(filePath, {}, true) } } + if (!slug) { return } + + if (!editor) { editor = this._windowManager.get(windowId) } + if (!editor) { return } + const win = editor.browserWindow + + // probably should pass this further down but at least createEditorWindow isn't setup great for taking params. + setTimeout(() => win.webContents.send('mt::scroll-to-header-by-name', slug), 250) }) ipcMain.on('app-open-files-by-id', (windowId, fileList) => { const openFilesInNewWindow = this._accessor.preferences.getItem('openFilesInNewWindow') diff --git a/src/main/menu/actions/file.js b/src/main/menu/actions/file.js index b743735a..b3f46a86 100644 --- a/src/main/menu/actions/file.js +++ b/src/main/menu/actions/file.js @@ -417,8 +417,25 @@ ipcMain.on('mt::format-link-click', (e, { data, dirname }) => { if (!data || (!data.href && !data.text)) { return } + let parsedUrl = null + let slug = null - const urlCandidate = data.href || data.text + let urlCandidate = data.href || data.text + if (typeof (urlCandidate) === 'string') { + try { + parsedUrl = new URL(urlCandidate, 'b:/fake/') + if (parsedUrl.protocol === 'b:') { // seems like a local file + if (parsedUrl.hash) { + slug = parsedUrl.hash.substring(1) + if (urlCandidate.endsWith(parsedUrl.hash)) { + urlCandidate = urlCandidate.substring(0, urlCandidate.length - parsedUrl.hash.length) + } + } + } + } catch { + + } + } if (URL_REG.test(urlCandidate)) { shell.openExternal(urlCandidate) return @@ -427,8 +444,12 @@ ipcMain.on('mt::format-link-click', (e, { data, dirname }) => { return } - const href = data.href + const href = urlCandidate if (!href) { + if (slug) { + const win = BrowserWindow.fromWebContents(e.sender) + gotoSlug(win, slug) + } return } @@ -443,7 +464,7 @@ ipcMain.on('mt::format-link-click', (e, { data, dirname }) => { pathname = path.normalize(pathname) if (isMarkdownFile(pathname)) { const win = BrowserWindow.fromWebContents(e.sender) - openFileOrFolder(win, pathname) + openFileOrFolder(win, pathname, slug) } else { shell.openPath(pathname) } @@ -533,11 +554,13 @@ export const openFolder = async win => { openFileOrFolder(win, filePaths[0]) } } - -export const openFileOrFolder = (win, pathname) => { +export const gotoSlug = (win, slug) => { + ipcMain.emit('scroll-to-header-by-name', win.id, slug) +} +export const openFileOrFolder = (win, pathname, slug) => { const resolvedPath = normalizeAndResolvePath(pathname) if (isFile(resolvedPath)) { - ipcMain.emit('app-open-file-by-id', win.id, resolvedPath) + ipcMain.emit('app-open-file-by-id', win.id, resolvedPath, slug) } else if (isDirectory(resolvedPath)) { ipcMain.emit('app-open-directory-by-id', win.id, resolvedPath) } else { diff --git a/src/renderer/components/editorWithTabs/editor.vue b/src/renderer/components/editorWithTabs/editor.vue index 0b407039..833e5306 100644 --- a/src/renderer/components/editorWithTabs/editor.vue +++ b/src/renderer/components/editorWithTabs/editor.vue @@ -588,6 +588,7 @@ export default { bus.$on('deleteParagraph', this.handleParagraph) bus.$on('insertParagraph', this.handleInsertParagraph) bus.$on('scroll-to-header', this.scrollToHeader) + bus.$on('scroll-to-header-by-name', this.scrollToHeaderByName) bus.$on('screenshot-captured', this.handleScreenShot) bus.$on('switch-spellchecker-language', this.switchSpellcheckLanguage) bus.$on('open-command-spellchecker-switch-language', this.openSpellcheckerLanguageCommand) @@ -907,6 +908,16 @@ export default { scrollToHeader (slug) { return this.scrollToElement(`#${slug}`) }, + tocNameToSlugName (slug) { + return slug.toLowerCase().replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, '-') + }, + scrollToHeaderByName (slug) { + this.editor.contentState.getTOC().forEach(item => { + if (this.tocNameToSlugName(item.content) === slug) { + this.scrollToHeader(item.slug) + } + }) + }, scrollToElement (selector) { // Scroll to search highlight word @@ -1142,6 +1153,7 @@ export default { bus.$off('deleteParagraph', this.handleParagraph) bus.$off('insertParagraph', this.handleInsertParagraph) bus.$off('scroll-to-header', this.scrollToHeader) + bus.$off('scroll-to-header-by-name', this.scrollToHeaderByName) bus.$off('screenshot-captured', this.handleScreenShot) bus.$off('switch-spellchecker-language', this.switchSpellcheckLanguage) bus.$off('open-command-spellchecker-switch-language', this.openSpellcheckerLanguageCommand) diff --git a/src/renderer/pages/app.vue b/src/renderer/pages/app.vue index cd1784a7..4e4dd220 100644 --- a/src/renderer/pages/app.vue +++ b/src/renderer/pages/app.vue @@ -138,6 +138,7 @@ export default { dispatch('LISTEN_FOR_UPDATE') // module: editor dispatch('LISTEN_SCREEN_SHOT') + dispatch('LISTEN_SCROLL_TO_HEADER') dispatch('ASK_FOR_USER_PREFERENCE') dispatch('LISTEN_TOGGLE_VIEW') dispatch('LISTEN_FOR_CLOSE') diff --git a/src/renderer/store/editor.js b/src/renderer/store/editor.js index 3b1c3ee6..6b7e6ac9 100644 --- a/src/renderer/store/editor.js +++ b/src/renderer/store/editor.js @@ -344,6 +344,11 @@ const actions = { bus.$emit('screenshot-captured') }) }, + LISTEN_SCROLL_TO_HEADER ({ commit }) { + ipcRenderer.on('mt::scroll-to-header-by-name', (e, slug) => { + bus.$emit('scroll-to-header-by-name', slug) + }) + }, // image path auto complement ASK_FOR_IMAGE_AUTO_PATH ({ commit, state }, src) {