mirror of
https://github.com/purocean/yn.git
synced 2026-08-29 01:21:05 +08:00
前端升级到 Vue 3.0
This commit is contained in:
+1
-1
@@ -6,6 +6,6 @@ dist
|
||||
vs
|
||||
out
|
||||
bin
|
||||
.yarnrc
|
||||
# .yarnrc
|
||||
b.bat
|
||||
*.pfx
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not ie <= 8
|
||||
not dead
|
||||
|
||||
+26
-10
@@ -3,16 +3,32 @@ module.exports = {
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
'extends': [
|
||||
'plugin:vue/essential',
|
||||
'@vue/standard'
|
||||
extends: [
|
||||
'plugin:vue/vue3-essential',
|
||||
'@vue/standard',
|
||||
'@vue/typescript/recommended'
|
||||
],
|
||||
rules: {
|
||||
'comma-dangle': 'off',
|
||||
'no-console': 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
|
||||
},
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
}
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/camelcase': 'off',
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'comma-dangle': 'off',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'**/__tests__/*.{j,t}s?(x)',
|
||||
'**/tests/unit/**/*.spec.{j,t}s?(x)'
|
||||
],
|
||||
env: {
|
||||
mocha: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
@@ -10,6 +11,7 @@ node_modules
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
registry "https://registry.yarnpkg.com"
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
# frontend2
|
||||
# frontend-vue3
|
||||
|
||||
## Project setup
|
||||
```
|
||||
@@ -7,22 +7,22 @@ yarn install
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
```
|
||||
yarn run serve
|
||||
yarn serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
```
|
||||
yarn run build
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Run your tests
|
||||
### Run your unit tests
|
||||
```
|
||||
yarn run test
|
||||
yarn test:unit
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
yarn run lint
|
||||
yarn lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/app'
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
|
||||
+59
-34
@@ -1,52 +1,77 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"name": "yn-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^2.6.5",
|
||||
"crypto-js": "^3.1.9-1",
|
||||
"dayjs": "^1.8.12",
|
||||
"echarts": "^4.2.1",
|
||||
"github-markdown-css": "^3.0.1",
|
||||
"highlight": "^0.2.4",
|
||||
"highlight.js": "^9.15.6",
|
||||
"core-js": "^3.6.5",
|
||||
"crypto-js": "^4.0.0",
|
||||
"dayjs": "^1.9.3",
|
||||
"echarts": "^4.9.0",
|
||||
"github-markdown-css": "^4.0.0",
|
||||
"highlight.js": "^10.3.1",
|
||||
"katex": "^0.6.0",
|
||||
"markdown-it": "^8.4.2",
|
||||
"markdown-it-attrs": "^2.3.2",
|
||||
"loadsh": "^0.0.4",
|
||||
"markdown-it": "^12.0.1",
|
||||
"markdown-it-attrs": "^3.0.3",
|
||||
"markdown-it-footnote": "^3.0.2",
|
||||
"markdown-it-katex": "^2.0.3",
|
||||
"markdown-it-multimd-table": "^3.1.3",
|
||||
"markdown-it-multimd-table": "^4.0.3",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"mermaid": "^8.2.6",
|
||||
"mime-types": "^2.1.24",
|
||||
"monaco-editor": "^0.17.1",
|
||||
"mermaid": "^8.8.2",
|
||||
"mime-types": "^2.1.27",
|
||||
"mitt": "^2.1.0",
|
||||
"monaco-editor": "^0.21.2",
|
||||
"normalize.css": "^8.0.1",
|
||||
"socket.io-client": "^2.2.0",
|
||||
"sortablejs": "^1.10.1",
|
||||
"transliteration": "^2.1.3",
|
||||
"turndown": "^5.0.3",
|
||||
"vue": "^2.6.10",
|
||||
"vue-awesome": "^3.5.4",
|
||||
"vue-bus": "^1.2.1",
|
||||
"vue-router": "^3.0.3",
|
||||
"vuex": "^3.0.1",
|
||||
"xterm": "^3.12.2"
|
||||
"socket.io-client": "^2.3.1",
|
||||
"sortablejs": "^1.12.0",
|
||||
"transliteration": "^2.1.11",
|
||||
"turndown": "^7.0.0",
|
||||
"vue": "^3.0.0",
|
||||
"vue-router": "^4.0.0-0",
|
||||
"vuex": "^4.0.0-0",
|
||||
"xterm": "^4.9.0",
|
||||
"xterm-addon-fit": "^0.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^3.6.0",
|
||||
"@vue/cli-plugin-eslint": "^3.6.0",
|
||||
"@vue/cli-service": "^3.6.0",
|
||||
"@vue/eslint-config-standard": "^4.0.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-vue": "^5.0.0",
|
||||
"less": "^3.0.4",
|
||||
"less-loader": "^4.1.0",
|
||||
"vue-template-compiler": "^2.5.21"
|
||||
"@types/chai": "^4.2.11",
|
||||
"@types/crypto-js": "^4.0.1",
|
||||
"@types/echarts": "^4.8.3",
|
||||
"@types/lodash": "^4.14.162",
|
||||
"@types/markdown-it": "^10.0.2",
|
||||
"@types/mermaid": "^8.2.1",
|
||||
"@types/mime-types": "^2.1.0",
|
||||
"@types/mocha": "^5.2.4",
|
||||
"@types/socket.io-client": "^1.4.34",
|
||||
"@types/sortablejs": "^1.10.6",
|
||||
"@types/turndown": "^5.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^2.33.0",
|
||||
"@typescript-eslint/parser": "^2.33.0",
|
||||
"@vue/cli-plugin-babel": "~4.5.0",
|
||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||
"@vue/cli-plugin-router": "~4.5.0",
|
||||
"@vue/cli-plugin-typescript": "~4.5.0",
|
||||
"@vue/cli-plugin-unit-mocha": "~4.5.0",
|
||||
"@vue/cli-plugin-vuex": "~4.5.0",
|
||||
"@vue/cli-service": "~4.5.0",
|
||||
"@vue/compiler-sfc": "^3.0.0",
|
||||
"@vue/eslint-config-standard": "^5.1.2",
|
||||
"@vue/eslint-config-typescript": "^5.0.2",
|
||||
"@vue/test-utils": "^2.0.0-0",
|
||||
"chai": "^4.1.2",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^7.0.0-0",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"typescript": "~3.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
<title>Yank Note</title>
|
||||
<script>
|
||||
// 强制 monaco-editor 使用 浏览器模式
|
||||
|
||||
+3
-15
@@ -1,22 +1,10 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view/>
|
||||
</div>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import 'normalize.css/normalize.css'
|
||||
import Vue from 'vue'
|
||||
import VueBus from 'vue-bus'
|
||||
<style lang="scss">
|
||||
@import url('~normalize.css/normalize.css');
|
||||
|
||||
Vue.use(VueBus)
|
||||
|
||||
export default {
|
||||
name: 'App'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="context-menu">
|
||||
<div class="mask" v-if="isShow" @click="hide" @contextmenu.prevent.stop="hide"></div>
|
||||
<ul class="menu" ref="menu" :style="{visibility: isShow ? 'visible' : 'hidden'}">
|
||||
<ul class="menu" ref="refMenu" :style="{visibility: isShow ? 'visible' : 'hidden'}">
|
||||
<template v-for="item in items">
|
||||
<li v-if="item.type === 'separator'" :key="item.id" :class="item.type || 'normal'"></li>
|
||||
<li v-else :key="item.id" @click="handleClick(item)" :class="item.type || 'normal'">{{item.label}}</li>
|
||||
@@ -10,66 +10,86 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
<script lang="ts">
|
||||
import { defineComponent, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { Components } from '../types'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'context-menu',
|
||||
props: {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
items: [], // [{ type: normal | 'separator', id: string, label: string, onClick: item => void }]
|
||||
isShow: false,
|
||||
setup () {
|
||||
const refMenu = ref<HTMLUListElement | null>(null)
|
||||
const items = ref<Components.ContextMenu.Item[]>([])
|
||||
const isShow = ref(false)
|
||||
|
||||
let mouseX = 0
|
||||
let mouseY = 0
|
||||
|
||||
function hideMenu () {
|
||||
isShow.value = false
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.mouseX = 0
|
||||
this.mouseY = 0
|
||||
window.addEventListener('blur', this.hide)
|
||||
window.addEventListener('mousemove', this.recordMousePosition)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('blur', this.hide)
|
||||
window.removeEventListener('mousemove', this.recordMousePosition)
|
||||
},
|
||||
methods: {
|
||||
handleClick (item) {
|
||||
|
||||
function hide () {
|
||||
items.value = []
|
||||
hideMenu()
|
||||
}
|
||||
|
||||
function handleClick (item: Components.ContextMenu.NormalItem) {
|
||||
item.onClick(item)
|
||||
this.hide()
|
||||
},
|
||||
recordMousePosition (e) {
|
||||
this.mouseX = e.clientX
|
||||
this.mouseY = e.clientY
|
||||
},
|
||||
showMenu () {
|
||||
hide()
|
||||
}
|
||||
|
||||
function recordMousePosition (e: MouseEvent) {
|
||||
mouseX = e.clientX
|
||||
mouseY = e.clientY
|
||||
}
|
||||
|
||||
function showMenu () {
|
||||
if (!refMenu.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const windowWidth = window.innerWidth
|
||||
const windowHeight = window.innerHeight
|
||||
|
||||
const menuWidth = this.$refs.menu.offsetWidth
|
||||
const menuHeight = this.$refs.menu.offsetHeight
|
||||
const menuWidth = refMenu.value.offsetWidth
|
||||
const menuHeight = refMenu.value.offsetHeight
|
||||
|
||||
const x = this.mouseX + menuWidth > windowWidth ? this.mouseX - menuWidth : this.mouseX
|
||||
const y = this.mouseY + menuHeight > windowHeight ? this.mouseY - menuHeight : this.mouseY
|
||||
const x = mouseX + menuWidth > windowWidth ? mouseX - menuWidth : mouseX
|
||||
const y = mouseY + menuHeight > windowHeight ? mouseY - menuHeight : mouseY
|
||||
|
||||
this.$refs.menu.style.left = x + 'px'
|
||||
this.$refs.menu.style.top = y + 'px'
|
||||
refMenu.value.style.left = x + 'px'
|
||||
refMenu.value.style.top = y + 'px'
|
||||
|
||||
this.isShow = true
|
||||
},
|
||||
hideMenu () {
|
||||
this.isShow = false
|
||||
},
|
||||
show (items) {
|
||||
this.items = items
|
||||
this.$nextTick(() => {
|
||||
this.showMenu()
|
||||
isShow.value = true
|
||||
}
|
||||
|
||||
function show (menuItems: Components.ContextMenu.Item[]) {
|
||||
items.value = menuItems
|
||||
nextTick(() => {
|
||||
showMenu()
|
||||
})
|
||||
},
|
||||
hide () {
|
||||
this.items = []
|
||||
this.hideMenu()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('blur', hide)
|
||||
window.addEventListener('mousemove', recordMousePosition)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('blur', hide)
|
||||
window.removeEventListener('mousemove', recordMousePosition)
|
||||
})
|
||||
|
||||
return {
|
||||
refMenu,
|
||||
isShow,
|
||||
items,
|
||||
hide,
|
||||
show,
|
||||
handleClick,
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
+193
-161
@@ -1,152 +1,125 @@
|
||||
<template>
|
||||
<div class="editor">
|
||||
<MonacoEditor
|
||||
ref="editor"
|
||||
ref="refEditor"
|
||||
class="editor"
|
||||
@change="setCurrentValue"
|
||||
@ready="editorReady"
|
||||
@scroll-view="syncScrollView"
|
||||
@paste-img="pasteImg"
|
||||
@upload-file="uploadFile"
|
||||
@change-document="val => $store.commit('app/setDocumentInfo', val)"
|
||||
@change-document="val => $store.commit('setDocumentInfo', val)"
|
||||
@xterm-run="val => $bus.emit('xterm-run', val)"
|
||||
@save="saveFile"></MonacoEditor>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { mapState } from 'vuex'
|
||||
<script lang="ts">
|
||||
import lodash from 'lodash'
|
||||
import dayjs from 'dayjs'
|
||||
import File from '@/lib/file'
|
||||
import Storage from '@/lib/Storage'
|
||||
import { encodeMarkdownLink } from '@/lib/utils'
|
||||
import MonacoEditor from './MonacoEditor'
|
||||
import { defineComponent, nextTick, onBeforeMount, onMounted, ref, toRefs, watch } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { encodeMarkdownLink } from '../useful/utils'
|
||||
import { useBus } from '../useful/bus'
|
||||
import { useModal } from '../useful/modal'
|
||||
import { useToast } from '../useful/toast'
|
||||
import File from '../useful/file'
|
||||
import Storage from '../useful/storage'
|
||||
import MonacoEditor from './MonacoEditor.vue'
|
||||
|
||||
const FILE_POSITION_KEY = 'filePosition'
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'editor',
|
||||
components: { MonacoEditor },
|
||||
data () {
|
||||
return {
|
||||
timer: null
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$bus.on('resize', this.$refs.editor.resize)
|
||||
this.$bus.on('editor-insert-value', this.$refs.editor.insert)
|
||||
this.$bus.on('editor-replace-value', this.$refs.editor.replaceValue)
|
||||
this.$bus.on('editor-toggle-wrap', this.$refs.editor.toggleWrap)
|
||||
this.$bus.on('file-new', this.createFile)
|
||||
this.restartTimer()
|
||||
setup (_, { emit }) {
|
||||
const bus = useBus()
|
||||
const store = useStore()
|
||||
const modal = useModal()
|
||||
const toast = useToast()
|
||||
|
||||
this.saveFileOpenPositionDebounce = _.debounce(line => {
|
||||
this.saveFileOpenPosition(line)
|
||||
}, 1000)
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$bus.off('resize', this.$refs.editor.resize)
|
||||
this.$bus.off('editor-insert-value', this.$refs.editor.insert)
|
||||
this.$bus.off('editor-replace-value', this.$refs.editor.replaceValue)
|
||||
this.$bus.off('editor-toggle-wrap', this.$refs.editor.toggleWrap)
|
||||
this.$bus.off('file-new', this.createFile)
|
||||
},
|
||||
methods: {
|
||||
setCurrentValue ({ uri, value }) {
|
||||
if (this.toUri(this.currentFile) === uri) {
|
||||
this.$store.commit('app/setCurrentContent', value)
|
||||
}
|
||||
},
|
||||
saveFileOpenPosition (top) {
|
||||
if (this.currentFile) {
|
||||
const map = Storage.get(FILE_POSITION_KEY, {})
|
||||
map[`${this.currentFile.repo}|${this.currentFile.path}`] = top
|
||||
Storage.set(FILE_POSITION_KEY, map)
|
||||
}
|
||||
},
|
||||
async createFile ({ file, content }) {
|
||||
try {
|
||||
// 加密文件内容
|
||||
if (File.isEncryptedFile(file)) {
|
||||
const password = await this.inputPassword('[创建] 请输入密码', file.name)
|
||||
const encrypted = File.encrypt(content, password)
|
||||
content = encrypted.content
|
||||
// 储存这次解密文件密码的 hash,用于下次判断是否输入了相同密码
|
||||
this.$store.commit('app/setPasswordHash', file, encrypted.passwordHash)
|
||||
}
|
||||
let timer: number | null = null
|
||||
const refEditor = ref<any>(null)
|
||||
const { currentFile, currentContent, previousContent, previousHash, passwordHash } = toRefs(store.state)
|
||||
|
||||
await File.write(file, content, 'new')
|
||||
const getEditor = () => refEditor.value
|
||||
const revealLine = (line: number) => getEditor().revealLine(line)
|
||||
const revealLineInCenter = (line: number) => getEditor().revealLineInCenter(line)
|
||||
const switchTodo = (line: number, checked: boolean) => getEditor().switchTodo(line, checked)
|
||||
const toUri = (file: any) => File.toUri(file)
|
||||
|
||||
this.$bus.emit('file-created', file)
|
||||
this.$store.commit('app/setCurrentFile', file)
|
||||
} catch (error) {
|
||||
this.$toast.show('warning', error.message)
|
||||
console.error(error)
|
||||
}
|
||||
},
|
||||
async inputPassword (title, filename) {
|
||||
const password = await this.$modal.input({ title, type: 'password', hint: filename })
|
||||
async function inputPassword (title: string, filename: string) {
|
||||
const password = await modal.input({ title, type: 'password', hint: filename })
|
||||
if (!password) {
|
||||
throw new Error('未输入密码')
|
||||
}
|
||||
|
||||
return password
|
||||
},
|
||||
clearTimer () {
|
||||
if (this.timer) {
|
||||
window.clearTimeout(this.timer)
|
||||
}
|
||||
},
|
||||
restartTimer () {
|
||||
this.clearTimer()
|
||||
}
|
||||
|
||||
if (!(this.currentFile && this.currentFile.repo && this.currentFile.path)) {
|
||||
return
|
||||
function saveFileOpenPosition (top: number) {
|
||||
if (currentFile.value) {
|
||||
const map = Storage.get(FILE_POSITION_KEY, {})
|
||||
map[`${currentFile.value.repo}|${currentFile.value.path}`] = top
|
||||
Storage.set(FILE_POSITION_KEY, map)
|
||||
}
|
||||
}
|
||||
|
||||
this.timer = window.setTimeout(() => {
|
||||
if (!this.currentFile || File.isEncryptedFile(this.currentFile)) { // 加密文件不自动保存
|
||||
return
|
||||
const saveFileOpenPositionDebounce = lodash.debounce((line: number) => {
|
||||
saveFileOpenPosition(line)
|
||||
}, 1000)
|
||||
|
||||
function setCurrentValue ({ uri, value }: { uri: string; value: any}) {
|
||||
if (toUri(currentFile.value) === uri) {
|
||||
store.commit('setCurrentContent', value)
|
||||
}
|
||||
}
|
||||
|
||||
function syncScrollView ({ line, top }: { line: number; top: number }) {
|
||||
emit('scroll-line', line)
|
||||
saveFileOpenPositionDebounce(top)
|
||||
}
|
||||
|
||||
async function createFile ({ file, content }: {file: any; content: any}) {
|
||||
try {
|
||||
// 加密文件内容
|
||||
if (File.isEncryptedFile(file)) {
|
||||
const password = await inputPassword('[创建] 请输入密码', file.name)
|
||||
const encrypted = File.encrypt(content, password)
|
||||
content = encrypted.content
|
||||
// 储存这次解密文件密码的 hash,用于下次判断是否输入了相同密码
|
||||
store.commit('setPasswordHash', { file, passwordHash: encrypted.passwordHash })
|
||||
}
|
||||
|
||||
this.saveFile()
|
||||
}, 2000)
|
||||
},
|
||||
editorReady () {
|
||||
this.$bus.emit('editor-ready')
|
||||
if (this.currentFile) {
|
||||
this.changeFile(this.currentFile)
|
||||
await File.write(file, content, 'new')
|
||||
|
||||
bus.emit('file-created', file)
|
||||
store.commit('setCurrentFile', file)
|
||||
} catch (error) {
|
||||
toast.show('warning', error.message)
|
||||
console.error(error)
|
||||
}
|
||||
},
|
||||
revealLine (line) {
|
||||
this.$refs.editor.revealLine(line)
|
||||
},
|
||||
revealLineInCenter (line) {
|
||||
this.$refs.editor.revealLineInCenter(line)
|
||||
},
|
||||
switchTodo (line, checked) {
|
||||
this.$refs.editor.switchTodo(line, checked)
|
||||
},
|
||||
syncScrollView ({ line, top }) {
|
||||
this.$emit('scroll-line', line)
|
||||
this.saveFileOpenPositionDebounce(top)
|
||||
},
|
||||
toUri (file) {
|
||||
return File.toUri(file)
|
||||
},
|
||||
async saveFile (f = null) {
|
||||
const file = f || this.currentFile
|
||||
}
|
||||
|
||||
function clearTimer () {
|
||||
if (timer) {
|
||||
window.clearTimeout(timer)
|
||||
timer = null
|
||||
}
|
||||
}
|
||||
|
||||
async function saveFile (f: any = null) {
|
||||
const file = f || currentFile.value
|
||||
|
||||
if (!(file && file.repo && file.path)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.previousContent === this.currentContent) {
|
||||
if (previousContent.value === currentContent.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.currentContent) {
|
||||
if (!currentContent.value) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -155,69 +128,87 @@ export default {
|
||||
}
|
||||
|
||||
try {
|
||||
let content = this.currentContent
|
||||
let content = currentContent.value
|
||||
|
||||
// 加密文件内容
|
||||
if (File.isEncryptedFile(file)) {
|
||||
const password = await this.inputPassword('[保存] 请输入密码', file.name)
|
||||
const password = await inputPassword('[保存] 请输入密码', file.name)
|
||||
const encrypted = File.encrypt(content, password)
|
||||
const oldPasswdHash = this.passwordHash[`${file.repo}|${file.path}`]
|
||||
const oldPasswdHash = passwordHash.value[`${file.repo}|${file.path}`]
|
||||
if (oldPasswdHash !== encrypted.passwordHash) {
|
||||
if (!(await this.$modal.confirm({ title: '提示', content: '密码和上一次输入的密码不一致,是否用新密码保存?' }))) {
|
||||
if (!(await modal.confirm({ title: '提示', content: '密码和上一次输入的密码不一致,是否用新密码保存?' }))) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
content = encrypted.content
|
||||
// 储存这次解密文件密码的 hash,用于下次判断是否输入了相同密码
|
||||
this.$store.commit('app/setPasswordHash', file, encrypted.passwordHash)
|
||||
store.commit('setPasswordHash', { file, passwordHash: encrypted.passwordHash })
|
||||
}
|
||||
|
||||
const { hash } = await File.write(file, content, this.previousHash)
|
||||
const { hash } = await File.write(file, content, previousHash.value)
|
||||
|
||||
this.$store.commit('app/setPreviousHash', hash)
|
||||
this.$store.commit('app/setPreviousContent', this.currentContent)
|
||||
this.$store.commit('app/setSavedAt', new Date())
|
||||
store.commit('setPreviousHash', hash)
|
||||
store.commit('setPreviousContent', currentContent.value)
|
||||
store.commit('setSavedAt', new Date())
|
||||
} catch (error) {
|
||||
this.$toast.show('warning', error.message)
|
||||
toast.show('warning', error.message)
|
||||
console.error(error)
|
||||
}
|
||||
},
|
||||
async pasteImg (file, asBase64) {
|
||||
console.log(file, asBase64)
|
||||
}
|
||||
|
||||
function restartTimer () {
|
||||
clearTimer()
|
||||
|
||||
if (!(currentFile.value && currentFile.value.repo && currentFile.value.path)) {
|
||||
return
|
||||
}
|
||||
|
||||
timer = window.setTimeout(() => {
|
||||
if (!currentFile.value || File.isEncryptedFile(currentFile.value)) { // 加密文件不自动保存
|
||||
return
|
||||
}
|
||||
|
||||
saveFile()
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
async function pasteImg (file: any, asBase64: boolean) {
|
||||
if (asBase64) {
|
||||
const uri = await File.toBase64URL(file)
|
||||
this.$refs.editor.insert(`\n`)
|
||||
getEditor().insert(`\n`)
|
||||
} else {
|
||||
const { relativePath } = await File.upload(this.currentFile.repo, this.currentFile.path, file)
|
||||
this.$bus.emit('file-uploaded', relativePath)
|
||||
this.$refs.editor.insert(`})\n`)
|
||||
const { relativePath } = await File.upload(currentFile.value.repo, currentFile.value.path, file)
|
||||
bus.emit('file-uploaded', relativePath)
|
||||
getEditor().insert(`})\n`)
|
||||
}
|
||||
},
|
||||
async uploadFile (file) {
|
||||
}
|
||||
|
||||
async function uploadFile (file: any) {
|
||||
const filename = `${dayjs().format('YYYYMMDDHHmmss')}.${file.name}`
|
||||
const { relativePath } = await File.upload(this.currentFile.repo, this.currentFile.path, file, filename)
|
||||
this.$bus.emit('file-uploaded', relativePath)
|
||||
this.$refs.editor.insert(`附件 [${dayjs().format('YYYY-MM-DD HH:mm')}]:[${file.name} (${(file.size / 1024).toFixed(2)}KiB)](${encodeMarkdownLink(relativePath)}){class=open target=_blank}\n`)
|
||||
},
|
||||
async changeFile (current, previous) {
|
||||
this.clearTimer()
|
||||
const { relativePath } = await File.upload(currentFile.value.repo, currentFile.value.path, file, filename)
|
||||
bus.emit('file-uploaded', relativePath)
|
||||
getEditor().insert(`附件 [${dayjs().format('YYYY-MM-DD HH:mm')}]:[${file.name} (${(file.size / 1024).toFixed(2)}KiB)](${encodeMarkdownLink(relativePath)}){class=open target=_blank}\n`)
|
||||
}
|
||||
|
||||
async function changeFile (current: any, previous?: any) {
|
||||
clearTimer()
|
||||
|
||||
if (previous && previous.repo && previous.path) {
|
||||
await this.saveFile(previous)
|
||||
await saveFile(previous)
|
||||
}
|
||||
|
||||
if (!current) {
|
||||
this.$store.commit('app/setPreviousContent', '\n')
|
||||
this.$refs.editor.setModel(this.toUri(current), '\n')
|
||||
this.$store.commit('app/setCurrentFile', null)
|
||||
store.commit('setPreviousContent', '\n')
|
||||
getEditor().setModel(toUri(current), '\n')
|
||||
store.commit('setCurrentFile', null)
|
||||
return
|
||||
}
|
||||
|
||||
if (current.content) { // 系统文件
|
||||
this.$store.commit('app/setPreviousContent', current.content)
|
||||
this.$store.commit('app/setSavedAt', null)
|
||||
this.$refs.editor.setModel(this.toUri(current), current.content)
|
||||
store.commit('setPreviousContent', current.content)
|
||||
store.commit('setSavedAt', null)
|
||||
getEditor().setModel(toUri(current), current.content)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -226,40 +217,81 @@ export default {
|
||||
|
||||
// 解密文件内容
|
||||
if (File.isEncryptedFile(current)) {
|
||||
const password = await this.inputPassword('[打开] 请输入密码', current.name)
|
||||
const password = await inputPassword('[打开] 请输入密码', current.name)
|
||||
const decrypted = File.decrypt(content, password)
|
||||
content = decrypted.content
|
||||
// 储存这次解密文件密码的 hash,用于下次判断是否输入了相同密码
|
||||
this.$store.commit('app/setPasswordHash', { file: current, passwordHash: decrypted.passwordHash })
|
||||
store.commit('setPasswordHash', { file: current, passwordHash: decrypted.passwordHash })
|
||||
}
|
||||
|
||||
this.$store.commit('app/setPreviousContent', content)
|
||||
this.$store.commit('app/setPreviousHash', hash)
|
||||
this.$store.commit('app/setSavedAt', null)
|
||||
this.$refs.editor.setModel(this.toUri(current), content)
|
||||
store.commit('setPreviousContent', content)
|
||||
store.commit('setPreviousHash', hash)
|
||||
store.commit('setSavedAt', null)
|
||||
getEditor().setModel(toUri(current), content)
|
||||
} catch (error) {
|
||||
this.$store.commit('app/setCurrentFile', null)
|
||||
this.$toast.show('warning', error.message)
|
||||
store.commit('setCurrentFile', null)
|
||||
toast.show('warning', error.message)
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
// 切换文件时候定位
|
||||
const top = Storage.get(FILE_POSITION_KEY, {})[`${current.repo}|${current.path}`] || 1
|
||||
this.$refs.editor.setScrollToTop(top)
|
||||
// 切换文件时候保留定位
|
||||
nextTick(() => {
|
||||
const top = Storage.get(FILE_POSITION_KEY, {})[`${current.repo}|${current.path}`] || 1
|
||||
getEditor().setScrollToTop(top)
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('app', ['currentFile', 'currentContent', 'previousContent', 'previousHash', 'passwordHash'])
|
||||
},
|
||||
watch: {
|
||||
currentFile (current, previous) {
|
||||
this.changeFile(current, previous)
|
||||
},
|
||||
currentContent () {
|
||||
this.restartTimer()
|
||||
|
||||
function editorReady () {
|
||||
bus.emit('editor-ready')
|
||||
if (currentFile.value) {
|
||||
changeFile(currentFile.value)
|
||||
}
|
||||
}
|
||||
|
||||
watch(currentFile, changeFile)
|
||||
watch(currentContent, restartTimer)
|
||||
|
||||
const resize = () => nextTick(() => getEditor().resize())
|
||||
const insert = (text?: string) => getEditor().insert(text)
|
||||
const replaceValue = (value?: { search: string; replace: string }) => {
|
||||
if (value) {
|
||||
getEditor().replaceValue(value.search, value.replace)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleWrap = () => getEditor().toggleWrap()
|
||||
|
||||
onMounted(() => {
|
||||
bus.on('resize', resize)
|
||||
bus.on('editor-insert-value', insert)
|
||||
bus.on('editor-replace-value', replaceValue)
|
||||
bus.on('editor-toggle-wrap', toggleWrap)
|
||||
bus.on('file-new', createFile as any)
|
||||
restartTimer()
|
||||
})
|
||||
|
||||
onBeforeMount(() => {
|
||||
bus.off('resize', resize)
|
||||
bus.off('editor-insert-value', insert)
|
||||
bus.off('editor-replace-value', replaceValue)
|
||||
bus.off('editor-toggle-wrap', toggleWrap)
|
||||
bus.off('file-new', createFile as any)
|
||||
})
|
||||
|
||||
return {
|
||||
refEditor,
|
||||
revealLine,
|
||||
revealLineInCenter,
|
||||
switchTodo,
|
||||
setCurrentValue,
|
||||
syncScrollView,
|
||||
pasteImg,
|
||||
uploadFile,
|
||||
editorReady,
|
||||
saveFile,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -2,108 +2,110 @@
|
||||
<Tabs :list="tabs" :value="current" @remove="removeTabs" @switch="switchTab" @change-list="setTabs"></Tabs>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import Tabs from './Tabs'
|
||||
import File from '@/lib/file'
|
||||
<script lang="ts">
|
||||
import { useStore } from 'vuex'
|
||||
import Tabs from './Tabs.vue'
|
||||
import File from '../useful/file'
|
||||
import { defineComponent, onBeforeMount, onBeforeUnmount, ref, toRefs, watch } from 'vue'
|
||||
import { Components } from '../types'
|
||||
|
||||
const blankUri = File.toUri(null)
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'file-tabs',
|
||||
components: { Tabs },
|
||||
data () {
|
||||
return {
|
||||
list: [],
|
||||
current: blankUri
|
||||
}
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('keydown', this.keydownHandler, true)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.keydownHandler)
|
||||
},
|
||||
methods: {
|
||||
keydownHandler (e) {
|
||||
const findTab = offset => {
|
||||
const tabs = [...this.tabs]
|
||||
setup () {
|
||||
const store = useStore()
|
||||
|
||||
if (tabs.length < 1) {
|
||||
const { currentFile, tabs } = toRefs(store.state)
|
||||
const list = ref<Components.FileTabs.Item[]>([])
|
||||
const current = ref(blankUri)
|
||||
|
||||
function setTabs (list: Components.FileTabs.Item[]) {
|
||||
store.commit('setTabs', list)
|
||||
}
|
||||
|
||||
function switchFile (file: any) {
|
||||
store.commit('setCurrentFile', file)
|
||||
}
|
||||
|
||||
function switchTab (item: Components.FileTabs.Item) {
|
||||
switchFile(item.payload.file)
|
||||
}
|
||||
|
||||
function removeTabs (items: Components.FileTabs.Item[]) {
|
||||
const keys = items.map(x => x.key)
|
||||
setTabs(tabs.value.filter((x: any) => keys.indexOf(x.key) === -1))
|
||||
}
|
||||
|
||||
function addTab (item: Components.FileTabs.Item) {
|
||||
const tab = tabs.value.find((x: any) => item.key === x.key)
|
||||
|
||||
// 没有打开此 Tab,新建一个
|
||||
if (!tab) {
|
||||
setTabs(tabs.value.concat([item]))
|
||||
}
|
||||
|
||||
current.value = item.key
|
||||
}
|
||||
|
||||
function keydownHandler (e: KeyboardEvent) {
|
||||
const findTab = (offset: number) => {
|
||||
const list = [...tabs.value]
|
||||
|
||||
if (list.length < 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
const currentIndex = tabs.findIndex(x => x.key === this.current)
|
||||
const currentIndex = list.findIndex(x => x.key === current.value)
|
||||
let index = currentIndex + offset
|
||||
|
||||
if (index < 0) {
|
||||
index = tabs.length - 1
|
||||
index = list.length - 1
|
||||
}
|
||||
|
||||
if (index >= tabs.length) {
|
||||
if (index >= list.length) {
|
||||
index = 0
|
||||
}
|
||||
|
||||
return tabs[index]
|
||||
return list[index]
|
||||
}
|
||||
|
||||
// 快捷键切换最近文档
|
||||
if (e.altKey && e.ctrlKey) {
|
||||
if (e.key === 'ArrowLeft') {
|
||||
const prev = findTab(-1)
|
||||
prev && this.switchTab(prev)
|
||||
prev && switchTab(prev)
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
const next = findTab(1)
|
||||
next && this.switchTab(next)
|
||||
next && switchTab(next)
|
||||
}
|
||||
}
|
||||
},
|
||||
setTabs (list) {
|
||||
this.$store.commit('app/setTabs', list)
|
||||
},
|
||||
switchTab (item) {
|
||||
this.switchFile(item.payload.file)
|
||||
},
|
||||
removeTabs (items) {
|
||||
const keys = items.map(x => x.key)
|
||||
const tabs = this.tabs.filter(x => keys.indexOf(x.key) === -1)
|
||||
this.setTabs(tabs)
|
||||
},
|
||||
addTab (item) {
|
||||
const tab = this.tabs.find(x => item.key === x.key)
|
||||
|
||||
// 没有打开此 Tab,新建一个
|
||||
if (!tab) {
|
||||
this.setTabs(this.tabs.concat([item]))
|
||||
}
|
||||
|
||||
this.current = item.key
|
||||
},
|
||||
switchFile (file) {
|
||||
this.$store.commit('app/setCurrentFile', file)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('app', ['currentFile', 'tabs'])
|
||||
},
|
||||
watch: {
|
||||
currentFile: {
|
||||
immediate: true,
|
||||
handler (file) {
|
||||
const uri = File.toUri(file)
|
||||
const item = {
|
||||
key: uri,
|
||||
label: file ? file.name : '空白页',
|
||||
description: file ? file.path : '空白页',
|
||||
payload: { file },
|
||||
}
|
||||
|
||||
this.addTab(item)
|
||||
onBeforeMount(() => {
|
||||
window.addEventListener('keydown', keydownHandler, true)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', keydownHandler)
|
||||
})
|
||||
|
||||
watch(currentFile, file => {
|
||||
const uri = File.toUri(file)
|
||||
const item = {
|
||||
key: uri,
|
||||
label: file ? file.name : '空白页',
|
||||
description: file ? file.path : '空白页',
|
||||
payload: { file },
|
||||
}
|
||||
},
|
||||
tabs (list) {
|
||||
|
||||
addTab(item)
|
||||
}, { immediate: true })
|
||||
|
||||
watch(tabs, list => {
|
||||
if (list.length < 1) {
|
||||
this.addTab({
|
||||
addTab({
|
||||
key: blankUri,
|
||||
label: '空白页',
|
||||
description: '空白页',
|
||||
@@ -111,12 +113,21 @@ export default {
|
||||
})
|
||||
}
|
||||
|
||||
const tab = list.find(x => x.key === this.current)
|
||||
const tab = list.find((x: any) => x.key === current.value)
|
||||
if (!tab) {
|
||||
const currentFile = list.length > 0 ? list[list.length - 1].payload.file : null
|
||||
this.switchFile(currentFile)
|
||||
switchFile(currentFile)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
list,
|
||||
current,
|
||||
tabs,
|
||||
removeTabs,
|
||||
switchTab,
|
||||
setTabs,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,55 +1,67 @@
|
||||
<template>
|
||||
<XMask :show="!!show" @close="show = false">
|
||||
<QuickOpen @choose-file="show" @close="show = false" :with-marked="show && show.withMarked"></QuickOpen>
|
||||
<XMask :show="show" @close="callback = null">
|
||||
<QuickOpen @choose-file="callback" @close="callback = false" :with-marked="withMarked"></QuickOpen>
|
||||
</XMask>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import XMask from './Mask'
|
||||
import QuickOpen from './QuickOpen'
|
||||
import { encodeMarkdownLink } from '@/lib/utils'
|
||||
<script lang="ts">
|
||||
import { useStore } from 'vuex'
|
||||
import { computed, defineComponent, onMounted, onUnmounted, ref, toRef } from 'vue'
|
||||
import { encodeMarkdownLink } from '../useful/utils'
|
||||
import { useBus } from '../useful/bus'
|
||||
import XMask from './Mask.vue'
|
||||
import QuickOpen from './QuickOpen.vue'
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'x-filter',
|
||||
components: { QuickOpen, XMask },
|
||||
data () {
|
||||
return {
|
||||
show: false,
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('keydown', this.keydownHandler, true)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.keydownHandler)
|
||||
},
|
||||
methods: {
|
||||
keydownHandler (e) {
|
||||
setup () {
|
||||
const store = useStore()
|
||||
const bus = useBus()
|
||||
|
||||
const currentFile = toRef(store.state, 'currentFile')
|
||||
const callback = ref<Function | null>(null)
|
||||
const withMarked = ref(true)
|
||||
|
||||
function keydownHandler (e: KeyboardEvent) {
|
||||
if (e.key === 'i' && e.ctrlKey && e.altKey) {
|
||||
this.show = f => {
|
||||
if (this.currentFile) {
|
||||
const relativePath = f.path.replace(this.currentFile.path.substr(0, this.currentFile.path.lastIndexOf('/')), '.')
|
||||
this.$bus.emit('editor-insert-value', `[${f.name.replace(/\.[^.]+$/, '')}](${encodeMarkdownLink(relativePath)})`)
|
||||
callback.value = (f: any) => {
|
||||
const file = currentFile.value
|
||||
if (file) {
|
||||
const relativePath = f.path.replace(file.path.substr(0, file.path.lastIndexOf('/')), '.')
|
||||
bus.emit('editor-insert-value', `[${f.name.replace(/\.[^.]+$/, '')}](${encodeMarkdownLink(relativePath)})`)
|
||||
}
|
||||
this.show = false
|
||||
callback.value = null
|
||||
}
|
||||
this.show.withMarked = false
|
||||
withMarked.value = false
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
} else if (e.key === 'p' && e.ctrlKey) {
|
||||
this.show = f => {
|
||||
this.$store.commit('app/setCurrentFile', f)
|
||||
this.show = false
|
||||
callback.value = (f: any) => {
|
||||
store.commit('setCurrentFile', f)
|
||||
callback.value = null
|
||||
}
|
||||
this.show.withMarked = true
|
||||
withMarked.value = true
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', keydownHandler, true)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', keydownHandler)
|
||||
})
|
||||
|
||||
const show = computed(() => !!callback.value)
|
||||
|
||||
return {
|
||||
show,
|
||||
callback,
|
||||
withMarked,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('app', ['currentFile'])
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -30,69 +30,56 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
<script lang="ts">
|
||||
import { defineComponent, onBeforeUnmount, onMounted, nextTick, ref, toRefs } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useBus } from '../useful/bus'
|
||||
import { $args } from '../useful/global-args'
|
||||
|
||||
let resizeOrigin = null
|
||||
let resizeOrigin: any = null
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'layout',
|
||||
mounted () {
|
||||
window.addEventListener('resize', this.emitResize)
|
||||
window.document.addEventListener('mousemove', this.resizeFrame)
|
||||
this.$bus.on('toggle-view', this.toggleView)
|
||||
this.$bus.on('toggle-side', this.toggleSide)
|
||||
this.$bus.on('toggle-xterm', this.toggleXterm)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('resize', this.emitResize)
|
||||
window.document.removeEventListener('mousemove', this.resizeFrame)
|
||||
this.$bus.off('toggle-side', this.toggleSide)
|
||||
this.$bus.off('toggle-view', this.toggleView)
|
||||
this.$bus.off('toggle-xterm', this.toggleXterm)
|
||||
},
|
||||
methods: {
|
||||
emitResize () {
|
||||
this.$bus.emit('resize')
|
||||
},
|
||||
toggleSide () {
|
||||
this.$store.commit('app/setShowSide', !this.showSide)
|
||||
this.$nextTick(() => this.$bus.emit('resize'), 500)
|
||||
},
|
||||
toggleView () {
|
||||
this.$store.commit('app/setShowView', !this.showView)
|
||||
this.$nextTick(() => this.$bus.emit('resize'), 500)
|
||||
},
|
||||
toggleXterm (val) {
|
||||
const showXterm = typeof val === 'boolean' ? val : !this.showXterm
|
||||
setup () {
|
||||
const bus = useBus()
|
||||
const store = useStore()
|
||||
|
||||
this.$store.commit('app/setShowXterm', showXterm)
|
||||
const { showView, showXterm, showSide } = toRefs(store.state)
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$nextTick(() => this.$bus.emit('resize'), 500)
|
||||
const aside = ref(null)
|
||||
const editor = ref(null)
|
||||
const terminal = ref(null)
|
||||
const refs: any = { aside, editor, terminal }
|
||||
|
||||
if (showXterm) {
|
||||
this.$bus.emit('xterm-init')
|
||||
function toggleSide () {
|
||||
store.commit('setShowSide', !showSide.value)
|
||||
nextTick(() => bus.emit('resize'))
|
||||
}
|
||||
|
||||
function toggleView () {
|
||||
store.commit('setShowView', !showView.value)
|
||||
nextTick(() => bus.emit('resize'))
|
||||
}
|
||||
|
||||
function toggleXterm (val?: boolean) {
|
||||
const show = typeof val === 'boolean' ? val : !showXterm.value
|
||||
|
||||
store.commit('setShowXterm', show)
|
||||
|
||||
nextTick(() => {
|
||||
nextTick(() => bus.emit('resize'))
|
||||
|
||||
if (showXterm.value) {
|
||||
bus.emit('xterm-init')
|
||||
}
|
||||
})
|
||||
},
|
||||
initResize (type, ref, min, max, e) {
|
||||
if (!resizeOrigin && type) {
|
||||
resizeOrigin = {
|
||||
min,
|
||||
max,
|
||||
type,
|
||||
ref,
|
||||
mouseX: e.clientX,
|
||||
mouseY: e.clientY,
|
||||
targetWidth: this.$refs[ref].clientWidth,
|
||||
targetHeight: this.$refs[ref].clientHeight,
|
||||
targetLeft: this.$refs[ref].clientLeft,
|
||||
targetTop: this.$refs[ref].clientTop,
|
||||
}
|
||||
}
|
||||
},
|
||||
resizeFrame (e) {
|
||||
}
|
||||
|
||||
function emitResize () {
|
||||
bus.emit('resize')
|
||||
}
|
||||
|
||||
function resizeFrame (e: MouseEvent) {
|
||||
if (e.buttons !== 1) {
|
||||
resizeOrigin = null
|
||||
return
|
||||
@@ -102,25 +89,70 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
const ref = refs[resizeOrigin.ref].value
|
||||
|
||||
if (resizeOrigin.type === 'right') {
|
||||
const offsetX = e.clientX - resizeOrigin.mouseX
|
||||
const width = (resizeOrigin.targetWidth + offsetX)
|
||||
this.$refs[resizeOrigin.ref].style.width = Math.min(resizeOrigin.max, Math.max(resizeOrigin.min, width)) + 'px'
|
||||
ref.style.width = Math.min(resizeOrigin.max, Math.max(resizeOrigin.min, width)) + 'px'
|
||||
} else if (resizeOrigin.type === 'top') {
|
||||
const offsetY = -(e.clientY - resizeOrigin.mouseY)
|
||||
const height = (resizeOrigin.targetHeight + offsetY)
|
||||
this.$refs[resizeOrigin.ref].style.height = Math.min(resizeOrigin.max, Math.max(resizeOrigin.min, height)) + 'px'
|
||||
ref.style.height = Math.min(resizeOrigin.max, Math.max(resizeOrigin.min, height)) + 'px'
|
||||
}
|
||||
this.$bus.emit('resize')
|
||||
bus.emit('resize')
|
||||
}
|
||||
|
||||
function initResize (type: any, ref: any, min: any, max: any, e: any) {
|
||||
const refEl = refs[ref].value
|
||||
if (!resizeOrigin && type) {
|
||||
resizeOrigin = {
|
||||
min,
|
||||
max,
|
||||
type,
|
||||
ref,
|
||||
mouseX: e.clientX,
|
||||
mouseY: e.clientY,
|
||||
targetWidth: refEl.clientWidth,
|
||||
targetHeight: refEl.clientHeight,
|
||||
targetLeft: refEl.clientLeft,
|
||||
targetTop: refEl.clientTop,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
bus.on('toggle-view', toggleView)
|
||||
bus.on('toggle-side', toggleSide)
|
||||
bus.on('toggle-xterm', toggleXterm)
|
||||
|
||||
window.addEventListener('resize', emitResize)
|
||||
window.document.addEventListener('mousemove', resizeFrame)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
bus.off('toggle-side', toggleSide)
|
||||
bus.off('toggle-view', toggleView)
|
||||
bus.off('toggle-xterm', toggleXterm)
|
||||
|
||||
window.removeEventListener('resize', emitResize)
|
||||
window.document.removeEventListener('mousemove', resizeFrame)
|
||||
})
|
||||
|
||||
const showFooter = $args().get('show-status-bar') !== 'false'
|
||||
|
||||
return {
|
||||
initResize,
|
||||
showSide,
|
||||
showXterm,
|
||||
showFooter,
|
||||
showView,
|
||||
aside,
|
||||
editor,
|
||||
terminal,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('app', ['showView', 'showXterm', 'showSide']),
|
||||
showFooter () {
|
||||
return window.$args().get('show-status-bar') !== 'false'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
<script lang="ts">
|
||||
import { defineComponent, onBeforeUnmount, onMounted } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'x-mask',
|
||||
props: {
|
||||
show: Boolean,
|
||||
@@ -20,22 +22,24 @@ export default {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('keydown', this.keydownHandler, true)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.keydownHandler)
|
||||
},
|
||||
methods: {
|
||||
keydownHandler (e) {
|
||||
if (e.key === 'Escape' && this.show) {
|
||||
this.escClooseable && this.$emit('close')
|
||||
} else if (e.key === 'Enter' && this.show) {
|
||||
this.$emit('enter')
|
||||
setup (props, { emit }) {
|
||||
function keydownHandler (e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && props.show) {
|
||||
props.escClooseable && emit('close')
|
||||
} else if (e.key === 'Enter' && props.show) {
|
||||
emit('key-enter')
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', keydownHandler, true)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', keydownHandler)
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<XMask :show="show" @close="cancel" @enter="ok" :mask-closeable="false" esc-closeable>
|
||||
<XMask :show="show" @close="cancel" @key-enter="ok" :mask-closeable="false" esc-closeable>
|
||||
<div class="wrapper" @click.stop>
|
||||
<h4>{{title}}</h4>
|
||||
<p v-if="content">{{content}}</p>
|
||||
<input v-if="type === 'input'" ref="input" :type="inputType" :placeholder="inputHint" v-model="inputValue">
|
||||
<input v-if="type === 'input'" ref="refInput" :type="inputType" :placeholder="inputHint" v-model="inputValue">
|
||||
<div class="action">
|
||||
<button @click="cancel">取消</button>
|
||||
<button class="primary" @click="ok">确定</button>
|
||||
@@ -12,77 +12,101 @@
|
||||
</XMask>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import XMask from './Mask'
|
||||
<script lang="ts">
|
||||
import { defineComponent, nextTick, ref } from 'vue'
|
||||
import XMask from './Mask.vue'
|
||||
import { Components } from '../types'
|
||||
|
||||
export default {
|
||||
type ModalType = '' | 'confirm' | 'input'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'modal-input',
|
||||
components: { XMask },
|
||||
data () {
|
||||
return {
|
||||
type: '',
|
||||
show: false,
|
||||
title: '',
|
||||
content: '',
|
||||
inputType: '',
|
||||
inputValue: '',
|
||||
inputHint: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handle (val) {
|
||||
this.show = false
|
||||
this.inputValue = ''
|
||||
setup () {
|
||||
const refInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const type = ref<ModalType>('')
|
||||
const show = ref(false)
|
||||
const title = ref('')
|
||||
const content = ref('')
|
||||
const inputType = ref('')
|
||||
const inputValue = ref('')
|
||||
const inputHint = ref('')
|
||||
|
||||
let resolveFun: Function | null = null
|
||||
|
||||
function handle (val: any) {
|
||||
show.value = false
|
||||
inputValue.value = ''
|
||||
|
||||
try {
|
||||
this.resolve && this.resolve(val)
|
||||
} catch (error) {
|
||||
throw error
|
||||
resolveFun && resolveFun(val)
|
||||
} finally {
|
||||
this.resolve = null
|
||||
resolveFun = null
|
||||
}
|
||||
},
|
||||
cancel () {
|
||||
this.handle(this.type === 'input' ? null : false)
|
||||
},
|
||||
ok () {
|
||||
this.handle(this.type === 'input' ? this.inputValue : true)
|
||||
},
|
||||
confirm ({ title, content }) {
|
||||
this.type = 'confirm'
|
||||
this.title = title || '提示'
|
||||
this.content = content || ''
|
||||
this.show = true
|
||||
}
|
||||
|
||||
function cancel () {
|
||||
handle(type.value === 'input' ? null : false)
|
||||
}
|
||||
|
||||
function ok () {
|
||||
handle(type.value === 'input' ? inputValue.value : true)
|
||||
}
|
||||
|
||||
function confirm (params: Components.Modal.ConfirmModalParams): Promise<boolean> {
|
||||
type.value = 'confirm'
|
||||
title.value = params.title || '提示'
|
||||
content.value = params.content || ''
|
||||
show.value = true
|
||||
|
||||
return new Promise(resolve => {
|
||||
this.resolve = resolve
|
||||
resolveFun = resolve
|
||||
})
|
||||
},
|
||||
input ({ type, title, hint, value, content, select = true }) {
|
||||
this.type = 'input'
|
||||
this.title = title || '请输入'
|
||||
this.content = content || ''
|
||||
this.inputType = type || 'text'
|
||||
this.inputValue = value || ''
|
||||
this.inputHint = hint || ''
|
||||
}
|
||||
|
||||
this.show = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.input.focus()
|
||||
function input (params: Components.Modal.InputModalParams): Promise<string> {
|
||||
type.value = 'input'
|
||||
title.value = params.title || '请输入'
|
||||
content.value = params.content || ''
|
||||
inputType.value = params.type || 'text'
|
||||
inputValue.value = params.value || ''
|
||||
inputHint.value = params.hint || ''
|
||||
|
||||
if (select) {
|
||||
this.$refs.input.select()
|
||||
if (Array.isArray(select)) {
|
||||
this.$refs.input.setSelectionRange(...select)
|
||||
show.value = true
|
||||
|
||||
nextTick(() => {
|
||||
refInput.value!!.focus()
|
||||
|
||||
if (params.select) {
|
||||
refInput.value!!.select()
|
||||
if (Array.isArray(params.select)) {
|
||||
refInput.value!!.setSelectionRange(...params.select)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return new Promise(resolve => {
|
||||
this.resolve = resolve
|
||||
resolveFun = resolve
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
refInput,
|
||||
ok,
|
||||
cancel,
|
||||
confirm,
|
||||
input,
|
||||
type,
|
||||
show,
|
||||
title,
|
||||
content,
|
||||
inputType,
|
||||
inputValue,
|
||||
inputHint,
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,51 +1,42 @@
|
||||
<template>
|
||||
<div id="editor"></div>
|
||||
<div id="editor" ref="refEditor"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import TurndownService from 'turndown'
|
||||
import * as monaco from 'monaco-editor'
|
||||
import { defineComponent, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { $args } from '../useful/global-args'
|
||||
import env from '../useful/env'
|
||||
|
||||
const models = {}
|
||||
const models: {[key: string]: monaco.editor.ITextModel} = {}
|
||||
|
||||
const keys = {}
|
||||
const keys: {[key: string]: boolean} = {}
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'monaco-editor',
|
||||
mounted () {
|
||||
this.editor = null
|
||||
if (!window.require) {
|
||||
const loaderScript = document.createElement('script')
|
||||
loaderScript.type = 'text/javascript'
|
||||
loaderScript.src = 'vs/loader.js'
|
||||
loaderScript.addEventListener('load', this.onGotAmdLoader)
|
||||
document.body.appendChild(loaderScript)
|
||||
} else {
|
||||
this.onGotAmdLoader()
|
||||
}
|
||||
setup (_, { emit }) {
|
||||
let editor: monaco.editor.IStandaloneCodeEditor | null = null
|
||||
const refEditor = ref<HTMLElement | null>(null)
|
||||
|
||||
window.addEventListener('keydown', this.recordKeys, true)
|
||||
window.addEventListener('keyup', this.recordKeys, true)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('paste', this.paste)
|
||||
window.removeEventListener('keydown', this.recordKeys)
|
||||
window.removeEventListener('keyup', this.recordKeys)
|
||||
},
|
||||
methods: {
|
||||
getModel (uri, value) {
|
||||
const getMonaco = () => (window as any).monaco
|
||||
const getEditor = () => editor!!
|
||||
const resize = () => editor && editor.layout()
|
||||
|
||||
function getModel (uri: string, value: string) {
|
||||
let model = models[uri]
|
||||
|
||||
if (!model) {
|
||||
model = window.monaco.editor.createModel(value, undefined, window.monaco.Uri.parse(uri))
|
||||
model.onDidChangeContent(e => {
|
||||
model = getMonaco().editor.createModel(value, undefined, getMonaco().Uri.parse(uri))
|
||||
model.onDidChangeContent(() => {
|
||||
let value = model.getValue()
|
||||
const eol = model.getEOL()
|
||||
if (!value.endsWith(eol)) {
|
||||
value += eol
|
||||
model.setValue(value)
|
||||
}
|
||||
this.$emit('change', { uri, value })
|
||||
emit('change', { uri, value })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -56,34 +47,213 @@ export default {
|
||||
models[uri] = model
|
||||
|
||||
return model
|
||||
},
|
||||
recordKeys (e) {
|
||||
}
|
||||
|
||||
function setModel (uri: string, value: string) {
|
||||
const model = getModel(uri, value || '')
|
||||
getEditor().setModel(model)
|
||||
}
|
||||
|
||||
function addAttachment () {
|
||||
const input = window.document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.onchange = () => {
|
||||
for (let i = 0; i < input.files!!.length; i++) {
|
||||
emit('upload-file', input.files!![i])
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
function insert (text: string) {
|
||||
const selection = getEditor().getSelection()!!
|
||||
getEditor().executeEdits('', [
|
||||
{
|
||||
range: new (getMonaco().Range)(selection.endLineNumber, selection.endColumn, selection.endLineNumber, selection.endColumn),
|
||||
text,
|
||||
forceMoveMarkers: true
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
function keyBind () {
|
||||
const KM = getMonaco().KeyMod
|
||||
const KC = getMonaco().KeyCode
|
||||
|
||||
getEditor().addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_F, addAttachment)
|
||||
|
||||
getEditor().addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_D, () => {
|
||||
insert(dayjs().format('YYYY-MM-DD'))
|
||||
})
|
||||
|
||||
getEditor().addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_T, () => {
|
||||
insert(dayjs().format('HH:mm:ss'))
|
||||
})
|
||||
|
||||
getEditor().addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_R, () => {
|
||||
emit('xterm-run', getEditor().getModel()!!.getValueInRange(getEditor().getSelection()!!))
|
||||
})
|
||||
|
||||
getEditor().addCommand(KM.CtrlCmd | KC.Enter, () => {
|
||||
insert(getEditor().getModel()!!.getEOL())
|
||||
})
|
||||
|
||||
getEditor().addCommand(KM.Shift | KC.Enter, () => {
|
||||
// getOneIndent 接口被移除了 https://github.com/microsoft/monaco-editor/issues/1565
|
||||
const getOneIndent = (editor: monaco.editor.IStandaloneCodeEditor) => {
|
||||
const options = getEditor().getModel()!!.getOptions()
|
||||
return options.insertSpaces ? ' '.repeat(options.tabSize) : '\t'
|
||||
}
|
||||
|
||||
insert(getOneIndent(getEditor()))
|
||||
})
|
||||
|
||||
getEditor().addCommand(KM.CtrlCmd | KM.Shift | KC.UpArrow, () => {
|
||||
getEditor().getAction('editor.action.moveLinesUpAction').run()
|
||||
})
|
||||
|
||||
getEditor().addCommand(KM.CtrlCmd | KM.Shift | KC.DownArrow, () => {
|
||||
getEditor().getAction('editor.action.moveLinesDownAction').run()
|
||||
})
|
||||
|
||||
getEditor().addCommand(KM.CtrlCmd | KM.Shift | KC.KEY_D, () => {
|
||||
getEditor().getAction('editor.action.copyLinesDownAction').run()
|
||||
})
|
||||
|
||||
getEditor().addCommand(KM.CtrlCmd | KC.KEY_S, () => {
|
||||
emit('save')
|
||||
})
|
||||
|
||||
getEditor().addCommand(KM.CtrlCmd | KC.KEY_J, () => {
|
||||
getEditor().getAction('editor.action.joinLines').run()
|
||||
})
|
||||
|
||||
getEditor().addCommand(KM.chord(KM.CtrlCmd | KC.KEY_K, KM.CtrlCmd | KC.KEY_U), () => {
|
||||
getEditor().getAction('editor.action.transformToUppercase').run()
|
||||
})
|
||||
|
||||
getEditor().addCommand(KM.chord(KM.CtrlCmd | KC.KEY_K, KM.CtrlCmd | KC.KEY_L), () => {
|
||||
getEditor().getAction('editor.action.transformToLowercase').run()
|
||||
})
|
||||
|
||||
getMonaco().languages.setLanguageConfiguration('markdown', {
|
||||
onEnterRules: [
|
||||
{ beforeText: /^\s*> .*$/, action: { indentAction: getMonaco().languages.IndentAction.None, appendText: '> ' } },
|
||||
{ beforeText: /^\s*\+ \[ \] .*$/, action: { indentAction: getMonaco().languages.IndentAction.None, appendText: '+ [ ] ' } },
|
||||
{ beforeText: /^\s*- \[ \] .*$/, action: { indentAction: getMonaco().languages.IndentAction.None, appendText: '- [ ] ' } },
|
||||
{ beforeText: /^\s*\* \[ \] .*$/, action: { indentAction: getMonaco().languages.IndentAction.None, appendText: '* [ ] ' } },
|
||||
{ beforeText: /^\s*\+ \[x\] .*$/, action: { indentAction: getMonaco().languages.IndentAction.None, appendText: '+ [ ] ' } },
|
||||
{ beforeText: /^\s*- \[x\] .*$/, action: { indentAction: getMonaco().languages.IndentAction.None, appendText: '- [ ] ' } },
|
||||
{ beforeText: /^\s*\* \[x\] .*$/, action: { indentAction: getMonaco().languages.IndentAction.None, appendText: '* [ ] ' } },
|
||||
{ beforeText: /^\s*\+ .*$/, action: { indentAction: getMonaco().languages.IndentAction.None, appendText: '+ ' } },
|
||||
{ beforeText: /^\s*- .*$/, action: { indentAction: getMonaco().languages.IndentAction.None, appendText: '- ' } },
|
||||
{ beforeText: /^\s*\* .*$/, action: { indentAction: getMonaco().languages.IndentAction.None, appendText: '* ' } },
|
||||
{ beforeText: /^\s*1. .*$/, action: { indentAction: getMonaco().languages.IndentAction.None, appendText: '1. ' } }
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
function paste (e: ClipboardEvent) {
|
||||
if (getEditor().hasTextFocus()) {
|
||||
const items = e.clipboardData!!.items
|
||||
if (keys.m || keys.M) { // 粘贴 HTML 转为 markdown
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].type.match(/^text\/html$/i)) {
|
||||
items[i].getAsString(str => {
|
||||
const md = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced', bulletListMarker: '+' }).turndown(str)
|
||||
insert(md)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
} else {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const fileType = items[i].type
|
||||
if (fileType.match(/^image\/(png|jpg|jpeg|gif)$/i)) {
|
||||
const asBase64 = keys.b || keys.B // Ctrl + v 粘贴的同时 按下了 B 键,就粘贴 base64 图像
|
||||
emit('paste-img', items[i].getAsFile(), asBase64)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function replaceLine (line: number, text: string) {
|
||||
const length = getEditor().getModel()!!.getLineLength(line)
|
||||
|
||||
getEditor().executeEdits('', [
|
||||
{ range: new (getMonaco().Range)(line, 1, line, length + 1), text }
|
||||
])
|
||||
}
|
||||
|
||||
function revealLineInCenter (line: number) {
|
||||
getEditor().revealLineInCenter(line)
|
||||
}
|
||||
|
||||
function revealLine (line: number) {
|
||||
getEditor().revealLine(line)
|
||||
}
|
||||
|
||||
function setScrollToTop (top: number) {
|
||||
getEditor().setScrollTop(top)
|
||||
}
|
||||
|
||||
function switchTodo (line: number, checked: boolean) {
|
||||
if (checked) {
|
||||
const value = getEditor().getModel()!!.getLineContent(line).replace('[ ]', `[x] ~~${dayjs().format('YYYY-MM-DD HH:mm')}~~`)
|
||||
replaceLine(line, value)
|
||||
} else {
|
||||
const value = getEditor().getModel()!!.getLineContent(line).replace(/(\[x\] ~~[\d-: ]+~~|\[x\])/, '[ ]')
|
||||
replaceLine(line, value)
|
||||
}
|
||||
}
|
||||
|
||||
function getValue () {
|
||||
return getEditor().getModel()!!.getValue(getMonaco().editor.DefaultEndOfLine.LF)
|
||||
}
|
||||
|
||||
function replaceValue (oldValue: string, newValue: string) {
|
||||
getEditor().getModel()!!.setValue(getValue().replace(oldValue, newValue))
|
||||
}
|
||||
|
||||
function getDucumentInfo () {
|
||||
const selection = getEditor().getSelection()!!
|
||||
|
||||
return {
|
||||
line: selection.positionLineNumber,
|
||||
column: selection.positionColumn,
|
||||
lineCount: getEditor().getModel()!!.getLineCount(),
|
||||
textLength: getValue().length,
|
||||
selectedLength: getEditor().getModel()!!.getValueInRange(selection).length
|
||||
}
|
||||
}
|
||||
|
||||
function toggleWrap () {
|
||||
const isWrapping = getEditor().getOption(monaco.editor.EditorOption.wrappingInfo).isViewportWrapping
|
||||
getEditor().updateOptions({ wordWrap: (isWrapping ? 'off' : 'on') })
|
||||
}
|
||||
|
||||
function recordKeys (e: KeyboardEvent) {
|
||||
if (e.type === 'keydown') {
|
||||
keys[e.key] = true
|
||||
} else {
|
||||
keys[e.key] = false
|
||||
}
|
||||
},
|
||||
resize () {
|
||||
if (this.editor) {
|
||||
this.editor.layout()
|
||||
}
|
||||
},
|
||||
onGotAmdLoader () {
|
||||
window.require(['vs/editor/editor.main'], () => {
|
||||
this.initMonaco()
|
||||
})
|
||||
},
|
||||
initMonaco () {
|
||||
window.supportedExtensions = window.monaco.languages.getLanguages().flatMap(x => x.extensions)
|
||||
}
|
||||
|
||||
const model = this.getModel('yank-note://system/blank.md', '')
|
||||
function initMonaco () {
|
||||
// window.supportedExtensions = getMonaco().languages.getLanguages().flatMap(x => x.extensions)
|
||||
|
||||
this.editor = window.monaco.editor.create(window.document.getElementById('editor'), {
|
||||
const model = getModel('yank-note://system/blank.md', '')
|
||||
|
||||
editor = (window as any).monaco.editor.create(refEditor.value, {
|
||||
value: '',
|
||||
theme: 'vs-dark',
|
||||
fontSize: 18,
|
||||
wordWrap: false,
|
||||
links: !env.isElectron,
|
||||
// wordWrapColumn: 40,
|
||||
// Set this to false to not auto word wrap minified files
|
||||
wordWrapMinified: true,
|
||||
@@ -97,197 +267,66 @@ export default {
|
||||
verticalScrollbarSize: 0
|
||||
},
|
||||
model,
|
||||
readOnly: window.$args().get('readonly') === 'true',
|
||||
readOnly: $args().get('readonly') === 'true',
|
||||
})
|
||||
|
||||
this.editor.onDidChangeCursorSelection(e => {
|
||||
this.$emit('change-document', this.getDucumentInfo())
|
||||
getEditor().onDidChangeCursorSelection(() => {
|
||||
emit('change-document', getDucumentInfo())
|
||||
})
|
||||
|
||||
this.editor.onDidScrollChange(e => {
|
||||
const line = this.editor.getVisibleRanges()[0].startLineNumber
|
||||
const top = this.editor.getScrollTop()
|
||||
this.$emit('scroll-view', { line, top })
|
||||
getEditor().onDidScrollChange(() => {
|
||||
const line = getEditor().getVisibleRanges()[0].startLineNumber
|
||||
const top = getEditor().getScrollTop()
|
||||
emit('scroll-view', { line, top })
|
||||
})
|
||||
|
||||
this.keyBind()
|
||||
window.addEventListener('paste', this.paste, true)
|
||||
keyBind()
|
||||
window.addEventListener('paste', paste as any, true)
|
||||
|
||||
setTimeout(() => {
|
||||
this.$emit('ready')
|
||||
emit('ready')
|
||||
}, 500)
|
||||
},
|
||||
keyBind () {
|
||||
const KM = window.monaco.KeyMod
|
||||
const KC = window.monaco.KeyCode
|
||||
|
||||
this.editor.addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_F, this.addAttachment)
|
||||
|
||||
this.editor.addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_D, () => {
|
||||
this.insert(dayjs().format('YYYY-MM-DD'))
|
||||
})
|
||||
|
||||
this.editor.addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_T, () => {
|
||||
this.insert(dayjs().format('HH:mm:ss'))
|
||||
})
|
||||
|
||||
this.editor.addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_R, () => {
|
||||
this.$emit('xterm-run', this.editor.getModel().getValueInRange(this.editor.getSelection()))
|
||||
})
|
||||
|
||||
this.editor.addCommand(KM.CtrlCmd | KC.Enter, () => {
|
||||
this.insert(this.editor.getModel().getEOL())
|
||||
})
|
||||
|
||||
this.editor.addCommand(KM.Shift | KC.Enter, () => {
|
||||
// getOneIndent 接口被移除了 https://github.com/microsoft/monaco-editor/issues/1565
|
||||
const getOneIndent = editor => {
|
||||
const options = editor.getModel().getOptions()
|
||||
return options.insertSpaces ? ' '.repeat(options.tabSize) : '\t'
|
||||
}
|
||||
|
||||
this.insert(getOneIndent(this.editor))
|
||||
})
|
||||
|
||||
this.editor.addCommand(KM.CtrlCmd | KM.Shift | KC.UpArrow, () => {
|
||||
this.editor.getAction('editor.action.moveLinesUpAction').run()
|
||||
})
|
||||
|
||||
this.editor.addCommand(KM.CtrlCmd | KM.Shift | KC.DownArrow, () => {
|
||||
this.editor.getAction('editor.action.moveLinesDownAction').run()
|
||||
})
|
||||
|
||||
this.editor.addCommand(KM.CtrlCmd | KM.Shift | KC.KEY_D, () => {
|
||||
this.editor.getAction('editor.action.copyLinesDownAction').run()
|
||||
})
|
||||
|
||||
this.editor.addCommand(KM.CtrlCmd | KC.KEY_S, () => {
|
||||
this.$emit('save')
|
||||
})
|
||||
|
||||
this.editor.addCommand(KM.CtrlCmd | KC.KEY_J, () => {
|
||||
this.editor.getAction('editor.action.joinLines').run()
|
||||
})
|
||||
|
||||
this.editor.addCommand(KM.chord(KM.CtrlCmd | KC.KEY_K, KM.CtrlCmd | KC.KEY_U), () => {
|
||||
this.editor.getAction('editor.action.transformToUppercase').run()
|
||||
})
|
||||
|
||||
this.editor.addCommand(KM.chord(KM.CtrlCmd | KC.KEY_K, KM.CtrlCmd | KC.KEY_L), () => {
|
||||
this.editor.getAction('editor.action.transformToLowercase').run()
|
||||
})
|
||||
|
||||
window.monaco.languages.setLanguageConfiguration('markdown', {
|
||||
onEnterRules: [
|
||||
{ beforeText: /^\s*> .*$/, action: { indentAction: window.monaco.languages.IndentAction.None, appendText: '> ' } },
|
||||
{ beforeText: /^\s*\+ \[ \] .*$/, action: { indentAction: window.monaco.languages.IndentAction.None, appendText: '+ [ ] ' } },
|
||||
{ beforeText: /^\s*- \[ \] .*$/, action: { indentAction: window.monaco.languages.IndentAction.None, appendText: '- [ ] ' } },
|
||||
{ beforeText: /^\s*\* \[ \] .*$/, action: { indentAction: window.monaco.languages.IndentAction.None, appendText: '* [ ] ' } },
|
||||
{ beforeText: /^\s*\+ \[x\] .*$/, action: { indentAction: window.monaco.languages.IndentAction.None, appendText: '+ [ ] ' } },
|
||||
{ beforeText: /^\s*- \[x\] .*$/, action: { indentAction: window.monaco.languages.IndentAction.None, appendText: '- [ ] ' } },
|
||||
{ beforeText: /^\s*\* \[x\] .*$/, action: { indentAction: window.monaco.languages.IndentAction.None, appendText: '* [ ] ' } },
|
||||
{ beforeText: /^\s*\+ .*$/, action: { indentAction: window.monaco.languages.IndentAction.None, appendText: '+ ' } },
|
||||
{ beforeText: /^\s*- .*$/, action: { indentAction: window.monaco.languages.IndentAction.None, appendText: '- ' } },
|
||||
{ beforeText: /^\s*\* .*$/, action: { indentAction: window.monaco.languages.IndentAction.None, appendText: '* ' } },
|
||||
{ beforeText: /^\s*1. .*$/, action: { indentAction: window.monaco.languages.IndentAction.None, appendText: '1. ' } }
|
||||
]
|
||||
})
|
||||
},
|
||||
paste (e) {
|
||||
if (this.editor.hasTextFocus()) {
|
||||
const items = e.clipboardData.items
|
||||
if (keys['m'] || keys['M']) { // 粘贴 HTML 转为 markdown
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].type.match(/^text\/html$/i)) {
|
||||
items[i].getAsString(str => {
|
||||
const md = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced', bulletListMarker: '+' }).turndown(str)
|
||||
this.insert(md)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
} else {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const fileType = items[i].type
|
||||
if (fileType.match(/^image\/(png|jpg|jpeg|gif)$/i)) {
|
||||
const asBase64 = keys['b'] || keys['B'] // Ctrl + v 粘贴的同时 按下了 B 键,就粘贴 base64 图像
|
||||
this.$emit('paste-img', items[i].getAsFile(), asBase64)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
addAttachment () {
|
||||
const input = window.document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.onchange = () => {
|
||||
for (let i = 0; i < input.files.length; i++) {
|
||||
this.$emit('upload-file', input.files[i])
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
},
|
||||
insert (text) {
|
||||
const selection = this.editor.getSelection()
|
||||
this.editor.executeEdits('', [
|
||||
{
|
||||
range: new window.monaco.Range(selection.endLineNumber, selection.endColumn, selection.endLineNumber, selection.endColumn),
|
||||
text,
|
||||
forceMoveMarkers: true
|
||||
}
|
||||
])
|
||||
},
|
||||
replaceLine (line, text) {
|
||||
const length = this.editor.getModel().getLineLength(line)
|
||||
|
||||
this.editor.executeEdits('', [
|
||||
{ range: new window.monaco.Range(line, 1, line, length + 1), text }
|
||||
])
|
||||
},
|
||||
revealLineInCenter (line) {
|
||||
this.editor.revealLineInCenter(line)
|
||||
},
|
||||
setScrollToTop (top) {
|
||||
this.editor.setScrollTop(top)
|
||||
},
|
||||
switchTodo (line, checked) {
|
||||
if (checked) {
|
||||
const value = this.editor.getModel().getLineContent(line).replace('[ ]', `[x] ~~${dayjs().format('YYYY-MM-DD HH:mm')}~~`)
|
||||
this.replaceLine(line, value)
|
||||
} else {
|
||||
const value = this.editor.getModel().getLineContent(line).replace(/(\[x\] ~~[\d-: ]+~~|\[x\])/, '[ ]')
|
||||
this.replaceLine(line, value)
|
||||
}
|
||||
},
|
||||
getValue () {
|
||||
return this.editor.getModel().getValue(window.monaco.editor.DefaultEndOfLine.LF)
|
||||
},
|
||||
setModel (uri, value) {
|
||||
const model = this.getModel(uri, value || '')
|
||||
this.editor.setModel(model)
|
||||
},
|
||||
replaceValue (oldValue, newValue) {
|
||||
this.editor.getModel().setValue(this.getValue().replace(oldValue, newValue))
|
||||
},
|
||||
getDucumentInfo () {
|
||||
const selection = this.editor.getSelection()
|
||||
|
||||
return {
|
||||
line: selection.positionLineNumber,
|
||||
column: selection.positionColumn,
|
||||
lineCount: this.editor.getModel().getLineCount(),
|
||||
textLength: this.getValue().length,
|
||||
selectedLength: this.editor.getModel().getValueInRange(selection).length
|
||||
}
|
||||
},
|
||||
toggleWrap () {
|
||||
const isWrapping = this.editor.getConfiguration().wrappingInfo.isViewportWrapping
|
||||
this.editor.updateOptions({ wordWrap: !isWrapping })
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
function onGotAmdLoader () {
|
||||
(window as any).require(['vs/editor/editor.main'], initMonaco)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!window.require) {
|
||||
const loaderScript = document.createElement('script')
|
||||
loaderScript.type = 'text/javascript'
|
||||
loaderScript.src = 'vs/loader.js'
|
||||
loaderScript.addEventListener('load', onGotAmdLoader)
|
||||
document.body.appendChild(loaderScript)
|
||||
} else {
|
||||
onGotAmdLoader()
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', recordKeys, true)
|
||||
window.addEventListener('keyup', recordKeys, true)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('paste', paste as any)
|
||||
window.removeEventListener('keydown', recordKeys)
|
||||
window.removeEventListener('keyup', recordKeys)
|
||||
})
|
||||
|
||||
return {
|
||||
refEditor,
|
||||
resize,
|
||||
revealLine,
|
||||
revealLineInCenter,
|
||||
setScrollToTop,
|
||||
switchTodo,
|
||||
replaceValue,
|
||||
toggleWrap,
|
||||
setModel,
|
||||
insert,
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
+270
-225
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div ref="view-wrapper" class="view" @scroll="handleScroll">
|
||||
<div ref="refViewWrapper" class="view" @scroll="handleScroll">
|
||||
<div class="action-bar" :style="{width: (width - 50) + 'px'}">
|
||||
<div :style="{background: todoCount ? '#4e4e4e' : 'transparent'}">
|
||||
<div v-if="todoCount" class="todo-progress">
|
||||
@@ -13,7 +13,7 @@
|
||||
<div v-else></div>
|
||||
<div class="convert">
|
||||
<iframe width="0" height="0" hidden id="preview_download" name="preview_download"></iframe>
|
||||
<form ref="convertForm" :action="`/api/convert/${convert.fileName}`" method="post" target="preview_download">
|
||||
<form ref="refConvertForm" :action="`/api/convert/${convert.fileName}`" method="post" target="preview_download">
|
||||
<input type="hidden" name="html" :value="convert.html">
|
||||
<input type="hidden" name="type" :value="convert.type">
|
||||
<!-- <button type="button" @click="convertFile('pdf')">pdf</button> -->
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="outline" class="outline">
|
||||
<div class="outline">
|
||||
<div style="padding: .5em;"><b>目录</b></div>
|
||||
<div class="catalog" :style="{maxHeight: (height - 120) + 'px'}">
|
||||
<div v-for="(head, index) in heads" :key="index" :style="{paddingLeft: `${head.level + 1}em`}" @click="syncScroll(head.sourceLine)">
|
||||
@@ -33,13 +33,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div :class="{'scroll-to-top': true, 'hide': scrollTop < 30}" :style="{top: (height - 40) + 'px'}" @click="scrollToTop">TOP</div>
|
||||
<article ref="view" class="markdown-body" @click.capture="handleClick"></article>
|
||||
<article ref="refView" class="markdown-body" @click.capture="handleClick"></article>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import _ from 'lodash'
|
||||
<script lang="ts">
|
||||
import loadsh from 'lodash'
|
||||
import { useStore } from 'vuex'
|
||||
import { computed, defineComponent, nextTick, onBeforeUnmount, onMounted, ref, toRefs, watch } from 'vue'
|
||||
import mime from 'mime-types'
|
||||
import Markdown from 'markdown-it'
|
||||
import TaskLists from 'markdown-it-task-lists'
|
||||
@@ -60,20 +61,22 @@ import DrawioPlugin from '../plugins/DrawioPlugin'
|
||||
import PlantumlPlugin from '../plugins/PlantumlPlugin'
|
||||
import MermaidPlugin from '../plugins/MermaidPlugin'
|
||||
import CodePlugin from '../plugins/CodePlugin'
|
||||
import file from '@/lib/file'
|
||||
import env from '@/lib/env'
|
||||
import { encodeMarkdownLink } from '@/lib/utils'
|
||||
import file from '../useful/file'
|
||||
import env from '../useful/env'
|
||||
import { encodeMarkdownLink } from '../useful/utils'
|
||||
|
||||
import 'github-markdown-css/github-markdown.css'
|
||||
import 'highlight.js/styles/atom-one-dark.css'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import { useBus } from '../useful/bus'
|
||||
import { useToast } from '../useful/toast'
|
||||
HighlightLineNumber.addStyles()
|
||||
|
||||
const markdown = Markdown({
|
||||
linkify: true,
|
||||
breaks: true,
|
||||
html: true,
|
||||
highlight: (str, lang) => {
|
||||
highlight: (str: string, lang: string) => {
|
||||
if (lang && Highlight.getLanguage(lang)) {
|
||||
try {
|
||||
return Highlight.highlight(lang, str).value
|
||||
@@ -93,117 +96,82 @@ const markdown = Markdown({
|
||||
.use(LinkTargetPlugin)
|
||||
.use(Footnote)
|
||||
.use(DrawioPlugin)
|
||||
.use(MultimdTable, { enableMultilineRows: true })
|
||||
.use(MultimdTable, { multiline: true })
|
||||
.use(MarkdownItToc)
|
||||
.use(MarkdownItECharts)
|
||||
.use(MermaidPlugin)
|
||||
.use(CodePlugin)
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'xview',
|
||||
data () {
|
||||
return {
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
heads: [],
|
||||
convert: {},
|
||||
todoCount: 0,
|
||||
todoDoneCount: 0,
|
||||
scrollTop: 0,
|
||||
setup (_, { emit }) {
|
||||
const bus = useBus()
|
||||
const store = useStore()
|
||||
const toast = useToast()
|
||||
|
||||
const { currentContent, currentFile } = toRefs(store.state)
|
||||
const fileRepo = computed(() => currentFile.value?.repo)
|
||||
const fileName = computed(() => currentFile.value?.name)
|
||||
const filePath = computed(() => currentFile.value?.path)
|
||||
|
||||
const refViewWrapper = ref<HTMLElement | null>(null)
|
||||
const refView = ref<HTMLElement | null>(null)
|
||||
const refConvertForm = ref<HTMLFormElement | null>(null)
|
||||
|
||||
const width = ref(1024)
|
||||
const height = ref(1024)
|
||||
const heads = ref<{
|
||||
tag: string;
|
||||
text: string;
|
||||
level: number;
|
||||
sourceLine: number;
|
||||
}[]>([])
|
||||
const convert = ref({ fileName: '', html: '', type: '' })
|
||||
const todoCount = ref(0)
|
||||
const todoDoneCount = ref(0)
|
||||
const scrollTop = ref(0)
|
||||
|
||||
function resizeHandler () {
|
||||
width.value = refViewWrapper.value!!.clientWidth
|
||||
height.value = refViewWrapper.value!!.clientHeight
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.updatePlantumlDebounce = _.debounce(() => {
|
||||
this.updatePlantuml()
|
||||
|
||||
function updatePlantuml () {
|
||||
const nodes = refView.value!!.querySelectorAll<HTMLImageElement>('img[data-plantuml-src]')
|
||||
nodes.forEach(el => {
|
||||
el.src = el.dataset.plantumlSrc || ''
|
||||
})
|
||||
}
|
||||
|
||||
function runAppletScript () {
|
||||
const nodes = refView.value!!.querySelectorAll<HTMLElement>('.applet[data-code]')
|
||||
nodes.forEach(AppletPlugin.runScript)
|
||||
}
|
||||
|
||||
const updatePlantumlDebounce = loadsh.debounce(() => {
|
||||
updatePlantuml()
|
||||
}, 3000, { leading: true })
|
||||
|
||||
this.runAppletScriptDebounce = _.debounce(() => {
|
||||
this.runAppletScript()
|
||||
const runAppletScriptDebounce = loadsh.debounce(() => {
|
||||
runAppletScript()
|
||||
}, 1000, { leading: true })
|
||||
|
||||
this.render = _.debounce(() => {
|
||||
// 编辑非 markdown 文件预览直接显示代码
|
||||
const content = (this.filePath || '').endsWith('.md')
|
||||
? this.currentContent
|
||||
: '```' + file.extname(this.fileName || '').replace(/^\./, '') + '\n' + this.currentContent + '```'
|
||||
this.$refs.view.innerHTML = markdown.render(this.replaceRelativeLink(content))
|
||||
this.runAppletScriptDebounce()
|
||||
this.initDrawio()
|
||||
this.updateOutline()
|
||||
this.updateTodoCount()
|
||||
this.updatePlantumlDebounce()
|
||||
MarkdownItECharts.update()
|
||||
MermaidPlugin.update()
|
||||
|
||||
for (let ele of document.querySelectorAll('code[class^="language-"]')) {
|
||||
HighlightLineNumber.lineNumbersBlock(ele)
|
||||
}
|
||||
|
||||
// 渲染完成后触发渲染完成事件
|
||||
this.$nextTick(() => this.$bus.emit('preview-rendered'))
|
||||
}, 500, { leading: true })
|
||||
|
||||
this.render()
|
||||
setTimeout(() => {
|
||||
this.updatePlantuml()
|
||||
}, 100)
|
||||
|
||||
window.addEventListener('keydown', this.keydownHandler, true)
|
||||
this.$bus.on('resize', this.resizeHandler)
|
||||
this.resizeHandler()
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.keydownHandler)
|
||||
this.$bus.off('resize', this.resizeHandler)
|
||||
},
|
||||
methods: {
|
||||
resizeHandler () {
|
||||
this.width = this.$refs['view-wrapper'].clientWidth
|
||||
this.height = this.$refs['view-wrapper'].clientHeight
|
||||
},
|
||||
async keydownHandler (e) {
|
||||
// 转换所有外链图片到本地
|
||||
if (e.key === 'l' && e.ctrlKey && e.altKey) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const result = []
|
||||
const imgList = [...this.$refs.view.querySelectorAll('img')]
|
||||
for (let i = 0; i < imgList.length; i++) {
|
||||
this.$toast.show('info', `正在转换外链图片 ${i + 1}/${imgList.length}`)
|
||||
|
||||
const img = imgList[i]
|
||||
const data = await this.transformImgOutLink(img)
|
||||
if (data) {
|
||||
result.push(data)
|
||||
}
|
||||
}
|
||||
result.forEach(data => this.$bus.emit('editor-replace-value', data.oldLink, data.replacedLink))
|
||||
this.$bus.emit('tree-refresh')
|
||||
}
|
||||
},
|
||||
handleScroll (e) {
|
||||
this.scrollTop = e.target.scrollTop
|
||||
},
|
||||
scrollToTop () {
|
||||
this.$refs['view-wrapper'].scrollTo(0, 0)
|
||||
this.syncScroll(1)
|
||||
},
|
||||
replaceRelativeLink (md) {
|
||||
if (!this.fileRepo) {
|
||||
function replaceRelativeLink (md: string) {
|
||||
if (!fileRepo.value) {
|
||||
return md
|
||||
}
|
||||
|
||||
if (this.fileRepo === '__help__') {
|
||||
return md.replace(/\[([^\]]*)\]\((\.\/[^)]*)\)/g, `[$1](api/help/file?path=$2)`)
|
||||
.replace(/<img([^>]*)src=["']?(\.\/[^\s'"]*)["']?/ig, `<img$1src="api/help/file?path=$2"`)
|
||||
if (fileRepo.value === '__help__') {
|
||||
return md.replace(/\[([^\]]*)\]\((\.\/[^)]*)\)/g, '[$1](api/help/file?path=$2)')
|
||||
.replace(/<img([^>]*)src=["']?(\.\/[^\s'"]*)["']?/ig, '<img$1src="api/help/file?path=$2"')
|
||||
}
|
||||
|
||||
if (!this.filePath) {
|
||||
if (!filePath.value) {
|
||||
return md
|
||||
}
|
||||
|
||||
const basePath = file.dirname(this.filePath)
|
||||
const repo = this.fileRepo
|
||||
const basePath = file.dirname(filePath.value)
|
||||
const repo = fileRepo.value
|
||||
|
||||
return md.replace(/\[([^\]]*)\]\(\.\/([^)]*)\)/g, (match, alt, path) => {
|
||||
path = decodeURI(path) // 提前解码一次,有的链接已经预先编码
|
||||
@@ -227,63 +195,79 @@ export default {
|
||||
const filePath = `${basePath}/${path}`
|
||||
return `<img${_}origin-src="${path}" src="api/attachment/${encodeURIComponent(fileName)}?repo=${repo}&path=${encodeURIComponent(filePath)}"`
|
||||
})
|
||||
},
|
||||
updatePlantuml () {
|
||||
const nodes = this.$refs.view.querySelectorAll('img[data-plantuml-src]')
|
||||
for (let ele of nodes) {
|
||||
ele.src = ele.dataset['plantumlSrc']
|
||||
}
|
||||
},
|
||||
runAppletScript () {
|
||||
const nodes = this.$refs.view.querySelectorAll('.applet[data-code]')
|
||||
for (const el of nodes) {
|
||||
AppletPlugin.runScript(el)
|
||||
}
|
||||
},
|
||||
initDrawio () {
|
||||
if (!this.$refs.view) {
|
||||
}
|
||||
|
||||
function initDrawio () {
|
||||
if (!refView.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const nodes = this.$refs.view.querySelectorAll('.drawio[data-file]')
|
||||
for (const el of nodes) {
|
||||
let originPath = el.dataset.file
|
||||
if (originPath) {
|
||||
originPath = decodeURI(originPath)
|
||||
const filepath = originPath.startsWith('/') ? originPath : (file.dirname(this.filePath) + '/' + originPath.replace(/^.\//, ''))
|
||||
DrawioPlugin.load(el, this.fileRepo, filepath)
|
||||
const nodes = refView.value!!.querySelectorAll<HTMLIFrameElement>('.drawio[data-url]')
|
||||
nodes.forEach(el => {
|
||||
const url = el.dataset.url
|
||||
if (url) {
|
||||
DrawioPlugin.load(el, url)
|
||||
} else {
|
||||
DrawioPlugin.load(el)
|
||||
}
|
||||
}
|
||||
},
|
||||
updateOutline () {
|
||||
})
|
||||
}
|
||||
|
||||
function updateOutline () {
|
||||
const tags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
|
||||
const nodes = this.$refs.view.querySelectorAll(tags.join(','))
|
||||
this.heads = Array.from(nodes).map(node => {
|
||||
const nodes = refView.value!!.querySelectorAll<HTMLHeadElement>(tags.join(','))
|
||||
heads.value = Array.from(nodes).map(node => {
|
||||
return {
|
||||
tag: node.tagName.toLowerCase(),
|
||||
text: node.innerText,
|
||||
level: tags.indexOf(node.tagName.toLowerCase()),
|
||||
sourceLine: parseInt(node.dataset['sourceLine'])
|
||||
sourceLine: parseInt(node.dataset.sourceLine || '0')
|
||||
}
|
||||
})
|
||||
},
|
||||
updateTodoCount () {
|
||||
this.todoCount = this.$refs.view.querySelectorAll('input[type=checkbox]').length
|
||||
this.todoDoneCount = this.$refs.view.querySelectorAll('input[type=checkbox][checked]').length
|
||||
},
|
||||
async transformImgOutLink (img) {
|
||||
const transform = ximg => {
|
||||
}
|
||||
|
||||
function updateTodoCount () {
|
||||
todoCount.value = refView.value!!.querySelectorAll('input[type=checkbox]').length
|
||||
todoDoneCount.value = refView.value!!.querySelectorAll('input[type=checkbox][checked]').length
|
||||
}
|
||||
|
||||
function hightCode () {
|
||||
const nodes = refView.value!!.querySelectorAll<HTMLImageElement>('code[class^="language-"]')
|
||||
nodes.forEach(el => {
|
||||
HighlightLineNumber.lineNumbersBlock(el)
|
||||
})
|
||||
}
|
||||
|
||||
const render = loadsh.debounce(() => {
|
||||
// 编辑非 markdown 文件预览直接显示代码
|
||||
const content = (filePath.value || '').endsWith('.md')
|
||||
? currentContent.value
|
||||
: '```' + file.extname(fileName.value || '').replace(/^\./, '') + '\n' + currentContent.value + '```'
|
||||
refView.value!!.innerHTML = markdown.render(replaceRelativeLink(content))
|
||||
runAppletScriptDebounce()
|
||||
initDrawio()
|
||||
updateOutline()
|
||||
updateTodoCount()
|
||||
updatePlantumlDebounce()
|
||||
hightCode()
|
||||
MarkdownItECharts.update()
|
||||
MermaidPlugin.update()
|
||||
|
||||
// 渲染完成后触发渲染完成事件
|
||||
nextTick(() => bus.emit('preview-rendered'))
|
||||
}, 500, { leading: true })
|
||||
|
||||
async function transformImgOutLink (img: HTMLImageElement) {
|
||||
const transform = (ximg: HTMLImageElement): Promise<string> => {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = ximg.naturalWidth
|
||||
canvas.height = ximg.naturalHeight
|
||||
canvas.getContext('2d').drawImage(ximg, 0, 0)
|
||||
canvas.getContext('2d')!!.drawImage(ximg, 0, 0)
|
||||
return new Promise((resolve, reject) => {
|
||||
canvas.toBlob(async blob => {
|
||||
try {
|
||||
const imgFile = new File([blob], 'file.png')
|
||||
const { relativePath } = await file.upload(this.fileRepo, this.filePath, imgFile)
|
||||
const imgFile = new File([blob!!], 'file.png')
|
||||
const { relativePath } = await file.upload(fileRepo.value, filePath.value, imgFile)
|
||||
resolve(relativePath)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
@@ -292,14 +276,15 @@ export default {
|
||||
})
|
||||
}
|
||||
|
||||
let replacedLink = null
|
||||
let replacedLink = ''
|
||||
const imgAttrSrc = img.getAttribute('src') || ''
|
||||
if (img.src.startsWith('data:')) {
|
||||
replacedLink = await transform(img)
|
||||
} else if (img.getAttribute('src').startsWith('http://') || img.getAttribute('src').startsWith('https://')) {
|
||||
} else if (imgAttrSrc.startsWith('http://') || imgAttrSrc.startsWith('https://')) {
|
||||
const res = await window.fetch(`api/proxy?url=${encodeURIComponent(img.src)}&headers=${img.getAttribute('headers') || ''}`)
|
||||
const blob = await res.blob()
|
||||
const imgFile = new File([blob], 'file.' + mime.extension(res.headers.get('content-type')))
|
||||
const { relativePath } = await file.upload(this.fileRepo, this.filePath, imgFile)
|
||||
const imgFile = new File([blob!!], 'file.' + mime.extension(res.headers.get('content-type')!!))
|
||||
const { relativePath } = await file.upload(fileRepo.value, filePath.value, imgFile)
|
||||
replacedLink = relativePath
|
||||
}
|
||||
|
||||
@@ -308,16 +293,55 @@ export default {
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
async handleClick (e) {
|
||||
const target = e.target
|
||||
}
|
||||
|
||||
async function keydownHandler (e: KeyboardEvent) {
|
||||
// 转换所有外链图片到本地
|
||||
if (e.key === 'l' && e.ctrlKey && e.altKey) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const result = []
|
||||
const imgList = refView.value!!.querySelectorAll('img')
|
||||
for (let i = 0; i < imgList.length; i++) {
|
||||
toast.show('info', `正在转换外链图片 ${i + 1}/${imgList.length}`)
|
||||
|
||||
const img = imgList[i]
|
||||
const data = await transformImgOutLink(img)
|
||||
if (data) {
|
||||
result.push(data)
|
||||
}
|
||||
}
|
||||
result.forEach(data => bus.emit('editor-replace-value', { search: data.oldLink, replace: data.replacedLink }))
|
||||
bus.emit('tree-refresh')
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll (e: any) {
|
||||
scrollTop.value = e.target.scrollTop
|
||||
}
|
||||
|
||||
function syncScroll (line: number) {
|
||||
emit('sync-scroll', line)
|
||||
}
|
||||
|
||||
function switchTodo (line: number, checked: boolean) {
|
||||
emit('switch-todo', line, checked)
|
||||
}
|
||||
|
||||
function scrollToTop () {
|
||||
refViewWrapper.value!!.scrollTo(0, 0)
|
||||
syncScroll(1)
|
||||
}
|
||||
|
||||
async function handleClick (e: MouseEvent) {
|
||||
const target = e.target as HTMLElement
|
||||
|
||||
const preventEvent = () => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
const handleLink = link => {
|
||||
const handleLink = (link: HTMLAnchorElement) => {
|
||||
// 系统中打开附件
|
||||
if (link.classList.contains('open')) {
|
||||
fetch(link.href.replace('api/attachment', 'api/open'))
|
||||
@@ -338,21 +362,21 @@ export default {
|
||||
|
||||
let path = tmp[0]
|
||||
if (path.startsWith('.')) { // 将相对路径转换为绝对路径
|
||||
path = file.dirname(this.filePath || '') + path.replace('.', '')
|
||||
path = file.dirname(filePath.value || '') + path.replace('.', '')
|
||||
}
|
||||
|
||||
// 打开文件
|
||||
this.$store.commit('app/setCurrentFile', {
|
||||
store.commit('setCurrentFile', {
|
||||
path,
|
||||
name: file.basename(path),
|
||||
repo: this.fileRepo,
|
||||
repo: fileRepo.value,
|
||||
type: 'file'
|
||||
})
|
||||
|
||||
// 跳转锚点
|
||||
const hash = tmp.slice(1).join('#')
|
||||
if (hash) {
|
||||
this.$bus.once('preview-rendered', () => {
|
||||
bus.once('preview-rendered', () => {
|
||||
const el = document.getElementById(hash) ||
|
||||
document.getElementById(encodeURIComponent(hash))
|
||||
|
||||
@@ -368,32 +392,35 @@ export default {
|
||||
|
||||
preventEvent()
|
||||
} else if (href && href.startsWith('#')) { // 处理 TOC 跳转
|
||||
document.getElementById(href.replace(/^#/, '')).scrollIntoView()
|
||||
const el = document.getElementById(href.replace(/^#/, ''))
|
||||
if (el) {
|
||||
el.scrollIntoView()
|
||||
}
|
||||
preventEvent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (target.tagName === 'A') {
|
||||
handleLink(target)
|
||||
handleLink(target as HTMLAnchorElement)
|
||||
}
|
||||
|
||||
// 复制标题链接
|
||||
if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].indexOf(target.tagName) > -1 && target.id && e.ctrlKey) {
|
||||
this.$bus.emit('copy-text', encodeMarkdownLink(this.filePath) + '#' + encodeMarkdownLink(decodeURIComponent(target.id)))
|
||||
bus.emit('copy-text', encodeMarkdownLink(filePath.value) + '#' + encodeMarkdownLink(decodeURIComponent(target.id)))
|
||||
return preventEvent()
|
||||
}
|
||||
|
||||
if (target.tagName === 'IMG') {
|
||||
const img = target
|
||||
const img = target as HTMLImageElement
|
||||
if (e.ctrlKey && e.shiftKey) { // 转换外链图片到本地
|
||||
const data = await this.transformImgOutLink(img)
|
||||
const data = await transformImgOutLink(img)
|
||||
if (data) {
|
||||
this.$bus.emit('tree-refresh')
|
||||
this.$bus.emit('editor-replace-value', data.oldLink, data.replacedLink)
|
||||
bus.emit('tree-refresh')
|
||||
bus.emit('editor-replace-value', { search: data.oldLink, replace: data.replacedLink })
|
||||
}
|
||||
} else if (img.parentElement.tagName === 'A') {
|
||||
handleLink(img.parentElement)
|
||||
} else if (img.parentElement!!.tagName === 'A') {
|
||||
handleLink(img.parentElement as HTMLAnchorElement)
|
||||
} else {
|
||||
env.openAlwaysOnTopWindow(img.src)
|
||||
}
|
||||
@@ -402,40 +429,37 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
if (target.tagName === 'INPUT' && target.parentElement.classList.contains('source-line')) {
|
||||
this.switchTodo(parseInt(target.parentElement.dataset['sourceLine']), target.checked)
|
||||
if (target.tagName === 'INPUT' && target.parentElement!!.classList.contains('source-line')) {
|
||||
switchTodo(parseInt(target.parentElement!!.dataset.sourceLine || '0'), (target as HTMLInputElement).checked)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
|
||||
if (target.classList.contains('source-line') && window.getSelection().toString().length < 1) {
|
||||
this.syncScroll(parseInt(target.dataset['sourceLine']))
|
||||
if (target.classList.contains('source-line') && window.getSelection()!!.toString().length < 1) {
|
||||
syncScroll(parseInt(target.dataset.sourceLine || '0'))
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
syncScroll (line) {
|
||||
this.$emit('sync-scroll', line)
|
||||
},
|
||||
switchTodo (line, checked) {
|
||||
this.$emit('switch-todo', line, checked)
|
||||
},
|
||||
revealLine (line) {
|
||||
}
|
||||
|
||||
function revealLine (line: number) {
|
||||
if (line <= 1) {
|
||||
this.$refs['view-wrapper'].scrollTo(0, 0)
|
||||
refViewWrapper.value!!.scrollTo(0, 0)
|
||||
return
|
||||
}
|
||||
|
||||
const nodes = this.$refs['view-wrapper'].querySelectorAll('.view .source-line')
|
||||
for (let ele of nodes) {
|
||||
if (parseInt(ele.dataset['sourceLine']) >= line) {
|
||||
ele.scrollIntoView()
|
||||
const nodes = refViewWrapper.value!!.querySelectorAll<HTMLElement>('.view .source-line')
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const el = nodes[i]
|
||||
if (parseInt(el.dataset.sourceLine || '0') >= line) {
|
||||
el.scrollIntoView()
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
convertFile (type) {
|
||||
}
|
||||
|
||||
function convertFile (type: string) {
|
||||
MarkdownItECharts.preparePrint()
|
||||
|
||||
let baseUrl = location.origin + location.pathname.substring(0, location.pathname.lastIndexOf('/')) + '/'
|
||||
@@ -445,43 +469,64 @@ export default {
|
||||
baseUrl = baseUrl.replace(/localhost/i, '127.0.0.1')
|
||||
}
|
||||
|
||||
this.convert = {
|
||||
fileName: (this.fileName || '未命名') + `.${type}`,
|
||||
html: this.$refs.view.outerHTML.replace(/src="api/g, `src="${baseUrl}api`),
|
||||
convert.value = {
|
||||
fileName: (fileName.value || '未命名') + `.${type}`,
|
||||
html: refView.value!!.outerHTML.replace(/src="api/g, `src="${baseUrl}api`),
|
||||
type
|
||||
}
|
||||
MarkdownItECharts.update()
|
||||
setTimeout(() => {
|
||||
this.$refs.convertForm.submit()
|
||||
refConvertForm.value!!.submit()
|
||||
}, 300)
|
||||
},
|
||||
print () {
|
||||
}
|
||||
|
||||
function print () {
|
||||
window.print()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('app', ['currentContent', 'currentFile']),
|
||||
fileRepo () {
|
||||
return this.currentFile && this.currentFile.repo
|
||||
},
|
||||
fileName () {
|
||||
return this.currentFile && this.currentFile.name
|
||||
},
|
||||
filePath () {
|
||||
return this.currentFile && this.currentFile.path
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentContent () {
|
||||
this.render()
|
||||
},
|
||||
filePath () {
|
||||
|
||||
onMounted(() => {
|
||||
render()
|
||||
setTimeout(() => {
|
||||
this.updatePlantuml()
|
||||
updatePlantuml()
|
||||
}, 100)
|
||||
|
||||
window.addEventListener('keydown', keydownHandler, true)
|
||||
bus.on('resize', resizeHandler)
|
||||
resizeHandler()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', keydownHandler)
|
||||
bus.off('resize', resizeHandler)
|
||||
})
|
||||
|
||||
watch(currentContent, render)
|
||||
watch(filePath, () => {
|
||||
setTimeout(() => {
|
||||
updatePlantuml()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
return {
|
||||
refViewWrapper,
|
||||
refView,
|
||||
width,
|
||||
height,
|
||||
heads,
|
||||
convert,
|
||||
todoCount,
|
||||
todoDoneCount,
|
||||
scrollTop,
|
||||
print,
|
||||
convertFile,
|
||||
handleScroll,
|
||||
scrollToTop,
|
||||
handleClick,
|
||||
revealLine,
|
||||
syncScroll,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -495,42 +540,42 @@ export default {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.markdown-body /deep/ a {
|
||||
.markdown-body ::v-deep(a) {
|
||||
color: #4c93e2;
|
||||
}
|
||||
|
||||
.markdown-body /deep/ tr {
|
||||
.markdown-body ::v-deep(tr) {
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
.markdown-body /deep/ * {
|
||||
.markdown-body ::v-deep(*) {
|
||||
border-color: #8b8d90;
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
.markdown-body /deep/ code {
|
||||
.markdown-body ::v-deep(code) {
|
||||
background: #464648;
|
||||
}
|
||||
|
||||
.markdown-body /deep/ table tr:nth-child(2n),
|
||||
.markdown-body /deep/ pre,
|
||||
.markdown-body /deep/ pre > code
|
||||
.markdown-body ::v-deep(table tr:nth-child(2n)),
|
||||
.markdown-body ::v-deep(pre),
|
||||
.markdown-body ::v-deep(pre > code)
|
||||
{
|
||||
background: #303133;
|
||||
}
|
||||
|
||||
.markdown-body /deep/ div.mermaid,
|
||||
.markdown-body /deep/ div.drawio-wrapper,
|
||||
.markdown-body /deep/ input,
|
||||
.markdown-body /deep/ img
|
||||
.markdown-body ::v-deep(div.mermaid),
|
||||
.markdown-body ::v-deep(div.drawio-wrapper),
|
||||
.markdown-body ::v-deep(input),
|
||||
.markdown-body ::v-deep(img)
|
||||
{
|
||||
transition: all .3s ease-in-out;
|
||||
filter: brightness(70%);
|
||||
}
|
||||
|
||||
.markdown-body /deep/ div.mermaid:hover,
|
||||
.markdown-body /deep/ div.drawio-wrapper:hover,
|
||||
.markdown-body /deep/ img:hover
|
||||
.markdown-body ::v-deep(div.mermaid:hover),
|
||||
.markdown-body ::v-deep(div.drawio-wrapper:hover),
|
||||
.markdown-body ::v-deep(img:hover)
|
||||
{
|
||||
filter: none;
|
||||
}
|
||||
@@ -669,17 +714,17 @@ button:hover {
|
||||
right: -60px;
|
||||
}
|
||||
|
||||
.markdown-body /deep/ table.hljs-ln tbody {
|
||||
.markdown-body ::v-deep(table.hljs-ln tbody) {
|
||||
display: table;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.markdown-body /deep/ fieldset {
|
||||
.markdown-body ::v-deep(fieldset) {
|
||||
border-style: solid;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.markdown-body /deep/ legend {
|
||||
.markdown-body ::v-deep(legend) {
|
||||
padding: 0 .2em;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
<div class="tab">
|
||||
<div v-for="tab in tabs" :key="tab.key" @click="switchTab(tab.key)" :class="{selected: currentTab === tab.key}">{{tab.label}}</div>
|
||||
</div>
|
||||
<input ref="input" v-model="searchText" type="text" class="input" @keydown.tab.prevent @keydown.up.prevent @keydown.down.prevent autofocus>
|
||||
<ul ref="result" class="result">
|
||||
<input ref="refInput" v-model="searchText" type="text" class="input" @keydown.tab.prevent @keydown.up.prevent @keydown.down.prevent>
|
||||
<ul ref="refResult" class="result">
|
||||
<li v-if="dataList === null">加载中……</li>
|
||||
<template v-else>
|
||||
<li
|
||||
v-for="item in dataList"
|
||||
v-for="(item, i) in dataList"
|
||||
:key="item.repo + item.path"
|
||||
:class="{selected: selected === item}"
|
||||
@click="chooseItem(item)">
|
||||
<span ref="fileName">
|
||||
<span :ref="el => refFilename[i] = el">
|
||||
{{item.name}}
|
||||
</span>
|
||||
<span ref="filePath" class="path">
|
||||
<span :ref="el => refFilepath[i] = el" class="path">
|
||||
[{{item.repo}}] {{item.path.substr(0, item.path.lastIndexOf('/'))}}
|
||||
</span>
|
||||
</li>
|
||||
@@ -25,85 +25,154 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import _ from 'lodash'
|
||||
import { mapState } from 'vuex'
|
||||
import file from '@/lib/file'
|
||||
import fuzzyMatch from '@/lib/fuzzyMatch'
|
||||
import { computed, defineComponent, nextTick, onMounted, ref, toRefs, watch } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import file from '../useful/file'
|
||||
import fuzzyMatch from '../useful/fuzzy-match'
|
||||
|
||||
export default {
|
||||
type TabKey = 'marked' | 'search' | 'file'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'quick-open',
|
||||
components: {},
|
||||
props: {
|
||||
withMarked: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
selected: null,
|
||||
searchText: '',
|
||||
currentTab: 'marked',
|
||||
list: [],
|
||||
lastFetchTime: 0,
|
||||
setup (props, { emit }) {
|
||||
const store = useStore()
|
||||
|
||||
const refInput = ref<HTMLInputElement | null>(null)
|
||||
const refResult = ref<HTMLUListElement | null>(null)
|
||||
const refFilename = ref<HTMLElement[]>([])
|
||||
const refFilepath = ref<HTMLElement[]>([])
|
||||
|
||||
const { currentRepo, recentOpenTime, tree, markedFiles } = toRefs(store.state)
|
||||
|
||||
const selected = ref<any>(null)
|
||||
const searchText = ref('')
|
||||
const currentTab = ref<TabKey>('marked')
|
||||
const list = ref<any>([])
|
||||
const lastFetchTime = ref(0)
|
||||
|
||||
const repo = computed(() => currentRepo.value?.name)
|
||||
|
||||
const tabs = computed(() => {
|
||||
const arr: {key: TabKey; label: string}[] = [
|
||||
{ key: 'file', label: '快速跳转' },
|
||||
{ key: 'search', label: '搜索内容' },
|
||||
]
|
||||
|
||||
if (props.withMarked) {
|
||||
arr.unshift({ key: 'marked', label: '已标记' })
|
||||
}
|
||||
|
||||
return arr
|
||||
})
|
||||
|
||||
const files = computed(() => {
|
||||
const travelFiles = (tree: any) => {
|
||||
let tmp: any[] = []
|
||||
|
||||
tree.forEach((node: any) => {
|
||||
if (node.type === 'file' && node.path.endsWith('.md')) {
|
||||
tmp.push(node)
|
||||
}
|
||||
|
||||
if (Array.isArray(node.children)) {
|
||||
tmp = tmp.concat(travelFiles(node.children))
|
||||
}
|
||||
})
|
||||
|
||||
return tmp
|
||||
}
|
||||
|
||||
return travelFiles(tree.value || [])
|
||||
})
|
||||
|
||||
function sortList (list: any) {
|
||||
if (list === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const map = (recentOpenTime.value || {})
|
||||
|
||||
return list.sort((a: any, b: any) => {
|
||||
const at = map[`${a.repo}|${a.path}`] || 0
|
||||
const bt = map[`${b.repo}|${b.path}`] || 0
|
||||
|
||||
return bt - at
|
||||
})
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.searchWithDebounce = _.debounce(async (text, call) => {
|
||||
if (this.repo && text.trim()) {
|
||||
|
||||
function filterFiles (files: any[], search: string, fuzzy: boolean) {
|
||||
if (!fuzzy) {
|
||||
search = search.toLowerCase()
|
||||
return files.filter(x => x.path.toLowerCase().indexOf(search) > -1)
|
||||
}
|
||||
|
||||
const tmp: any[] = []
|
||||
|
||||
files.forEach(x => {
|
||||
const result = fuzzyMatch(search, x.path)
|
||||
|
||||
if (result.matched) {
|
||||
tmp.push({ ...x })
|
||||
}
|
||||
})
|
||||
|
||||
return tmp.sort((a, b) => b.score - a.score)
|
||||
}
|
||||
|
||||
const dataList = computed(() => {
|
||||
if (!list.value) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 筛选一下,搜索全文不筛选
|
||||
const arr = currentTab.value === 'search' ? list.value : filterFiles(list.value, searchText.value.trim(), false)
|
||||
|
||||
// 按照最近使用时间排序
|
||||
return sortList(arr).slice(0, 70)
|
||||
})
|
||||
|
||||
const searchWithDebounce = _.debounce(async (text: string, call: Function) => {
|
||||
if (repo.value && text.trim()) {
|
||||
const fetchTime = new Date().getTime()
|
||||
this.lastFetchTime = fetchTime
|
||||
const data = await file.search(this.repo, text.trim())
|
||||
lastFetchTime.value = fetchTime
|
||||
const data = await file.search(repo.value, text.trim())
|
||||
// 总是保证最后的搜索结果出现在列表
|
||||
if (fetchTime >= this.lastFetchTime) {
|
||||
if (fetchTime >= lastFetchTime.value) {
|
||||
call(data)
|
||||
}
|
||||
} else {
|
||||
call([])
|
||||
}
|
||||
}, 500)
|
||||
},
|
||||
async mounted () {
|
||||
this.$refs.input.focus()
|
||||
this.updateDataSource()
|
||||
await this.$store.dispatch('app/fetchMarkedFiles')
|
||||
this.updateDataSource()
|
||||
},
|
||||
methods: {
|
||||
travelFiles (tree) {
|
||||
let tmp = []
|
||||
|
||||
tree.forEach(node => {
|
||||
if (node.type === 'file' && node.path.endsWith('.md')) {
|
||||
tmp.push(node)
|
||||
}
|
||||
|
||||
if (Array.isArray(node.children)) {
|
||||
tmp = tmp.concat(this.travelFiles(node.children))
|
||||
}
|
||||
})
|
||||
|
||||
return tmp
|
||||
},
|
||||
highlightText (search) {
|
||||
if (this.$refs.fileName && this.$refs.filePath) {
|
||||
function highlightText (search: string) {
|
||||
if (refFilename.value && refFilepath.value) {
|
||||
search = search.toLowerCase()
|
||||
|
||||
const openF = '(#$*B'
|
||||
const closeF = '#$*B)'
|
||||
|
||||
const escape = function (s) {
|
||||
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
|
||||
}
|
||||
const escape = (s: string) => s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
|
||||
|
||||
const openR = new RegExp(escape(openF), 'g')
|
||||
const closeR = new RegExp(escape(closeF), 'g')
|
||||
|
||||
this.$refs.fileName.concat(this.$refs.filePath).forEach(function (it) {
|
||||
;(refFilename.value || []).concat(refFilepath.value || []).forEach(function (it: any) {
|
||||
if (!it) {
|
||||
return
|
||||
}
|
||||
|
||||
let text = ''
|
||||
|
||||
it.innerText.split('').forEach(char => {
|
||||
it.innerText.split('').forEach((char: string) => {
|
||||
if (search.indexOf(char.toLowerCase()) > -1) {
|
||||
text += `${openF}${char}${closeF}`
|
||||
} else {
|
||||
@@ -116,161 +185,125 @@ export default {
|
||||
it.innerHTML = it.innerHTML.replace(openR, '<b>').replace(closeR, '</b>')
|
||||
})
|
||||
}
|
||||
},
|
||||
filterFiles (files, search, fuzzy) {
|
||||
if (!fuzzy) {
|
||||
search = search.toLowerCase()
|
||||
return files.filter(x => x.path.toLowerCase().indexOf(search) > -1)
|
||||
}
|
||||
}
|
||||
|
||||
const tmp = []
|
||||
|
||||
files.forEach(x => {
|
||||
const result = fuzzyMatch(search, x.path)
|
||||
|
||||
if (result.matched) {
|
||||
tmp.push({ ...x })
|
||||
}
|
||||
})
|
||||
|
||||
return tmp.sort((a, b) => b.score - a.score)
|
||||
},
|
||||
updateDataSource () {
|
||||
if (this.currentTab === 'file') {
|
||||
this.list = this.files
|
||||
} else if (this.currentTab === 'marked') {
|
||||
this.list = this.markedFiles
|
||||
} else if (this.currentTab === 'search') {
|
||||
this.list = null
|
||||
this.searchWithDebounce(this.searchText.trim(), data => {
|
||||
if (this.currentTab === 'search') {
|
||||
this.list = data
|
||||
function updateDataSource () {
|
||||
if (currentTab.value === 'file') {
|
||||
list.value = files.value
|
||||
} else if (currentTab.value === 'marked') {
|
||||
list.value = markedFiles.value
|
||||
} else if (currentTab.value === 'search') {
|
||||
list.value = null
|
||||
searchWithDebounce(searchText.value.trim(), (data: any[]) => {
|
||||
if (currentTab.value === 'search') {
|
||||
list.value = data
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
updateSelected (item = null) {
|
||||
if (this.dataList === null) {
|
||||
}
|
||||
|
||||
function updateSelected (item: any = null) {
|
||||
if (dataList.value === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (item) {
|
||||
this.selected = item
|
||||
selected.value = item
|
||||
} else {
|
||||
this.selected = this.dataList.length > 0 ? this.dataList[0] : null
|
||||
selected.value = dataList.value.length > 0 ? dataList.value[0] : null
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
const li = this.$refs.result.querySelector('li.selected')
|
||||
if (li) {
|
||||
li.scrollIntoViewIfNeeded()
|
||||
nextTick(() => {
|
||||
if (refResult.value) {
|
||||
const li = refResult.value.querySelector<any>('li.selected')
|
||||
if (li) {
|
||||
li.scrollIntoViewIfNeeded()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
selectItem (inc) {
|
||||
if (!this.dataList || this.dataList.length < 1) {
|
||||
this.updateSelected()
|
||||
}
|
||||
|
||||
function selectItem (inc: number) {
|
||||
if (!dataList.value || dataList.value.length < 1) {
|
||||
updateSelected()
|
||||
return
|
||||
}
|
||||
|
||||
const currentIndex = this.dataList.findIndex(x => this.selected === x)
|
||||
const currentIndex = dataList.value.findIndex((x: any) => selected.value === x)
|
||||
|
||||
let index = currentIndex + inc
|
||||
if (index > this.dataList.length - 1) {
|
||||
if (index > dataList.value.length - 1) {
|
||||
index = 0
|
||||
} else if (index < 0) {
|
||||
index = this.dataList.length - 1
|
||||
index = dataList.value.length - 1
|
||||
}
|
||||
|
||||
this.updateSelected(this.dataList[index])
|
||||
},
|
||||
chooseItem (item = null) {
|
||||
const file = item || this.selected
|
||||
updateSelected(dataList.value[index])
|
||||
}
|
||||
|
||||
function chooseItem (item: any = null) {
|
||||
const file = item || selected.value
|
||||
if (file) {
|
||||
this.$emit('choose-file', file)
|
||||
emit('choose-file', file)
|
||||
}
|
||||
},
|
||||
switchTab (tab) {
|
||||
}
|
||||
|
||||
function switchTab (tab: TabKey| number) {
|
||||
if (typeof tab === 'string') {
|
||||
this.currentTab = tab
|
||||
currentTab.value = tab
|
||||
return
|
||||
}
|
||||
|
||||
const tabs = this.tabs.map(x => x.key)
|
||||
const arr = tabs.value.map(x => x.key)
|
||||
|
||||
const index = tabs.indexOf(this.currentTab) + tab
|
||||
this.currentTab = tabs[index > -1 ? index : tabs.length - 1] || tabs[0]
|
||||
},
|
||||
sortList (list) {
|
||||
if (list === null) {
|
||||
return
|
||||
const index = arr.indexOf(currentTab.value) + tab
|
||||
currentTab.value = arr[index > -1 ? index : arr.length - 1] || arr[0]
|
||||
}
|
||||
|
||||
watch(() => props.withMarked, val => {
|
||||
if (!val && currentTab.value === 'marked') {
|
||||
currentTab.value = 'file'
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
const map = (this.recentOpenTime || {})
|
||||
watch(searchText, () => updateDataSource())
|
||||
|
||||
return list.sort((a, b) => {
|
||||
const at = map[`${a.repo}|${a.path}`] || 0
|
||||
const bt = map[`${b.repo}|${b.path}`] || 0
|
||||
watch(dataList, () => {
|
||||
updateSelected()
|
||||
if (currentTab.value !== 'search') { // 搜索模式不高亮字符
|
||||
nextTick(() => highlightText(searchText.value.trim()))
|
||||
}
|
||||
})
|
||||
|
||||
return bt - at
|
||||
})
|
||||
watch(currentTab, () => {
|
||||
list.value = null
|
||||
refInput.value!!.focus()
|
||||
updateDataSource()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
refInput.value!!.focus()
|
||||
updateDataSource()
|
||||
await store.dispatch('fetchMarkedFiles')
|
||||
updateDataSource()
|
||||
})
|
||||
|
||||
return {
|
||||
refInput,
|
||||
refResult,
|
||||
refFilename,
|
||||
refFilepath,
|
||||
tabs,
|
||||
currentTab,
|
||||
searchText,
|
||||
dataList,
|
||||
selected,
|
||||
selectItem,
|
||||
chooseItem,
|
||||
switchTab,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
withMarked: {
|
||||
immediate: true,
|
||||
handler (val) {
|
||||
if (!val && this.currentTab === 'marked') {
|
||||
this.currentTab = 'file'
|
||||
}
|
||||
}
|
||||
},
|
||||
searchText () {
|
||||
this.updateDataSource()
|
||||
},
|
||||
dataList () {
|
||||
this.updateSelected()
|
||||
this.$nextTick(() => this.highlightText(this.searchText.trim()))
|
||||
},
|
||||
currentTab () {
|
||||
this.list = null
|
||||
this.$refs.input.focus()
|
||||
this.updateDataSource()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('app', ['currentRepo', 'recentOpenTime', 'tree', 'markedFiles']),
|
||||
tabs () {
|
||||
const tabs = [
|
||||
{ key: 'file', label: '快速跳转' },
|
||||
{ key: 'search', label: '搜索内容' },
|
||||
]
|
||||
|
||||
if (this.withMarked) {
|
||||
tabs.unshift({ key: 'marked', label: '已标记' })
|
||||
}
|
||||
|
||||
return tabs
|
||||
},
|
||||
files () {
|
||||
return this.travelFiles(this.tree || [])
|
||||
},
|
||||
repo () {
|
||||
return this.currentRepo && this.currentRepo.name
|
||||
},
|
||||
dataList () {
|
||||
if (!this.list) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 筛选一下,搜索全文不筛选
|
||||
const list = this.currentTab === 'search' ? this.list : this.filterFiles(this.list, this.searchText.trim(), false)
|
||||
|
||||
// 按照最近使用时间排序
|
||||
return this.sortList(list).slice(0, 70)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -333,12 +366,12 @@ export default {
|
||||
padding-left: .3em;
|
||||
}
|
||||
|
||||
.result li span.path /deep/ b {
|
||||
.result li span.path ::v-deep b {
|
||||
color: #ababab;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.result li /deep/ b {
|
||||
.result li ::v-deep(b) {
|
||||
color: #c6d2ca;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@@ -8,68 +8,74 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import file from '@/lib/file'
|
||||
<script lang="ts">
|
||||
import { defineComponent, onBeforeMount, onBeforeUnmount, toRefs, watch } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import file from '../useful/file'
|
||||
import { useBus } from '../useful/bus'
|
||||
import { $args } from '../useful/global-args'
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'repository-switch',
|
||||
components: {},
|
||||
props: {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
current: null,
|
||||
setup () {
|
||||
const store = useStore()
|
||||
const bus = useBus()
|
||||
|
||||
const { currentRepo, repositories } = toRefs(store.state)
|
||||
|
||||
function choose (repo: any) {
|
||||
if (repo.name !== currentRepo.value?.name) {
|
||||
store.commit('setCurrentRepo', repo)
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$bus.on('switch-repo-by-name', this.chooseRepoByName)
|
||||
this.$bus.on('editor-ready', this.initRepo)
|
||||
this.$store.dispatch('app/fetchRepositories')
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$bus.off('switch-repo-by-name', this.chooseRepoByName)
|
||||
this.$bus.off('editor-ready', this.initRepo)
|
||||
},
|
||||
methods: {
|
||||
initRepo () {
|
||||
const initRepoName = window.$args().get('init-repo')
|
||||
const initFilePath = window.$args().get('init-file')
|
||||
|
||||
function chooseRepoByName (name?: string) {
|
||||
if (name && repositories.value[name]) {
|
||||
choose({ name, path: repositories.value[name] })
|
||||
}
|
||||
}
|
||||
|
||||
function initRepo () {
|
||||
const initRepoName = $args().get('init-repo')
|
||||
const initFilePath = $args().get('init-file')
|
||||
|
||||
if (initRepoName) {
|
||||
this.chooseRepoByName(initRepoName)
|
||||
chooseRepoByName(initRepoName)
|
||||
}
|
||||
|
||||
if (initFilePath) {
|
||||
this.$store.commit('app/setCurrentFile', { repo: this.currentRepo.name, name: file.basename(initFilePath), path: initFilePath })
|
||||
}
|
||||
},
|
||||
chooseRepoByName (name) {
|
||||
if (this.repositories[name]) {
|
||||
this.choose({ name, path: this.repositories[name] })
|
||||
}
|
||||
},
|
||||
choose (repo) {
|
||||
if (repo.name !== this.currentRepo.name) {
|
||||
this.$store.commit('app/setCurrentRepo', repo)
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
repositories (val) {
|
||||
const keys = Object.keys(val)
|
||||
if (!this.currentRepo || keys.indexOf(this.currentRepo.name) < 0) {
|
||||
if (keys.length > 0) {
|
||||
const name = keys[0]
|
||||
this.$store.commit('app/setCurrentRepo', { name, path: val[name] })
|
||||
}
|
||||
store.commit('setCurrentFile', { repo: currentRepo.value.name, name: file.basename(initFilePath), path: initFilePath })
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
bus.on('switch-repo-by-name', chooseRepoByName)
|
||||
bus.on('editor-ready', initRepo)
|
||||
store.dispatch('fetchRepositories')
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
bus.off('switch-repo-by-name', chooseRepoByName)
|
||||
bus.off('editor-ready', initRepo)
|
||||
})
|
||||
|
||||
watch(repositories, val => {
|
||||
const keys = Object.keys(val)
|
||||
if (!currentRepo.value || keys.indexOf(currentRepo.value.name) < 0) {
|
||||
if (keys.length > 0) {
|
||||
const name = keys[0]
|
||||
store.commit('setCurrentRepo', { name, path: val[name] })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
currentRepo,
|
||||
repositories,
|
||||
choose,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('app', ['currentRepo', 'repositories']),
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -19,76 +19,95 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import RepositorySwitch from './RepositorySwitch'
|
||||
<script lang="ts">
|
||||
import { defineComponent, onBeforeUnmount, onMounted, toRef } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useBus } from '../useful/bus'
|
||||
import RepositorySwitch from './RepositorySwitch.vue'
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'status-bar',
|
||||
components: { RepositorySwitch },
|
||||
created () {
|
||||
window.addEventListener('keydown', this.keydownHandler, true)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.keydownHandler)
|
||||
},
|
||||
methods: {
|
||||
toggleSide () {
|
||||
this.$bus.emit('toggle-side')
|
||||
},
|
||||
toggleView () {
|
||||
this.$bus.emit('toggle-view')
|
||||
},
|
||||
toggleXterm () {
|
||||
this.$bus.emit('toggle-xterm')
|
||||
},
|
||||
toggleFeature () {
|
||||
this.$store.dispatch('app/showHelp', 'FEATURES.md')
|
||||
},
|
||||
toggleReadme () {
|
||||
this.$store.dispatch('app/showHelp', 'README.md')
|
||||
},
|
||||
toggleWrap () {
|
||||
this.$bus.emit('editor-toggle-wrap')
|
||||
},
|
||||
keydownHandler (e) {
|
||||
setup () {
|
||||
const bus = useBus()
|
||||
const store = useStore()
|
||||
const documentInfo = toRef(store.state, 'documentInfo')
|
||||
|
||||
function toggleSide () {
|
||||
bus.emit('toggle-side')
|
||||
}
|
||||
|
||||
function toggleView () {
|
||||
bus.emit('toggle-view')
|
||||
}
|
||||
|
||||
function toggleXterm () {
|
||||
bus.emit('toggle-xterm')
|
||||
}
|
||||
|
||||
function toggleFeature () {
|
||||
store.dispatch('showHelp', 'FEATURES.md')
|
||||
}
|
||||
|
||||
function toggleReadme () {
|
||||
store.dispatch('showHelp', 'README.md')
|
||||
}
|
||||
|
||||
function toggleWrap () {
|
||||
bus.emit('editor-toggle-wrap')
|
||||
}
|
||||
|
||||
function keydownHandler (e: KeyboardEvent) {
|
||||
if (e.key === 'e' && e.altKey) {
|
||||
this.toggleSide()
|
||||
toggleSide()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
if (e.key === 'v' && e.altKey) {
|
||||
this.toggleView()
|
||||
toggleView()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
if (e.key === 'o' && e.altKey) {
|
||||
this.toggleXterm()
|
||||
toggleXterm()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
if (e.key === 'w' && e.altKey) {
|
||||
this.toggleWrap()
|
||||
toggleWrap()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
if (e.key === 'h' && e.altKey) {
|
||||
this.toggleReadme()
|
||||
toggleReadme()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', keydownHandler, true)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', keydownHandler)
|
||||
})
|
||||
|
||||
return {
|
||||
documentInfo,
|
||||
toggleSide,
|
||||
toggleWrap,
|
||||
toggleView,
|
||||
toggleXterm,
|
||||
toggleReadme,
|
||||
toggleFeature,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
},
|
||||
computed: {
|
||||
...mapState('app', ['documentInfo'])
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="svg-icon" :style="{color}" v-html="html" :title="title"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'svg-icon',
|
||||
props: {
|
||||
name: String,
|
||||
title: String,
|
||||
color: String,
|
||||
},
|
||||
setup (props) {
|
||||
const icons: { [key: string]: string } = {
|
||||
'thumbtack-solid': '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="thumbtack" class="svg-inline--fa fa-thumbtack fa-w-12" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M298.028 214.267L285.793 96H328c13.255 0 24-10.745 24-24V24c0-13.255-10.745-24-24-24H56C42.745 0 32 10.745 32 24v48c0 13.255 10.745 24 24 24h42.207L85.972 214.267C37.465 236.82 0 277.261 0 328c0 13.255 10.745 24 24 24h136v104.007c0 1.242.289 2.467.845 3.578l24 48c2.941 5.882 11.364 5.893 14.311 0l24-48a8.008 8.008 0 0 0 .845-3.578V352h136c13.255 0 24-10.745 24-24-.001-51.183-37.983-91.42-85.973-113.733z"></path></svg>',
|
||||
'folder-plus-solid': '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="folder-plus" class="svg-inline--fa fa-folder-plus fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M464,128H272L208,64H48A48,48,0,0,0,0,112V400a48,48,0,0,0,48,48H464a48,48,0,0,0,48-48V176A48,48,0,0,0,464,128ZM359.5,296a16,16,0,0,1-16,16h-64v64a16,16,0,0,1-16,16h-16a16,16,0,0,1-16-16V312h-64a16,16,0,0,1-16-16V280a16,16,0,0,1,16-16h64V200a16,16,0,0,1,16-16h16a16,16,0,0,1,16,16v64h64a16,16,0,0,1,16,16Z"></path></svg>',
|
||||
'times-solid': '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="times" class="svg-inline--fa fa-times fa-w-11" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 512"><path fill="currentColor" d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"></path></svg>',
|
||||
}
|
||||
|
||||
const html = ref(icons[props.name!!])
|
||||
|
||||
return { html }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.svg-icon ::v-deep(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div ref="tabs" class="tabs">
|
||||
<div ref="refTabs" class="tabs">
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item.key"
|
||||
@@ -10,72 +10,97 @@
|
||||
@click="switchTab(item)">
|
||||
<div class="label">{{item.label}}</div>
|
||||
<div class="close" @click.prevent.stop="removeTabs([item])">
|
||||
<y-icon class="close-icon" name="times" title="关闭"></y-icon>
|
||||
<svg-icon name="times-solid" title="关闭" style="width: 12px; height: 12px;" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import Sortable from 'sortablejs'
|
||||
import 'vue-awesome/icons/times'
|
||||
import { defineComponent, onMounted, ref } from 'vue'
|
||||
import { useContextMenu } from '../useful/context-menu'
|
||||
import { Components } from '../types'
|
||||
import SvgIcon from './SvgIcon.vue'
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'tabs',
|
||||
components: { SvgIcon },
|
||||
props: {
|
||||
value: String,
|
||||
list: Array,
|
||||
list: {
|
||||
type: Array as () => Components.Tabs.Item[],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
Sortable.create(this.$refs.tabs, {
|
||||
animation: 250,
|
||||
ghostClass: 'on-sort',
|
||||
direction: 'horizontal',
|
||||
onEnd: ({ oldIndex, newIndex }) => {
|
||||
this.swapTab(oldIndex, newIndex)
|
||||
setup (props, { emit }) {
|
||||
const refTabs = ref<HTMLElement | null>(null)
|
||||
const contextMenu = useContextMenu()
|
||||
|
||||
function switchTab (item: Components.Tabs.Item) {
|
||||
if (item.key !== props.value) {
|
||||
emit('input', item.key)
|
||||
emit('switch', item)
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
showContextMenu (item) {
|
||||
this.$contextMenu.show([
|
||||
{ id: 'close', label: '关闭', onClick: () => this.removeTabs([item]) },
|
||||
{ id: 'close-other', label: '关闭其他', onClick: () => this.removeOther(item) },
|
||||
{ id: 'close-right', label: '关闭到右侧', onClick: () => this.removeRight(item) },
|
||||
{ id: 'close-left', label: '关闭到左侧', onClick: () => this.removeLeft(item) },
|
||||
{ id: 'close-all', label: '全部关闭', onClick: () => this.removeAll(item) },
|
||||
])
|
||||
},
|
||||
switchTab (item) {
|
||||
if (item.key !== this.value) {
|
||||
this.$emit('input', item.key)
|
||||
this.$emit('switch', item)
|
||||
}
|
||||
},
|
||||
removeOther (item) {
|
||||
this.removeTabs(this.list.filter(x => x.key !== item.key))
|
||||
},
|
||||
removeRight (item) {
|
||||
this.removeTabs(this.list.slice(this.list.findIndex(x => x.key === item.key) + 1))
|
||||
},
|
||||
removeLeft (item) {
|
||||
this.removeTabs(this.list.slice(0, this.list.findIndex(x => x.key === item.key)))
|
||||
},
|
||||
removeAll () {
|
||||
this.removeTabs(this.list)
|
||||
},
|
||||
removeTabs (items) {
|
||||
this.$emit('remove', items)
|
||||
},
|
||||
swapTab (oldIndex, newIndex) {
|
||||
const list = this.list
|
||||
}
|
||||
|
||||
function removeTabs (items: Components.Tabs.Item[]) {
|
||||
emit('remove', items)
|
||||
}
|
||||
|
||||
function removeOther (item: Components.Tabs.Item) {
|
||||
removeTabs(props.list.filter(x => x.key !== item.key))
|
||||
}
|
||||
|
||||
function removeRight (item: Components.Tabs.Item) {
|
||||
removeTabs(props.list.slice(props.list.findIndex(x => x.key === item.key) + 1))
|
||||
}
|
||||
|
||||
function removeLeft (item: Components.Tabs.Item) {
|
||||
removeTabs(props.list.slice(0, props.list.findIndex(x => x.key === item.key)))
|
||||
}
|
||||
|
||||
function removeAll () {
|
||||
removeTabs(props.list)
|
||||
}
|
||||
|
||||
function swapTab (oldIndex: number, newIndex: number) {
|
||||
const list = props.list
|
||||
const tmp = list[oldIndex]
|
||||
list[oldIndex] = list[newIndex]
|
||||
list[newIndex] = tmp
|
||||
this.$emit('change-list', list)
|
||||
emit('change-list', list)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
function showContextMenu (item: Components.Tabs.Item) {
|
||||
contextMenu.show([
|
||||
{ id: 'close', label: '关闭', onClick: () => removeTabs([item]) },
|
||||
{ id: 'close-other', label: '关闭其他', onClick: () => removeOther(item) },
|
||||
{ id: 'close-right', label: '关闭到右侧', onClick: () => removeRight(item) },
|
||||
{ id: 'close-left', label: '关闭到左侧', onClick: () => removeLeft(item) },
|
||||
{ id: 'close-all', label: '全部关闭', onClick: () => removeAll() },
|
||||
])
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
Sortable.create(refTabs.value!!, {
|
||||
animation: 250,
|
||||
ghostClass: 'on-sort',
|
||||
direction: 'horizontal',
|
||||
onEnd: ({ oldIndex, newIndex }: { oldIndex?: number; newIndex?: number }) => {
|
||||
swapTab(oldIndex || 0, newIndex || 0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
refTabs,
|
||||
switchTab,
|
||||
showContextMenu,
|
||||
removeTabs,
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -124,14 +149,10 @@ export default {
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: #aaa;
|
||||
color: rgb(212, 212, 212);
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
zoom: .6;
|
||||
}
|
||||
|
||||
.tab.on-sort {
|
||||
background: rgb(112, 112, 112);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="title-bar" :style="titleBarStyles">
|
||||
<div v-if="win && !isMaximized" class="resizer"></div>
|
||||
<div v-if="hasWin && !isMaximized" class="resizer"></div>
|
||||
<h4 class="title">
|
||||
<img v-if="win" @dblclick="close" class="logo" src="~@/assets/icon.png" alt="logo">
|
||||
<img v-if="hasWin" @dblclick="close" class="logo" src="~@/assets/icon.png" alt="logo">
|
||||
<span>{{statusText}}</span>
|
||||
</h4>
|
||||
<div class="action" v-if="win">
|
||||
<div class="action" v-if="hasWin">
|
||||
<div title="置顶窗口" :class="{btn: true, pin: true, ontop: isAlwaysOnTop}" @click="toggleAlwaysOnTop">
|
||||
<y-icon class="pin-icon" name="thumbtack"></y-icon>
|
||||
<svg-icon color="hsla(0, 0%, 100%, .5)" name="thumbtack-solid"></svg-icon>
|
||||
</div>
|
||||
<div title="最小化" class="btn" @click="minimize">
|
||||
<div class="icon minimize"></div>
|
||||
@@ -25,141 +25,160 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import env from '@/lib/env'
|
||||
import 'vue-awesome/icons/thumbtack'
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onBeforeUnmount, onMounted, ref, toRefs, watch } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import env from '../useful/env'
|
||||
import SvgIcon from './SvgIcon.vue'
|
||||
|
||||
const isElectron = env.isElectron
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'title-bar',
|
||||
data () {
|
||||
return {
|
||||
win: null,
|
||||
isMaximized: false,
|
||||
isAlwaysOnTop: false,
|
||||
isFocused: false,
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (!isElectron) {
|
||||
window.onbeforeunload = () => {
|
||||
return !this.saved || null
|
||||
components: { SvgIcon },
|
||||
setup () {
|
||||
const store = useStore()
|
||||
|
||||
const { currentFile, savedAt, currentContent, previousContent } = toRefs(store.state)
|
||||
const saved = computed(() => currentContent.value === previousContent.value)
|
||||
|
||||
let win: any = null
|
||||
|
||||
const hasWin = ref(false)
|
||||
const isMaximized = ref(false)
|
||||
const isAlwaysOnTop = ref(false)
|
||||
const isFocused = ref(false)
|
||||
|
||||
function updateWindowStatus () {
|
||||
if (win) {
|
||||
isMaximized.value = win.isMaximized()
|
||||
isAlwaysOnTop.value = win.isAlwaysOnTop()
|
||||
isFocused.value = win.isFocused()
|
||||
}
|
||||
}
|
||||
|
||||
if (isElectron && env.require) {
|
||||
this.win = env.require('electron').remote.getCurrentWindow()
|
||||
this.updateWindowStatus()
|
||||
this.win.on('maximize', this.updateWindowStatus)
|
||||
this.win.on('restore', this.updateWindowStatus)
|
||||
this.win.on('unmaximize', this.updateWindowStatus)
|
||||
this.win.on('always-on-top-changed', this.updateWindowStatus)
|
||||
this.win.on('focus', this.updateWindowStatus)
|
||||
this.win.on('blur', this.updateWindowStatus)
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
if (!isElectron) {
|
||||
window.onbeforeunload = null
|
||||
function toggleAlwaysOnTop () {
|
||||
win && win.setAlwaysOnTop(!win.isAlwaysOnTop())
|
||||
}
|
||||
|
||||
if (this.win) {
|
||||
this.win.removeListener('maximize', this.updateWindowStatus)
|
||||
this.win.removeListener('restore', this.updateWindowStatus)
|
||||
this.win.removeListener('unmaximize', this.updateWindowStatus)
|
||||
this.win.removeListener('minimize', this.updateWindowStatus)
|
||||
this.win.removeListener('always-on-top-changed', this.updateWindowStatus)
|
||||
this.win.removeListener('focus', this.updateWindowStatus)
|
||||
this.win.removeListener('blur', this.updateWindowStatus)
|
||||
function unmaximize () {
|
||||
win && win.unmaximize()
|
||||
}
|
||||
|
||||
this.win = null
|
||||
},
|
||||
computed: {
|
||||
...mapState('app', ['currentFile', 'savedAt', 'previousContent', 'currentContent']),
|
||||
titleBarStyles () {
|
||||
if (isElectron && !this.isFocused) {
|
||||
return { background: '#818181' }
|
||||
function minimize () {
|
||||
win && win.minimize()
|
||||
}
|
||||
|
||||
function maximize () {
|
||||
// 最大化后取消窗口置顶
|
||||
win && win.maximize()
|
||||
win && win.setAlwaysOnTop(false)
|
||||
}
|
||||
|
||||
function close () {
|
||||
win && win.close()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!isElectron) {
|
||||
window.onbeforeunload = () => {
|
||||
return !saved.value || null
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.saved) {
|
||||
return { background: '#ff9800ad' }
|
||||
if (isElectron && env.require) {
|
||||
win = env.require('electron').remote.getCurrentWindow()
|
||||
hasWin.value = true
|
||||
updateWindowStatus()
|
||||
win.on('maximize', updateWindowStatus)
|
||||
win.on('restore', updateWindowStatus)
|
||||
win.on('unmaximize', updateWindowStatus)
|
||||
win.on('always-on-top-changed', updateWindowStatus)
|
||||
win.on('focus', updateWindowStatus)
|
||||
win.on('blur', updateWindowStatus)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (!isElectron) {
|
||||
window.onbeforeunload = null
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
saved () {
|
||||
return this.previousContent === this.currentContent
|
||||
},
|
||||
status () {
|
||||
if (this.savedAt === null && this.currentFile) {
|
||||
return '加载完毕'
|
||||
} else if (this.savedAt) {
|
||||
return '保存于:' + this.savedAt.toLocaleString()
|
||||
if (win) {
|
||||
win.removeListener('maximize', updateWindowStatus)
|
||||
win.removeListener('restore', updateWindowStatus)
|
||||
win.removeListener('unmaximize', updateWindowStatus)
|
||||
win.removeListener('minimize', updateWindowStatus)
|
||||
win.removeListener('always-on-top-changed', updateWindowStatus)
|
||||
win.removeListener('focus', updateWindowStatus)
|
||||
win.removeListener('blur', updateWindowStatus)
|
||||
}
|
||||
|
||||
return ''
|
||||
},
|
||||
statusText () {
|
||||
const file = this.currentFile
|
||||
win = null
|
||||
hasWin.value = false
|
||||
})
|
||||
|
||||
const statusText = computed(() => {
|
||||
let status = ''
|
||||
|
||||
if (savedAt.value === null && currentFile.value) {
|
||||
status = '加载完毕'
|
||||
} else if (savedAt.value) {
|
||||
status = '保存于:' + savedAt.value.toLocaleString()
|
||||
}
|
||||
|
||||
const file = currentFile.value
|
||||
if (file) {
|
||||
if (file.repo === '__help__') {
|
||||
return file.title
|
||||
}
|
||||
|
||||
if (file.path && file.repo) {
|
||||
return `${file.path}-${this.status} [${file.repo}]`
|
||||
return `${file.path}-${status} [${file.repo}]`
|
||||
} else {
|
||||
return file.name
|
||||
}
|
||||
} else {
|
||||
return '未打开文件'
|
||||
}
|
||||
})
|
||||
|
||||
const titleBarStyles = computed(() => {
|
||||
if (isElectron && !isFocused.value) {
|
||||
return { background: '#818181' }
|
||||
}
|
||||
|
||||
if (!saved.value) {
|
||||
return { background: '#ff9800ad' }
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
watch(statusText, () => {
|
||||
document.title = currentFile.value ? (currentFile.value.name || currentFile.value.title || 'Yank Note') : '未打开文件'
|
||||
}, { immediate: true })
|
||||
|
||||
watch(saved, val => {
|
||||
// 暴露文档保存状态给 Electron 用
|
||||
(window as any).documentSaved = val
|
||||
}, { immediate: true })
|
||||
|
||||
return {
|
||||
hasWin,
|
||||
isMaximized,
|
||||
isAlwaysOnTop,
|
||||
isFocused,
|
||||
toggleAlwaysOnTop,
|
||||
maximize,
|
||||
unmaximize,
|
||||
minimize,
|
||||
close,
|
||||
statusText,
|
||||
titleBarStyles,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
statusText: {
|
||||
immediate: true,
|
||||
handler (val) {
|
||||
document.title = this.currentFile ? (this.currentFile.name || this.currentFile.title || 'Yank Note') : '未打开文件'
|
||||
},
|
||||
},
|
||||
saved: {
|
||||
immediate: true,
|
||||
handler (val) {
|
||||
window.documentSaved = val
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateWindowStatus () {
|
||||
if (this.win) {
|
||||
this.isMaximized = this.win.isMaximized()
|
||||
this.isAlwaysOnTop = this.win.isAlwaysOnTop()
|
||||
this.isFocused = this.win.isFocused()
|
||||
}
|
||||
},
|
||||
toggleAlwaysOnTop () {
|
||||
this.win && this.win.setAlwaysOnTop(!this.win.isAlwaysOnTop())
|
||||
},
|
||||
unmaximize () {
|
||||
this.win && this.win.unmaximize()
|
||||
},
|
||||
minimize () {
|
||||
this.win && this.win.minimize()
|
||||
},
|
||||
maximize () {
|
||||
// 最大化后取消窗口置顶
|
||||
this.win && this.win.maximize()
|
||||
this.win && this.win.setAlwaysOnTop(false)
|
||||
},
|
||||
close () {
|
||||
this.win && this.win.close()
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -260,8 +279,4 @@ export default {
|
||||
.action .btn.pin.ontop {
|
||||
background-color: hsla(0, 0%, 100%, .3)
|
||||
}
|
||||
|
||||
.action .btn.pin .pin-icon {
|
||||
fill: hsla(0, 0%, 100%, .5)
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,24 +2,33 @@
|
||||
<div v-if="toast" :class="{toast: true, [`toast-${toast.type}`]: true}">{{toast.content}}</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { Components } from '../types'
|
||||
|
||||
interface ToastData {
|
||||
type: Components.Toast.ToastType;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'toast',
|
||||
data () {
|
||||
return {
|
||||
toast: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show (type, content, timeout = 2000) {
|
||||
this.toast = { type, content }
|
||||
setup () {
|
||||
const toast = ref<ToastData | null>()
|
||||
|
||||
function show (type: Components.Toast.ToastType, content: string, timeout = 2000) {
|
||||
toast.value = { type, content }
|
||||
|
||||
setTimeout(() => {
|
||||
this.toast = null
|
||||
toast.value = null
|
||||
}, timeout)
|
||||
},
|
||||
}
|
||||
|
||||
return { toast, show }
|
||||
},
|
||||
}
|
||||
methods: {
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<aside class="side" @contextmenu.exact.prevent="showContextMenu" @dblclick="refresh();refreshRepo()" title="双击此处刷新目录树">
|
||||
<aside class="side" @contextmenu.exact.prevent="showContextMenu" @dblclick="refreshRepo" title="双击此处刷新目录树">
|
||||
<div class="loading" v-if="tree === null"> 加载中 </div>
|
||||
<template v-else>
|
||||
<TreeNode v-for="item in tree" :item="item" :key="item.path" />
|
||||
@@ -7,65 +7,72 @@
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import TreeNode from './TreeNode'
|
||||
<script lang="ts">
|
||||
import { defineComponent, onBeforeMount, onBeforeUnmount, toRefs, watch } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useBus } from '../useful/bus'
|
||||
import { useContextMenu } from '../useful/context-menu'
|
||||
import TreeNode from './TreeNode.vue'
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'tree',
|
||||
components: { TreeNode },
|
||||
data () {
|
||||
return {
|
||||
setup () {
|
||||
const bus = useBus()
|
||||
const store = useStore()
|
||||
const contextMenu = useContextMenu()
|
||||
|
||||
const { currentRepo, tree } = toRefs(store.state)
|
||||
|
||||
function refreshTree () {
|
||||
store.dispatch('fetchTree', currentRepo.value)
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$bus.on('file-created', this.refresh)
|
||||
this.$bus.on('file-moved', this.refresh)
|
||||
this.$bus.on('file-deleted', this.refresh)
|
||||
this.$bus.on('file-uploaded', this.refresh)
|
||||
this.$bus.on('file-marked', this.refresh)
|
||||
this.$bus.on('file-unmarked', this.refresh)
|
||||
this.$bus.on('tree-refresh', this.refresh)
|
||||
this.refresh()
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$bus.off('file-created', this.refresh)
|
||||
this.$bus.off('file-moved', this.refresh)
|
||||
this.$bus.off('file-deleted', this.refresh)
|
||||
this.$bus.off('file-uploaded', this.refresh)
|
||||
this.$bus.off('file-marked', this.refresh)
|
||||
this.$bus.off('file-unmarked', this.refresh)
|
||||
this.$bus.off('tree-refresh', this.refresh)
|
||||
},
|
||||
methods: {
|
||||
showContextMenu () {
|
||||
this.$contextMenu.show([
|
||||
|
||||
function refreshRepo () {
|
||||
refreshTree()
|
||||
store.dispatch('fetchRepositories')
|
||||
}
|
||||
|
||||
function showContextMenu () {
|
||||
contextMenu.show([
|
||||
{
|
||||
id: 'refresh',
|
||||
label: '刷新目录树',
|
||||
onClick: () => {
|
||||
this.refresh()
|
||||
this.refreshRepo()
|
||||
}
|
||||
onClick: refreshRepo
|
||||
}
|
||||
])
|
||||
},
|
||||
refresh () {
|
||||
this.$store.dispatch('app/fetchTree', this.currentRepo)
|
||||
},
|
||||
refreshRepo () {
|
||||
this.$store.dispatch('app/fetchRepositories')
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
bus.on('file-created', refreshTree)
|
||||
bus.on('file-moved', refreshTree)
|
||||
bus.on('file-deleted', refreshTree)
|
||||
bus.on('file-uploaded', refreshTree)
|
||||
bus.on('file-marked', refreshTree)
|
||||
bus.on('file-unmarked', refreshTree)
|
||||
bus.on('tree-refresh', refreshTree)
|
||||
refreshTree()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
bus.off('file-created', refreshTree)
|
||||
bus.off('file-moved', refreshTree)
|
||||
bus.off('file-deleted', refreshTree)
|
||||
bus.off('file-uploaded', refreshTree)
|
||||
bus.off('file-marked', refreshTree)
|
||||
bus.off('file-unmarked', refreshTree)
|
||||
bus.off('tree-refresh', refreshTree)
|
||||
})
|
||||
|
||||
watch(currentRepo, refreshTree)
|
||||
|
||||
return {
|
||||
tree,
|
||||
refreshRepo,
|
||||
showContextMenu,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentRepo () {
|
||||
this.refresh()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('app', ['currentRepo', 'tree']),
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="tree-node">
|
||||
<details ref="dir" @keydown.enter.prevent v-if="item.type === 'dir'" class="name" :title="item.name + '\n\n' + dirTitle" :open="item.path === '/'">
|
||||
<details ref="refDir" @keydown.enter.prevent v-if="item.type === 'dir'" class="name" :title="item.name + '\n\n' + dirTitle" :open="item.path === '/'">
|
||||
<summary
|
||||
class="folder"
|
||||
:style="{background: selected ? '#313131' : 'none'}"
|
||||
@@ -14,17 +14,14 @@
|
||||
{{ item.name === '/' ? currentRepoName : item.name }} <span class="count">({{item.children.length}})</span>
|
||||
</div>
|
||||
<div class="item-action">
|
||||
<y-icon class="icon" name="folder-plus" @click.native.exact.stop.prevent="createFile()" title="创建文件"></y-icon>
|
||||
<!-- <EditIcon class="icon" @click.native.exact.stop.prevent="renameFile" title="重命名/移动(Ctrl + 右键)"></EditIcon>
|
||||
<ShareIcon class="icon" @click.native.exact.stop.prevent="revealInExplorer" title="系统中打开(Ctrl + 单击)"></ShareIcon>
|
||||
<TrashIcon class="icon" @click.native.exact.stop.prevent="deleteFile" title="删除(Shift + 右键)"></TrashIcon> -->
|
||||
<svg-icon class="icon" color="hsla(0, 0%, 100%, .5)" name="folder-plus-solid" @click.exact.stop.prevent="createFile()" title="创建文件"></svg-icon>
|
||||
</div>
|
||||
</div>
|
||||
</summary>
|
||||
<tree-node v-for="x in item.children" :key="x.path" :item="x"></tree-node>
|
||||
</details>
|
||||
<div
|
||||
ref="file"
|
||||
ref="refFile"
|
||||
v-else
|
||||
:class="{name: true, 'file-name': true, selected}"
|
||||
:title="item.name + '\n\n' + fileTitle"
|
||||
@@ -34,139 +31,60 @@
|
||||
@contextmenu.ctrl.exact.prevent.stop="renameFile"
|
||||
@contextmenu.shift.exact.prevent.stop="deleteFile">
|
||||
<div class="item">
|
||||
<div :class="{'item-label': true, marked: !!item.marked}"> {{ item.name }} </div>
|
||||
<div :class="{'item-label': true, marked}"> {{ item.name }} </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import File from '@/lib/file'
|
||||
import Extensions from '@/lib/extensions'
|
||||
import DrawioPlugin from '@/plugins/DrawioPlugin'
|
||||
import 'vue-awesome/icons/folder-plus'
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, nextTick, ref, toRefs, watch } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useBus } from '../useful/bus'
|
||||
import { useToast } from '../useful/toast'
|
||||
import { useModal } from '../useful/modal'
|
||||
import { useContextMenu } from '../useful/context-menu'
|
||||
import File from '../useful/file'
|
||||
import { Components } from '../types'
|
||||
import Extensions from '../useful/extensions'
|
||||
import SvgIcon from './SvgIcon.vue'
|
||||
import DrawioPlugin from '../plugins/DrawioPlugin'
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'tree-node',
|
||||
props: {
|
||||
item: Object,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
dirTitle: [
|
||||
'"Ctrl + 右键" 重命名目录',
|
||||
'"Shift + 右键" 删除目录',
|
||||
'"Ctrl + 单击" 在操作系统中打开目录',
|
||||
'"Ctrl + Alt + 单击" 在终端中打开'
|
||||
].join('\n'),
|
||||
fileTitle: [
|
||||
`创建于:${this.item.birthtime ? new Date(this.item.birthtime).toLocaleString() : '无'}`,
|
||||
`更新于:${this.item.mtime ? new Date(this.item.mtime).toLocaleString() : '无'}`,
|
||||
'',
|
||||
'"Ctrl + 右键" 重命名文件',
|
||||
'"Shift + 右键" 删除文件',
|
||||
'"Ctrl + 单击" 使用系统程序打开文件',
|
||||
'".c.md" 结尾的文件为加密文件'
|
||||
].join('\n')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showContextMenu (item) {
|
||||
const menu = [
|
||||
{ id: 'rename', label: '重命名 / 移动', onClick: () => this.renameFile() },
|
||||
{ id: 'delete', label: '删除', onClick: () => this.deleteFile() },
|
||||
{ type: 'separator' },
|
||||
{ id: 'openInOS', label: '在系统中打开', onClick: () => this.revealInExplorer() },
|
||||
{ id: 'openInOS', label: '刷新目录树', onClick: () => this.$bus.emit('tree-refresh') },
|
||||
]
|
||||
components: { SvgIcon },
|
||||
props: ['item'],
|
||||
setup (props) {
|
||||
const store = useStore()
|
||||
const bus = useBus()
|
||||
const toast = useToast()
|
||||
const modal = useModal()
|
||||
const contextMenu = useContextMenu()
|
||||
|
||||
if (item.type === 'dir') {
|
||||
this.$contextMenu.show([
|
||||
{ id: 'create', label: '创建新文件', onClick: () => this.createFile() }
|
||||
].concat(menu).concat([
|
||||
{ id: 'openInTerminal', label: '在终端中打开', onClick: () => this.revealInXterminal() }
|
||||
]))
|
||||
} else {
|
||||
// markdown 文件可以被标记
|
||||
if (this.item.path.endsWith('.md')) {
|
||||
const additional = [
|
||||
{ id: 'mark', label: this.item.marked ? '取消标记' : '标记文件', onClick: () => this.toggleMark() },
|
||||
]
|
||||
const refDir = ref<any>(null)
|
||||
const refFile = ref<any>(null)
|
||||
const localMarked = ref<boolean | null>(null)
|
||||
|
||||
// 非加密文件增加复制重复菜单
|
||||
if (!File.isEncryptedFile(this.item)) {
|
||||
additional.push({ id: 'duplicate', label: '重复文件', onClick: () => this.duplicateFile() })
|
||||
}
|
||||
watch(() => props.item, () => {
|
||||
localMarked.value = null
|
||||
})
|
||||
|
||||
this.$contextMenu.show(additional.concat(menu, [{ id: 'create', label: '当前目录创建新文件', onClick: () => this.createFile() }]))
|
||||
} else {
|
||||
this.$contextMenu.show(menu)
|
||||
}
|
||||
}
|
||||
},
|
||||
async duplicateFile () {
|
||||
let newPath = await this.$modal.input({
|
||||
title: '重复文件',
|
||||
hint: '目标路径',
|
||||
content: '当前路径:' + this.item.path,
|
||||
value: this.item.path,
|
||||
// 默认选中文件名
|
||||
select: [this.item.path.lastIndexOf('/') + 1, this.item.name.lastIndexOf('.') > -1 ? this.item.path.lastIndexOf('.') : this.item.path.length, 'forward']
|
||||
})
|
||||
const { currentFile, currentRepo } = toRefs(store.state)
|
||||
|
||||
if (!newPath) {
|
||||
return
|
||||
}
|
||||
|
||||
newPath = newPath.replace(/\/$/, '')
|
||||
|
||||
const { content } = await File.read({ path: this.item.path, repo: this.item.repo })
|
||||
await this.createFile(newPath, content)
|
||||
},
|
||||
async toggleMark () {
|
||||
if (this.item.marked) {
|
||||
this.item.marked = false
|
||||
await File.unmark(this.item)
|
||||
this.$bus.emit('file-unmarked', this.item)
|
||||
} else {
|
||||
this.item.marked = true
|
||||
await File.mark(this.item)
|
||||
this.$bus.emit('file-marked', this.item)
|
||||
}
|
||||
},
|
||||
select (item) {
|
||||
if (item.type !== 'dir') {
|
||||
if (Extensions.supported(item.name)) {
|
||||
this.$store.commit('app/setCurrentFile', item)
|
||||
} else {
|
||||
if (item.path.toLowerCase().endsWith('.drawio')) {
|
||||
DrawioPlugin.open(item)
|
||||
} else {
|
||||
File.openInOS(this.item)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
revealInExplorer () {
|
||||
File.openInOS(this.item)
|
||||
},
|
||||
revealInXterminal () {
|
||||
const path = this.currentRepo ? this.currentRepo.path + this.item.path : ''
|
||||
|
||||
this.$bus.emit('xterm-run', `--yank-note-run-command-cd-- ${path}`)
|
||||
},
|
||||
async createFile (path = null, content = null) {
|
||||
async function createFile (path: string | null = null, content: string | null = null) {
|
||||
if (path === null) {
|
||||
const currentPath = this.item.type === 'dir' ? this.item.path : File.dirname(this.item.path)
|
||||
const currentPath = props.item.type === 'dir' ? props.item.path : File.dirname(props.item.path)
|
||||
|
||||
let filename = await this.$modal.input({
|
||||
let filename = await modal.input({
|
||||
title: '创建文件(加密文件以 .c.md 结尾)',
|
||||
hint: '文件路径',
|
||||
content: '当前路径:' + currentPath,
|
||||
value: 'new.md'
|
||||
value: 'new.md',
|
||||
select: true
|
||||
})
|
||||
|
||||
console.log('filename', filename)
|
||||
|
||||
if (!filename) {
|
||||
return
|
||||
}
|
||||
@@ -186,7 +104,7 @@ export default {
|
||||
|
||||
const file = {
|
||||
type: 'file',
|
||||
repo: this.item.repo,
|
||||
repo: props.item.repo,
|
||||
path,
|
||||
name: filename,
|
||||
}
|
||||
@@ -195,21 +113,17 @@ export default {
|
||||
content = `# ${filename.replace(/\.md$/i, '')}\n`
|
||||
}
|
||||
|
||||
this.$bus.emit('file-new', { file, content })
|
||||
},
|
||||
async renameFile () {
|
||||
if (this.item.path === '/') {
|
||||
this.$toast.show('warning', '不能移动根目录')
|
||||
return
|
||||
}
|
||||
bus.emit('file-new', { file, content })
|
||||
}
|
||||
|
||||
let newPath = await this.$modal.input({
|
||||
title: '移动文件',
|
||||
hint: '新的路径',
|
||||
content: '当前路径:' + this.item.path,
|
||||
value: this.item.path,
|
||||
async function duplicateFile () {
|
||||
let newPath = await modal.input({
|
||||
title: '重复文件',
|
||||
hint: '目标路径',
|
||||
content: '当前路径:' + props.item.path,
|
||||
value: props.item.path,
|
||||
// 默认选中文件名
|
||||
select: [this.item.path.lastIndexOf('/') + 1, this.item.name.lastIndexOf('.') > -1 ? this.item.path.lastIndexOf('.') : this.item.path.length, 'forward']
|
||||
select: [props.item.path.lastIndexOf('/') + 1, props.item.name.lastIndexOf('.') > -1 ? props.item.path.lastIndexOf('.') : props.item.path.length, 'forward']
|
||||
})
|
||||
|
||||
if (!newPath) {
|
||||
@@ -217,97 +131,228 @@ export default {
|
||||
}
|
||||
|
||||
newPath = newPath.replace(/\/$/, '')
|
||||
const oldPath = this.item.path.replace(/\/$/, '')
|
||||
|
||||
const { content } = await File.read({ path: props.item.path, repo: props.item.repo })
|
||||
await createFile(newPath, content)
|
||||
}
|
||||
|
||||
async function toggleMark () {
|
||||
if (props.item.marked) {
|
||||
localMarked.value = false
|
||||
await File.unmark(props.item)
|
||||
bus.emit('file-unmarked', props.item)
|
||||
} else {
|
||||
localMarked.value = true
|
||||
await File.mark(props.item)
|
||||
bus.emit('file-marked', props.item)
|
||||
}
|
||||
}
|
||||
|
||||
function select (item: any) {
|
||||
if (item.type !== 'dir') {
|
||||
if (Extensions.supported(item.name)) {
|
||||
store.commit('setCurrentFile', item)
|
||||
} else {
|
||||
if (item.path.toLowerCase().endsWith('.drawio')) {
|
||||
DrawioPlugin.open(item)
|
||||
} else {
|
||||
File.openInOS(props.item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function revealInExplorer () {
|
||||
File.openInOS(props.item)
|
||||
}
|
||||
|
||||
function revealInXterminal () {
|
||||
const path = currentRepo.value ? currentRepo.value.path + props.item.path : ''
|
||||
|
||||
bus.emit('xterm-run', `--yank-note-run-command-cd-- ${path}`)
|
||||
}
|
||||
|
||||
async function renameFile () {
|
||||
if (props.item.path === '/') {
|
||||
toast.show('warning', '不能移动根目录')
|
||||
return
|
||||
}
|
||||
|
||||
let newPath = await modal.input({
|
||||
title: '移动文件',
|
||||
hint: '新的路径',
|
||||
content: '当前路径:' + props.item.path,
|
||||
value: props.item.path,
|
||||
// 默认选中文件名
|
||||
select: [props.item.path.lastIndexOf('/') + 1, props.item.name.lastIndexOf('.') > -1 ? props.item.path.lastIndexOf('.') : props.item.path.length, 'forward']
|
||||
})
|
||||
|
||||
if (!newPath) {
|
||||
return
|
||||
}
|
||||
|
||||
newPath = newPath.replace(/\/$/, '')
|
||||
const oldPath = props.item.path.replace(/\/$/, '')
|
||||
|
||||
if (newPath === oldPath) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO 文件统一处理
|
||||
// this.$bus.emit('file-move', {file, newPath})
|
||||
// bus.emit('file-move', {file, newPath})
|
||||
const newFile = {
|
||||
name: File.basename(newPath),
|
||||
path: newPath,
|
||||
repo: this.item.repo,
|
||||
type: this.item.type
|
||||
repo: props.item.repo,
|
||||
type: props.item.type
|
||||
}
|
||||
|
||||
await File.move(this.item, newPath)
|
||||
await File.move(props.item, newPath)
|
||||
// 重命名当前文件或父目录后,切换到新位置
|
||||
if (this.currentFile && (File.isSameFile(this.item, this.currentFile) || (this.item.type === 'dir' && File.isBelongTo(this.item.path, this.currentFile.path)))) {
|
||||
if (currentFile.value && (File.isSameFile(props.item, currentFile.value) || (props.item.type === 'dir' && File.isBelongTo(props.item.path, currentFile.value.path)))) {
|
||||
if (newFile.type === 'file') {
|
||||
this.$store.commit('app/setCurrentFile', newFile)
|
||||
store.commit('setCurrentFile', newFile)
|
||||
} else {
|
||||
// TODO 切换到新位置
|
||||
this.$store.commit('app/setCurrentFile', null)
|
||||
store.commit('setCurrentFile', null)
|
||||
}
|
||||
}
|
||||
|
||||
this.$bus.emit('file-moved', newFile)
|
||||
},
|
||||
async deleteFile () {
|
||||
if (this.item.path === '/') {
|
||||
this.$toast.show('warning', '不能删除根目录')
|
||||
bus.emit('file-moved', newFile)
|
||||
}
|
||||
|
||||
async function deleteFile () {
|
||||
if (props.item.path === '/') {
|
||||
toast.show('warning', '不能删除根目录')
|
||||
return
|
||||
}
|
||||
|
||||
const confirm = await this.$modal.confirm({ title: '删除文件', content: `确定要删除 [${this.item.path}] 吗?` })
|
||||
const confirm = await modal.confirm({ title: '删除文件', content: `确定要删除 [${props.item.path}] 吗?` })
|
||||
|
||||
if (confirm) {
|
||||
await File.delete(this.item)
|
||||
await File.delete(props.item)
|
||||
|
||||
// 删除当前文件或父目录后,关闭当前文件
|
||||
if (this.currentFile && (File.isSameFile(this.item, this.currentFile) || (this.item.type === 'dir' && File.isBelongTo(this.item.path, this.currentFile.path)))) {
|
||||
this.$store.commit('app/setCurrentFile', null)
|
||||
if (currentFile.value && (File.isSameFile(props.item, currentFile.value) || (props.item.type === 'dir' && File.isBelongTo(props.item.path, currentFile.value.path)))) {
|
||||
store.commit('setCurrentFile', null)
|
||||
}
|
||||
|
||||
this.$bus.emit('file-deleted', this.item)
|
||||
bus.emit('file-deleted', props.item)
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
currentRepoName () {
|
||||
return this.currentRepo ? this.currentRepo.name : '/'
|
||||
},
|
||||
...mapState('app', ['currentFile', 'currentRepo']),
|
||||
selected () {
|
||||
if (!this.currentFile) {
|
||||
}
|
||||
|
||||
function showContextMenu (item: any) {
|
||||
const menu: Components.ContextMenu.Item[] = [
|
||||
{ id: 'delete', label: '删除', onClick: () => deleteFile() },
|
||||
{ id: 'rename', label: '重命名 / 移动', onClick: () => renameFile() },
|
||||
{ type: 'separator' },
|
||||
{ id: 'openInOS', label: '在系统中打开', onClick: () => revealInExplorer() },
|
||||
{ id: 'openInOS', label: '刷新目录树', onClick: () => bus.emit('tree-refresh') },
|
||||
]
|
||||
|
||||
if (item.type === 'dir') {
|
||||
contextMenu.show([
|
||||
{ id: 'create', label: '创建新文件', onClick: () => createFile() },
|
||||
...menu,
|
||||
{ id: 'openInTerminal', label: '在终端中打开', onClick: () => revealInXterminal() }
|
||||
])
|
||||
} else {
|
||||
// markdown 文件可以被标记
|
||||
if (props.item.path.endsWith('.md')) {
|
||||
const additional: Components.ContextMenu.Item[] = [
|
||||
{ id: 'mark', label: props.item.marked ? '取消标记' : '标记文件', onClick: () => toggleMark() },
|
||||
]
|
||||
|
||||
// 非加密文件增加复制重复菜单
|
||||
if (!File.isEncryptedFile(props.item)) {
|
||||
additional.push({ id: 'duplicate', label: '重复文件', onClick: () => duplicateFile() })
|
||||
}
|
||||
|
||||
contextMenu.show([
|
||||
...additional,
|
||||
...menu,
|
||||
{ id: 'create', label: '当前目录创建新文件', onClick: () => createFile() }
|
||||
])
|
||||
} else {
|
||||
contextMenu.show(menu)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const currentRepoName = computed(() => currentRepo.value?.name ?? '/')
|
||||
|
||||
const selected = computed(() => {
|
||||
if (!currentFile.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.item.type === 'dir') {
|
||||
return this.currentFile.repo === this.item.repo && this.currentFile.path.startsWith(this.item.path + '/')
|
||||
if (props.item.type === 'dir') {
|
||||
return currentFile.value.repo === props.item.repo && currentFile.value.path.startsWith(props.item.path + '/')
|
||||
}
|
||||
|
||||
return this.currentFile.repo === this.item.repo && this.currentFile.path === this.item.path
|
||||
},
|
||||
shouldOpen () {
|
||||
return this.item.type === 'dir' && this.currentFile && this.currentFile.path.startsWith(this.item.path + '/') && this.currentFile.repo === this.item.repo
|
||||
return currentFile.value.repo === props.item.repo && currentFile.value.path === props.item.path
|
||||
})
|
||||
|
||||
const shouldOpen = computed(() => {
|
||||
return props.item.type === 'dir' && currentFile.value && currentFile.value.path.startsWith(props.item.path + '/') && currentFile.value.repo === props.item.repo
|
||||
})
|
||||
|
||||
const marked = computed(() => localMarked.value ?? props.item.marked)
|
||||
|
||||
watch(selected, val => {
|
||||
if (val && props.item.type === 'file') {
|
||||
nextTick(() => {
|
||||
refFile.value.scrollIntoViewIfNeeded()
|
||||
})
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
watch(shouldOpen, val => {
|
||||
if (val) {
|
||||
nextTick(() => {
|
||||
refDir.value.open = true
|
||||
})
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
const dirTitle = computed(() => [
|
||||
'"Ctrl + 右键" 重命名目录',
|
||||
'"Shift + 右键" 删除目录',
|
||||
'"Ctrl + 单击" 在操作系统中打开目录',
|
||||
'"Ctrl + Alt + 单击" 在终端中打开'
|
||||
].join('\n'))
|
||||
|
||||
const fileTitle = computed(() => [
|
||||
`创建于:${props.item.birthtime ? new Date(props.item.birthtime).toLocaleString() : '无'}`,
|
||||
`更新于:${props.item.mtime ? new Date(props.item.mtime).toLocaleString() : '无'}`,
|
||||
'',
|
||||
'"Ctrl + 右键" 重命名文件',
|
||||
'"Shift + 右键" 删除文件',
|
||||
'"Ctrl + 单击" 使用系统程序打开文件',
|
||||
'".c.md" 结尾的文件为加密文件'
|
||||
].join('\n'))
|
||||
|
||||
return {
|
||||
refDir,
|
||||
refFile,
|
||||
dirTitle,
|
||||
fileTitle,
|
||||
currentRepoName,
|
||||
selected,
|
||||
marked,
|
||||
|
||||
showContextMenu,
|
||||
duplicateFile,
|
||||
toggleMark,
|
||||
select,
|
||||
revealInExplorer,
|
||||
revealInXterminal,
|
||||
createFile,
|
||||
renameFile,
|
||||
deleteFile
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selected: {
|
||||
immediate: true,
|
||||
handler (val) {
|
||||
if (val && this.item.type === 'file') {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.file.scrollIntoViewIfNeeded()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
shouldOpen: {
|
||||
immediate: true,
|
||||
handler (val) {
|
||||
if (val) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.dir.open = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -365,14 +410,15 @@ summary > .item {
|
||||
padding: 0 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 22px;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
border-radius: 2px;
|
||||
fill: #999999;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.item-action .icon:hover {
|
||||
background: #757575;
|
||||
fill: #eee;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.item:hover .item-action {
|
||||
|
||||
@@ -1,35 +1,45 @@
|
||||
<template>
|
||||
<div class="xterm">
|
||||
<div ref="xterm" style="height: 100%"></div>
|
||||
<div ref="refXterm" style="height: 100%"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import io from 'socket.io-client'
|
||||
import { defineComponent, nextTick, onBeforeMount, onBeforeUnmount, ref } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { Terminal } from 'xterm'
|
||||
import * as fit from 'xterm/lib/addons/fit/fit'
|
||||
import 'xterm/src/xterm.css'
|
||||
import { FitAddon } from 'xterm-addon-fit'
|
||||
import { useBus } from '../useful/bus'
|
||||
|
||||
Terminal.applyAddon(fit)
|
||||
|
||||
let xterm = null
|
||||
let socket = null
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'xterm',
|
||||
created () {
|
||||
window.runInXterm = this.runInXterm
|
||||
this.$bus.on('xterm-run', this.handleRunInXterm)
|
||||
this.$bus.on('xterm-init', this.init)
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.runInXterm = null
|
||||
this.$bus.off('xterm-init', this.init)
|
||||
this.$bus.off('xterm-run', this.handleRunInXterm)
|
||||
this.$bus.off('resize', this.handleRunInXterm)
|
||||
},
|
||||
methods: {
|
||||
init () {
|
||||
setup () {
|
||||
const bus = useBus()
|
||||
const store = useStore()
|
||||
|
||||
const refXterm = ref<HTMLElement | null>(null)
|
||||
|
||||
let xterm: Terminal | null = null
|
||||
let socket: SocketIOClient.Socket | null = null
|
||||
|
||||
const fitAddon = new FitAddon()
|
||||
|
||||
function fitXterm () {
|
||||
if (store.state.showXterm) {
|
||||
fitAddon.fit()
|
||||
}
|
||||
}
|
||||
|
||||
function input (data: string) {
|
||||
socket!!.emit('input', data)
|
||||
}
|
||||
|
||||
function focus () {
|
||||
xterm!!.focus()
|
||||
}
|
||||
|
||||
function init () {
|
||||
if (!xterm) {
|
||||
xterm = new Terminal({
|
||||
cols: 80,
|
||||
@@ -43,20 +53,22 @@ export default {
|
||||
}
|
||||
})
|
||||
|
||||
xterm.open(this.$refs.xterm)
|
||||
xterm.fit()
|
||||
this.$bus.on('resize', this.fitXterm)
|
||||
xterm.loadAddon(fitAddon)
|
||||
|
||||
xterm.open(refXterm.value!!)
|
||||
fitAddon.fit()
|
||||
bus.on('resize', fitXterm)
|
||||
}
|
||||
|
||||
if (!socket) {
|
||||
socket = io({ path: '/ws' })
|
||||
|
||||
xterm.on('resize', size => socket.emit('resize', [size.cols, size.rows]))
|
||||
xterm.on('data', data => this.input(data))
|
||||
socket.on('output', arrayBuffer => xterm.write(arrayBuffer))
|
||||
xterm.onResize(size => socket!!.emit('resize', [size.cols, size.rows]))
|
||||
xterm.onData(input)
|
||||
socket.on('output', (arrayBuffer: any) => xterm!!.write(arrayBuffer))
|
||||
socket.on('disconnect', () => {
|
||||
xterm.clear()
|
||||
this.$bus.emit('toggle-xterm', false)
|
||||
xterm!!.clear()
|
||||
bus.emit('toggle-xterm', false)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -64,26 +76,15 @@ export default {
|
||||
socket.connect()
|
||||
}
|
||||
|
||||
this.focus()
|
||||
},
|
||||
fitXterm () {
|
||||
xterm.fit()
|
||||
},
|
||||
input (data) {
|
||||
socket.emit('input', data)
|
||||
},
|
||||
focus () {
|
||||
xterm.focus()
|
||||
},
|
||||
handleRunInXterm (code) {
|
||||
this.runInXterm(null, code, false)
|
||||
},
|
||||
runInXterm (language, code, exit = true) {
|
||||
this.$bus.emit('toggle-xterm', true)
|
||||
this.$nextTick(() => {
|
||||
this.init()
|
||||
focus()
|
||||
}
|
||||
|
||||
const map = {
|
||||
function runInXterm (language: string, code: string, exit = true) {
|
||||
bus.emit('toggle-xterm', true)
|
||||
nextTick(() => {
|
||||
init()
|
||||
|
||||
const map: {[key: string]: {start: string; exit: string; eol: string}} = {
|
||||
bat: { start: 'cmd.exe', exit: 'exit', eol: '\r\n' },
|
||||
bash: { start: 'bash', exit: 'exit', eol: '\n' },
|
||||
php: { start: 'php -a', exit: 'exit', eol: '\n' },
|
||||
@@ -92,38 +93,60 @@ export default {
|
||||
js: { start: 'node', exit: '.exit', eol: '\n' }
|
||||
}
|
||||
|
||||
const run = (code, eol) => {
|
||||
code.split('\n').forEach((x, i) => {
|
||||
this.input(x.trim())
|
||||
this.input(eol)
|
||||
const run = (code: string, eol: string) => {
|
||||
code.split('\n').forEach(x => {
|
||||
input(x.trim())
|
||||
input(eol)
|
||||
})
|
||||
}
|
||||
|
||||
if (!language) {
|
||||
if (!language || language === '_') {
|
||||
run(code, '\n')
|
||||
} else if (map[language]) {
|
||||
this.input(map[language].start)
|
||||
this.input(map[language].eol)
|
||||
input(map[language].start)
|
||||
input(map[language].eol)
|
||||
|
||||
// 延迟一下等待子进程启动
|
||||
setTimeout(() => {
|
||||
run(code, map[language].eol)
|
||||
|
||||
if (exit) {
|
||||
this.input(map[language].exit)
|
||||
this.input(map[language].eol)
|
||||
input(map[language].exit)
|
||||
input(map[language].eol)
|
||||
}
|
||||
}, 400)
|
||||
} else {
|
||||
xterm.write(`不支持 ${language} 语言\n`)
|
||||
xterm!!.write(`不支持 ${language} 语言\n`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleRunInXterm (code?: string) {
|
||||
runInXterm('_', code || '', false)
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
(window as any).runInXterm = runInXterm
|
||||
bus.on('xterm-run', handleRunInXterm)
|
||||
bus.on('xterm-init', init)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
(window as any).runInXterm = null
|
||||
bus.off('xterm-init', init)
|
||||
bus.off('xterm-run', handleRunInXterm)
|
||||
})
|
||||
|
||||
return {
|
||||
refXterm,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import url('~xterm/css/xterm.css');
|
||||
|
||||
.xterm {
|
||||
box-sizing: border-box;
|
||||
padding: 5px;
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import Menu from '@/components/ContextMenu'
|
||||
|
||||
export default {
|
||||
install (Vue, options) {
|
||||
const MenuInstance = Vue.extend(Menu)
|
||||
const instance = new MenuInstance()
|
||||
document.body.appendChild(instance.$mount().$el)
|
||||
|
||||
Vue.prototype.$contextMenu = instance
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
const nodeProcess = window && (window.process || window.nodeProcess)
|
||||
const nodeModule = window && (window.module || window.nodeModule)
|
||||
const nodeRequire = window && (window.require || window.nodeRequire)
|
||||
|
||||
const isElectron = !!(nodeProcess && nodeProcess.versions && nodeProcess.versions['electron'])
|
||||
|
||||
const openAlwaysOnTopWindow = (url, target = '_blank') => {
|
||||
if (isElectron) {
|
||||
const opener = window.open(url, target, 'frame=true,alwaysOnTop=true')
|
||||
|
||||
// 让弹出的窗口置顶
|
||||
opener.eval(`require('electron').remote.getCurrentWindow().setAlwaysOnTop(true)`)
|
||||
|
||||
return opener
|
||||
} else {
|
||||
return window.open(url, target)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
process: nodeProcess,
|
||||
module: nodeModule,
|
||||
require: nodeRequire,
|
||||
isElectron,
|
||||
openAlwaysOnTopWindow,
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import Icon from 'vue-awesome/components/Icon'
|
||||
import router from './router'
|
||||
import store from './store/index'
|
||||
import modal from './modal'
|
||||
import contextMenu from './contextMenu'
|
||||
import toast from './toast'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
Vue.use(modal)
|
||||
Vue.use(contextMenu)
|
||||
Vue.use(toast)
|
||||
Vue.component('y-icon', Icon)
|
||||
|
||||
window.$args = () => {
|
||||
return new URLSearchParams(location.search)
|
||||
}
|
||||
|
||||
const appVm = new Vue({
|
||||
router,
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#app')
|
||||
|
||||
window.appVm = appVm
|
||||
@@ -0,0 +1,20 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
|
||||
import bus from './useful/bus'
|
||||
import toast from './useful/toast'
|
||||
import modal from './useful/modal'
|
||||
import contextmenu from './useful/context-menu'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(bus)
|
||||
app.use(store)
|
||||
app.use(router)
|
||||
app.use(toast)
|
||||
app.use(modal)
|
||||
app.use(contextmenu)
|
||||
|
||||
app.mount('#app')
|
||||
@@ -1,11 +0,0 @@
|
||||
import ModalUi from '@/components/ModalUi'
|
||||
|
||||
export default {
|
||||
install (Vue, options) {
|
||||
const ModalUiInstance = Vue.extend(ModalUi)
|
||||
const instance = new ModalUiInstance()
|
||||
document.body.appendChild(instance.$mount().$el)
|
||||
|
||||
Vue.prototype.$modal = instance
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Layout>
|
||||
<TitleBar slot="header"></TitleBar>
|
||||
<StatusBar slot="footer"></StatusBar>
|
||||
<Tree slot="left"></Tree>
|
||||
<Xterm slot="terminal"></Xterm>
|
||||
<template slot="editor">
|
||||
<FileTabs />
|
||||
<Editor ref="editor" @scroll-line="line => $refs.preview.revealLine(line)" />
|
||||
</template>
|
||||
<Preview
|
||||
ref="preview"
|
||||
slot="preview"
|
||||
@sync-scroll="line => $refs.editor.revealLineInCenter(line)"
|
||||
@switch-todo="(line, checked) => $refs.editor.switchTodo(line, checked)" />
|
||||
</Layout>
|
||||
<XFilter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import Layout from '@/components/Layout'
|
||||
import TitleBar from '@/components/TitleBar'
|
||||
import FileTabs from '@/components/FileTabs'
|
||||
import StatusBar from '@/components/StatusBar'
|
||||
import Tree from '@/components/Tree'
|
||||
import Editor from '@/components/Editor'
|
||||
import Preview from '@/components/Preview'
|
||||
import RunPlugin from '../plugins/RunPlugin'
|
||||
import XFilter from '@/components/Filter'
|
||||
import Xterm from '@/components/Xterm'
|
||||
|
||||
export default {
|
||||
name: 'x-main',
|
||||
components: { Layout, TitleBar, StatusBar, Tree, Editor, Preview, XFilter, Xterm, FileTabs },
|
||||
mounted () {
|
||||
RunPlugin.clearCache()
|
||||
this.$bus.on('editor-ready', this.init)
|
||||
this.$bus.on('copy-text', this.copyText)
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$bus.off('editor-ready', this.init)
|
||||
this.$bus.off('copy-text', this.copyText)
|
||||
},
|
||||
methods: {
|
||||
copyText (text) {
|
||||
const input = document.createElement('input')
|
||||
input.style.position = 'absolute'
|
||||
input.style.background = 'red'
|
||||
input.style.left = '-999999px'
|
||||
input.style.top = '-999999px'
|
||||
input.style.zIndex = -1000
|
||||
input.style.opacity = 0
|
||||
input.value = text
|
||||
document.body.appendChild(input)
|
||||
input.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(input)
|
||||
this.$toast.show('info', '已复制')
|
||||
},
|
||||
init () {
|
||||
if (!this.currentFile) {
|
||||
this.$store.dispatch('app/showHelp', 'README.md')
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState('app', ['currentFile'])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,9 +1,10 @@
|
||||
import Vue from 'vue'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import dayjs from 'dayjs'
|
||||
import Markdown from 'markdown-it'
|
||||
import { useBus } from '@/useful/bus'
|
||||
|
||||
const RunPlugin = (md) => {
|
||||
const temp = md.renderer.rules.fence.bind(md.renderer.rules)
|
||||
const RunPlugin = (md: Markdown) => {
|
||||
const temp = md.renderer.rules.fence!!.bind(md.renderer.rules)
|
||||
md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
|
||||
const token = tokens[idx]
|
||||
|
||||
@@ -32,7 +33,7 @@ const RunPlugin = (md) => {
|
||||
}
|
||||
}
|
||||
|
||||
RunPlugin.runScript = (el) => {
|
||||
RunPlugin.runScript = (el: HTMLElement) => {
|
||||
const appletId = el.id
|
||||
const appletCode = el.dataset.code
|
||||
if (!appletCode) {
|
||||
@@ -111,28 +112,29 @@ RunPlugin.runScript = (el) => {
|
||||
${appletCode}
|
||||
`
|
||||
|
||||
const appVm = window.appVm
|
||||
const bus = useBus()
|
||||
|
||||
iframe.className = 'applet-iframe'
|
||||
iframe.frameBorder = '0'
|
||||
iframe.width = '100%'
|
||||
iframe.onload = function () {
|
||||
const resize = () => {
|
||||
this.height = this.contentDocument.documentElement.scrollHeight + 'px'
|
||||
appVm.$bus.emit('resize')
|
||||
iframe.height = iframe.contentDocument!!.documentElement.scrollHeight + 'px'
|
||||
bus.emit('resize')
|
||||
}
|
||||
resize()
|
||||
|
||||
const win = iframe.contentWindow as any
|
||||
|
||||
// 注入变量
|
||||
this.contentWindow.resize = resize
|
||||
this.contentWindow.appletFrame = this
|
||||
this.contentWindow.appletId = appletId
|
||||
this.contentWindow.Vue = Vue
|
||||
this.contentWindow.CryptoJS = CryptoJS
|
||||
this.contentWindow.dayjs = dayjs
|
||||
win.resize = resize
|
||||
win.appletFrame = this
|
||||
win.appletId = appletId
|
||||
win.CryptoJS = CryptoJS
|
||||
win.dayjs = dayjs
|
||||
|
||||
// 调用初始化方法
|
||||
this.contentWindow.init && this.contentWindow.init()
|
||||
win.init && win.init()
|
||||
}
|
||||
el.innerHTML = ''
|
||||
el.appendChild(iframe)
|
||||
@@ -1,12 +0,0 @@
|
||||
export default (md) => {
|
||||
const Fun = fn => (tokens, idx, options, env, slf) => {
|
||||
if (tokens[idx].attrIndex('title') < 0) {
|
||||
tokens[idx].attrPush(['title', 'Ctrl + 单击复制代码'])
|
||||
tokens[idx].attrPush(['onclick', `event.ctrlKey && window.appVm.$bus.emit('copy-text', this.innerText)`])
|
||||
}
|
||||
|
||||
return (fn)(tokens, idx, options, env, slf)
|
||||
}
|
||||
|
||||
md.renderer.rules.code_inline = Fun(md.renderer.rules.code_inline.bind(md.renderer.rules))
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import Markdown from 'markdown-it'
|
||||
|
||||
export default (md: Markdown) => {
|
||||
const Fun = (fn: Function) => (tokens: any, idx: any, options: any, env: any, slf: any) => {
|
||||
if (tokens[idx].attrIndex('title') < 0) {
|
||||
tokens[idx].attrPush(['title', 'Ctrl + 单击复制代码'])
|
||||
tokens[idx].attrPush(['onclick', "event.ctrlKey && window.globalBus.emit('copy-text', this.innerText)"])
|
||||
}
|
||||
|
||||
return (fn)(tokens, idx, options, env, slf)
|
||||
}
|
||||
|
||||
md.renderer.rules.code_inline = Fun(md.renderer.rules.code_inline!!.bind(md.renderer.rules))
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
import file from '@/lib/file'
|
||||
import env from '@/lib/env'
|
||||
import Markdown from 'markdown-it'
|
||||
import file from '@/useful/file'
|
||||
import env from '@/useful/env'
|
||||
import { useBus } from '@/useful/bus'
|
||||
|
||||
const Plugin = md => {
|
||||
const renderHtml = ({ file, content }) => {
|
||||
const Plugin = (md: Markdown) => {
|
||||
const renderHtml = ({ url, content }: any) => {
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.className = 'drawio'
|
||||
iframe.frameBorder = '0'
|
||||
iframe.style.border = '0'
|
||||
iframe.width = '100%'
|
||||
iframe.height = '300px'
|
||||
iframe.dataset['file'] = file || ''
|
||||
iframe.dataset['content'] = content || ''
|
||||
iframe.dataset.url = url || ''
|
||||
iframe.dataset.content = content || ''
|
||||
|
||||
return `
|
||||
<div class="drawio-wrapper" style="position: relative">
|
||||
@@ -18,24 +20,24 @@ const Plugin = md => {
|
||||
`
|
||||
}
|
||||
|
||||
const linkTemp = md.renderer.rules.link_open.bind(md.renderer.rules)
|
||||
const linkTemp = md.renderer.rules.link_open!!.bind(md.renderer.rules)
|
||||
md.renderer.rules.link_open = (tokens, idx, options, env, slf) => {
|
||||
const token = tokens[idx]
|
||||
|
||||
if (token.attrGet('title') !== '--drawio--') {
|
||||
if (token.attrGet('link-type') !== 'drawio') {
|
||||
return linkTemp(tokens, idx, options, env, slf)
|
||||
}
|
||||
|
||||
const file = token.attrGet('href')
|
||||
const url = token.attrGet('href')
|
||||
const nextToken = tokens[idx + 1]
|
||||
if (nextToken && nextToken.type === 'text') {
|
||||
nextToken.content = ''
|
||||
}
|
||||
|
||||
return renderHtml({ file })
|
||||
return renderHtml({ url })
|
||||
}
|
||||
|
||||
const fenceTemp = md.renderer.rules.fence.bind(md.renderer.rules)
|
||||
const fenceTemp = md.renderer.rules.fence!!.bind(md.renderer.rules)
|
||||
md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
|
||||
const token = tokens[idx]
|
||||
|
||||
@@ -49,8 +51,12 @@ const Plugin = md => {
|
||||
}
|
||||
}
|
||||
|
||||
const buildSrcdoc = async ({ repo, path, content }) => {
|
||||
if (!content) {
|
||||
type F = { repo?: string; path?: string; url?: string; content: string }
|
||||
|
||||
const buildSrcdoc = async ({ repo, path, content, url }: F) => {
|
||||
if (url) {
|
||||
content = await (await fetch(url)).text()
|
||||
} else if (!content && repo && path) {
|
||||
content = (await file.read({ repo, path })).content
|
||||
}
|
||||
|
||||
@@ -58,7 +64,7 @@ const buildSrcdoc = async ({ repo, path, content }) => {
|
||||
|
||||
const div = document.createElement('div')
|
||||
div.className = 'mxgraph'
|
||||
div.dataset['mxgraph'] = JSON.stringify({
|
||||
div.dataset.mxgraph = JSON.stringify({
|
||||
highlight: '#00afff',
|
||||
lightbox: false,
|
||||
nav: true,
|
||||
@@ -113,7 +119,7 @@ const buildSrcdoc = async ({ repo, path, content }) => {
|
||||
`
|
||||
}
|
||||
|
||||
const openInNewWindow = srcdoc => {
|
||||
const openInNewWindow = (srcdoc: string) => {
|
||||
const opener = env.openAlwaysOnTopWindow('about:blank')
|
||||
const frame = document.createElement('iframe')
|
||||
frame.width = '100%'
|
||||
@@ -137,18 +143,19 @@ const openInNewWindow = srcdoc => {
|
||||
}
|
||||
}
|
||||
|
||||
Plugin.load = async (el, repo, path) => {
|
||||
const content = el.dataset['content']
|
||||
Plugin.load = async (el: HTMLIFrameElement, url?: string) => {
|
||||
const bus = useBus()
|
||||
const content = el.dataset.content || ''
|
||||
|
||||
const srcdoc = await buildSrcdoc({ repo, path, content })
|
||||
const srcdoc = await buildSrcdoc({ content, url })
|
||||
|
||||
const resize = () => {
|
||||
el.contentDocument.body.style.height = 'auto'
|
||||
el.contentDocument.documentElement.style.height = 'auto'
|
||||
el.height = el.contentDocument.documentElement.offsetHeight + 'px'
|
||||
el.contentDocument.body.style.height = el.contentDocument.body.clientHeight + 'px'
|
||||
el.contentDocument.documentElement.style.height = '100%'
|
||||
window.appVm.$bus.emit('resize')
|
||||
el.contentDocument!!.body.style.height = 'auto'
|
||||
el.contentDocument!!.documentElement.style.height = 'auto'
|
||||
el.height = el.contentDocument!!.documentElement.offsetHeight + 'px'
|
||||
el.contentDocument!!.body.style.height = el.contentDocument!!.body.clientHeight + 'px'
|
||||
el.contentDocument!!.documentElement.style.height = '100%'
|
||||
bus.emit('resize')
|
||||
}
|
||||
|
||||
el.onload = () => setTimeout(resize, 300)
|
||||
@@ -168,10 +175,10 @@ Plugin.load = async (el, repo, path) => {
|
||||
action.style.cssText = 'position: absolute; right: 15px; top: 3px; z-index: 1;'
|
||||
action.appendChild(button2)
|
||||
action.appendChild(button1)
|
||||
el.parentElement.appendChild(action)
|
||||
el.parentElement!!.appendChild(action)
|
||||
}
|
||||
|
||||
Plugin.open = async file => {
|
||||
Plugin.open = async (file: F) => {
|
||||
const srcdoc = await buildSrcdoc(file)
|
||||
openInNewWindow(srcdoc)
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
import echarts from 'echarts'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import Markdown from 'markdown-it'
|
||||
|
||||
const renderHtml = code => {
|
||||
const renderHtml = (code: string) => {
|
||||
const id = `echart-${CryptoJS.MD5(code).toString()}-${Math.random().toString(36).substr(2)}`
|
||||
|
||||
return `<div class="echarts" id="${id}" data-code="${encodeURIComponent(code)}"></div>`
|
||||
}
|
||||
|
||||
const EChartsPlugin = md => {
|
||||
const temp = md.renderer.rules.fence.bind(md.renderer.rules)
|
||||
const EChartsPlugin = (md: Markdown) => {
|
||||
const temp = md.renderer.rules.fence!!.bind(md.renderer.rules)
|
||||
|
||||
md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
|
||||
const token = tokens[idx]
|
||||
@@ -21,19 +22,20 @@ const EChartsPlugin = md => {
|
||||
}
|
||||
}
|
||||
|
||||
let charts = {}
|
||||
let charts: {[key: string]: any} = {}
|
||||
|
||||
EChartsPlugin.update = (theme = 'dark', animation = undefined, renderImg = false) => {
|
||||
EChartsPlugin.update = (theme = 'dark', animation: boolean | undefined = undefined, renderImg = false) => {
|
||||
Object.values(charts).forEach(x => x.chart.dispose())
|
||||
charts = {}
|
||||
|
||||
for (let ele of document.getElementsByClassName('echarts')) {
|
||||
for (const ele of document.getElementsByClassName('echarts')) {
|
||||
const el = ele as HTMLDivElement
|
||||
try {
|
||||
const id = ele.getAttribute('id')
|
||||
const code = ele.dataset['code']
|
||||
const id = el.getAttribute('id')!!
|
||||
const code = el.dataset.code
|
||||
|
||||
if (code) {
|
||||
const chart = echarts.init(ele, theme)
|
||||
const chart = echarts.init(el, theme)
|
||||
// eslint-disable-next-line
|
||||
eval(`(${decodeURIComponent(code)})(chart)`)
|
||||
|
||||
@@ -45,15 +47,15 @@ EChartsPlugin.update = (theme = 'dark', animation = undefined, renderImg = false
|
||||
|
||||
if (renderImg) {
|
||||
img = new Image()
|
||||
img.width = ele.clientWidth
|
||||
img.height = ele.clientHeight
|
||||
img.src = chart.getDataURL()
|
||||
img.width = el.clientWidth
|
||||
img.height = el.clientHeight
|
||||
img.src = chart.getDataURL({})
|
||||
}
|
||||
|
||||
charts[id] = { chart, img }
|
||||
}
|
||||
} catch (error) {
|
||||
ele.innerText = error
|
||||
el.innerText = error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,12 +63,12 @@ EChartsPlugin.update = (theme = 'dark', animation = undefined, renderImg = false
|
||||
EChartsPlugin.preparePrint = () => {
|
||||
EChartsPlugin.update('light', false, true)
|
||||
|
||||
for (let ele of document.getElementsByClassName('echarts')) {
|
||||
for (const ele of document.getElementsByClassName('echarts')) {
|
||||
while (ele.firstChild) {
|
||||
ele.firstChild.remove()
|
||||
}
|
||||
|
||||
const id = ele.getAttribute('id')
|
||||
const id = ele.getAttribute('id')!!
|
||||
|
||||
if (charts[id] && charts[id].img) {
|
||||
ele.appendChild(charts[id].img)
|
||||
+11
-9
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// https://github.com/wcoder/highlightjs-line-numbers.js
|
||||
|
||||
const TABLE_NAME = 'hljs-ln'
|
||||
@@ -23,7 +25,7 @@ function addStyles () {
|
||||
document.getElementsByTagName('head')[0].appendChild(css)
|
||||
}
|
||||
|
||||
function lineNumbersBlock (element, options) {
|
||||
function lineNumbersBlock (element: any, options?: any) {
|
||||
if (typeof element !== 'object') return
|
||||
|
||||
async(function () {
|
||||
@@ -31,7 +33,7 @@ function lineNumbersBlock (element, options) {
|
||||
})
|
||||
}
|
||||
|
||||
function lineNumbersInternal (element, options) {
|
||||
function lineNumbersInternal (element: any, options: any) {
|
||||
// define options or set default
|
||||
options = options || {
|
||||
singleLine: false
|
||||
@@ -45,7 +47,7 @@ function lineNumbersInternal (element, options) {
|
||||
return addLineNumbersBlockFor(element.innerHTML, firstLineIndex)
|
||||
}
|
||||
|
||||
function addLineNumbersBlockFor (inputHtml, firstLineIndex) {
|
||||
function addLineNumbersBlockFor (inputHtml: any, firstLineIndex: any) {
|
||||
var lines = getLines(inputHtml)
|
||||
|
||||
// if last line contains only carriage return remove it
|
||||
@@ -88,7 +90,7 @@ function addLineNumbersBlockFor (inputHtml, firstLineIndex) {
|
||||
* Doing deep passage on child nodes.
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
function duplicateMultilineNodes (element) {
|
||||
function duplicateMultilineNodes (element: any) {
|
||||
var nodes = element.childNodes
|
||||
for (var node in nodes) {
|
||||
if (nodes.hasOwnProperty(node)) {
|
||||
@@ -108,7 +110,7 @@ function duplicateMultilineNodes (element) {
|
||||
* Method for fix multi-line elements implementation in highlight.js
|
||||
* @param {HTMLElement} element
|
||||
*/
|
||||
function duplicateMultilineNode (element) {
|
||||
function duplicateMultilineNode (element: any) {
|
||||
var className = element.className
|
||||
|
||||
if (!/hljs-/.test(className)) return
|
||||
@@ -123,16 +125,16 @@ function duplicateMultilineNode (element) {
|
||||
element.innerHTML = result.trim()
|
||||
}
|
||||
|
||||
function getLines (text) {
|
||||
function getLines (text: string) {
|
||||
if (text.length === 0) return []
|
||||
return text.split(BREAK_LINE_REGEXP)
|
||||
}
|
||||
|
||||
function getLinesCount (text) {
|
||||
function getLinesCount (text: string) {
|
||||
return (text.trim().match(BREAK_LINE_REGEXP) || []).length
|
||||
}
|
||||
|
||||
function async (func) {
|
||||
function async (func: Function) {
|
||||
setTimeout(func, 0)
|
||||
}
|
||||
|
||||
@@ -141,7 +143,7 @@ function async (func) {
|
||||
* @param {string} format
|
||||
* @param {array} args
|
||||
*/
|
||||
function format (format, args) {
|
||||
function format (format: string, args: any) {
|
||||
return format.replace(/\{(\d+)\}/g, function (m, n) {
|
||||
return args[n] ? args[n] : m
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
const LinkTargetPlugin = (md) => {
|
||||
const LinkTargetBlank = (tokens, idx, options, env, slf) => {
|
||||
if (tokens[idx].attrIndex('target') < 0) {
|
||||
tokens[idx].attrPush(['target', '_blank'])
|
||||
}
|
||||
|
||||
return slf.renderToken(tokens, idx, options, env, slf)
|
||||
}
|
||||
|
||||
md.renderer.rules.link_open = LinkTargetBlank
|
||||
}
|
||||
|
||||
export default LinkTargetPlugin
|
||||
@@ -0,0 +1,13 @@
|
||||
import Markdown from 'markdown-it'
|
||||
|
||||
const LinkTargetPlugin = (md: Markdown) => {
|
||||
md.renderer.rules.link_open = (tokens, idx, options, _, slf) => {
|
||||
if (tokens[idx].attrIndex('target') < 0) {
|
||||
tokens[idx].attrPush(['target', '_blank'])
|
||||
}
|
||||
|
||||
return slf.renderToken(tokens, idx, options)
|
||||
}
|
||||
}
|
||||
|
||||
export default LinkTargetPlugin
|
||||
@@ -1,56 +0,0 @@
|
||||
import mermaid from 'mermaid'
|
||||
|
||||
const mermaidChart = (code) => {
|
||||
try {
|
||||
mermaid.parse(code)
|
||||
return `<div class="mermaid">${code}</div>`
|
||||
} catch ({ str, hash }) {
|
||||
return `<pre>${str}</pre>`
|
||||
}
|
||||
}
|
||||
|
||||
const MermaidPlugin = (md) => {
|
||||
md.mermaid = mermaid
|
||||
mermaid.loadPreferences = (preferenceStore) => {
|
||||
let mermaidTheme = preferenceStore.get('mermaid-theme')
|
||||
if (mermaidTheme === undefined) {
|
||||
mermaidTheme = 'default'
|
||||
}
|
||||
let ganttAxisFormat = preferenceStore.get('gantt-axis-format')
|
||||
if (ganttAxisFormat === undefined) {
|
||||
ganttAxisFormat = '%Y-%m-%d'
|
||||
}
|
||||
mermaid.initialize({
|
||||
theme: mermaidTheme,
|
||||
gantt: { axisFormatter: [
|
||||
[ganttAxisFormat, (d) => {
|
||||
return d.getDay() === 1
|
||||
}]
|
||||
] }
|
||||
})
|
||||
return {
|
||||
'mermaid-theme': mermaidTheme,
|
||||
'gantt-axis-format': ganttAxisFormat
|
||||
}
|
||||
}
|
||||
|
||||
const temp = md.renderer.rules.fence.bind(md.renderer.rules)
|
||||
md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
|
||||
const token = tokens[idx]
|
||||
const code = token.content.trim()
|
||||
if (token.info === 'mermaid') {
|
||||
return mermaidChart(code)
|
||||
}
|
||||
const firstLine = code.split(/\n/)[0].trim()
|
||||
if (firstLine === 'gantt' || firstLine === 'sequenceDiagram' || firstLine.match(/^graph (?:TB|BT|RL|LR|TD);?$/)) {
|
||||
return mermaidChart(code)
|
||||
}
|
||||
return temp(tokens, idx, options, env, slf)
|
||||
}
|
||||
}
|
||||
|
||||
MermaidPlugin.update = () => {
|
||||
mermaid.init()
|
||||
}
|
||||
|
||||
export default MermaidPlugin
|
||||
@@ -0,0 +1,35 @@
|
||||
import Markdown from 'markdown-it'
|
||||
import mermaid from 'mermaid'
|
||||
|
||||
const mermaidChart = (code: string) => {
|
||||
try {
|
||||
mermaid.parse(code)
|
||||
return `<div class="mermaid">${code}</div>`
|
||||
} catch ({ str, hash }) {
|
||||
return `<pre>${str}</pre>`
|
||||
}
|
||||
}
|
||||
|
||||
const MermaidPlugin = (md: Markdown) => {
|
||||
mermaid.initialize({})
|
||||
|
||||
const temp = md.renderer.rules.fence!!.bind(md.renderer.rules)
|
||||
md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
|
||||
const token = tokens[idx]
|
||||
const code = token.content.trim()
|
||||
if (token.info === 'mermaid') {
|
||||
return mermaidChart(code)
|
||||
}
|
||||
const firstLine = code.split(/\n/)[0].trim()
|
||||
if (firstLine === 'gantt' || firstLine === 'sequenceDiagram' || firstLine.match(/^graph (?:TB|BT|RL|LR|TD);?$/)) {
|
||||
return mermaidChart(code)
|
||||
}
|
||||
return temp(tokens, idx, options, env, slf)
|
||||
}
|
||||
}
|
||||
|
||||
MermaidPlugin.update = () => {
|
||||
mermaid.init('.mermaid')
|
||||
}
|
||||
|
||||
export default MermaidPlugin
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,9 +1,10 @@
|
||||
import CryptoJS from 'crypto-js'
|
||||
import Markdown from 'markdown-it'
|
||||
|
||||
const cachePrefix = 'run_code_result_'
|
||||
|
||||
const RunPlugin = (md) => {
|
||||
const temp = md.renderer.rules.fence.bind(md.renderer.rules)
|
||||
const RunPlugin = (md: Markdown) => {
|
||||
const temp = md.renderer.rules.fence!!.bind(md.renderer.rules)
|
||||
md.renderer.rules.fence = (tokens, idx, options, env, slf) => {
|
||||
const token = tokens[idx]
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
export const injectLineNumbers = (tokens, idx, options, env, slf) => {
|
||||
import Markdown from 'markdown-it'
|
||||
import Renderer from 'markdown-it/lib/renderer'
|
||||
|
||||
export const injectLineNumbers: Renderer.RenderRule = (tokens, idx, options, _, slf) => {
|
||||
if (tokens[idx].map) {
|
||||
const line = tokens[idx].map[0]
|
||||
const line = tokens[idx].map!![0]
|
||||
tokens[idx].attrJoin('class', 'source-line')
|
||||
tokens[idx].attrSet('data-source-line', String(line + 1))
|
||||
}
|
||||
return slf.renderToken(tokens, idx, options, env, slf)
|
||||
return slf.renderToken(tokens, idx, options)
|
||||
}
|
||||
|
||||
const SourceLinePlugin = (md) => {
|
||||
const SourceLinePlugin = (md: Markdown) => {
|
||||
md.renderer.rules.paragraph_open = injectLineNumbers
|
||||
md.renderer.rules.heading_open = injectLineNumbers
|
||||
md.renderer.rules.list_item_open = injectLineNumbers
|
||||
@@ -1,8 +1,10 @@
|
||||
// https://github.com/Oktavilla/markdown-it-table-of-contents
|
||||
|
||||
import Markdown from 'markdown-it'
|
||||
|
||||
import { injectLineNumbers } from './SourceLinePlugin'
|
||||
|
||||
const slugify = s => 'h-' + encodeURIComponent(String(s).trim().toLowerCase().replace(/\s+/g, '-'))
|
||||
const slugify = (s: string) => 'h-' + encodeURIComponent(String(s).trim().toLowerCase().replace(/\s+/g, '-'))
|
||||
|
||||
const defaults = {
|
||||
level: [2, 3],
|
||||
@@ -16,14 +18,14 @@ const defaults = {
|
||||
containerFooterHtml: undefined
|
||||
}
|
||||
|
||||
export default (md, o) => {
|
||||
export default (md: Markdown, o: any) => {
|
||||
const options = Object.assign({}, defaults, o)
|
||||
const tocRegexp = options.markerPattern
|
||||
let gstate
|
||||
let gstate: any
|
||||
|
||||
function toc (state, silent) {
|
||||
var token
|
||||
var match
|
||||
function toc (state: any, silent: any) {
|
||||
let token
|
||||
let match
|
||||
|
||||
// Reject if the token does not start with [
|
||||
if (state.src.charCodeAt(state.pos) !== 0x5B) {
|
||||
@@ -36,7 +38,7 @@ export default (md, o) => {
|
||||
|
||||
// Detect TOC markdown
|
||||
match = tocRegexp.exec(state.src.substr(state.pos))
|
||||
match = !match ? [] : match.filter(m => !!m)
|
||||
match = !match ? [] : match.filter((m: any) => !!m)
|
||||
if (match.length < 1) {
|
||||
return false
|
||||
}
|
||||
@@ -59,7 +61,7 @@ export default (md, o) => {
|
||||
token = state.push('toc_close', 'toc', -1)
|
||||
|
||||
// Update pos so the parser can continue
|
||||
var newline = state.src.indexOf('\n', state.pos)
|
||||
const newline = state.src.indexOf('\n', state.pos)
|
||||
if (newline !== -1) {
|
||||
state.pos = newline
|
||||
} else {
|
||||
@@ -69,8 +71,59 @@ export default (md, o) => {
|
||||
return true
|
||||
}
|
||||
|
||||
md.renderer.rules.toc_open = function (tokens, index) {
|
||||
var tocOpenHtml = `<div class="${options.containerClass}">`
|
||||
function renderChildrenTokens (pos: any, tokens: any) {
|
||||
const headings = []
|
||||
let buffer = ''
|
||||
let currentLevel
|
||||
let subHeadings
|
||||
const size = tokens.length
|
||||
let i = pos
|
||||
while (i < size) {
|
||||
const token = tokens[i]
|
||||
const heading = tokens[i - 1]
|
||||
const level = token.tag && parseInt(token.tag.substr(1, 1))
|
||||
if (token.type !== 'heading_close' || options.level.indexOf(level) === -1 || heading.type !== 'inline') {
|
||||
i++
|
||||
continue // Skip if not matching criteria
|
||||
}
|
||||
if (!currentLevel) {
|
||||
currentLevel = level // We init with the first found level
|
||||
} else {
|
||||
if (level > currentLevel) {
|
||||
subHeadings = renderChildrenTokens(i, tokens)
|
||||
buffer += subHeadings[1]
|
||||
i = subHeadings[0]
|
||||
continue
|
||||
}
|
||||
if (level < currentLevel) {
|
||||
// Finishing the sub headings
|
||||
buffer += '</li>'
|
||||
headings.push(buffer)
|
||||
return [i, `<${options.type}>${headings.join('')}</${options.type}>`]
|
||||
}
|
||||
if (level === currentLevel) {
|
||||
// Finishing the sub headings
|
||||
buffer += '</li>'
|
||||
headings.push(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
// 给标题加上 id
|
||||
|
||||
const slug = options.slugify(heading.content)
|
||||
|
||||
buffer = `<li><a href="#${slug}">`
|
||||
buffer += typeof options.format === 'function' ? options.format(heading.content) : heading.content
|
||||
buffer += '</a>'
|
||||
i++
|
||||
}
|
||||
buffer += buffer === '' ? '' : '</li>'
|
||||
headings.push(buffer)
|
||||
return [i, `<${options.type}>${headings.join('')}</${options.type}>`]
|
||||
}
|
||||
|
||||
md.renderer.rules.toc_open = function () {
|
||||
let tocOpenHtml = `<div class="${options.containerClass}">`
|
||||
|
||||
if (options.containerHeaderHtml) {
|
||||
tocOpenHtml += options.containerHeaderHtml
|
||||
@@ -79,17 +132,17 @@ export default (md, o) => {
|
||||
return tocOpenHtml
|
||||
}
|
||||
|
||||
md.renderer.rules.toc_close = function (tokens, index) {
|
||||
var tocFooterHtml = ''
|
||||
md.renderer.rules.toc_close = function () {
|
||||
let tocFooterHtml = ''
|
||||
|
||||
if (options.containerFooterHtml) {
|
||||
tocFooterHtml = options.containerFooterHtml
|
||||
}
|
||||
|
||||
return tocFooterHtml + `</div>`
|
||||
return tocFooterHtml + '</div>'
|
||||
}
|
||||
|
||||
md.renderer.rules.toc_body = function (tokens, index) {
|
||||
md.renderer.rules.toc_body = function () {
|
||||
if (options.forceFullToc) {
|
||||
/*
|
||||
|
||||
@@ -106,12 +159,12 @@ export default (md, o) => {
|
||||
- heading 1
|
||||
|
||||
*/
|
||||
var tocBody = ''
|
||||
var pos = 0
|
||||
var tokenLength = gstate && gstate.tokens && gstate.tokens.length
|
||||
let tocBody = ''
|
||||
let pos = 0
|
||||
const tokenLength = gstate && gstate.tokens && gstate.tokens.length
|
||||
|
||||
while (pos < tokenLength) {
|
||||
var tocHierarchy = renderChildrenTokens(pos, gstate.tokens)
|
||||
const tocHierarchy = renderChildrenTokens(pos, gstate.tokens)
|
||||
pos = tocHierarchy[0]
|
||||
tocBody += tocHierarchy[1]
|
||||
}
|
||||
@@ -138,60 +191,10 @@ export default (md, o) => {
|
||||
return injectLineNumbers(tokens, idx, opt, env, slf)
|
||||
}
|
||||
|
||||
function renderChildrenTokens (pos, tokens) {
|
||||
let headings = []
|
||||
let buffer = ''
|
||||
let currentLevel
|
||||
let subHeadings
|
||||
let size = tokens.length
|
||||
let i = pos
|
||||
while (i < size) {
|
||||
var token = tokens[i]
|
||||
var heading = tokens[i - 1]
|
||||
var level = token.tag && parseInt(token.tag.substr(1, 1))
|
||||
if (token.type !== 'heading_close' || options.level.indexOf(level) === -1 || heading.type !== 'inline') {
|
||||
i++
|
||||
continue // Skip if not matching criteria
|
||||
}
|
||||
if (!currentLevel) {
|
||||
currentLevel = level // We init with the first found level
|
||||
} else {
|
||||
if (level > currentLevel) {
|
||||
subHeadings = renderChildrenTokens(i, tokens)
|
||||
buffer += subHeadings[1]
|
||||
i = subHeadings[0]
|
||||
continue
|
||||
}
|
||||
if (level < currentLevel) {
|
||||
// Finishing the sub headings
|
||||
buffer += `</li>`
|
||||
headings.push(buffer)
|
||||
return [i, `<${options.type}>${headings.join('')}</${options.type}>`]
|
||||
}
|
||||
if (level === currentLevel) {
|
||||
// Finishing the sub headings
|
||||
buffer += `</li>`
|
||||
headings.push(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
// 给标题加上 id
|
||||
|
||||
const slug = options.slugify(heading.content)
|
||||
|
||||
buffer = `<li><a href="#${slug}">`
|
||||
buffer += typeof options.format === 'function' ? options.format(heading.content) : heading.content
|
||||
buffer += `</a>`
|
||||
i++
|
||||
}
|
||||
buffer += buffer === '' ? '' : `</li>`
|
||||
headings.push(buffer)
|
||||
return [i, `<${options.type}>${headings.join('')}</${options.type}>`]
|
||||
}
|
||||
|
||||
// Catch all the tokens for iteration later
|
||||
md.core.ruler.push('grab_state', function (state) {
|
||||
gstate = state
|
||||
return true
|
||||
})
|
||||
|
||||
// Insert TOC
|
||||
@@ -1,12 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import Main from './pages/Main'
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
export default new Router({
|
||||
mode: 'history',
|
||||
routes: [
|
||||
{ path: '/', name: 'main', component: Main },
|
||||
]
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
|
||||
import Main from '@/views/Main.vue'
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Main',
|
||||
component: Main
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
documentSaved: boolean;
|
||||
runInXterm: Function;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'markdown-it-task-lists'
|
||||
declare module 'markdown-it-attrs'
|
||||
declare module 'markdown-it-multimd-table'
|
||||
declare module 'markdown-it-footnote'
|
||||
declare module 'markdown-it-katex'
|
||||
@@ -1,16 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {
|
||||
|
||||
},
|
||||
mutations: {
|
||||
|
||||
},
|
||||
actions: {
|
||||
|
||||
}
|
||||
})
|
||||
@@ -1,20 +1,25 @@
|
||||
import Storage from '@/lib/Storage'
|
||||
import file from '@/lib/file'
|
||||
import { createStore } from 'vuex'
|
||||
import Storage from '@/useful/storage'
|
||||
import file from '@/useful/file'
|
||||
|
||||
const getLastOpenFile = repo => {
|
||||
/**
|
||||
* 获取上一次打开的文件
|
||||
* @param repoName 仓库名称
|
||||
*/
|
||||
const getLastOpenFile = (repoName?: string) => {
|
||||
const currentFile = Storage.get('currentFile')
|
||||
const recentOpenTime = Storage.get('recentOpenTime', {})
|
||||
const recentOpenTime = Storage.get('recentOpenTime', {}) as {[key: string]: number}
|
||||
|
||||
if (!repo) {
|
||||
if (!repoName) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (currentFile && currentFile.repo === repo.name) {
|
||||
if (currentFile && currentFile.repo === repoName) {
|
||||
return currentFile
|
||||
}
|
||||
|
||||
const item = Object.entries(recentOpenTime)
|
||||
.filter(x => x[0].startsWith(repo.name + '|'))
|
||||
.filter(x => x[0].startsWith(repoName + '|'))
|
||||
.sort((a, b) => b[1] - a[1])[0]
|
||||
|
||||
if (!item) {
|
||||
@@ -26,11 +31,10 @@ const getLastOpenFile = repo => {
|
||||
return null
|
||||
}
|
||||
|
||||
return { repo: repo.name, name: file.basename(path), path }
|
||||
return { repo: repoName, name: file.basename(path), path }
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
export default createStore({
|
||||
state: {
|
||||
repositories: {},
|
||||
tree: null,
|
||||
@@ -42,7 +46,7 @@ export default {
|
||||
previousContent: '',
|
||||
previousHash: '',
|
||||
currentRepo: Storage.get('currentRepo'),
|
||||
currentFile: getLastOpenFile(Storage.get('currentRepo')),
|
||||
currentFile: getLastOpenFile(Storage.get('currentRepo')?.name),
|
||||
recentOpenTime: Storage.get('recentOpenTime', {}),
|
||||
tabs: Storage.get('tabs', []),
|
||||
passwordHash: {},
|
||||
@@ -102,7 +106,7 @@ export default {
|
||||
},
|
||||
setCurrentRepo (state, data) {
|
||||
state.currentRepo = data
|
||||
state.currentFile = getLastOpenFile(data)
|
||||
state.currentFile = getLastOpenFile(data?.name)
|
||||
state.tree = null
|
||||
Storage.set('currentRepo', data)
|
||||
},
|
||||
@@ -120,7 +124,7 @@ export default {
|
||||
},
|
||||
actions: {
|
||||
async fetchMarkedFiles ({ commit }) {
|
||||
const files = (await file.markedFiles()).map(x => ({ ...x, name: file.basename(x.path) }))
|
||||
const files = (await file.markedFiles()).map((x: any) => ({ ...x, name: file.basename(x.path) }))
|
||||
commit('setMarkedFiles', files)
|
||||
},
|
||||
async fetchRepositories ({ commit }) {
|
||||
@@ -147,4 +151,4 @@ export default {
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import app from './app'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
strict: process.env.NODE_ENV !== 'production',
|
||||
modules: {
|
||||
app,
|
||||
}
|
||||
})
|
||||
@@ -1,11 +0,0 @@
|
||||
import Toast from '@/components/Toast'
|
||||
|
||||
export default {
|
||||
install (Vue, options) {
|
||||
const ToastInstance = Vue.extend(Toast)
|
||||
const instance = new ToastInstance()
|
||||
document.body.appendChild(instance.$mount().$el)
|
||||
|
||||
Vue.prototype.$toast = instance
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
export namespace Components {
|
||||
export namespace Modal {
|
||||
export type ConfirmModalParams = { title?: string; content?: string }
|
||||
|
||||
export type InputModalParams = {
|
||||
type?: string;
|
||||
title?: string;
|
||||
content?: string;
|
||||
value?: string;
|
||||
hint?: string;
|
||||
select?: boolean | [number, number, 'forward' | 'backward' | 'none'];
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Toast {
|
||||
export type ToastType = 'warning' | 'info'
|
||||
}
|
||||
|
||||
export namespace ContextMenu {
|
||||
export type SeparatorItem = { type: 'separator' }
|
||||
|
||||
export type NormalItem = {
|
||||
type?: 'normal';
|
||||
id: string;
|
||||
label: string;
|
||||
onClick: (item?: NormalItem) => void;
|
||||
}
|
||||
|
||||
export type Item = SeparatorItem | NormalItem
|
||||
}
|
||||
|
||||
export namespace Tabs {
|
||||
export interface Item {
|
||||
key: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
payload: any;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace FileTabs {
|
||||
export interface Item extends Tabs.Item {
|
||||
payload: {
|
||||
file: any; // TODO 文件类型
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { App } from 'vue'
|
||||
import mitt, { Emitter, EventType, Handler, WildcardHandler } from 'mitt'
|
||||
|
||||
const emitter = mitt()
|
||||
|
||||
emitter.on('*', (type, payload) => {
|
||||
console.log('debug bus >', type, payload)
|
||||
})
|
||||
|
||||
interface XEmitter extends Emitter {
|
||||
once<T = any>(type: EventType, handler: Handler<T>): void;
|
||||
once(type: '*', handler: WildcardHandler): void;
|
||||
}
|
||||
|
||||
(emitter as any).once = (type: any, handler: any) => {
|
||||
const wrappedHandler = (evt: any) => {
|
||||
handler(evt)
|
||||
emitter.off(type, wrappedHandler)
|
||||
}
|
||||
emitter.on(type, wrappedHandler)
|
||||
}
|
||||
|
||||
export function useBus () {
|
||||
return emitter as XEmitter
|
||||
}
|
||||
|
||||
export default function install (app: App) {
|
||||
app.config.globalProperties.$bus = emitter
|
||||
;(window as any).globalBus = emitter
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { App, ComponentPublicInstance, createApp } from 'vue'
|
||||
import { Components } from '@/types'
|
||||
import Modal from '@/components/ContextMenu.vue'
|
||||
|
||||
interface Instance extends ComponentPublicInstance {
|
||||
show: (menuItems: Components.ContextMenu.Item[]) => void;
|
||||
}
|
||||
|
||||
let instance: Instance
|
||||
|
||||
export function useContextMenu (): Instance {
|
||||
return instance
|
||||
}
|
||||
|
||||
export default function install (app: App) {
|
||||
const toast = createApp(Modal)
|
||||
const el = document.createElement('div')
|
||||
document.body.appendChild(el)
|
||||
|
||||
instance = toast.mount(el) as Instance
|
||||
app.config.globalProperties.$modal = instance
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useToast } from '@/useful/toast'
|
||||
|
||||
export function copyText (text?: string) {
|
||||
if (text === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const input = document.createElement('input')
|
||||
input.style.position = 'absolute'
|
||||
input.style.background = 'red'
|
||||
input.style.left = '-999999px'
|
||||
input.style.top = '-999999px'
|
||||
input.style.zIndex = '-1000'
|
||||
input.style.opacity = '0'
|
||||
input.value = text
|
||||
document.body.appendChild(input)
|
||||
input.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(input)
|
||||
toast.show('info', '已复制')
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import CryptoJS from 'crypto-js'
|
||||
|
||||
const getCryptKey = password => {
|
||||
const getCryptKey = (password: string) => {
|
||||
if (!password) {
|
||||
throw new Error('未输入密码')
|
||||
}
|
||||
@@ -8,9 +8,9 @@ const getCryptKey = password => {
|
||||
return CryptoJS.MD5(password).toString().substr(0, 16)
|
||||
}
|
||||
|
||||
const encrypt = (content, password) => {
|
||||
let key = getCryptKey(password)
|
||||
let iv = key
|
||||
const encrypt = (content: any, password: string) => {
|
||||
let key: any = getCryptKey(password)
|
||||
let iv: any = key
|
||||
const passwordHash = CryptoJS.MD5(key).toString()
|
||||
|
||||
key = CryptoJS.enc.Utf8.parse(key)
|
||||
@@ -25,9 +25,9 @@ const encrypt = (content, password) => {
|
||||
return { content: encrypted.toString(), passwordHash }
|
||||
}
|
||||
|
||||
const decrypt = (content, password) => {
|
||||
let key = getCryptKey(password)
|
||||
let iv = key
|
||||
const decrypt = (content: any, password: string) => {
|
||||
let key: any = getCryptKey(password)
|
||||
let iv: any = key
|
||||
const passwordHash = CryptoJS.MD5(key).toString()
|
||||
|
||||
key = CryptoJS.enc.Utf8.parse(key)
|
||||
@@ -47,11 +47,11 @@ const decrypt = (content, password) => {
|
||||
return { content: result, passwordHash }
|
||||
}
|
||||
|
||||
const md5 = content => {
|
||||
const md5 = (content: any) => {
|
||||
return CryptoJS.MD5(content).toString()
|
||||
}
|
||||
|
||||
const binMd5 = data => {
|
||||
const binMd5 = (data: any) => {
|
||||
return md5(CryptoJS.enc.Latin1.parse(data))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
const _window = window as any
|
||||
|
||||
interface Process extends NodeJS.Process {
|
||||
versions?: any;
|
||||
}
|
||||
|
||||
const nodeProcess: Process = window && (window.process || _window.nodeProcess)
|
||||
const nodeModule = window && (window.module || _window.nodeModule)
|
||||
const nodeRequire = window && (window.require || _window.nodeRequire)
|
||||
|
||||
const isElectron = !!(nodeProcess?.versions?.electron)
|
||||
|
||||
const openAlwaysOnTopWindow = (url: string, target = '_blank') => {
|
||||
if (isElectron) {
|
||||
const opener: any = window.open(url, target, 'frame=true,alwaysOnTop=true')
|
||||
|
||||
// 让弹出的窗口置顶
|
||||
opener.eval("require('electron').remote.getCurrentWindow().setAlwaysOnTop(true)")
|
||||
|
||||
return opener
|
||||
} else {
|
||||
return window.open(url, target)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
process: nodeProcess,
|
||||
module: nodeModule,
|
||||
require: nodeRequire,
|
||||
isElectron,
|
||||
openAlwaysOnTopWindow,
|
||||
}
|
||||
@@ -144,7 +144,7 @@ const extensions = [
|
||||
'.gql'
|
||||
]
|
||||
|
||||
const supported = name => {
|
||||
const supported = (name: string) => {
|
||||
return extensions.includes(file.extname(name.toLowerCase()))
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import Crypto from './Crypto'
|
||||
import Crypto from './crypto'
|
||||
import { slugify } from 'transliteration'
|
||||
|
||||
const fetchHttp = async (...params) => {
|
||||
const response = await fetch(...params)
|
||||
// TODO 文件类型
|
||||
type F = { repo: string; path: string };
|
||||
|
||||
const fetchHttp = async (input: RequestInfo, init?: RequestInit) => {
|
||||
const response = await fetch(input, init)
|
||||
const result = await response.json()
|
||||
if (result.status !== 'ok') {
|
||||
throw new Error(result.message)
|
||||
@@ -11,31 +14,31 @@ const fetchHttp = async (...params) => {
|
||||
return result
|
||||
}
|
||||
|
||||
const isBelongTo = (path, sub) => {
|
||||
const isBelongTo = (path: string, sub: string) => {
|
||||
return sub.startsWith(path.replace(/\/$/, '') + '/')
|
||||
}
|
||||
|
||||
const isEncryptedFile = file => {
|
||||
const isEncryptedFile = (file: any) => {
|
||||
return file && file.path.endsWith('.c.md')
|
||||
}
|
||||
|
||||
const isSameFile = (a, b) => {
|
||||
const isSameFile = (a?: F | null, b?: F | null) => {
|
||||
return a && b && a.repo === b.repo && a.path === b.path
|
||||
}
|
||||
|
||||
const dirname = path => {
|
||||
const dirname = (path: string) => {
|
||||
return path.substr(0, path.lastIndexOf('/'))
|
||||
}
|
||||
|
||||
const basename = path => {
|
||||
const basename = (path: string) => {
|
||||
return path.substr(path.lastIndexOf('/') + 1)
|
||||
}
|
||||
|
||||
const extname = path => {
|
||||
const extname = (path: string) => {
|
||||
return path.substr(path.lastIndexOf('.'))
|
||||
}
|
||||
|
||||
const toUri = (file) => {
|
||||
const toUri = (file?: F | null) => {
|
||||
if (file && file.repo && file.path) {
|
||||
return encodeURI(`yank-note://${file.repo}/${file.path.replace(/^\//, '')}`)
|
||||
} else {
|
||||
@@ -43,7 +46,7 @@ const toUri = (file) => {
|
||||
}
|
||||
}
|
||||
|
||||
const decrypt = (content, password) => {
|
||||
const decrypt = (content: any, password: string) => {
|
||||
if (!password) {
|
||||
throw new Error('未输入解密密码')
|
||||
}
|
||||
@@ -51,7 +54,7 @@ const decrypt = (content, password) => {
|
||||
return Crypto.decrypt(content, password)
|
||||
}
|
||||
|
||||
const encrypt = (content, password) => {
|
||||
const encrypt = (content: any, password: string) => {
|
||||
if (!password) {
|
||||
throw new Error('未输入解密密码')
|
||||
}
|
||||
@@ -59,15 +62,15 @@ const encrypt = (content, password) => {
|
||||
return Crypto.encrypt(content, password)
|
||||
}
|
||||
|
||||
const read = async ({ path, repo }) => {
|
||||
const read = async ({ path, repo }: F) => {
|
||||
const result = await fetchHttp(`/api/file?path=${encodeURIComponent(path)}&repo=${encodeURIComponent(repo)}`)
|
||||
const hash = result.data.hash
|
||||
let content = result.data.content
|
||||
const content = result.data.content
|
||||
|
||||
return { content, hash }
|
||||
}
|
||||
|
||||
const write = async ({ repo, path }, content, oldHash) => {
|
||||
const write = async ({ repo, path }: F, content: any, oldHash: string) => {
|
||||
const result = await fetchHttp('/api/file', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -77,7 +80,7 @@ const write = async ({ repo, path }, content, oldHash) => {
|
||||
return { hash: result.data }
|
||||
}
|
||||
|
||||
const move = async ({ repo, path }, newPath) => {
|
||||
const move = async ({ repo, path }: F, newPath: string) => {
|
||||
return fetchHttp('/api/file', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -85,15 +88,15 @@ const move = async ({ repo, path }, newPath) => {
|
||||
})
|
||||
}
|
||||
|
||||
const deleteFile = async ({ path, repo }) => {
|
||||
const deleteFile = async ({ path, repo }: F) => {
|
||||
return fetchHttp(`/api/file?path=${encodeURIComponent(path)}&repo=${repo}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
const mark = async ({ path, repo }) => {
|
||||
const mark = async ({ path, repo }: F) => {
|
||||
return fetchHttp(`/api/mark?path=${encodeURIComponent(path)}&repo=${repo}`, { method: 'POST' })
|
||||
}
|
||||
|
||||
const unmark = async ({ path, repo }) => {
|
||||
const unmark = async ({ path, repo }: F) => {
|
||||
return fetchHttp(`/api/mark?path=${encodeURIComponent(path)}&repo=${repo}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
@@ -102,12 +105,12 @@ const markedFiles = async () => {
|
||||
return data
|
||||
}
|
||||
|
||||
const fetchTree = async repo => {
|
||||
const fetchTree = async (repo: string) => {
|
||||
const result = await fetchHttp(`/api/tree?repo=${repo}`)
|
||||
return result.data
|
||||
}
|
||||
|
||||
const fetchHelpContent = async (doc) => {
|
||||
const fetchHelpContent = async (doc: string) => {
|
||||
const result = await fetchHttp('/api/help?doc=' + doc)
|
||||
return result.data.content
|
||||
}
|
||||
@@ -117,12 +120,12 @@ const fetchRepositories = async () => {
|
||||
return result.data
|
||||
}
|
||||
|
||||
const search = async (repo, text) => {
|
||||
const search = async (repo: string, text: string) => {
|
||||
const result = await fetchHttp(`/api/search?repo=${repo}&search=${encodeURIComponent(text)}`)
|
||||
return result.data
|
||||
}
|
||||
|
||||
const upload = async (repo, belongPath, uploadFile, name = null) => {
|
||||
const upload = async (repo: string, belongPath: string, uploadFile: any, name: string | null = null): Promise<{repo: string; path: string; relativePath: string}> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
belongPath = belongPath.replace(/\\/g, '/')
|
||||
|
||||
@@ -153,11 +156,11 @@ const upload = async (repo, belongPath, uploadFile, name = null) => {
|
||||
})
|
||||
}
|
||||
|
||||
const openInOS = async ({ repo, path }) => {
|
||||
const openInOS = async ({ repo, path }: F) => {
|
||||
return fetchHttp(`/api/open?repo=${encodeURIComponent(repo)}&path=${encodeURIComponent(path)}`)
|
||||
}
|
||||
|
||||
const toBase64URL = async file => new Promise((resolve, reject) => {
|
||||
const toBase64URL = async (file: any) => new Promise((resolve, reject) => {
|
||||
const fr = new FileReader()
|
||||
fr.readAsDataURL(file)
|
||||
fr.onload = () => resolve(fr.result)
|
||||
@@ -1,6 +1,8 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// https://github.com/forrestthewoods/lib_fts/blob/master/code/fts_fuzzy_match.js
|
||||
|
||||
const fuzzyMatch = function (pattern, str) {
|
||||
const fuzzyMatch = function (pattern: any, str: any) {
|
||||
// Score consts
|
||||
var adjacencyBonus = 5 // bonus for adjacent matches
|
||||
var separatorBonus = 10 // bonus if match occurs after a separator
|
||||
@@ -0,0 +1,3 @@
|
||||
export function $args () {
|
||||
return new URLSearchParams(location.search)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { App, ComponentPublicInstance, createApp } from 'vue'
|
||||
import { Components } from '@/types'
|
||||
import Modal from '@/components/ModalUi.vue'
|
||||
|
||||
interface Instance extends ComponentPublicInstance {
|
||||
confirm: (params: Components.Modal.ConfirmModalParams) => Promise<boolean>;
|
||||
input: (params: Components.Modal.InputModalParams) => Promise<string>;
|
||||
}
|
||||
|
||||
let instance: Instance
|
||||
|
||||
export function useModal (): Instance {
|
||||
return instance
|
||||
}
|
||||
|
||||
export default function install (app: App) {
|
||||
const toast = createApp(Modal)
|
||||
const el = document.createElement('div')
|
||||
document.body.appendChild(el)
|
||||
|
||||
instance = toast.mount(el) as Instance
|
||||
app.config.globalProperties.$modal = instance
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useStore } from 'vuex'
|
||||
import { useBus } from './bus'
|
||||
import RunPlugin from '@/plugins/RunPlugin'
|
||||
import { copyText } from './copy-text'
|
||||
|
||||
export default function startup () {
|
||||
const bus = useBus()
|
||||
const store = useStore()
|
||||
|
||||
RunPlugin.clearCache()
|
||||
|
||||
bus.on('editor-ready', () => {
|
||||
const { currentFile } = store.state
|
||||
|
||||
if (!currentFile) {
|
||||
// 当前没打开文件,直接打开 README
|
||||
store.dispatch('showHelp', 'README.md')
|
||||
}
|
||||
})
|
||||
|
||||
bus.on('copy-text', copyText)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
const get = function (key, defaultValue) {
|
||||
const get = function (key: string, defaultValue?: any) {
|
||||
try {
|
||||
return typeof window.localStorage[key] === 'undefined' ? defaultValue : JSON.parse(window.localStorage[key])
|
||||
} catch (error) {
|
||||
@@ -6,7 +6,7 @@ const get = function (key, defaultValue) {
|
||||
}
|
||||
}
|
||||
|
||||
const set = function (key, value) {
|
||||
const set = function (key: string, value: any) {
|
||||
window.localStorage[key] = JSON.stringify(value)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { App, ComponentPublicInstance, createApp } from 'vue'
|
||||
import Toast from '@/components/Toast.vue'
|
||||
import { Components } from '@/types'
|
||||
|
||||
interface ToastInstance extends ComponentPublicInstance {
|
||||
show: (type: Components.Toast.ToastType, content: string, timeout?: number) => void;
|
||||
}
|
||||
|
||||
let instance: ToastInstance
|
||||
|
||||
export function useToast (): ToastInstance {
|
||||
return instance
|
||||
}
|
||||
|
||||
export default function install (app: App) {
|
||||
const toast = createApp(Toast)
|
||||
const el = document.createElement('div')
|
||||
document.body.appendChild(el)
|
||||
|
||||
instance = toast.mount(el) as ToastInstance
|
||||
app.config.globalProperties.$toast = instance
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export const encodeMarkdownLink = path => {
|
||||
export const encodeMarkdownLink = (path: string) => {
|
||||
return path
|
||||
.replace(/\(/g, '%28')
|
||||
.replace(/\)/g, '%29')
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<Layout>
|
||||
<template v-slot:header>
|
||||
<TitleBar />
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<StatusBar />
|
||||
</template>
|
||||
<template v-slot:left>
|
||||
<Tree />
|
||||
</template>
|
||||
<template v-slot:terminal>
|
||||
<Xterm />
|
||||
</template>
|
||||
<template v-slot:editor>
|
||||
<FileTabs />
|
||||
<Editor ref="refEditor" @scroll-line="line => refPreview.revealLine(line)" />
|
||||
</template>
|
||||
<template v-slot:preview>
|
||||
<Preview
|
||||
ref="refPreview"
|
||||
@sync-scroll="line => refEditor.revealLineInCenter(line)"
|
||||
@switch-todo="(line, checked) => refEditor.switchTodo(line, checked)" />
|
||||
</template>
|
||||
</Layout>
|
||||
<XFilter />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref } from 'vue'
|
||||
import startup from '../useful/startup'
|
||||
import Layout from '../components/Layout.vue'
|
||||
import TitleBar from '../components/TitleBar.vue'
|
||||
import StatusBar from '../components/StatusBar.vue'
|
||||
import Tree from '../components/Tree.vue'
|
||||
import Xterm from '../components/Xterm.vue'
|
||||
import FileTabs from '../components/FileTabs.vue'
|
||||
import Editor from '../components/Editor.vue'
|
||||
import Preview from '../components/Preview.vue'
|
||||
|
||||
// import RunPlugin from ''../plugins/RunPlugin'
|
||||
import XFilter from '../components/Filter.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'x-main',
|
||||
components: {
|
||||
Layout,
|
||||
TitleBar,
|
||||
StatusBar,
|
||||
Tree,
|
||||
Xterm,
|
||||
FileTabs,
|
||||
Editor,
|
||||
Preview,
|
||||
XFilter,
|
||||
},
|
||||
setup () {
|
||||
const refEditor = ref<any>(null)
|
||||
const refPreview = ref<any>(null)
|
||||
|
||||
onMounted(startup)
|
||||
|
||||
return { refEditor, refPreview }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,13 @@
|
||||
import { expect } from 'chai'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import HelloWorld from '@/components/HelloWorld.vue'
|
||||
|
||||
describe('HelloWorld.vue', () => {
|
||||
it('renders props.msg when passed', () => {
|
||||
const msg = 'new message'
|
||||
const wrapper = shallowMount(HelloWorld, {
|
||||
props: { msg }
|
||||
})
|
||||
expect(wrapper.text()).to.include(msg)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"types": [
|
||||
"webpack-env",
|
||||
"mocha",
|
||||
"chai"
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
"lib": [
|
||||
"esnext",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"scripthost"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
+6052
-5198
File diff suppressed because it is too large
Load Diff
+61
-13
@@ -36,18 +36,65 @@ Yank-Note 很多功能可以直接使用的快捷键操作
|
||||
支持使用脚注[^1]语法[^2]
|
||||
|
||||
## Mermaid 图形解析
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Hard] -->|Text| B(Round)
|
||||
B --> C{Decision}
|
||||
C -->|One| D[Result 1]
|
||||
C -->|Two| E[Result 2]
|
||||
```
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Alice
|
||||
participant Bob
|
||||
Alice->>John: Hello John, how are you?
|
||||
loop Healthcheck
|
||||
John->>John: Fight against hypochondria
|
||||
end
|
||||
Note right of John: Rational thoughts <br/>prevail!
|
||||
John-->>Alice: Great!
|
||||
John->>Bob: How about you?
|
||||
Bob-->>John: Jolly good!
|
||||
Alice->>John: Hello John, how are you?
|
||||
loop Healthcheck
|
||||
John->>John: Fight against hypochondria
|
||||
end
|
||||
Note right of John: Rational thoughts!
|
||||
John-->>Alice: Great!
|
||||
John->>Bob: How about you?
|
||||
Bob-->>John: Jolly good!
|
||||
```
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
section Section
|
||||
Completed :done, des1, 2014-01-06,2014-01-08
|
||||
Active :active, des2, 2014-01-07, 3d
|
||||
Parallel 1 : des3, after des1, 1d
|
||||
Parallel 2 : des4, after des1, 1d
|
||||
Parallel 3 : des5, after des3, 1d
|
||||
Parallel 4 : des6, after des4, 1d
|
||||
```
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Still
|
||||
Still --> [*]
|
||||
Still --> Moving
|
||||
Moving --> Still
|
||||
Moving --> Crash
|
||||
Crash --> [*]
|
||||
```
|
||||
|
||||
```mermaid
|
||||
pie
|
||||
"Dogs" : 386
|
||||
"Cats" : 85
|
||||
"Rats" : 15
|
||||
```
|
||||
|
||||
```mermaid
|
||||
journey
|
||||
title My working day
|
||||
section Go to work
|
||||
Make tea: 5: Me
|
||||
Go upstairs: 3: Me
|
||||
Do work: 1: Me, Cat
|
||||
section Go home
|
||||
Go downstairs: 5: Me
|
||||
Sit down: 3: Me
|
||||
```
|
||||
|
||||
## Plantuml 图形解析
|
||||
@@ -253,10 +300,10 @@ xml 代码块 第一行注释需要有 `--drawio--` 文字
|
||||
```
|
||||
|
||||
### 嵌入本地 drawio 文件
|
||||
链接标题(title)需要是 `--drawio--` 字符串。使用链接的形式也不会影响其他 Markdown 解析器解析。
|
||||
链接属性 `link-type` 值需要是 `drawio` 字符串。使用链接的形式也不会影响其他 Markdown 解析器解析。
|
||||
|
||||
```markdown
|
||||
[drawio](./test.drawio "--drawio--")
|
||||
[drawio](./test.drawio){link-type="drawio"}
|
||||
```
|
||||
|
||||
## 编辑器快捷键
|
||||
@@ -291,7 +338,8 @@ xml 代码块 第一行注释需要有 `--drawio--` 文字
|
||||
+ `Ctrl + K, Ctrl + U` 转换大写
|
||||
+ `Ctrl + K, Ctrl + L` 转换小写
|
||||
+ `Ctrl + Alt + R` 在内置终端里面运行选中内容
|
||||
+ `Ctrl + B + V` 粘贴 HTML 富文本
|
||||
+ `Ctrl + M + V` 粘贴 HTML 富文本
|
||||
+ `Ctrl + B + V` 粘贴图片为 base64 链接
|
||||
|
||||
## 元素属性书写
|
||||
此功能使用如下库实现 [markdown-it-attrs](https://github.com/arve0/markdown-it-attrs)
|
||||
|
||||
Reference in New Issue
Block a user