From 764466754925d53129dafb7228cd8c6b0e6e28ba Mon Sep 17 00:00:00 2001 From: purocean Date: Mon, 3 Jan 2022 20:28:49 +0800 Subject: [PATCH] feat: add drag and drop sorting to the table --- src/renderer/plugins/markdown-render-vnode.ts | 3 + src/renderer/plugins/markdown-table.ts | 174 +++++++++++++++++- src/renderer/support/constant.ts | 1 + src/share/i18n/languages/en.ts | 1 + src/share/i18n/languages/zh-CN.ts | 1 + 5 files changed, 173 insertions(+), 7 deletions(-) diff --git a/src/renderer/plugins/markdown-render-vnode.ts b/src/renderer/plugins/markdown-render-vnode.ts index 838e2954..3961d7d4 100644 --- a/src/renderer/plugins/markdown-render-vnode.ts +++ b/src/renderer/plugins/markdown-render-vnode.ts @@ -234,6 +234,9 @@ function render (this: Renderer, tokens: Token[], options: any, env: any) { return tokens.map((token, i) => { setSourceLine(token, env) + if (token.block) { + token.attrSet(DOM_ATTR_NAME.TOKEN_IDX, i.toString()) + } const type = token.type diff --git a/src/renderer/plugins/markdown-table.ts b/src/renderer/plugins/markdown-table.ts index 7ce8ca93..83618fe6 100644 --- a/src/renderer/plugins/markdown-table.ts +++ b/src/renderer/plugins/markdown-table.ts @@ -1,15 +1,23 @@ +import { nextTick } from 'vue' +import Sortable from 'sortablejs' +import { sortBy } from 'lodash-es' import type { Plugin } from '@fe/context' import { useModal } from '@fe/support/ui/modal' import { hasCtrlCmd } from '@fe/core/command' +import { registerHook } from '@fe/core/hook' +import { DOM_ATTR_NAME } from '@fe/support/constant' import { useToast } from '@fe/support/ui/toast' import { disableSyncScrollAwhile } from '@fe/services/view' import * as editor from '@fe/services/editor' import { t } from '@fe/services/i18n' +import { getLogger } from '@fe/utils' import type Token from 'markdown-it/lib/token' import type Renderer from 'markdown-it/lib/renderer' import type { Components } from '@fe/types' -const cellClassName = 'yank-table-cell' +const tableSortMode = 'sort-mode' +const cellClassName = 'yn-table-cell' +const logger = getLogger('markdown-table') function editWrapper, U> (fn: (...args: T) => U) { return function (...args: T) { @@ -176,6 +184,7 @@ async function editTableCell (start: number, end: number, cellIndex: number, inp if (input) { input.value = cellText + input.select() value = await (new Promise((resolve) => { const cancel = () => { resetInput(input) @@ -367,8 +376,12 @@ function deleteRow (td: HTMLTableCellElement) { deleteLine(start) } -function processColumns (td: HTMLTableCellElement, process: (columns: string[], row: Row) => void) { +function processColumns (td: HTMLTableCellElement, process: (columns: string[]) => void) { const rows = getRows(td) + if (rows.some(({ start, end }) => !checkLineNumber(start, end))) { + return + } + const hr = rows.find(x => x.type === 'hr') if (!hr) { return @@ -377,15 +390,12 @@ function processColumns (td: HTMLTableCellElement, process: (columns: string[], const refText = getLineContent(hr.start) rows.forEach((row) => { - const { start, end } = row - if (!checkLineNumber(start, end)) { - return - } + const { start } = row const content = getLineContent(start) const columns = escapedSplit(content) - process(columns, row) + process(columns) replaceLine(start, columnsToStr(columns, refText)) }) @@ -446,6 +456,123 @@ function deleteCol (td: HTMLTableCellElement) { }) } +function sortCol (td: HTMLTableCellElement, oldIndex: number, newIndex: number) { + processColumns(td, columns => { + if (columns.length - 1 < Math.max(oldIndex, newIndex)) { + return + } + + const text = columns[oldIndex] + columns.splice(oldIndex, 1) + columns.splice(newIndex, 0, text) + }) +} + +function sortRow (td: HTMLTableCellElement, oldIndex: number, newIndex: number) { + const rows = sortBy(getRows(td).filter(x => x.type === 'body'), x => x.start) + if (rows.length - 1 < Math.max(oldIndex, newIndex)) { + return + } + + if (rows.some(({ start, end }) => !checkLineNumber(start, end))) { + return + } + + const oldLine = rows[oldIndex].start + const newLine = rows[newIndex].start + + const content = getLineContent(oldLine) + deleteLine(oldLine) + const prevContent = getLineContent(newLine) + replaceLine(newLine, `${content}\n${prevContent}`) +} + +function toggleSortMode (td: HTMLTableCellElement, flag: boolean) { + const table: (HTMLElement & { colSortable?: Sortable, rowSortable?: Sortable }) | null | undefined = td.parentElement?.parentElement?.parentElement + if (!table || table.tagName !== 'TABLE') { + return + } + + logger.debug('toggleSortMode', flag) + + const theadFirstTr = table.querySelector('thead > tr') + const tbody = table.querySelector('tbody') + + const clean = () => { + logger.debug('toggleSortMode', 'clean') + table.onblur = null + table.onkeydown = null + table.removeAttribute(tableSortMode) + table.colSortable?.destroy() + delete table.colSortable + table.rowSortable?.destroy() + table.blur() + delete table.rowSortable + } + + if (flag && theadFirstTr && tbody) { + clean() + + // restore origin order for vue vnode + const restoreOrder = (getSortable: () => Sortable, oldIndex: number, newIndex: number) => { + registerHook('VIEW_RENDER', () => { + const sortable = getSortable() + if (sortable) { + const arr = sortable.toArray() + const id = arr[newIndex] + arr.splice(newIndex, 1) + arr.splice(oldIndex, 0, id) + sortable.sort(arr) + } + }, true) + } + + table.colSortable = Sortable.create(theadFirstTr, { + animation: 200, + direction: 'horizontal', + dataIdAttr: DOM_ATTR_NAME.TOKEN_IDX, + onEnd: ({ oldIndex, newIndex }) => { + if (typeof oldIndex === 'number' && typeof newIndex === 'number' && oldIndex !== newIndex) { + restoreOrder(() => table.colSortable!, oldIndex, newIndex) + sortCol(td, oldIndex, newIndex) + nextTick(() => toggleSortMode(td, true)) + } + } + }) + + table.rowSortable = Sortable.create(tbody, { + animation: 200, + direction: 'vertical', + handle: 'td:first-of-type', + dataIdAttr: DOM_ATTR_NAME.TOKEN_IDX, + onEnd: ({ oldIndex, newIndex }) => { + if (typeof oldIndex === 'number' && typeof newIndex === 'number' && oldIndex !== newIndex) { + restoreOrder(() => table.rowSortable!, oldIndex, newIndex) + sortRow(td, oldIndex, newIndex) + nextTick(() => toggleSortMode(td, true)) + } + } + }) + + table.tabIndex = -1 + + table.onblur = () => { + toggleSortMode(td, false) + } + + table.onkeydown = (e) => { + if (e.key === 'Escape') { + toggleSortMode(td, false) + } + } + + table.setAttribute(tableSortMode, 'true') + table.focus() + } else { + clean() + } +} + export default { name: 'markdown-table', register: (ctx) => { @@ -494,6 +621,30 @@ export default { color: #999; font-family: monospace; } + + .markdown-view .markdown-body .table-wrapper > table[sort-mode], + .markdown-view .markdown-body .table-wrapper > table[sort-mode] tr { + outline: none; + } + + .markdown-view .markdown-body .table-wrapper > table[sort-mode] td:hover, + .markdown-view .markdown-body .table-wrapper > table[sort-mode] th:hover { + background: initial; + } + + .markdown-view .markdown-body .table-wrapper > table[sort-mode] thead tr:first-of-type th { + background: var(--g-color-86); + cursor: ew-resize; + } + + .markdown-view .markdown-body .table-wrapper > table[sort-mode] tbody tr td:first-of-type { + background: var(--g-color-86); + cursor: ns-resize; + } + + .markdown-view .markdown-body .table-wrapper > table[sort-mode] .sortable-ghost { + opacity: 0.5; + } } `) @@ -590,6 +741,15 @@ export default { } }, { type: 'separator' }, + { + id: 'plugin.table.cell-edit.sort-mode', + type: 'normal', + label: ctx.i18n.t('table-cell-edit.context-menu.sort-mode'), + onClick: () => { + toggleSortMode(target, true) + } + }, + { type: 'separator' }, { id: 'plugin.table.cell-edit.align-left', type: 'normal', diff --git a/src/renderer/support/constant.ts b/src/renderer/support/constant.ts index f50fdaa1..dfd2b966 100644 --- a/src/renderer/support/constant.ts +++ b/src/renderer/support/constant.ts @@ -4,6 +4,7 @@ export const DOM_ATTR_NAME = { ORIGIN_SRC: 'origin-src', ORIGIN_HREF: 'origin-href', LOCAL_IMAGE: 'local-image', + TOKEN_IDX: 'data-token-idx', } export const DOM_CLASS_NAME = { diff --git a/src/share/i18n/languages/en.ts b/src/share/i18n/languages/en.ts index 07bfecf3..6cf5f161 100644 --- a/src/share/i18n/languages/en.ts +++ b/src/share/i18n/languages/en.ts @@ -388,6 +388,7 @@ const data = { 'context-menu': { 'edit': 'Edit', 'quick-edit': 'Quit Edit', + 'sort-mode': 'Sort Mode', 'align-left': 'Align Left', 'align-center': 'Align Center', 'align-right': 'Align Right', diff --git a/src/share/i18n/languages/zh-CN.ts b/src/share/i18n/languages/zh-CN.ts index ce4b0883..fa3a4ce1 100644 --- a/src/share/i18n/languages/zh-CN.ts +++ b/src/share/i18n/languages/zh-CN.ts @@ -389,6 +389,7 @@ const data: BaseLanguage = { 'context-menu': { 'edit': '编辑', 'quick-edit': '快速编辑', + 'sort-mode': '排序模式', 'align-left': '左对齐', 'align-center': '居中', 'align-right': '右对齐',