feat: 增加复制粘贴功能
This commit is contained in:
+86
-12
@@ -10,13 +10,19 @@ import {
|
|||||||
import { Menu } from "antd";
|
import { Menu } from "antd";
|
||||||
import state from "../store/state";
|
import state from "../store/state";
|
||||||
import store from "../store/global";
|
import store from "../store/global";
|
||||||
import { createNotation, isNote, notations as N } from "../util/notation";
|
|
||||||
import {
|
import {
|
||||||
|
cloneNotation,
|
||||||
|
createNotation,
|
||||||
|
isNote,
|
||||||
|
notations as N,
|
||||||
|
} from "../util/notation";
|
||||||
|
import {
|
||||||
|
cloneParagraph,
|
||||||
createParagraph,
|
createParagraph,
|
||||||
createParagraphWithNotations,
|
createParagraphWithNotations,
|
||||||
} from "../util/paragraph";
|
} from "../util/paragraph";
|
||||||
import { exportFile, loadFile, saveFile } from "../util/file";
|
import { exportFile, loadFile, saveFile } from "../util/file";
|
||||||
import { findParagraphAndNotation } from "../util/editor";
|
import { findParagraphAndNotation, resetGlobalData } from "../util/editor";
|
||||||
import {
|
import {
|
||||||
go,
|
go,
|
||||||
runInWrappedAction,
|
runInWrappedAction,
|
||||||
@@ -27,15 +33,18 @@ import {
|
|||||||
const { SubMenu } = Menu;
|
const { SubMenu } = Menu;
|
||||||
|
|
||||||
const handleOpenFile = unwrappedAction((ev) => {
|
const handleOpenFile = unwrappedAction((ev) => {
|
||||||
// TODO: working
|
|
||||||
const file = ev.target.files[0];
|
const file = ev.target.files[0];
|
||||||
loadFile(file);
|
loadFile(file);
|
||||||
});
|
});
|
||||||
const handleSaveFile = unwrappedAction(() => {
|
const handleSaveFile = () => {
|
||||||
saveFile();
|
saveFile();
|
||||||
});
|
};
|
||||||
const handleExportFile = () => {
|
const handleExportFile = unwrappedAction(() => {
|
||||||
|
state.selectedNotationKey = null;
|
||||||
exportFile();
|
exportFile();
|
||||||
|
});
|
||||||
|
const handleCreate = () => {
|
||||||
|
resetGlobalData();
|
||||||
};
|
};
|
||||||
const handleEditMenu = wrappedAction(({ key }) => {
|
const handleEditMenu = wrappedAction(({ key }) => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
@@ -149,7 +158,6 @@ const handleKeyPress = wrappedAction((ev) => {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case inputKey === "l" && !ctrl && !shift:
|
|
||||||
case inputKey === "enter" && !ctrl && !shift: {
|
case inputKey === "enter" && !ctrl && !shift: {
|
||||||
if (paragraph.notations[notationIndex + 1]) {
|
if (paragraph.notations[notationIndex + 1]) {
|
||||||
state.selectedNotationKey =
|
state.selectedNotationKey =
|
||||||
@@ -161,7 +169,15 @@ const handleKeyPress = wrappedAction((ev) => {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case inputKey === "h" && !ctrl && !shift:
|
case inputKey === "l" && !ctrl && !shift: {
|
||||||
|
const nextNotation =
|
||||||
|
paragraph.notations[notationIndex + 1] ||
|
||||||
|
store.paragraphs[paragraphIndex + 1].notations?.at(0);
|
||||||
|
if (nextNotation) {
|
||||||
|
state.selectedNotationKey = nextNotation.key;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case inputKey === "enter" && !ctrl && shift: {
|
case inputKey === "enter" && !ctrl && shift: {
|
||||||
if (paragraph.notations[notationIndex - 1]) {
|
if (paragraph.notations[notationIndex - 1]) {
|
||||||
state.selectedNotationKey =
|
state.selectedNotationKey =
|
||||||
@@ -169,6 +185,15 @@ const handleKeyPress = wrappedAction((ev) => {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case inputKey === "h" && !ctrl && !shift: {
|
||||||
|
const prevNotation =
|
||||||
|
paragraph.notations[notationIndex - 1] ||
|
||||||
|
store.paragraphs[paragraphIndex - 1].notations?.at(-1);
|
||||||
|
if (prevNotation) {
|
||||||
|
state.selectedNotationKey = prevNotation.key;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case inputKey === "enter" && ctrl && !shift: {
|
case inputKey === "enter" && ctrl && !shift: {
|
||||||
const newNotation = createNotation();
|
const newNotation = createNotation();
|
||||||
if (
|
if (
|
||||||
@@ -206,6 +231,7 @@ const handleKeyPress = wrappedAction((ev) => {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case inputKey === "backspace" && !ctrl && !shift:
|
||||||
case inputKey === "delete" && !ctrl && !shift: {
|
case inputKey === "delete" && !ctrl && !shift: {
|
||||||
paragraph.notations.splice(notationIndex, 1);
|
paragraph.notations.splice(notationIndex, 1);
|
||||||
const currNotation =
|
const currNotation =
|
||||||
@@ -214,6 +240,17 @@ const handleKeyPress = wrappedAction((ev) => {
|
|||||||
state.selectedNotationKey = currNotation?.key;
|
state.selectedNotationKey = currNotation?.key;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case inputKey === "backspace" && !ctrl && shift:
|
||||||
|
case inputKey === "delete" && !ctrl && shift: {
|
||||||
|
store.paragraphs.splice(paragraphIndex, 1);
|
||||||
|
const currParagraph =
|
||||||
|
store.paragraphs[paragraphIndex] || store.paragraphs.at(-1);
|
||||||
|
const currNotation =
|
||||||
|
currParagraph?.notations?.at(notationIndex) ||
|
||||||
|
currParagraph?.notations?.at(-1);
|
||||||
|
state.selectedNotationKey = currNotation?.key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case inputKey === "=" && !ctrl && !shift: {
|
case inputKey === "=" && !ctrl && !shift: {
|
||||||
if (typeof paragraph.alignJustify !== "boolean") {
|
if (typeof paragraph.alignJustify !== "boolean") {
|
||||||
paragraph.alignJustify = !(
|
paragraph.alignJustify = !(
|
||||||
@@ -224,6 +261,40 @@ const handleKeyPress = wrappedAction((ev) => {
|
|||||||
paragraph.alignJustify = !paragraph.alignJustify;
|
paragraph.alignJustify = !paragraph.alignJustify;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case inputKey === "c" && ctrl && !shift: {
|
||||||
|
ev.preventDefault();
|
||||||
|
state.clipboardContent = notation;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case inputKey === "c" && ctrl && shift: {
|
||||||
|
ev.preventDefault();
|
||||||
|
state.clipboardContent = paragraph;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case inputKey === "v" && ctrl && !shift: {
|
||||||
|
ev.preventDefault();
|
||||||
|
if (state.clipboardContent) {
|
||||||
|
switch (state.clipboardContent.type) {
|
||||||
|
case "notation":
|
||||||
|
paragraph.notations.splice(
|
||||||
|
notationIndex + 1,
|
||||||
|
0,
|
||||||
|
cloneNotation(state.clipboardContent)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "paragraph":
|
||||||
|
store.paragraphs.splice(
|
||||||
|
paragraphIndex + 1,
|
||||||
|
0,
|
||||||
|
cloneParagraph(state.clipboardContent)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
state.clipboardContent = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -231,9 +302,12 @@ const handleKeyPress = wrappedAction((ev) => {
|
|||||||
// 仅未选中符号时
|
// 仅未选中符号时
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case ["h", "j", "k", "l"].includes(inputKey): {
|
case ["h", "j", "k", "l"].includes(inputKey): {
|
||||||
state.selectedNotationKey =
|
const { notation: n } = findParagraphAndNotation(
|
||||||
state.lastSelectedNotationKey ||
|
state.lastSelectedNotationKey
|
||||||
store.paragraphs?.at(0)?.notations?.at(0).key;
|
);
|
||||||
|
state.selectedNotationKey = (
|
||||||
|
n || store.paragraphs?.at(0)?.notations?.at(0)
|
||||||
|
)?.key;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -279,7 +353,7 @@ const handleKeyPress = wrappedAction((ev) => {
|
|||||||
|
|
||||||
const fileMenu = (
|
const fileMenu = (
|
||||||
<Menu>
|
<Menu>
|
||||||
<Menu.Item key="create" icon={<PlusOutlined />}>
|
<Menu.Item key="create" icon={<PlusOutlined />} onClick={handleCreate}>
|
||||||
新建
|
新建
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="open" icon={<FolderOpenOutlined />}>
|
<Menu.Item key="open" icon={<FolderOpenOutlined />}>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ const getNotationContextMenu = (notation, paragraph) => {
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
)}
|
)}
|
||||||
<Menu.Item key="delete" icon={<DeleteOutlined />}>
|
<Menu.Item key="delete" icon={<DeleteOutlined />}>
|
||||||
删除
|
删除符号
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-6
@@ -14,12 +14,8 @@ let globalState = observable({
|
|||||||
helpDialogVisible: false,
|
helpDialogVisible: false,
|
||||||
// 配置项弹窗是否可见
|
// 配置项弹窗是否可见
|
||||||
configDialogVisible: false,
|
configDialogVisible: false,
|
||||||
// 持久化的文件
|
// 复制剪贴的内容
|
||||||
file: null,
|
clipboardContent: null,
|
||||||
// ref
|
|
||||||
refs: {
|
|
||||||
svg: null,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default globalState;
|
export default globalState;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import { remove, transaction } from "mobx";
|
||||||
|
import state from "../store/state";
|
||||||
import store from "../store/global";
|
import store from "../store/global";
|
||||||
|
import { clearHistory, unwrappedAction } from "../store/history";
|
||||||
import { createParagraphWithNotations } from "../util/paragraph";
|
import { createParagraphWithNotations } from "../util/paragraph";
|
||||||
import { VERSION } from "./version";
|
import { VERSION } from "./version";
|
||||||
|
|
||||||
@@ -49,8 +52,23 @@ function findParagraphAndNotation(key) {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resetGlobalData = unwrappedAction((globalData) => {
|
||||||
|
if (!globalData) {
|
||||||
|
globalData = getDefaultGlobalDataWidthNotations();
|
||||||
|
}
|
||||||
|
state.lastSelectedNotationKey = state.selectedNotationKey = state.tieSourceKey = null;
|
||||||
|
clearHistory();
|
||||||
|
transaction(() => {
|
||||||
|
for (const key of Object.keys(store)) {
|
||||||
|
remove(store, key);
|
||||||
|
}
|
||||||
|
Object.assign(store, globalData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getDefaultGlobalData,
|
getDefaultGlobalData,
|
||||||
findParagraphAndNotation,
|
findParagraphAndNotation,
|
||||||
getDefaultGlobalDataWidthNotations,
|
getDefaultGlobalDataWidthNotations,
|
||||||
|
resetGlobalData,
|
||||||
};
|
};
|
||||||
|
|||||||
+5
-12
@@ -1,11 +1,9 @@
|
|||||||
import { message } from "antd";
|
import { message } from "antd";
|
||||||
import { remove, transaction } from "mobx";
|
|
||||||
import { toJS } from "mobx";
|
import { toJS } from "mobx";
|
||||||
import globalState from "../store/state";
|
|
||||||
import store from "../store/global";
|
import store from "../store/global";
|
||||||
import { clearHistory } from "../store/history";
|
|
||||||
import domtoimage from "./dom-to-image";
|
import domtoimage from "./dom-to-image";
|
||||||
import { VERSION } from "./version";
|
import { VERSION } from "./version";
|
||||||
|
import { resetGlobalData } from "./editor";
|
||||||
|
|
||||||
function read(file) {
|
function read(file) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -23,14 +21,7 @@ async function loadFile(file) {
|
|||||||
message.error("不支持的文件格式,请使用最新版本");
|
message.error("不支持的文件格式,请使用最新版本");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
globalState.lastSelectedNotationKey = globalState.selectedNotationKey = null;
|
resetGlobalData(data);
|
||||||
clearHistory();
|
|
||||||
transaction(() => {
|
|
||||||
for (const key of Object.keys(store)) {
|
|
||||||
remove(store, key);
|
|
||||||
}
|
|
||||||
Object.assign(store, data);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveFile() {
|
function saveFile() {
|
||||||
@@ -45,7 +36,9 @@ function saveFile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function exportFile() {
|
function exportFile() {
|
||||||
domtoimage.toPng(document.querySelector("#temp_svg")).then((dataUrl) => {
|
domtoimage
|
||||||
|
.toPng(document.querySelector("#temp_svg"), { bgcolor: "white" })
|
||||||
|
.then((dataUrl) => {
|
||||||
const downloadLink = document.createElement("a");
|
const downloadLink = document.createElement("a");
|
||||||
downloadLink.download = (store.title || "未标题") + ".png";
|
downloadLink.download = (store.title || "未标题") + ".png";
|
||||||
downloadLink.href = dataUrl;
|
downloadLink.href = dataUrl;
|
||||||
|
|||||||
+10
-1
@@ -1,3 +1,5 @@
|
|||||||
|
import { toJS } from "mobx";
|
||||||
|
|
||||||
const notations = {
|
const notations = {
|
||||||
zero: "0",
|
zero: "0",
|
||||||
do: "1",
|
do: "1",
|
||||||
@@ -31,6 +33,7 @@ function isSeparator(notationOrNote) {
|
|||||||
// 创建符号
|
// 创建符号
|
||||||
function createNotation(initial) {
|
function createNotation(initial) {
|
||||||
const n = {
|
const n = {
|
||||||
|
type: "notation",
|
||||||
key: `n_${String(Math.random())}`,
|
key: `n_${String(Math.random())}`,
|
||||||
note: "0",
|
note: "0",
|
||||||
octave: 0,
|
octave: 0,
|
||||||
@@ -46,5 +49,11 @@ function createNotation(initial) {
|
|||||||
}
|
}
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
function cloneNotation(notation) {
|
||||||
|
const origin = JSON.parse(JSON.stringify(toJS(notation)));
|
||||||
|
delete origin.key;
|
||||||
|
const n = createNotation(origin);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
export { notations, isNote, isSeparator, createNotation };
|
export { notations, isNote, isSeparator, createNotation, cloneNotation };
|
||||||
|
|||||||
+10
-2
@@ -1,8 +1,10 @@
|
|||||||
import { createNotation, notations as N } from "./notation";
|
import { toJS } from "mobx";
|
||||||
|
import { cloneNotation, createNotation, notations as N } from "./notation";
|
||||||
|
|
||||||
// 新建段落
|
// 新建段落
|
||||||
function createParagraph(initial) {
|
function createParagraph(initial) {
|
||||||
const p = {
|
const p = {
|
||||||
|
type: "paragraph",
|
||||||
key: `p_${String(Math.random())}`,
|
key: `p_${String(Math.random())}`,
|
||||||
// 符号列表
|
// 符号列表
|
||||||
notations: [],
|
notations: [],
|
||||||
@@ -25,5 +27,11 @@ function createParagraphWithNotations() {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function cloneParagraph(paragraph) {
|
||||||
|
const origin = JSON.parse(JSON.stringify(toJS(paragraph)));
|
||||||
|
delete origin.key;
|
||||||
|
origin.notations = paragraph.notations.map((n) => cloneNotation(n));
|
||||||
|
return createParagraph(origin);
|
||||||
|
}
|
||||||
|
|
||||||
export { createParagraph, createParagraphWithNotations };
|
export { createParagraph, createParagraphWithNotations, cloneParagraph };
|
||||||
|
|||||||
@@ -1,30 +1,19 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
import state from "../../store/state";
|
|
||||||
import store from "../../store/global";
|
import store from "../../store/global";
|
||||||
import Styles from "./index.module.css";
|
import Styles from "./index.module.css";
|
||||||
|
|
||||||
// ENHANCE: 增加整体缩放能力
|
// ENHANCE: 增加整体缩放能力
|
||||||
|
|
||||||
|
// FIXME: 修改获取svg元素的方式
|
||||||
function Canvas({ children, ...props }) {
|
function Canvas({ children, ...props }) {
|
||||||
const ref = useRef();
|
|
||||||
useEffect(() => {
|
|
||||||
state.refs.svg = ref;
|
|
||||||
return () => {
|
|
||||||
state.refs.svg = null;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
ref={ref}
|
|
||||||
id="temp_svg"
|
id="temp_svg"
|
||||||
xmlns=" http://www.w3.org/2000/svg"
|
xmlns=" http://www.w3.org/2000/svg"
|
||||||
className={Styles.svg}
|
className={Styles.svg}
|
||||||
// viewBox={`0 0 ${store.canvasWidth} ${store.canvasHeight}`}
|
// viewBox={`0 0 ${store.canvasWidth} ${store.canvasHeight}`}
|
||||||
width={store.canvasWidth}
|
width={store.canvasWidth}
|
||||||
height={store.canvasHeight}
|
height={store.canvasHeight}
|
||||||
// TODO: do this
|
|
||||||
onContextMenu={(ev) => ev.preventDefault()}
|
onContextMenu={(ev) => ev.preventDefault()}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ function HelpModal({ visible, onVisibleChange }) {
|
|||||||
<KeyboardDescItem kbd="?" desc="显示此帮助" />
|
<KeyboardDescItem kbd="?" desc="显示此帮助" />
|
||||||
<KeyboardDescItem kbd="Ctrl+Z" desc="撤销" />
|
<KeyboardDescItem kbd="Ctrl+Z" desc="撤销" />
|
||||||
<KeyboardDescItem kbd="Ctrl+Shift+Z" desc="重做" />
|
<KeyboardDescItem kbd="Ctrl+Shift+Z" desc="重做" />
|
||||||
|
<KeyboardDescItem kbd="Ctrl+O" desc="打开文件" />
|
||||||
<KeyboardDescItem kbd="h j k l" desc="移动焦点" />
|
<KeyboardDescItem kbd="h j k l" desc="移动焦点" />
|
||||||
</ul>
|
</ul>
|
||||||
<h3>仅选中符号时</h3>
|
<h3>仅选中符号时</h3>
|
||||||
@@ -55,8 +56,12 @@ function HelpModal({ visible, onVisibleChange }) {
|
|||||||
<KeyboardDescItem kbd="Shift+Enter" desc="移动到上一个音符" />
|
<KeyboardDescItem kbd="Shift+Enter" desc="移动到上一个音符" />
|
||||||
<KeyboardDescItem kbd="Ctrl+Enter" desc="在当前位置后插入音符" />
|
<KeyboardDescItem kbd="Ctrl+Enter" desc="在当前位置后插入音符" />
|
||||||
<KeyboardDescItem kbd="Delete" desc="删除当前音符" />
|
<KeyboardDescItem kbd="Delete" desc="删除当前音符" />
|
||||||
|
<KeyboardDescItem kbd="Shift+Delete" desc="删除当前段落" />
|
||||||
<KeyboardDescItem kbd="=" desc="段落是否两端对齐" />
|
<KeyboardDescItem kbd="=" desc="段落是否两端对齐" />
|
||||||
<KeyboardDescItem kbd="h j k l" desc="Vi风格移动焦点" />
|
<KeyboardDescItem kbd="h j k l" desc="Vi风格移动焦点" />
|
||||||
|
<KeyboardDescItem kbd="Ctrl+C" desc="复制当前音符" />
|
||||||
|
<KeyboardDescItem kbd="Ctrl+Shift+C" desc="复制当前段落" />
|
||||||
|
<KeyboardDescItem kbd="Ctrl+V" desc="粘贴" />
|
||||||
</ul>
|
</ul>
|
||||||
<h2>提示</h2>
|
<h2>提示</h2>
|
||||||
<ul className={Styles.list}>
|
<ul className={Styles.list}>
|
||||||
|
|||||||
Reference in New Issue
Block a user