mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 22:22:18 +08:00
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:
parent
eff619df34
commit
c0c8ea4b15
@ -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"
|
||||
|
@ -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 })
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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?
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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}$/
|
||||
|
||||
|
@ -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>
|
||||
|
1
src/renderer/assets/icons/close.svg
Normal file
1
src/renderer/assets/icons/close.svg
Normal 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 |
@ -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>
|
||||
|
@ -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: {
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user