diff --git a/app/src/menus/navigation.ts b/app/src/menus/navigation.ts index 8368e1f24..99eba2bc1 100644 --- a/app/src/menus/navigation.ts +++ b/app/src/menus/navigation.ts @@ -200,6 +200,7 @@ export const initFileMenu = (notebookId: string, pathString: string, liElement: icon: "iconCopy", submenu: (copySubMenu(id, false) as IMenu[]).concat([{ label: window.siyuan.languages.duplicate, + accelerator: window.siyuan.config.keymap.editor.general.duplicate.custom, click() { fetchPost("/api/filetree/duplicateDoc", { id diff --git a/app/src/protyle/gutter/index.ts b/app/src/protyle/gutter/index.ts index e1d0f73ca..5bca7b659 100644 --- a/app/src/protyle/gutter/index.ts +++ b/app/src/protyle/gutter/index.ts @@ -21,7 +21,6 @@ import {getContenteditableElement, getTopAloneElement, isNotEditBlock} from "../ import * as dayjs from "dayjs"; import {fetchPost, fetchSyncPost} from "../../util/fetch"; import {cancelSB, insertEmptyBlock, jumpToParentNext} from "../../block/util"; -import {scrollCenter} from "../../util/highlightById"; import {countBlockWord} from "../../layout/status"; /// #if !MOBILE import {openFileById} from "../../editor/util"; @@ -29,6 +28,7 @@ import {openFileById} from "../../editor/util"; import {Constants} from "../../constants"; import {openMobileFileById} from "../../mobile/editor"; import {mathRender} from "../markdown/mathRender"; +import {duplicateBlock} from "../wysiwyg/commonHotkey"; export class Gutter { public element: HTMLElement; @@ -543,46 +543,55 @@ export class Gutter { window.siyuan.menus.menu.append(new MenuItem({ label: window.siyuan.languages.copy, icon: "iconCopy", - accelerator: "⌘C", - click() { - if (isNotEditBlock(selectsElement[0])) { + type: "submenu", + submenu: [{ + label: window.siyuan.languages.copy, + accelerator: "⌘C", + click() { + if (isNotEditBlock(selectsElement[0])) { + let html = ""; + selectsElement.forEach(item => { + html += removeEmbed(item); + }); + writeText(protyle.lute.BlockDOM2StdMd(html).trimEnd()); + } else { + focusByRange(getEditorRange(selectsElement[0])); + document.execCommand("copy"); + } + } + }, { + label: window.siyuan.languages.copyPlainText, + accelerator: window.siyuan.config.keymap.editor.general.copyPlainText.custom, + click() { let html = ""; selectsElement.forEach(item => { - html += removeEmbed(item); - }); - writeText(protyle.lute.BlockDOM2StdMd(html).trimEnd()); - } else { - focusByRange(getEditorRange(selectsElement[0])); - document.execCommand("copy"); - } - } - }).element); - window.siyuan.menus.menu.append(new MenuItem({ - label: window.siyuan.languages.copyPlainText, - accelerator: window.siyuan.config.keymap.editor.general.copyPlainText.custom, - click() { - let html = ""; - selectsElement.forEach(item => { - item.querySelectorAll('[contenteditable="true"]').forEach(editItem => { - const cloneNode = editItem.cloneNode(true) as HTMLElement; - cloneNode.querySelectorAll('[data-type="backslash"]').forEach(slashItem => { - slashItem.firstElementChild.remove(); + item.querySelectorAll('[contenteditable="true"]').forEach(editItem => { + const cloneNode = editItem.cloneNode(true) as HTMLElement; + cloneNode.querySelectorAll('[data-type="backslash"]').forEach(slashItem => { + slashItem.firstElementChild.remove(); + }); + html += cloneNode.textContent + "\n"; }); - html += cloneNode.textContent + "\n"; }); - }); - writeText(html.trimEnd()); - } - }).element); - window.siyuan.menus.menu.append(new MenuItem({ - label: window.siyuan.languages.copy + " HTML", - click() { - let html = ""; - selectsElement.forEach(item => { - html += item.outerHTML; - }); - writeText(protyle.lute.BlockDOM2HTML(html)); - } + writeText(html.trimEnd()); + } + }, { + label: window.siyuan.languages.copy + " HTML", + click() { + let html = ""; + selectsElement.forEach(item => { + html += item.outerHTML; + }); + writeText(protyle.lute.BlockDOM2HTML(html)); + } + }, { + label: window.siyuan.languages.duplicate, + accelerator: window.siyuan.config.keymap.editor.general.duplicate.custom, + disabled: protyle.disabled, + click() { + duplicateBlock(selectsElement, protyle); + } + }] }).element); if (protyle.disabled) { return; @@ -948,26 +957,10 @@ export class Gutter { } }, { label: window.siyuan.languages.duplicate, + accelerator: window.siyuan.config.keymap.editor.general.duplicate.custom, disabled: protyle.disabled, click() { - const tempElement = nodeElement.cloneNode(true) as HTMLElement; - const newId = Lute.NewNodeID(); - tempElement.setAttribute("data-node-id", newId); - tempElement.querySelectorAll("[data-node-id]").forEach(item => { - item.setAttribute("data-node-id", Lute.NewNodeID()); - }); - nodeElement.after(tempElement); - scrollCenter(protyle); - transaction(protyle, [{ - action: "insert", - data: nodeElement.nextElementSibling.outerHTML, - id: newId, - previousID: id, - }], [{ - action: "delete", - id: newId, - }]); - focusBlock(tempElement); + duplicateBlock([nodeElement], protyle); } }]) }).element); diff --git a/app/src/protyle/wysiwyg/commonHotkey.ts b/app/src/protyle/wysiwyg/commonHotkey.ts index 3bee1211d..eb76d49fe 100644 --- a/app/src/protyle/wysiwyg/commonHotkey.ts +++ b/app/src/protyle/wysiwyg/commonHotkey.ts @@ -2,6 +2,7 @@ import {matchHotKey} from "../util/hotKey"; import {fetchPost} from "../../util/fetch"; import {writeText} from "../util/compatibility"; import { + focusBlock, focusByOffset, getSelectionOffset, setFirstNodeRange, @@ -16,6 +17,8 @@ import {getContenteditableElement} from "./getBlock"; import {hasClosestByMatchTag} from "../util/hasClosest"; import {hideElements} from "../ui/hideElements"; import {countBlockWord} from "../../layout/status"; +import {scrollCenter} from "../../util/highlightById"; +import {transaction} from "./transaction"; export const commonHotkey = (protyle: IProtyle, event: KeyboardEvent) => { const target = event.target as HTMLElement; @@ -100,7 +103,7 @@ export const upSelect = (options: { options.event.preventDefault(); return; } - } else{ + } else { // 选中上一个节点的处理在 toolbar/index.ts 中 `shift+方向键或三击选中` return; } @@ -180,3 +183,35 @@ export const getStartEndElement = (selectElements: NodeListOf | Element endElement }; }; + +export const duplicateBlock = (nodeElements: Element[], protyle: IProtyle) => { + let focusElement + const doOperations: IOperation[] = [] + const undoOperations: IOperation[] = [] + nodeElements.forEach((item, index) => { + const tempElement = item.cloneNode(true) as HTMLElement; + if (index === nodeElements.length - 1) { + focusElement = tempElement + } + const newId = Lute.NewNodeID(); + tempElement.setAttribute("data-node-id", newId); + tempElement.querySelectorAll("[data-node-id]").forEach(childItem => { + childItem.setAttribute("data-node-id", Lute.NewNodeID()); + }); + item.classList.remove("protyle-wysiwyg--select"); + item.after(tempElement); + doOperations.push({ + action: "insert", + data: tempElement.outerHTML, + id: newId, + previousID: item.getAttribute("data-node-id"), + }) + undoOperations.push({ + action: "delete", + id: newId, + }) + }) + transaction(protyle, doOperations, undoOperations); + focusBlock(focusElement); + scrollCenter(protyle); +} diff --git a/app/src/protyle/wysiwyg/keydown.ts b/app/src/protyle/wysiwyg/keydown.ts index 7efd01275..eb98269a0 100644 --- a/app/src/protyle/wysiwyg/keydown.ts +++ b/app/src/protyle/wysiwyg/keydown.ts @@ -42,7 +42,7 @@ import {isLocalPath} from "../../util/pathName"; /// #if !MOBILE import {openBy, openFileById} from "../../editor/util"; /// #endif -import {commonHotkey, downSelect, getStartEndElement, upSelect} from "./commonHotkey"; +import {commonHotkey, downSelect, duplicateBlock, getStartEndElement, upSelect} from "./commonHotkey"; import {linkMenu, refMenu, setFold, zoomOut} from "../../menus/protyle"; import {removeEmbed} from "./removeEmbed"; import {openAttr} from "../../menus/commonMenuItem"; @@ -1446,6 +1446,17 @@ export const keydown = (protyle: IProtyle, editorElement: HTMLElement) => { return; } + if (matchHotKey(window.siyuan.config.keymap.editor.general.duplicate.custom, event)) { + event.preventDefault(); + event.stopPropagation(); + let selectsElement: HTMLElement[] = Array.from(protyle.wysiwyg.element.querySelectorAll(".protyle-wysiwyg--select")); + if (selectsElement.length === 0) { + selectsElement = [nodeElement]; + } + duplicateBlock(selectsElement, protyle); + return; + } + // tab 需等待 list 和 table 处理完成 if (event.key === "Tab" && !event.ctrlKey && !isCtrl(event) && !event.altKey) { event.preventDefault();