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
This commit is contained in:
Ran Luo 2019-03-24 20:20:44 +08:00 committed by Felix Häusler
parent eff619df34
commit c0c8ea4b15
15 changed files with 189 additions and 9 deletions

View File

@ -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"

View File

@ -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 })
}

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -30,6 +30,11 @@ 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'])) {
eventCenter.dispatch('format-click', {
event,
formatType: 'image',
data: event.target.getAttribute('src')
})
selectionText(markedImageText)
} else if (mathText) {
selectionText(mathText)

View File

@ -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

View File

@ -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)

View File

@ -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:[<MIME-type>][;charset=<encoding>][;base64],<data>
const DATA_URL_REG = /^data:image\/[\w+-]+(;[\w-]+=[\w-]+|;base64)*,[a-zA-Z0-9+/]+={0,2}$/

View File

@ -24,6 +24,7 @@
:source-code="sourceCode"
:show-tab-bar="showTabBar"
:text-direction="textDirection"
:platform="platform"
></editor-with-tabs>
<aidou></aidou>
<upload-image></upload-image>

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1553334996824" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2737" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M521.693867 449.297067L111.4112 39.0144a51.2 51.2 0 1 0-72.430933 72.362667l410.282666 410.3168-410.282666 410.3168a51.2 51.2 0 1 0 72.3968 72.3968l410.3168-410.282667 410.3168 410.282667a51.2 51.2 0 1 0 72.3968-72.362667l-410.282667-410.350933 410.282667-410.282667a51.2 51.2 0 1 0-72.3968-72.3968l-410.282667 410.282667z" p-id="2738"></path></svg>

After

Width:  |  Height:  |  Size: 735 B

View File

@ -10,6 +10,20 @@
ref="editor"
class="editor-component"
></div>
<div
class="image-viewer"
v-show="imageViewerVisible"
>
<span class="icon-close" @click="setImageViewerVisible(false)">
<svg :viewBox="CloseIcon.viewBox">
<use :xlink:href="CloseIcon.url"></use>
</svg>
</span>
<div
ref="imageViewer"
>
</div>
</div>
<el-dialog
:visible.sync="dialogTableVisible"
:show-close="isShowClose"
@ -60,6 +74,7 @@
<script>
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;
}
</style>

View File

@ -9,6 +9,7 @@
:markdown="markdown"
:cursor="cursor"
:text-direction="textDirection"
:platform="platform"
></editor>
<source-code
v-if="sourceCode"
@ -51,6 +52,10 @@
textDirection: {
type: String,
required: true
},
platform: {
type: String,
required: true
}
},
components: {

View File

@ -146,6 +146,9 @@ const actions = {
ASK_FOR_INSERT_IMAGE ({ commit }, type) {
ipcRenderer.send('AGANI::ask-for-insert-image', type)
},
FORMAT_LINK_CLICK ({ commit }, { data, dirname }) {
ipcRenderer.send('AGANI::format-link-click', { data, dirname })
},
// image path auto complement
ASK_FOR_IMAGE_AUTO_PATH ({ commit, state }, src) {
const { pathname } = state.currentFile

View File

@ -10856,6 +10856,11 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
view-image@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/view-image/-/view-image-0.0.1.tgz#f97a6c23e881caa173d83b148ef7ee8adbb890e5"
integrity sha512-xo6/w370OxBo9QLMZOXmRa0gSXNr9PfMQv4hFMZa6z8YFoyi77Q7/FRghANnPF9bILkX18fJDsC9fT1KlRzSDA==
vm-browserify@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"