Add source editing toggle beside the outline button
This commit is contained in:
1 parent
73ff88c3c3
commit
15e036b9cd
7 files changed
+37
-11
No files matched your search
@@ -22,6 +22,7 @@ const paths = {
|
|||||||
palette: 'M12 22a10 10 0 1 1 10-10c0 2.8-2.2 4-4 4h-2a2 2 0 0 0-1.5 3.3A1.7 1.7 0 0 1 12 22zM7.5 10.5h.01M12 7.5h.01M16.5 10.5h.01',
|
palette: 'M12 22a10 10 0 1 1 10-10c0 2.8-2.2 4-4 4h-2a2 2 0 0 0-1.5 3.3A1.7 1.7 0 0 1 12 22zM7.5 10.5h.01M12 7.5h.01M16.5 10.5h.01',
|
||||||
user: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',
|
user: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',
|
||||||
outline: 'M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01',
|
outline: 'M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01',
|
||||||
|
source: 'm8 6-6 6 6 6m8-12 6 6-6 6',
|
||||||
command: 'M18 3a3 3 0 0 0-3 3v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 0 0 0-6z',
|
command: 'M18 3a3 3 0 0 0-3 3v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 0 0 0-6z',
|
||||||
info: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM12 11v6M12 7h.01',
|
info: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM12 11v6M12 7h.01',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { nextTick, ref } from 'vue'
|
|||||||
import { commandTitle } from './commands'
|
import { commandTitle } from './commands'
|
||||||
import Icon from './Icon.vue'
|
import Icon from './Icon.vue'
|
||||||
import { noteTitle } from './paths'
|
import { noteTitle } from './paths'
|
||||||
import { closeTab, ws } from './workspace'
|
import { closeTab, toggleSourceMode, ws } from './workspace'
|
||||||
|
|
||||||
// 拖动排序:拖到某个标签的左半边插到它前面,右半边插到它后面
|
// 拖动排序:拖到某个标签的左半边插到它前面,右半边插到它后面
|
||||||
const TAB_TYPE = 'application/x-rawmark-tab'
|
const TAB_TYPE = 'application/x-rawmark-tab'
|
||||||
@@ -103,9 +103,14 @@ function closeRight(path: string) {
|
|||||||
<Icon name="close" :size="12" />
|
<Icon name="close" :size="12" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button v-if="ws.active" class="rm-icon-btn rm-tabs-outline" :class="{ on: ws.outlineOpen }" :title="commandTitle('view.outline')" @click="ws.outlineOpen = !ws.outlineOpen">
|
<div v-if="ws.active" class="rm-tabs-actions">
|
||||||
<Icon name="outline" />
|
<button class="rm-icon-btn" :title="ws.sourceMode ? '关闭源码编辑' : '开启源码编辑'" :aria-pressed="ws.sourceMode" @mousedown.prevent @click="toggleSourceMode">
|
||||||
</button>
|
<Icon name="source" />
|
||||||
|
</button>
|
||||||
|
<button class="rm-icon-btn rm-tabs-outline" :class="{ on: ws.outlineOpen }" :title="commandTitle('view.outline')" @click="ws.outlineOpen = !ws.outlineOpen">
|
||||||
|
<Icon name="outline" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<template v-if="menu">
|
<template v-if="menu">
|
||||||
<div class="rm-menu-backdrop" @click="menu = null" @contextmenu.prevent="menu = null" />
|
<div class="rm-menu-backdrop" @click="menu = null" @contextmenu.prevent="menu = null" />
|
||||||
<div ref="menuElement" class="rm-menu rm-tab-menu" :style="{ left: menu.x + 'px', top: menu.y + 'px' }">
|
<div ref="menuElement" class="rm-menu rm-tab-menu" :style="{ left: menu.x + 'px', top: menu.y + 'px' }">
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { noteTitle } from './paths'
|
|||||||
import { SIDEBAR_WIDTH, settings, updateSettings } from './settings'
|
import { SIDEBAR_WIDTH, settings, updateSettings } from './settings'
|
||||||
import Sidebar from './Sidebar.vue'
|
import Sidebar from './Sidebar.vue'
|
||||||
import TabBar from './TabBar.vue'
|
import TabBar from './TabBar.vue'
|
||||||
import { activeTab, flushAll, readyToReload, startWorkspace, stopWorkspace, toast, ws } from './workspace'
|
import { activeTab, flushAll, readyToReload, startWorkspace, stopWorkspace, toast, toggleSourceMode, ws } from './workspace'
|
||||||
|
|
||||||
const emit = defineEmits<{ logout: [] }>()
|
const emit = defineEmits<{ logout: [] }>()
|
||||||
const title = computed(() => (ws.active ? noteTitle(ws.active) : 'RawMark'))
|
const title = computed(() => (ws.active ? noteTitle(ws.active) : 'RawMark'))
|
||||||
@@ -110,6 +110,7 @@ onBeforeUnmount(() => {
|
|||||||
<button class="rm-icon-btn" title="目录" @click="ws.sidebarOpen = true"><Icon name="menu" :size="20" /></button>
|
<button class="rm-icon-btn" title="目录" @click="ws.sidebarOpen = true"><Icon name="menu" :size="20" /></button>
|
||||||
<button class="rm-mobile-title" title="切换笔记" @click="ws.switcher = ''">{{ title }}</button>
|
<button class="rm-mobile-title" title="切换笔记" @click="ws.switcher = ''">{{ title }}</button>
|
||||||
<span class="rm-save-state" :class="{ on: saving }" />
|
<span class="rm-save-state" :class="{ on: saving }" />
|
||||||
|
<button v-if="ws.active" class="rm-icon-btn" :title="ws.sourceMode ? '关闭源码编辑' : '开启源码编辑'" :aria-pressed="ws.sourceMode" @mousedown.prevent @click="toggleSourceMode"><Icon name="source" :size="20" /></button>
|
||||||
<button v-if="ws.active" class="rm-icon-btn" title="大纲" @click="ws.outlineOpen = !ws.outlineOpen"><Icon name="outline" :size="20" /></button>
|
<button v-if="ws.active" class="rm-icon-btn" title="大纲" @click="ws.outlineOpen = !ws.outlineOpen"><Icon name="outline" :size="20" /></button>
|
||||||
<button class="rm-icon-btn" title="快速打开" @click="ws.switcher = ''"><Icon name="search" :size="20" /></button>
|
<button class="rm-icon-btn" title="快速打开" @click="ws.switcher = ''"><Icon name="search" :size="20" /></button>
|
||||||
<button class="rm-icon-btn" title="命令" @click="ws.switcher = '>'"><Icon name="command" :size="20" /></button>
|
<button class="rm-icon-btn" title="命令" @click="ws.switcher = '>'"><Icon name="command" :size="20" /></button>
|
||||||
|
|||||||
+3
-2
@@ -55,6 +55,7 @@ button {
|
|||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
.rm-icon-btn:hover { background: var(--rm-hover); color: var(--rm-text); }
|
.rm-icon-btn:hover { background: var(--rm-hover); color: var(--rm-text); }
|
||||||
|
.rm-icon-btn[aria-pressed="true"] { background: var(--rm-active); color: var(--rm-accent-strong); }
|
||||||
|
|
||||||
.rm-splash,
|
.rm-splash,
|
||||||
.rm-empty {
|
.rm-empty {
|
||||||
@@ -284,8 +285,8 @@ button {
|
|||||||
}
|
}
|
||||||
.rm-tabs.empty { display: none; }
|
.rm-tabs.empty { display: none; }
|
||||||
.rm-tabs-side,
|
.rm-tabs-side,
|
||||||
.rm-tabs-outline { flex: none; align-self: center; margin-bottom: 4px; }
|
.rm-tabs-actions { flex: none; align-self: center; margin-bottom: 4px; }
|
||||||
.rm-tabs-outline { margin-left: auto; position: sticky; right: 0; background: var(--rm-bg); }
|
.rm-tabs-actions { display: flex; margin-left: auto; position: sticky; right: 0; background: var(--rm-bg); }
|
||||||
.rm-tabs-outline.on { color: var(--rm-accent-strong); }
|
.rm-tabs-outline.on { color: var(--rm-accent-strong); }
|
||||||
.rm-tab.dragging { opacity: 0.5; }
|
.rm-tab.dragging { opacity: 0.5; }
|
||||||
.rm-tab.drop-before { box-shadow: inset 2px 0 0 var(--rm-accent); }
|
.rm-tab.drop-before { box-shadow: inset 2px 0 0 var(--rm-accent); }
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { EditorView } from '@codemirror/view'
|
|||||||
import { nextTick, reactive, watch } from 'vue'
|
import { nextTick, reactive, watch } from 'vue'
|
||||||
import { editorCommands } from '../editor/shortcuts'
|
import { editorCommands } from '../editor/shortcuts'
|
||||||
import { refreshImages } from '../editor/livepreview'
|
import { refreshImages } from '../editor/livepreview'
|
||||||
import { createEditorState, insertImageFiles, pasteImages, languageCompartment, pluginCompartment, pluginEditorExtensions, pluginLanguage, preferencesCompartment } from '../editor'
|
import { createEditorState, insertImageFiles, pasteImages, languageCompartment, pluginCompartment, pluginEditorExtensions, pluginLanguage, preferencesCompartment, previewCompartment, previewExtensions } from '../editor'
|
||||||
import { handleShortcut, registerCommand } from './commands'
|
import { handleShortcut, registerCommand } from './commands'
|
||||||
import { api, ApiError, encodePath, type Entry } from './api'
|
import { api, ApiError, encodePath, type Entry } from './api'
|
||||||
import { drafts } from './drafts'
|
import { drafts } from './drafts'
|
||||||
@@ -73,6 +73,7 @@ export const ws = reactive({
|
|||||||
sidebarCollapsed: layout.sidebarCollapsed ?? false,
|
sidebarCollapsed: layout.sidebarCollapsed ?? false,
|
||||||
// 大纲面板(电脑上在右侧常驻,手机上为浮层)
|
// 大纲面板(电脑上在右侧常驻,手机上为浮层)
|
||||||
outlineOpen: layout.outlineOpen ?? false,
|
outlineOpen: layout.outlineOpen ?? false,
|
||||||
|
sourceMode: false,
|
||||||
// 最近打开的笔记,最新的在前
|
// 最近打开的笔记,最新的在前
|
||||||
recent: JSON.parse(localStorage.getItem(RECENT_KEY) ?? '[]') as string[],
|
recent: JSON.parse(localStorage.getItem(RECENT_KEY) ?? '[]') as string[],
|
||||||
toasts: [] as Toast[],
|
toasts: [] as Toast[],
|
||||||
@@ -149,6 +150,17 @@ export function editorView(): EditorView | null {
|
|||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toggleSourceMode() {
|
||||||
|
ws.sourceMode = !ws.sourceMode
|
||||||
|
const effects = previewCompartment.reconfigure(previewExtensions(ws.sourceMode))
|
||||||
|
for (const [path, state] of editorStates) {
|
||||||
|
if (path === ws.active && view) {
|
||||||
|
view.dispatch({ effects })
|
||||||
|
editorStates.set(path, view.state)
|
||||||
|
} else editorStates.set(path, state.update({ effects }).state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 从相册/文件中选择图片插入到光标处 */
|
/** 从相册/文件中选择图片插入到光标处 */
|
||||||
export function insertImagesFromPicker() {
|
export function insertImagesFromPicker() {
|
||||||
const v = view
|
const v = view
|
||||||
@@ -186,6 +198,7 @@ function buildState(path: string, doc: string): EditorState {
|
|||||||
shortcuts: [],
|
shortcuts: [],
|
||||||
spellcheck: settings.spellcheck,
|
spellcheck: settings.spellcheck,
|
||||||
tabSize: settings.tabSize,
|
tabSize: settings.tabSize,
|
||||||
|
sourceMode: ws.sourceMode,
|
||||||
resolveImage: (src) => {
|
resolveImage: (src) => {
|
||||||
const url = resolveAssetUrl(ref.path, src)
|
const url = resolveAssetUrl(ref.path, src)
|
||||||
const entry = ws.entries.find((e) => url === '/api/v1/raw/' + encodePath(e.path))
|
const entry = ws.entries.find((e) => url === '/api/v1/raw/' + encodePath(e.path))
|
||||||
@@ -1068,6 +1081,7 @@ function registerBuiltinCommands() {
|
|||||||
{ id: 'app.settings', title: '设置', keys: ['Mod+,'], run: () => (ws.settings = 'index') },
|
{ id: 'app.settings', title: '设置', keys: ['Mod+,'], run: () => (ws.settings = 'index') },
|
||||||
{ id: 'view.sidebar', title: '显示/隐藏侧栏', keys: ['Mod+\\'], run: toggleSidebar },
|
{ id: 'view.sidebar', title: '显示/隐藏侧栏', keys: ['Mod+\\'], run: toggleSidebar },
|
||||||
{ id: 'view.outline', title: '显示/隐藏大纲', keys: ['Mod+Shift+O'], when: hasNote, run: () => (ws.outlineOpen = !ws.outlineOpen) },
|
{ id: 'view.outline', title: '显示/隐藏大纲', keys: ['Mod+Shift+O'], when: hasNote, run: () => (ws.outlineOpen = !ws.outlineOpen) },
|
||||||
|
{ id: 'view.source', title: '开启/关闭源码编辑', when: hasNote, run: toggleSourceMode },
|
||||||
{ id: 'app.trash', title: '打开回收站', run: () => (ws.settings = 'trash') },
|
{ id: 'app.trash', title: '打开回收站', run: () => (ws.settings = 'trash') },
|
||||||
{ id: 'note.share', title: '分享当前笔记', when: hasNote, run: () => (ws.sharing = ws.active) },
|
{ id: 'note.share', title: '分享当前笔记', when: hasNote, run: () => (ws.sharing = ws.active) },
|
||||||
{ id: 'app.images', title: '管理根目录图片', run: () => (ws.images = '') },
|
{ id: 'app.images', title: '管理根目录图片', run: () => (ws.images = '') },
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ export { insertImageFiles, pasteImages } from './paste'
|
|||||||
export const pluginCompartment = new Compartment()
|
export const pluginCompartment = new Compartment()
|
||||||
export const languageCompartment = new Compartment()
|
export const languageCompartment = new Compartment()
|
||||||
export const preferencesCompartment = new Compartment()
|
export const preferencesCompartment = new Compartment()
|
||||||
|
export const previewCompartment = new Compartment()
|
||||||
|
export function previewExtensions(sourceMode: boolean): Extension {
|
||||||
|
return sourceMode ? EditorView.contentAttributes.of({ class: 'cm-rm-source' }) : [livePreview, tableEditing, renderedBlocks]
|
||||||
|
}
|
||||||
export function pluginLanguage() { return new Language(
|
export function pluginLanguage() { return new Language(
|
||||||
markdownLanguage.data,
|
markdownLanguage.data,
|
||||||
(markdownLanguage.parser as MarkdownParser).configure([typoraExtensions, ...[...markdownSyntax.values()].map((entry) => entry.value)]),
|
(markdownLanguage.parser as MarkdownParser).configure([typoraExtensions, ...[...markdownSyntax.values()].map((entry) => entry.value)]),
|
||||||
@@ -33,6 +37,7 @@ export interface EditorStateOptions {
|
|||||||
doc: string
|
doc: string
|
||||||
spellcheck?: boolean
|
spellcheck?: boolean
|
||||||
tabSize?: number
|
tabSize?: number
|
||||||
|
sourceMode?: boolean
|
||||||
shortcuts?: KeyBinding[]
|
shortcuts?: KeyBinding[]
|
||||||
resolveImage?: (src: string) => string
|
resolveImage?: (src: string) => string
|
||||||
// 文档变化回调,仅对挂载在视图上的状态触发;未激活标签页的状态由调用方直接更新
|
// 文档变化回调,仅对挂载在视图上的状态触发;未激活标签页的状态由调用方直接更新
|
||||||
@@ -54,11 +59,9 @@ export function createEditorState(opts: EditorStateOptions): EditorState {
|
|||||||
Prec.high(keymap.of(markdownKeymap)),
|
Prec.high(keymap.of(markdownKeymap)),
|
||||||
Prec.high(keymap.of(opts.shortcuts ?? defaultEditorKeys)),
|
Prec.high(keymap.of(opts.shortcuts ?? defaultEditorKeys)),
|
||||||
markdownTheme,
|
markdownTheme,
|
||||||
livePreview,
|
|
||||||
codeBlockHighlight,
|
codeBlockHighlight,
|
||||||
editorFocus,
|
editorFocus,
|
||||||
tableEditing,
|
previewCompartment.of(previewExtensions(opts.sourceMode ?? false)),
|
||||||
renderedBlocks,
|
|
||||||
// 可配置操作不再通过 CM 的默认绑定重复注册。
|
// 可配置操作不再通过 CM 的默认绑定重复注册。
|
||||||
keymap.of(defaultKeymap.filter((binding) => binding.run !== selectAll)),
|
keymap.of(defaultKeymap.filter((binding) => binding.run !== selectAll)),
|
||||||
preferencesCompartment.of([
|
preferencesCompartment.of([
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ body {
|
|||||||
padding: 32px 24px 40vh;
|
padding: 32px 24px 40vh;
|
||||||
caret-color: #ff0000;
|
caret-color: #ff0000;
|
||||||
}
|
}
|
||||||
|
.cm-editor .cm-content.cm-rm-source { font-family: var(--rm-font-mono); }
|
||||||
.cm-editor .cm-cursor {
|
.cm-editor .cm-cursor {
|
||||||
border-left: 2px solid #ff0000;
|
border-left: 2px solid #ff0000;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user