diff --git a/README.md b/README.md index 4be2ab5e..bd7d26f8 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ + HTML 解析:可以直接在文档里面使用 HTML 代码,也可以使用快捷键粘贴复制 HTML 为 Markdown + docx 导出:后端使用 pandoc 做转换器 + TOC 支持:生成 TOC 在需要生成目录的地方写入 `[toc]{type: "ol", level: [1,2,3]}` 即可 -+ 编辑表格单元格:快捷编辑表格单元格 ++ 编辑表格单元格:双击表格单元格即可快速编辑 + 复制标题链接:复制标题链接路径到剪切板,便于插入到其他文件 + 嵌入小工具:文档支持内嵌 HTML 小工具 + 嵌入 Plantuml 图形:需要安装 Java,graphviz diff --git a/frontend/src/components/Preview.vue b/frontend/src/components/Preview.vue index c16687e3..dd911cb6 100644 --- a/frontend/src/components/Preview.vue +++ b/frontend/src/components/Preview.vue @@ -33,7 +33,7 @@
TOP
-
+
@@ -176,7 +176,7 @@ export default defineComponent({ todoDoneCount.value = refView.value!!.querySelectorAll('input[type=checkbox][checked]').length } - const render = lodash.debounce(() => { + const render = () => { // 编辑非 markdown 文件预览直接显示代码 const content = (filePath.value || '').endsWith('.md') ? currentContent.value @@ -189,7 +189,9 @@ export default defineComponent({ triggerHook('ON_VIEW_RENDER', { getViewDom }) nextTick(() => triggerHook('ON_VIEW_RENDERED', { getViewDom })) - }, 500, { leading: true }) + } + + const renderDebonce = lodash.debounce(render, 500, { leading: true }) async function keydownHandler (e: KeyboardEvent) { triggerHook('ON_VIEW_KEY_DOWN', e, getViewDom()) @@ -208,6 +210,10 @@ export default defineComponent({ syncScroll(1) } + function handleDbClick (e: MouseEvent) { + triggerHook('ON_VIEW_ELEMENT_DBCLICK', e) + } + async function handleClick (e: MouseEvent) { triggerHook('ON_VIEW_ELEMENT_CLICK', e) @@ -261,7 +267,7 @@ export default defineComponent({ } onMounted(() => { - nextTick(render) + nextTick(renderDebonce) triggerHook('ON_VIEW_MOUNTED', { getViewDom }) window.addEventListener('keydown', keydownHandler, true) bus.on('resize', resizeHandler) @@ -275,7 +281,7 @@ export default defineComponent({ bus.off('view-rerender', render) }) - watch(currentContent, () => autoPreview.value && render()) + watch(currentContent, () => autoPreview.value && renderDebonce()) watch(filePath, () => { // 切换文件后,开启自动预览 store.commit('setAutoPreview', true) @@ -298,6 +304,7 @@ export default defineComponent({ handleScroll, scrollToTop, handleClick, + handleDbClick, revealLine, syncScroll, } diff --git a/frontend/src/plugins/markdown-source-line.ts b/frontend/src/plugins/markdown-source-line.ts index 3eb27fbe..09245e69 100644 --- a/frontend/src/plugins/markdown-source-line.ts +++ b/frontend/src/plugins/markdown-source-line.ts @@ -24,7 +24,8 @@ export default { md.renderer.rules.heading_open = injectLineNumbers md.renderer.rules.list_item_open = injectLineNumbers md.renderer.rules.table_open = injectLineNumbers - md.renderer.rules.td_open = buildFunction('yank-td') + md.renderer.rules.td_open = buildFunction('yank-table-cell') + md.renderer.rules.th_open = buildFunction('yank-table-cell') }) } } as Plugin diff --git a/frontend/src/plugins/table-cell-edit.ts b/frontend/src/plugins/table-cell-edit.ts index 401ae819..693593a3 100644 --- a/frontend/src/plugins/table-cell-edit.ts +++ b/frontend/src/plugins/table-cell-edit.ts @@ -4,13 +4,14 @@ import { Plugin, Ctx } from '@/useful/plugin' import { hasCtrlCmd } from '@/useful/shortcut' import { useToast } from '@/useful/toast' -const editTableCell = async (start: number, end: number, cellIndex: number) => { +const editTableCell = async (start: number, end: number, cellIndex: number, input: HTMLTextAreaElement | null) => { const toast = useToast() const modal = useModal() const bus = useBus() if (end - start !== 1) { toast.show('warning', '暂只支持编辑单行文本') + input && input.remove() return } @@ -55,23 +56,58 @@ const editTableCell = async (start: number, end: number, cellIndex: number) => { bus.emit('editor-get-line', { line: start, callback: (val: string) => { text = val.trim() } }) const columns = escapedSplit(text) - const cellText = columns[cellIndex] + const cellText = columns[cellIndex].trim() if (typeof cellText !== 'string') { toast.show('warning', '编辑错误') return } - let value = await modal.input({ - title: '编辑单元格', - type: 'textarea', - value: cellText, - modalWidth: '600px', - hint: '单元格内容', - }) - if (typeof value !== 'string') { - toast.show('warning', '取消编辑') - return + let value = cellText + if (input) { + input.value = cellText + value = await (new Promise((resolve) => { + const cancel = () => { + input.onblur = () => undefined + input.remove() + } + + const ok = () => { + if (input.value === cellText) { + cancel() + } else { + resolve(input.value) + bus.emit('view-rerender') + } + } + + input.onblur = ok + + input.onkeydown = e => { + if (e.key === 'Escape') { + cancel() + } + + if (e.key === 'Enter' && hasCtrlCmd(e)) { + ok() + } + } + })) + } else { + const inputVal = await modal.input({ + title: '编辑单元格', + type: 'textarea', + value: cellText, + modalWidth: '600px', + hint: '单元格内容', + }) + + if (typeof inputVal !== 'string') { + toast.show('warning', '取消编辑') + return + } + + value = inputVal } if (!value.startsWith(' ') && cellIndex > 0) value = ' ' + value @@ -88,7 +124,7 @@ const editTableCell = async (start: number, end: number, cellIndex: number) => { export default { name: 'table-cell-edit', register: (ctx: Ctx) => { - ctx.registerHook('ON_VIEW_ELEMENT_CLICK', async (e: MouseEvent) => { + const handleClick = async (e: MouseEvent, modal: boolean) => { const target = e.target as HTMLElement const preventEvent = () => { @@ -97,7 +133,11 @@ export default { return true } - if (target.tagName === 'TD' && target.classList.contains('yank-td') && hasCtrlCmd(e)) { + if (['TD', 'TH'].includes(target.tagName) && target.classList.contains('yank-table-cell')) { + if ((hasCtrlCmd(e) && !modal) || (!hasCtrlCmd(e) && modal)) { + return false + } + const start = parseInt(target.dataset.sourceLine || '0') const end = parseInt(target.dataset.sourceLineEnd || '0') const td = target as HTMLTableDataCellElement @@ -105,12 +145,45 @@ export default { .slice(0, td.cellIndex) .reduce((prev, current) => prev + current.colSpan, 0) - editTableCell(start, end, cellIndex) + const input = modal ? null : document.createElement('textarea') + + if (input) { + target.style.position = 'relative' + input.style.left = '0' + input.style.top = '0' + input.style.height = '100%' + input.style.width = '100%' + input.style.boxSizing = 'border-box' + input.style.color = '#fff' + input.style.borderColor = '#93e632' + input.style.backgroundColor = '#272a2b' + input.style.position = 'absolute' + input.style.display = 'block' + input.style.padding = '4px' + input.autofocus = true + input.placeholder = 'ESC 取消' + setTimeout(() => { + input.focus() + }, 0) + + target.appendChild(input) + } + + editTableCell(start, end, cellIndex, input) return preventEvent() } return false + } + + ctx.registerHook('ON_VIEW_ELEMENT_DBCLICK', e => handleClick(e, false)) + ctx.registerHook('ON_VIEW_ELEMENT_CLICK', e => handleClick(e, true)) + ctx.registerHook('ON_VIEW_RENDERED', ({ getViewDom }) => { + const view: HTMLElement = getViewDom() + view.querySelectorAll('.yank-table-cell').forEach(td => { + (td as HTMLElement).title = '双击编辑' + }) }) } } as Plugin diff --git a/frontend/src/useful/plugin/view.ts b/frontend/src/useful/plugin/view.ts index f09e1502..ba54608b 100644 --- a/frontend/src/useful/plugin/view.ts +++ b/frontend/src/useful/plugin/view.ts @@ -1,4 +1,5 @@ export type HookType = 'ON_VIEW_ELEMENT_CLICK' + | 'ON_VIEW_ELEMENT_DBCLICK' | 'ON_VIEW_KEY_DOWN' | 'ON_VIEW_RENDER' | 'ON_VIEW_RENDERED' diff --git a/help/FEATURES.md b/help/FEATURES.md index 1664517e..ad9f013e 100644 --- a/help/FEATURES.md +++ b/help/FEATURES.md @@ -354,7 +354,8 @@ xml 代码块 第一行注释需要有 `--drawio--` 文字 粘贴图片为 base64 链接 | Ctrl + B + V | Cmd + B + V 转换单个外链图片到本地 | Ctrl + Shift + 单击图片 | Cmd + Shift + 单击图片 转换所有外链图片到本地 | Ctrl + Shift + L | Cmd + Shift + L -编辑表格单元格 | Ctrl + 单击单元格 | Cmd + 单击单元格 +编辑表格单元格 | 双击单元格 | 双击单元格 +编辑表格单元格(弹出框) | Ctrl + 单击单元格 | Cmd + 单击单元格 或 复制文档标题链接 | Ctrl + 单击标题 | Cmd + 单击标题 切换编辑器 Tab | Ctrl + Alt + Left/Right | Ctrl + Option + Left/Rig 切换侧栏 | Alt + E | Option + E