From c0c8ea4b15e32367e9b5d13a5ce8757062c9d851 Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Sun, 24 Mar 2019 20:20:44 +0800 Subject: [PATCH] feat: open external link and local markdown file (#790) * feat: open external link and local markdown file * image view * browse image by CmdOrCtrl + click * use esc to close image viewer * change image viewer z-index to prevent math render show on top of it * support windows and linux --- package.json | 1 + src/main/actions/file.js | 21 ++++- src/main/config.js | 2 + src/muya/lib/config/index.js | 2 + src/muya/lib/contentState/clickCtrl.js | 53 +++++++++++ src/muya/lib/eventHandler/clickEvent.js | 7 +- .../lib/parser/render/renderInlines/emoji.js | 2 +- .../parser/render/renderInlines/inlineMath.js | 2 +- src/muya/lib/utils/index.js | 4 +- src/renderer/app.vue | 1 + src/renderer/assets/icons/close.svg | 1 + .../components/editorWithTabs/editor.vue | 89 ++++++++++++++++++- .../components/editorWithTabs/index.vue | 5 ++ src/renderer/store/editor.js | 3 + yarn.lock | 5 ++ 15 files changed, 189 insertions(+), 9 deletions(-) create mode 100644 src/renderer/assets/icons/close.svg diff --git a/package.json b/package.json index e20c0846..7b165b08 100644 --- a/package.json +++ b/package.json @@ -170,6 +170,7 @@ "vega": "^5.2.0", "vega-embed": "^4.0.0-rc1", "vega-lite": "^3.0.0-rc15", + "view-image": "^0.0.1", "vue": "^2.6.8", "vue-electron": "^1.0.6", "vuex": "^3.1.0" diff --git a/src/main/actions/file.js b/src/main/actions/file.js index e83acf21..a07ebad1 100644 --- a/src/main/actions/file.js +++ b/src/main/actions/file.js @@ -2,9 +2,9 @@ import fs from 'fs' // import chokidar from 'chokidar' import path from 'path' import { promisify } from 'util' -import { BrowserWindow, dialog, ipcMain } from 'electron' +import { BrowserWindow, dialog, ipcMain, shell } from 'electron' import appWindow from '../window' -import { EXTENSION_HASN, EXTENSIONS, PANDOC_EXTENSIONS } from '../config' +import { EXTENSION_HASN, EXTENSIONS, PANDOC_EXTENSIONS, URL_REG } from '../config' import { loadMarkdownFile, writeFile, writeMarkdownFile } from '../utils/filesystem' import appMenu from '../menu' import { getPath, isMarkdownFile, log, isFile, isDirectory, getRecommendTitle } from '../utils' @@ -282,6 +282,23 @@ ipcMain.on('AGANI::ask-for-open-project-in-sidebar', e => { } }) +ipcMain.on('AGANI::format-link-click', (e, { data, dirname }) => { + if (URL_REG.test(data.href)) { + return shell.openExternal(data.href) + } + let pathname = null + if (path.isAbsolute(data.href) && isMarkdownFile(data.href)) { + pathname = data.href + } + if (!path.isAbsolute(data.href) && isMarkdownFile(path.join(dirname, data.href))) { + pathname = path.join(dirname, data.href) + } + if (pathname) { + const win = BrowserWindow.fromWebContents(e.sender) + return openFileOrFolder(win, pathname) + } +}) + export const exportFile = (win, type) => { win.webContents.send('AGANI::export', { type }) } diff --git a/src/main/config.js b/src/main/config.js index daa177b8..0d4d400c 100644 --- a/src/main/config.js +++ b/src/main/config.js @@ -77,3 +77,5 @@ export const LF_LINE_ENDING_REG = /(?:[^\r]\n)|(?:^\n$)/ export const CRLF_LINE_ENDING_REG = /\r\n/ export const GITHUB_REPO_URL = 'https://github.com/marktext/marktext' +// copy from muya +export const URL_REG = /^http(s)?:\/\/([a-z0-9\-._~]+\.[a-z]{2,}|[0-9.]+|localhost|\[[a-f0-9.:]+\])(:[0-9]{1,5})?(\/[\S]+)?/i diff --git a/src/muya/lib/config/index.js b/src/muya/lib/config/index.js index 8f3b2865..616ae917 100644 --- a/src/muya/lib/config/index.js +++ b/src/muya/lib/config/index.js @@ -236,3 +236,5 @@ export const MUYA_DEFAULT_OPTION = { export const isInElectron = window && window.process && window.process.type === 'renderer' export const isOsx = window && window.navigator && /Mac/.test(window.navigator.platform) +// http[s] (domain or IPv4 or localhost or IPv6) [port] /not-white-space +export const URL_REG = /^http(s)?:\/\/([a-z0-9\-._~]+\.[a-z]{2,}|[0-9.]+|localhost|\[[a-f0-9.:]+\])(:[0-9]{1,5})?\/[\S]+/i diff --git a/src/muya/lib/contentState/clickCtrl.js b/src/muya/lib/contentState/clickCtrl.js index 1e5caad9..bf1aa717 100644 --- a/src/muya/lib/contentState/clickCtrl.js +++ b/src/muya/lib/contentState/clickCtrl.js @@ -5,6 +5,59 @@ const clickCtrl = ContentState => { ContentState.prototype.clickHandler = function (event) { const { eventCenter } = this.muya const { start, end } = selection.getCursorRange() + // format-click + const node = selection.getSelectionStart() + if (node.classList.contains('ag-inline-rule')) { + let formatType = null + let data = null + switch (node.tagName) { + case 'SPAN': { + if (node.hasAttribute('data-emoji')) { + formatType = 'emoji' + data = node.getAttribute('data-emoji') + } else if (node.classList.contains('ag-math-text')) { + formatType = 'inline_math' + data = node.innerHTML + } + break + } + case 'A': { + formatType = 'link' // auto link or []() link + data = { + text: node.innerHTML, + href: node.getAttribute('href') + } + break + } + case 'STRONG': { + formatType = 'strong' + data = node.innerHTML + break + } + case 'EM': { + formatType = 'em' + data = node.innerHTML + break + } + case 'DEL': { + formatType = 'del' + data = node.innerHTML + break + } + case 'CODE': { + formatType = 'inline_code' + data = node.innerHTML + break + } + } + if (formatType) { + eventCenter.dispatch('format-click', { + event, + formatType, + data, + }) + } + } const block = this.getBlock(start.key) let needRender = false // is show format float box? diff --git a/src/muya/lib/eventHandler/clickEvent.js b/src/muya/lib/eventHandler/clickEvent.js index 6de67784..2cc48bc5 100644 --- a/src/muya/lib/eventHandler/clickEvent.js +++ b/src/muya/lib/eventHandler/clickEvent.js @@ -30,7 +30,12 @@ class ClickEvent { const mathRender = target.closest(`.${CLASS_OR_ID['AG_MATH_RENDER']}`) const mathText = mathRender && mathRender.previousElementSibling if (markedImageText && markedImageText.classList.contains(CLASS_OR_ID['AG_IMAGE_MARKED_TEXT'])) { - selectionText(markedImageText) + eventCenter.dispatch('format-click', { + event, + formatType: 'image', + data: event.target.getAttribute('src') + }) + selectionText(markedImageText) } else if (mathText) { selectionText(mathText) } diff --git a/src/muya/lib/parser/render/renderInlines/emoji.js b/src/muya/lib/parser/render/renderInlines/emoji.js index 7ff19105..2bad973b 100644 --- a/src/muya/lib/parser/render/renderInlines/emoji.js +++ b/src/muya/lib/parser/render/renderInlines/emoji.js @@ -7,7 +7,7 @@ export default function emoji (h, cursor, block, token, outerClass) { const className = this.getClassName(outerClass, block, token, cursor) const validation = validEmoji(token.content) const finalClass = validation ? className : CLASS_OR_ID['AG_WARN'] - const CONTENT_CLASSNAME = `span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKED_TEXT']}` + const CONTENT_CLASSNAME = `span.${finalClass}.${CLASS_OR_ID['AG_INLINE_RULE']}.${CLASS_OR_ID['AG_EMOJI_MARKED_TEXT']}` let startMarkerCN = `span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKER']}` let endMarkerCN = startMarkerCN let content = token.content diff --git a/src/muya/lib/parser/render/renderInlines/inlineMath.js b/src/muya/lib/parser/render/renderInlines/inlineMath.js index dce6f89d..8ab94557 100644 --- a/src/muya/lib/parser/render/renderInlines/inlineMath.js +++ b/src/muya/lib/parser/render/renderInlines/inlineMath.js @@ -39,7 +39,7 @@ export default function displayMath (h, cursor, block, token, outerClass) { return [ h(`span.${className}.${CLASS_OR_ID['AG_MATH_MARKER']}`, startMarker), h(`span.${className}.${CLASS_OR_ID['AG_MATH']}`, [ - h(`span.${CLASS_OR_ID['AG_MATH_TEXT']}`, content), + h(`span.${CLASS_OR_ID['AG_INLINE_RULE']}.${CLASS_OR_ID['AG_MATH_TEXT']}`, content), h(previewSelector, { attrs: { contenteditable: 'false' } }, mathVnode) diff --git a/src/muya/lib/utils/index.js b/src/muya/lib/utils/index.js index 992906fc..c2496eaa 100644 --- a/src/muya/lib/utils/index.js +++ b/src/muya/lib/utils/index.js @@ -2,7 +2,7 @@ // todo@jocs: remove the use of `axios` in muya import axios from 'axios' import createDOMPurify from 'dompurify' -import { isInElectron } from '../config' +import { isInElectron, URL_REG } from '../config' const ID_PREFIX = 'ag-' let id = 0 @@ -171,8 +171,6 @@ export const checkImageContentType = async url => { */ export const getImageInfo = (src, baseUrl = window.DIRNAME) => { const EXT_REG = /\.(jpeg|jpg|png|gif|svg|webp)(?=\?|$)/i - // http[s] (domain or IPv4 or localhost or IPv6) [port] /not-white-space - const URL_REG = /^http(s)?:\/\/([a-z0-9\-._~]+\.[a-z]{2,}|[0-9.]+|localhost|\[[a-f0-9.:]+\])(:[0-9]{1,5})?\/[\S]+/i // data:[][;charset=][;base64], const DATA_URL_REG = /^data:image\/[\w+-]+(;[\w-]+=[\w-]+|;base64)*,[a-zA-Z0-9+/]+={0,2}$/ diff --git a/src/renderer/app.vue b/src/renderer/app.vue index 6f053ab1..ba80a83a 100644 --- a/src/renderer/app.vue +++ b/src/renderer/app.vue @@ -24,6 +24,7 @@ :source-code="sourceCode" :show-tab-bar="showTabBar" :text-direction="textDirection" + :platform="platform" > diff --git a/src/renderer/assets/icons/close.svg b/src/renderer/assets/icons/close.svg new file mode 100644 index 00000000..4e707327 --- /dev/null +++ b/src/renderer/assets/icons/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/components/editorWithTabs/editor.vue b/src/renderer/components/editorWithTabs/editor.vue index b5366da7..d70f8229 100644 --- a/src/renderer/components/editorWithTabs/editor.vue +++ b/src/renderer/components/editorWithTabs/editor.vue @@ -10,6 +10,20 @@ ref="editor" class="editor-component" > +
+ + + + + +
+
+
import { mapState } from 'vuex' + import ViewImage from 'view-image' import Muya from 'muya/lib' import TablePicker from 'muya/lib/ui/tablePicker' import QuickInsert from 'muya/lib/ui/quickInsert' @@ -75,6 +90,8 @@ import { DEFAULT_EDITOR_FONT_FAMILY } from '@/config' import 'muya/themes/light.css' + import CloseIcon from '@/assets/icons/close.svg' + import 'view-image/lib/imgViewer.css' const STANDAR_Y = 320 @@ -91,7 +108,8 @@ textDirection: { type: String, required: true - } + }, + platform: String }, computed: { ...mapState({ @@ -115,12 +133,14 @@ }, data () { this.defaultFontFamily = DEFAULT_EDITOR_FONT_FAMILY + this.CloseIcon = CloseIcon return { selectionChange: null, editor: null, pathname: '', isShowClose: false, dialogTableVisible: false, + imageViewerVisible: false, tableChecker: { rows: 4, columns: 3 @@ -243,6 +263,25 @@ this.$store.dispatch('LISTEN_FOR_CONTENT_CHANGE', changes) }) + this.editor.on('format-click', ({ event, formatType, data }) => { + const isOsx = this.platform === 'darwin' + const ctrlOrMeta = (isOsx && event.metaKey) || (!isOsx && event.ctrlKey) + if (formatType === 'link' && ctrlOrMeta) { + this.$store.dispatch('FORMAT_LINK_CLICK', { data, dirname: window.DIRNAME }) + } else if (formatType === 'image' && ctrlOrMeta) { + if (this.imageViewer) { + this.imageViewer.destroy() + } + + this.imageViewer = new ViewImage(this.$refs.imageViewer, { + url: data, + snapView: true + }) + + this.setImageViewerVisible(true) + } + }) + this.editor.on('selectionChange', changes => { const { y } = changes.cursorCoords if (this.typewriter) { @@ -260,14 +299,25 @@ this.editor.on('contextmenu', (event, selectionChanges) => { showContextMenu(event, selectionChanges) }) + document.addEventListener('keyup', this.keyup) }) }, methods: { + keyup (event) { + if (event.key === 'Escape') { + this.setImageViewerVisible(false) + } + }, + handleImagePath (files) { const { editor } = this editor && editor.showAutoImagePath(files) }, + setImageViewerVisible (status) { + this.imageViewerVisible = status + }, + handleUndo () { if (this.editor) { this.editor.undo() @@ -453,6 +503,8 @@ bus.$off('copy-block', this.handleCopyBlock) bus.$off('print', this.handlePrint) + document.removeEventListener('keyup', this.keyup) + this.editor.destroy() this.editor = null } @@ -506,4 +558,39 @@ padding-top: calc(50vh - 136px); padding-bottom: calc(50vh - 54px); } + .image-viewer { + position: fixed; + backdrop-filter: blur(5px); + top: 0; + right: 0; + left: 0; + bottom: 0; + background: rgba(0, 0, 0, .8); + z-index: 11; + & .icon-close { + z-index: 1000; + width: 30px; + height: 30px; + position: absolute; + top: 50px; + left: 50px; + display: block; + & svg { + fill: #efefef; + width: 100%; + height: 100%; + } + } + } + .iv-container { + width: 100%; + height: 100%; + } + .iv-snap-view { + opacity: 1; + bottom: 20px; + right: 20px; + top: auto; + left: auto; + } diff --git a/src/renderer/components/editorWithTabs/index.vue b/src/renderer/components/editorWithTabs/index.vue index 9582c6af..d38a1d44 100644 --- a/src/renderer/components/editorWithTabs/index.vue +++ b/src/renderer/components/editorWithTabs/index.vue @@ -9,6 +9,7 @@ :markdown="markdown" :cursor="cursor" :text-direction="textDirection" + :platform="platform" >