feat: add action bar

This commit is contained in:
purocean
2022-03-25 17:41:28 +08:00
parent 5c243037a4
commit 55b6408cb7
16 changed files with 336 additions and 63 deletions
+137
View File
@@ -0,0 +1,137 @@
<template>
<div class="action-bar">
<div class="btns">
<div>
<div class="btn flat" @click="toggleOutline()" :title="$t(showOutline ? 'files' : 'outline')">
<svg-icon v-if="showOutline" name="folder-tree-solid" />
<svg-icon v-else name="list" />
</div>
</div>
</div>
<div class="title">{{$t(showOutline ? 'outline' : 'files')}}</div>
<div class="btns" v-if="navigation">
<div v-for="(item, i) in navigation.items" :key="i">
<div
v-if="item.showInActionBar && item.type === 'btn'"
:class="{ btn: true, flat: item.flat, disabled: item.disabled, checked: item.checked }"
:title="item.title"
@click.stop="item.onClick"
>
<svg-icon :name="item.icon" />
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onBeforeUnmount, ref, toRef } from 'vue'
import { useStore } from 'vuex'
import { registerAction, removeAction } from '@fe/core/action'
import { getSchema, Schema } from '@fe/services/control-center'
import type { AppState } from '@fe/support/store'
import SvgIcon from './SvgIcon.vue'
import { useI18n } from '@fe/services/i18n'
import { toggleOutline } from '@fe/services/layout'
const store = useStore<AppState>()
const navigation = ref<Schema['navigation']>()
const showOutline = toRef(store.state, 'showOutline')
useI18n()
registerAction({
name: 'action-bar.refresh',
handler () {
navigation.value = getSchema().navigation
}
})
onBeforeUnmount(() => {
removeAction('action-bar.refresh')
})
</script>
<style lang="scss" scoped>
@import '@fe/styles/mixins.scss';
.action-bar {
height: 30px;
flex: none;
border-bottom: 1px solid var(--g-color-87);
z-index: 1;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
& > .title {
position: absolute;
margin: auto;
width: 100%;
text-align: center;
display: block;
font-size: 14px;
color: var(--g-color-20);
z-index: -1;
user-select: none;
}
& > div {
display: flex;
}
.btn {
width: 30px;
height: 30px;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
font-size: 12px;
color: var(--g-color-20);
border-radius: var(--g-border-radius);
margin: 4px;
transition: .1s ease-in-out;
&.flat {
width: 30px;
height: 30px;
margin: 0;
}
&.checked {
background: var(--g-color-96);
}
.svg-icon {
width: 12px;
height: 12px;
pointer-events: none;
}
&:not(.disabled):not(.checked):hover {
background: var(--g-color-92);
}
&.disabled {
color: var(--g-color-50);
cursor: default;
}
}
}
@include dark-theme {
.action-bar {
.btn {
&.checked {
background: var(--g-color-60);
}
&:not(.disabled):not(.checked):hover {
background: var(--g-color-76);
}
}
}
}
</style>
+12
View File
@@ -231,6 +231,18 @@ export default defineComponent({
flex: none;
height: 100%;
border-right: 1px solid var(--g-color-86);
display: flex;
flex-direction: column;
background: var(--g-color-98);
::v-deep(.action-bar .btns) {
opacity: 0;
transition: 0.2s;
}
&:hover ::v-deep(.action-bar .btns) {
opacity: 1;
}
}
.sash-right {
+107
View File
@@ -0,0 +1,107 @@
<template>
<div class="outline-toc">
<div v-if="(headings || heads).length < 1" class="empty">Empty</div>
<div
class="heading"
v-for="(head, index) in (headings || heads)"
:key="index"
:class="head.class"
:style="{paddingLeft: `${head.level + 1}em`}"
@click="handleClickItem(head)">
{{ head.text }}
<span class="tag-name">{{head.tag}}</span>
</div>
</div>
</template>
<script lang="ts">
import { registerHook, removeHook } from '@fe/core/hook'
import { revealLineInCenter } from '@fe/services/editor'
import { getHeadings, Heading } from '@fe/services/view'
import { defineComponent, onBeforeUnmount, ref } from 'vue'
export default defineComponent({
name: 'outline',
props: {
headings: Array as () => Heading[],
autoScroll: {
type: Boolean,
default: true,
}
},
setup (props, { emit }) {
function handleClickItem (heading: Heading) {
emit('click-item', heading)
if (props.autoScroll) {
revealLineInCenter(heading.sourceLine)
}
}
const heads = ref<Heading[]>([])
function refresh () {
if (props.headings) {
heads.value = []
} else {
heads.value = getHeadings()
}
}
registerHook('VIEW_RENDERED', refresh)
onBeforeUnmount(() => {
removeHook('VIEW_RENDERED', refresh)
})
return { heads, handleClickItem }
},
})
</script>
<style lang="scss" scoped>
@import '@fe/styles/mixins.scss';
.outline-toc {
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 16px;
user-select: none;
& > .empty {
text-align: center;
padding-top: 4em;
font-size: 20px;
color: var(--g-color-40);
font-weight: lighter;
}
& > .heading {
font-size: 14px;
line-height: 18px;
padding: 7px .5em;
display: flex;
border-radius: var(--g-border-radius);
cursor: pointer;
&:hover {
background: rgba(0, 0, 0, 0.1);
}
&.tag-h1 {
font-weight: bold;
}
.tag-name {
color: var(--g-color-60);
font-size: 12px;
padding-left: 0.5em;
}
}
}
@include dark-theme {
.outline-toc > .heading:hover {
background: rgba(255, 255, 255, 0.14);
}
}
</style>
+6 -49
View File
@@ -26,15 +26,7 @@
</div>
</div>
<div class="catalog" :style="{maxHeight: `min(${(height - 120) + 'px'}, 70vh)`}">
<div
v-for="(head, index) in heads"
:key="index"
:class="head.class"
:style="{paddingLeft: `${head.level + 1}em`}"
@click="syncScroll(head.sourceLine)">
{{ head.text }}
<span class="tag-name">{{head.tag}}</span>
</div>
<Outline :headings="heads" :auto-scroll="false" @click-item="(head: any) => syncScroll(head.sourceLine)" />
</div>
</div>
<div :class="{'scroll-to-top': true, 'hide': scrollTop < 30}" :style="scrollToTopStyle" @click="scrollToTop">TOP</div>
@@ -55,7 +47,7 @@ import { registerHook, removeHook, triggerHook } from '@fe/core/hook'
import { registerAction, removeAction } from '@fe/core/action'
import { revealLineInCenter } from '@fe/services/editor'
import { showExport, toUri } from '@fe/services/document'
import { getContextMenuItems } from '@fe/services/view'
import { getContextMenuItems, getHeadings, Heading } from '@fe/services/view'
import { useContextMenu } from '@fe/support/ui/context-menu'
import { DOM_ATTR_NAME } from '@fe/support/constant'
import { useI18n } from '@fe/services/i18n'
@@ -64,6 +56,7 @@ import type { RenderEnv } from '@fe/types'
import Render from './Render.vue'
import SvgIcon from './SvgIcon.vue'
import Outline from './Outline.vue'
import 'highlight.js/styles/atom-one-dark.css'
import 'katex/dist/katex.min.css'
@@ -72,7 +65,7 @@ const logger = getLogger('preview')
export default defineComponent({
name: 'xview',
components: { Render, SvgIcon },
components: { Render, SvgIcon, Outline },
setup () {
useI18n()
@@ -93,13 +86,7 @@ export default defineComponent({
const width = ref(1024)
const height = ref(1024)
const heads = ref<{
tag: string;
class: string[];
text: string;
level: number;
sourceLine: number;
}[]>([])
const heads = ref<Heading[]>([])
const todoCount = ref(0)
const todoDoneCount = ref(0)
const scrollTop = ref(0)
@@ -122,18 +109,7 @@ export default defineComponent({
}
function updateOutline () {
const tags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
const nodes = refView.value!.querySelectorAll<HTMLHeadElement>(tags.join(','))
heads.value = Array.from(nodes).map(node => {
const tag = node.tagName.toLowerCase()
return {
tag,
class: [...node.classList, 'tag-' + tag],
text: node.innerText,
level: tags.indexOf(tag),
sourceLine: parseInt(node.dataset.sourceLine || '0')
}
})
heads.value = getHeadings()
}
function updateTodoCount () {
@@ -447,25 +423,6 @@ export default defineComponent({
cursor: pointer;
overflow: auto;
overflow-x: hidden;
padding-bottom: 1em;
& > div {
font-size: 14px;
line-height: 18px;
padding: 7px .5em;
display: flex;
border-radius: var(--g-border-radius);
&:hover {
background: var(--g-color-75);
}
.tag-name {
color: var(--g-color-60);
font-size: 12px;
padding-left: 0.5em;
}
}
}
.outline-title {
+1
View File
@@ -48,6 +48,7 @@ export default defineComponent({
'star-solid': '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="star" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" class="svg-inline--fa fa-star fa-w-18"><path fill="currentColor" d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z" class=""></path></svg>',
'star-regular': '<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="star" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" class="svg-inline--fa fa-star fa-w-18"><path fill="currentColor" d="M528.1 171.5L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6zM388.6 312.3l23.7 138.4L288 385.4l-124.3 65.3 23.7-138.4-100.6-98 139-20.2 62.2-126 62.2 126 139 20.2-100.6 98z" class=""></path></svg>',
'pen-solid': '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="pen" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="svg-inline--fa fa-pen fa-w-16"><path fill="currentColor" d="M290.74 93.24l128.02 128.02-277.99 277.99-114.14 12.6C11.35 513.54-1.56 500.62.14 485.34l12.7-114.22 277.9-277.88zm207.2-19.06l-60.11-60.11c-18.75-18.75-49.16-18.75-67.91 0l-56.55 56.55 128.02 128.02 56.55-56.55c18.75-18.76 18.75-49.16 0-67.91z" class=""></path></svg>',
'folder-tree-solid': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M544 32h-112l-32-32H320c-17.62 0-32 14.38-32 32v160c0 17.62 14.38 32 32 32h224c17.62 0 32-14.38 32-32V64C576 46.38 561.6 32 544 32zM544 320h-112l-32-32H320c-17.62 0-32 14.38-32 32v160c0 17.62 14.38 32 32 32h224c17.62 0 32-14.38 32-32v-128C576 334.4 561.6 320 544 320zM64 16C64 7.125 56.88 0 48 0h-32C7.125 0 0 7.125 0 16V416c0 17.62 14.38 32 32 32h224v-64H64V160h192V96H64V16z"/></svg>',
}
const name = props.name!
-1
View File
@@ -92,7 +92,6 @@ export default defineComponent({
width: 100%;
box-sizing: border-box;
overflow: auto;
background: var(--g-color-98);
padding-bottom: 20px;
}
@@ -93,6 +93,7 @@ export default {
flat: true,
title: ctx.i18n.t('control-center.navigation.back', ctx.command.getKeysLabel(backId)),
disabled: idx <= 0,
showInActionBar: true,
onClick: () => ctx.action.getActionHandler(backId)()
},
{
@@ -101,6 +102,7 @@ export default {
flat: true,
title: ctx.i18n.t('control-center.navigation.forward', ctx.command.getKeysLabel(forwardId)),
disabled: idx >= stack.length - 1,
showInActionBar: true,
onClick: () => ctx.action.getActionHandler(forwardId)()
},
)
+10 -10
View File
@@ -46,31 +46,31 @@ export default {
counter(h6counter) ".\\0000a0\\0000a0";
}
.markdown-view .outline .tag-h1.show-number { counter-reset: outline-h2counter; }
.markdown-view .outline .tag-h2.show-number { counter-reset: outline-h3counter; }
.markdown-view .outline .tag-h3.show-number { counter-reset: outline-h4counter; }
.markdown-view .outline .tag-h4.show-number { counter-reset: outline-h5counter; }
.markdown-view .outline .tag-h5.show-number { counter-reset: outline-h6counter; }
.outline-toc .tag-h1.show-number { counter-reset: outline-h2counter; }
.outline-toc .tag-h2.show-number { counter-reset: outline-h3counter; }
.outline-toc .tag-h3.show-number { counter-reset: outline-h4counter; }
.outline-toc .tag-h4.show-number { counter-reset: outline-h5counter; }
.outline-toc .tag-h5.show-number { counter-reset: outline-h6counter; }
.markdown-view .outline .tag-h2.show-number:before {
.outline-toc .tag-h2.show-number:before {
counter-increment: outline-h2counter;
content: counter(outline-h2counter) ".\\0000a0\\0000a0";
}
.markdown-view .outline .tag-h3.show-number:before {
.outline-toc .tag-h3.show-number:before {
counter-increment: outline-h3counter;
content: counter(outline-h2counter) "."
counter(outline-h3counter) ".\\0000a0\\0000a0";
}
.markdown-view .outline .tag-h4.show-number:before {
.outline-toc .tag-h4.show-number:before {
counter-increment: outline-h4counter;
content: counter(outline-h2counter) "."
counter(outline-h3counter) "."
counter(outline-h4counter) ".\\0000a0\\0000a0";
}
.markdown-view .outline .tag-h5.show-number:before {
.outline-toc .tag-h5.show-number:before {
counter-increment: outline-h5counter;
content: counter(outline-h2counter) "."
counter(outline-h3counter) "."
@@ -78,7 +78,7 @@ export default {
counter(outline-h5counter) ".\\0000a0\\0000a0";
}
.markdown-view .outline .tag-h6.show-number:before {
.outline-toc .tag-h6.show-number:before {
counter-increment: outline-h6counter;
content: counter(outline-h2counter) "."
counter(outline-h3counter) "."
@@ -111,6 +111,7 @@ export default {
icon: 'search-solid',
flat: true,
title: ctx.i18n.t('control-center.navigation.goto', ctx.command.getKeysLabel('filter.show-quick-open')),
showInActionBar: true,
onClick: () => {
ctx.action.getActionHandler('filter.show-quick-open')()
ctx.controlCenter.toggle(false)
+2
View File
@@ -12,6 +12,7 @@ export type Item = {
icon: string,
title: string,
onClick?: () => void,
showInActionBar?: boolean,
}
export type SchemaItem = { items: Item[] }
@@ -26,6 +27,7 @@ export type SchemaTapper = (schema: Schema) => void
const _refresh = debounce(() => {
getActionHandler('control-center.refresh')()
getActionHandler('action-bar.refresh')()
}, 10)
/**
+7
View File
@@ -67,6 +67,13 @@ export function toggleXterm (visible?: boolean) {
}
})
}
/**
* Toggle outline visible.
* @param visible
*/
export function toggleOutline (visible?: boolean) {
store.commit('setShowOutline', typeof visible === 'boolean' ? visible : !store.state.showOutline)
}
registerAction({ name: 'layout.toggle-side', handler: toggleSide, keys: [Alt, 'e'] })
registerAction({ name: 'layout.toggle-editor', handler: toggleEditor, keys: [Alt, 'x'] })
+30
View File
@@ -12,6 +12,13 @@ import { switchDoc } from './document'
export type MenuItem = Components.ContextMenu.Item
export type BuildContextMenu = (items: MenuItem[], e: MouseEvent) => void
export type Heading = {
tag: string;
class: string[];
text: string;
level: number;
sourceLine: number;
}
let tmpEnableSyncScroll = true
let syncScrollTimer: any
@@ -168,6 +175,29 @@ export function getViewDom () {
return getActionHandler('view.get-view-dom')()
}
/**
* Get Headings
*/
export function getHeadings (): Heading[] {
const tags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
const dom = getViewDom()
if (!dom) {
return []
}
const nodes = dom.querySelectorAll<HTMLHeadElement>(tags.join(','))
return Array.from(nodes).map(node => {
const tag = node.tagName.toLowerCase()
return {
tag,
class: [...node.classList, 'tag-' + tag],
text: node.innerText,
level: tags.indexOf(tag),
sourceLine: parseInt(node.dataset.sourceLine || '0')
}
})
}
/**
* Get render env.
* @returns
+4
View File
@@ -11,6 +11,7 @@ export const initState = {
showView: storage.get('showView', true),
showEditor: storage.get('showEditor', true),
showXterm: false,
showOutline: false,
autoPreview: true,
syncScroll: true,
showSetting: false,
@@ -88,6 +89,9 @@ export default createStore({
setShowXterm (state, data) {
state.showXterm = data
},
setShowOutline (state, data) {
state.showOutline = data
},
setInComposition (state, data) {
state.inComposition = data
},
+13 -3
View File
@@ -7,7 +7,9 @@
<StatusBar />
</template>
<template v-slot:left>
<Tree />
<ActionBar />
<Outline v-show="showOutline" />
<Tree v-show="!showOutline" />
</template>
<template v-slot:terminal>
<Xterm @hide="hideXterm" />
@@ -29,10 +31,12 @@
</template>
<script lang="ts">
import { defineComponent, onMounted } from 'vue'
import { defineComponent, onMounted, toRef } from 'vue'
import { useStore } from 'vuex'
import startup from '@fe/startup'
import { getActionHandler } from '@fe/core/action'
import { FLAG_DISABLE_XTERM } from '@fe/support/args'
import type { AppState } from '@fe/support/store'
import Layout from '@fe/components/Layout.vue'
import TitleBar from '@fe/components/TitleBar.vue'
import StatusBar from '@fe/components/StatusBar.vue'
@@ -48,6 +52,8 @@ import Premium from '@fe/components/Premium.vue'
import XFilter from '@fe/components/Filter.vue'
import ControlCenter from '@fe/components/ControlCenter.vue'
import DocHistory from '@fe/components/DocHistory.vue'
import ActionBar from '@fe/components/ActionBar.vue'
import Outline from '@fe/components/Outline.vue'
export default defineComponent({
name: 'x-main',
@@ -66,8 +72,12 @@ export default defineComponent({
ExportPanel,
ControlCenter,
DocHistory,
ActionBar,
Outline,
},
setup () {
const store = useStore<AppState>()
const showOutline = toRef(store.state, 'showOutline')
onMounted(startup)
const classes = {
@@ -78,7 +88,7 @@ export default defineComponent({
getActionHandler('layout.toggle-xterm')(false)
}
return { classes, hideXterm }
return { classes, hideXterm, showOutline }
}
})
</script>
+2
View File
@@ -26,6 +26,8 @@ const data = {
'close': 'Close',
'discard': 'Discard',
'edit': 'Edit',
'outline': 'Outline',
'files': 'Files',
'premium': {
'need-purchase': '[%s] Premium is required',
'buy-license': 'Buy License',
+2
View File
@@ -27,6 +27,8 @@ const data: BaseLanguage = {
'close': '关闭',
'edit': '编辑',
'discard': '放弃',
'outline': '目录',
'files': '文件',
'premium': {
'need-purchase': '[%s] 需要高级版',
'buy-license': '立即购买',